Node SDK — @wenex/sdk
The @wenex/sdk package is the official TypeScript/JavaScript client for the Wenex Platform. It wraps the REST and GraphQL APIs with typed methods, supports streaming via SSE, Brotli compression of query strings, and multi-tenant usage.
npm: @wenex/sdkRepository: wenex-org/platform-sdk
Installation
npm install @wenex/sdk axios
# or
pnpm add @wenex/sdk axiosBrotli compression of the query string (config.brotli) works out of the box — the encoder is a bundled dependency (@piercefreeman/brotli-compress). The optional brotli-wasm peer is needed only for the exported Brotli helper, which decodes/encodes payloads for your own use — add brotli-wasm to the install above only if you call that helper directly.
Quick Start
import axios from 'axios';
import { Platform } from '@wenex/sdk';
// 1. Create an axios instance configured for the gateway
const http = axios.create({
baseURL: 'http://localhost:3010',
headers: {
Authorization: `Bearer ${process.env.WENEX_TOKEN}`,
},
});
// 2. Build the Platform client
const platform = Platform.build(http);
// 3. Use any service
const users = await platform.identity.users.find({ query: {} });
console.log(users); // User[]Platform Class
Platform is the root client. It lazily instantiates service clients on first access.
class Platform {
// Service clients (lazily initialized)
get auth(): Auth.Client
get identity(): Identity.Client
get financial(): Financial.Client
get career(): Career.Client
get domain(): Domain.Client
get essential(): Essential.Client
get context(): Context.Client
get general(): General.Client
get special(): Special.Client
get touch(): Touch.Client
get content(): Content.Client
get logistic(): Logistic.Client
get conjoint(): Conjoint.Client
get thing(): Thing.Client
get graphql(): GraphqlService
static build(axios: AxiosInstance, prefix?: string): Platform
}The optional prefix parameter is prepended to all request paths (useful when the API is served under a sub-path).
Authentication
With a JWT
const http = axios.create({
baseURL: 'http://localhost:3010',
headers: { Authorization: `Bearer ${jwtToken}` },
});
const platform = Platform.build(http);With an APT (long-lived token)
const http = axios.create({
baseURL: 'http://localhost:3010',
headers: { Authorization: `Bearer ${aptToken}` },
});
const platform = Platform.build(http);Obtaining a token via the SDK
const platform = Platform.build(axios.create({ baseURL: 'http://localhost:3010' }));
const { access_token } = await platform.auth.auths.token({
grant_type: GrantType.password, // import { GrantType } from '@wenex/sdk/common/core/enums'
client_id: CLIENT_ID, // required on every grant type
username: 'admin@example.com',
password: '<a-strong-password>',
});
// Re-build with the token
const authedPlatform = Platform.build(
axios.create({
baseURL: 'http://localhost:3010',
headers: { Authorization: `Bearer ${access_token}` },
}),
);RestfulService Methods
Every collection (users, accounts, products, etc.) extends RestfulService, which provides these typed methods:
class RestfulService<T extends Core, D extends Dto<Core>> {
// Count documents matching a query
count(query: Query<T>, config?: RequestConfig<T>): Promise<number>
// Create one document
create(data: D, config?: RequestConfig<T>): Promise<Serializer<T>>
// Create many documents
createBulk(data: Items<D>, config?: RequestConfig<T>): Promise<Serializer<T>[]>
// Stream documents via SSE
cursor(filter: FilterOne<T>, options: FetchEventSourceInit, config?: RequestConfig<T>): Promise<void>
// Find documents with full filter support
find(filter: Filter<T>, config?: RequestConfig<T>): Promise<Serializer<T>[]>
// Find one document by ID
findById(id: string, config?: RequestConfig<T>): Promise<Serializer<T>>
// Update one document by ID
updateById(id: string, data: Optional<D>, config?: RequestConfig<T>): Promise<Serializer<T>>
// Update many documents matching a query
updateBulk(data: Optional<D>, query: Query<T>, config?: RequestConfig<T>): Promise<number>
// Soft-delete by ID
deleteById(id: string, config?: RequestConfig<T>): Promise<Serializer<T>>
// Restore a soft-deleted document
restoreById(id: string, config?: RequestConfig<T>): Promise<Serializer<T>>
// Hard-delete by ID (permanent)
destroyById(id: string, config?: RequestConfig<T>): Promise<Serializer<T>>
}RequestConfig Options
The config parameter is an extension of Axios AxiosRequestConfig:
// type RequestConfig<T extends object = Core> = Omit<AxiosRequestConfig, "params" | "headers"> & {
params?: {
zone?: Permute<Zone>; // Zone = 'own' | 'share' | 'group' | 'client'; a comma-separated list, each zone at most once ('own,share', 'client,own,share', …)
skip?: number;
limit?: number;
sort?: Pagination<T>['sort'];
[key: string]: any;
};
headers?: {
[k in keyof Metadata]?: string | number | boolean | null;
};
fullResponse?: boolean; // If true, returns the full axios response instead of unwrapped data
brotli?: { quality: number } | boolean; // Enable Brotli compression
}CRUD Examples
Identity — Users
const users = platform.identity.users;
// Count all users
const total = await users.count({});
console.log(`Total users: ${total}`);
// Count with filter
const activeCount = await users.count({ status: 'active' });
// Create a user
const newUser = await users.create({
username: 'jdoe',
email: 'jdoe@example.com',
password: '<a-strong-password>',
name: 'John Doe',
});
console.log(newUser.id);
// Create many users
const created = await users.createBulk({
items: [
{ username: 'alice', email: 'alice@example.com' },
{ username: 'bob', email: 'bob@example.com' },
],
});
// Find with filter and pagination
const page1 = await users.find({
query: { status: 'active' },
pagination: { limit: 20, skip: 0, sort: { created_at: -1 } },
projection: { username: 1, email: 1 },
});
// Find one by ID
const user = await users.findById('64a1b2c3d4e5f6a7b8c9d0e1');
// Update
const updated = await users.updateById('64a1b2c3d4e5f6a7b8c9d0e1', {
name: 'Jonathan Doe',
});
// Bulk update
const count = await users.updateBulk(
{ status: 'inactive' },
{ archived: true },
);
// Soft delete
const deleted = await users.deleteById('64a1b2c3d4e5f6a7b8c9d0e1');
// Restore
const restored = await users.restoreById('64a1b2c3d4e5f6a7b8c9d0e1');
// Hard delete (permanent — requires manage scope)
await users.destroyById('64a1b2c3d4e5f6a7b8c9d0e1');Financial — Transactions
const txns = platform.financial.transactions;
// Find completed transactions over $100 with account populated
const results = await txns.find({
query: { status: 'completed', amount: { $gte: 100 } },
populate: [{ path: 'account', select: ['name', 'balance'] }],
pagination: { limit: 50, skip: 0, sort: { created_at: -1 } },
});Auth — Token and Verify
const auth = platform.auth.auths;
// Get token
const { access_token } = await auth.token({
grant_type: GrantType.password,
client_id: CLIENT_ID, // required — `AuthenticationRequest.client_id` is not optional
username: 'admin@example.com',
password: 'secret',
});
// Verify token
const claims = await auth.verify();
console.log(claims.subject, claims.scope);
// Check permission — returns { granted, policies }
const { granted } = await auth.can({ action: 'read', object: 'identity:users' });
// Logout
await auth.logout();Auth — APTs
const apts = platform.auth.apts;
// Create an APT
const apt = await apts.create({
name: 'my-bot',
expires_at: Date.now() + 30 * 24 * 60 * 60 * 1000, // required — epoch milliseconds, later than now and under a year out
scopes: [Scope.ReadIdentityUsers], // `Scope` enum from '@wenex/sdk/common/core' — the string 'read:identity:users' is its value
subjects: ['bot@example.com'],
});
console.log(apt.token); // Store this — only shown once
// List APTs
const myApts = await apts.find({ query: {} });
// Revoke an APT
await apts.deleteById(apt.id);Streaming (Cursor)
await platform.identity.users.cursor(
{ query: { status: 'active' } },
{
onmessage(event) {
if (event.event === 'end') {
console.log('Stream complete');
return;
}
const user = JSON.parse(event.data);
console.log('User:', user.id, user.username);
},
onerror(err) {
console.error(err);
throw err; // Re-throw to stop retries
},
},
);GraphQL via SDK
The SDK exposes a graphql client for executing raw GraphQL operations:
const gql = platform.graphql;
// request({ query | mutation, variables }, config?) — the operation rides an object, not a bare string
const result = await gql.request<{ findIdentityUser: { items: { id: string; username: string; email: string }[] } }>({
query: `
query {
findIdentityUser(filter: { query: {} }) {
items { id username email }
}
}
`,
});
// With variables
const result2 = await gql.request({
query: `query FindById($id: String!) {
findIdentityUserById(id: $id) { data { id username email } }
}`,
variables: { id: '64a1b2c3d4e5f6a7b8c9d0e1' },
});
// A reply carrying `errors` is thrown (as the errors array); otherwise `data` is returned unwrapped.Zone Filtering
Pass zone in config.params:
// My own documents only
const mine = await platform.identity.users.find(
{ query: {} },
{ params: { zone: 'own' } },
);
// Shared with me
const shared = await platform.identity.users.find(
{ query: {} },
{ params: { zone: 'own,share' } },
);Full Response Mode
By default the SDK unwraps the response envelope and returns the data directly. Set fullResponse: true to receive the platform's own response envelope instead of the unwrapped payload (the Axios response object, headers included, is never surfaced):
const response = await platform.identity.users.find(
{ query: {} },
{ fullResponse: true },
);
// response = { items: [...] } (a count() call yields { total: N })Brotli Compression
config.brotli compresses the query string — the filter/query/pagination params a read sends — into one base64url q= parameter (getParams in axios.util.ts, via the bundled @piercefreeman/brotli-compress). Request bodies are sent as-is; brotli-wasm is not involved:
await platform.identity.users.find(
{ query: { status: 'active' }, pagination: { limit: 50 } },
{ brotli: { quality: 6 } }, // quality 1–11; default 8
);
// Boolean shorthand (uses the default quality)
await platform.identity.users.find(filter, { brotli: true });The gateway decodes q= transparently; the compressed form is what lets a long $in filter fit in a URL.
Multi-Tenant Usage
Tenancy is bound to the token, not to a request header — there is no x-domain override. A JWT is issued for a specific OAuth client (cid), and the client's coworkers[] registration is embedded as the token's coworker claim. The Platform auto-populates each document's clients[] field with the writer's cid plus every coworker ID, so data is shared across clients in the same Coworkers Space.
To read documents shared by coworker clients, request the client zone — it matches the token's cid against each document's clients[]:
// Documents owned by, or shared with, the token's client (coworker space)
const shared = await platform.content.notes.find(
{ query: {} },
{ params: { zone: 'client' } },
);
// Broaden to include your own + shared + coworker documents
const all = await platform.content.notes.find(
{ query: {} },
{ params: { zone: 'own,share,client' } },
);To act as a different tenant, obtain a token issued for that client (via platform.auth.auths.token(...)) and build a Platform instance with it — the tenant scope follows from the token's claims, not from any per-request header.
See Coworkers Space and Access Control for the full model.
TypeScript Tips
The Platform class accepts a generic Properties type parameter for extending entity types with custom properties:
interface MyUserProps {
preferredLanguage: string;
tier: 'free' | 'pro' | 'enterprise';
}
const platform = Platform.build<MyUserProps>(http);
// platform.identity.users.find() returns User<MyUserProps>[]
const users = await platform.identity.users.find({ query: {} });
users[0].props?.preferredLanguage; // typedService Client Reference
platform.X | Collections available |
|---|---|
platform.auth | .auths, .apts, .grants |
platform.identity | .users, .profiles, .sessions |
platform.financial | .accounts, .wallets, .invoices, .transactions, .currencies |
platform.career | .businesses, .branches, .employees, .products, .services, .stocks, .stores, .customers |
platform.domain | .apps, .clients |
platform.essential | .sagas (saga stages: platform.essential.sagas.stages) |
platform.context | .configs, .settings |
platform.general | .activities, .artifacts, .comments, .events, .workflows |
platform.special | .files, .stats |
platform.touch | .emails, .notices, .pushes, .smss |
platform.content | .notes, .posts, .tickets |
platform.logistic | .locations, .drivers, .vehicles, .travels, .cargoes |
platform.conjoint | .accounts, .channels, .contacts, .members, .messages |
platform.thing | .devices, .sensors, .metrics |
Customizing the SDK
Platform is designed to be extended. You can build a custom client that adds domain-specific services, overrides existing ones with extra methods, and sets a path prefix for all requests.
Extending Platform
import { Platform } from '@wenex/sdk';
import type { AxiosInstance } from 'axios';
export class CustomClient<Properties extends object = object> extends Platform<Properties> {
constructor(axios: AxiosInstance) {
super(axios, '/prefix/'); // optional — prepended to every request path
}
protected _orders?: OrdersService;
get orders() {
return (this._orders ??= OrdersService.build(this.axios));
}
static override build<Properties extends object = object>(axios: AxiosInstance) {
return new CustomClient<Properties>(axios);
}
}Adding a New Service
A service wraps one or more HTTP endpoints using the shared axios instance:
import type { AxiosInstance } from 'axios';
import { RequestConfig } from '@wenex/sdk/common/core/types';
export class OrdersService {
protected readonly url = (path?: string) => `/orders${path ? `/${path}` : ''}`;
constructor(protected axios: AxiosInstance) {}
list(config?: RequestConfig): Promise<Order[]> {
return this.axios.get<Order[]>(this.url(), config).then(r => r.data);
}
create(data: CreateOrderDto, config?: RequestConfig): Promise<Order> {
return this.axios.post<Order>(this.url(), data, config).then(r => r.data);
}
static build(axios: AxiosInstance) {
return new OrdersService(axios);
}
}Extending an Existing Collection
Extend a built-in service class to add extra methods while keeping all standard CRUD operations:
import { UsersService } from '@wenex/sdk/services/identity';
import { RequestConfig } from '@wenex/sdk/common/core/types';
export class CustomUsersService<Properties extends object = object> extends UsersService<Properties> {
constructor(axios: AxiosInstance) {
super(axios, '/prefix/');
}
search(query: string, config?: RequestConfig): Promise<User[]> {
return this.post(this.url('search'), { query }, config);
}
static override build<Properties extends object = object>(axios: AxiosInstance) {
return new CustomUsersService<Properties>(axios);
}
}Then override the collection inside a custom domain client:
import { IdentityClient } from '@wenex/sdk';
export class CustomIdentityClient<Properties extends object = object> extends IdentityClient<Properties> {
protected override _users?: CustomUsersService<Properties>;
constructor(axios: AxiosInstance) {
super(axios, '/prefix/');
}
override get users() {
return (this._users ??= CustomUsersService.build<Properties>(this.axios));
}
static override build<Properties extends object = object>(axios: AxiosInstance) {
return new CustomIdentityClient<Properties>(axios);
}
}Wire the custom domain client back into the top-level client:
export class CustomClient<Properties extends object = object> extends Platform<Properties> {
protected override _identity?: CustomIdentityClient<Properties>;
override get identity() {
return (this._identity ??= CustomIdentityClient.build<Properties>(this.axios));
}
// ...
}Usage
const client = CustomClient.build(http);
// All platform services work as normal
const users = await client.identity.users.find({ query: {} });
// Extended method available
const results = await client.identity.users.search('jane');
// Custom service available
const orders = await client.orders.list();