@forumulate/sdk 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auto-pagination for every cursor-paged list.
3
+ *
4
+ * A list call returns a `PagePromise`: `await` it for the first page exactly
5
+ * as the API answered it, or `for await` it for every row across every page.
6
+ *
7
+ * ```ts
8
+ * const first = await forumulate.blog.list({ orgId, spaceId }); // one page
9
+ * for await (const post of forumulate.blog.list({ orgId, spaceId })) { … } // all rows
10
+ * for await (const page of forumulate.blog.list({ orgId, spaceId }).pages()) { … }
11
+ * ```
12
+ *
13
+ * The walk follows the envelope the API documents (`page.nextCursor`, rows
14
+ * under `data`). Rankings answer `nextCursor: null` with `hasMore: true` and
15
+ * continue by `?page=` — the walk does that too, so a caller never needs to
16
+ * know which kind of list it is holding.
17
+ */
18
+ export type PagePromise<P, R> = Promise<P> & AsyncIterable<R> & {
19
+ /** Every page, as answered, starting with the first. */
20
+ pages(): AsyncIterable<P>;
21
+ };
22
+ /** The rows of one page: `data`, else the one array the answer carries. */
23
+ export declare function rowsOf(page: unknown): unknown[];
24
+ /** The query for the page after `page`, or `null` when this was the last. */
25
+ export declare function nextQuery(page: unknown, query: Record<string, unknown>): Record<string, unknown> | null;
26
+ export declare function pagePromise<P, R>(fetchPage: (query: Record<string, unknown>) => Promise<P>, query?: Record<string, unknown>): PagePromise<P, R>;
27
+ //# sourceMappingURL=pagination.d.ts.map
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Auto-pagination for every cursor-paged list.
4
+ *
5
+ * A list call returns a `PagePromise`: `await` it for the first page exactly
6
+ * as the API answered it, or `for await` it for every row across every page.
7
+ *
8
+ * ```ts
9
+ * const first = await forumulate.blog.list({ orgId, spaceId }); // one page
10
+ * for await (const post of forumulate.blog.list({ orgId, spaceId })) { … } // all rows
11
+ * for await (const page of forumulate.blog.list({ orgId, spaceId }).pages()) { … }
12
+ * ```
13
+ *
14
+ * The walk follows the envelope the API documents (`page.nextCursor`, rows
15
+ * under `data`). Rankings answer `nextCursor: null` with `hasMore: true` and
16
+ * continue by `?page=` — the walk does that too, so a caller never needs to
17
+ * know which kind of list it is holding.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.rowsOf = rowsOf;
21
+ exports.nextQuery = nextQuery;
22
+ exports.pagePromise = pagePromise;
23
+ /** The rows of one page: `data`, else the one array the answer carries. */
24
+ function rowsOf(page) {
25
+ if (Array.isArray(page))
26
+ return page;
27
+ if (!page || typeof page !== 'object')
28
+ return [];
29
+ const o = page;
30
+ if (Array.isArray(o.data))
31
+ return o.data;
32
+ const arrays = Object.entries(o).filter(([k, v]) => Array.isArray(v) && k !== 'warnings');
33
+ return arrays.length === 1 ? arrays[0][1] : [];
34
+ }
35
+ /** The query for the page after `page`, or `null` when this was the last. */
36
+ function nextQuery(page, query) {
37
+ if (!page || typeof page !== 'object')
38
+ return null;
39
+ const o = page;
40
+ const info = o.page && typeof o.page === 'object' ? o.page : o;
41
+ const cursor = info.nextCursor ?? info.next_cursor;
42
+ if (typeof cursor === 'string' && cursor !== '') {
43
+ if (cursor === query.cursor)
44
+ return null; // a cursor that does not move would loop forever
45
+ const { page: _drop, ...rest } = query;
46
+ return { ...rest, cursor };
47
+ }
48
+ if (info.hasMore ?? info.has_more) {
49
+ const n = Number(query.page ?? 1);
50
+ return { ...query, page: (Number.isFinite(n) ? n : 1) + 1 };
51
+ }
52
+ return null;
53
+ }
54
+ function pagePromise(fetchPage, query = {}) {
55
+ const first = fetchPage(query);
56
+ // Iterating without awaiting must not leave the first page's failure "unhandled";
57
+ // an `await` of the promise itself still sees the rejection.
58
+ first.catch(() => undefined);
59
+ async function* pages() {
60
+ let page = await first;
61
+ let q = query;
62
+ for (;;) {
63
+ yield page;
64
+ q = nextQuery(page, q);
65
+ if (!q || rowsOf(page).length === 0)
66
+ return;
67
+ page = await fetchPage(q);
68
+ }
69
+ }
70
+ async function* rows() {
71
+ for await (const page of pages())
72
+ yield* rowsOf(page);
73
+ }
74
+ return Object.assign(first, {
75
+ pages: () => ({ [Symbol.asyncIterator]: pages }),
76
+ [Symbol.asyncIterator]: rows,
77
+ });
78
+ }
79
+ //# sourceMappingURL=pagination.js.map
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The resource namespaces — `client.forum.topics.list({ orgId, spaceId })` —
3
+ * built from the generated route table (`operations.gen.ts`) and typed from
4
+ * the generated `operations` interface (`schema.ts`).
5
+ *
6
+ * Nothing here names a route. Placement and method names come from the
7
+ * operationIds (`<Tag>_<method>`), so the SDK cannot drift from the spec: a
8
+ * refresh of `openapi.json` followed by `generate-operations` is the whole
9
+ * change, and `coverage.test.ts` fails if the two disagree.
10
+ *
11
+ * Call shape, for every method:
12
+ *
13
+ * method({ ...pathParams, query?, body?, headers?, idempotencyKey?, signal?, maxRetries? })
14
+ *
15
+ * Path params sit at the top level because every call has them and they are
16
+ * never optional; `query` and `body` are nested because their names are the
17
+ * API's and could collide with anything. An argument that is none of these is
18
+ * refused rather than dropped: `{ orgId, limit: 5 }` would otherwise read as
19
+ * "limit honoured" while sending no limit at all.
20
+ */
21
+ import type { operations } from './schema';
22
+ import type { CallOptions } from './http';
23
+ import type { PagePromise } from './pagination';
24
+ import type { QueryPart, Simplify } from './typed';
25
+ import { OPERATIONS } from './operations.gen';
26
+ type Ops = typeof OPERATIONS;
27
+ export type OperationTag = keyof Ops;
28
+ export type OperationId = keyof operations & string;
29
+ type Op<K> = K extends keyof operations ? operations[K] : never;
30
+ type HasKeys<T> = [keyof T] extends [never] ? false : true;
31
+ type PathPart<O> = O extends {
32
+ parameters: {
33
+ path: infer P;
34
+ };
35
+ } ? HasKeys<P> extends true ? P : unknown : unknown;
36
+ type BodyContent<R> = R extends {
37
+ content: infer C;
38
+ } ? C extends {
39
+ 'application/json': infer B;
40
+ } ? B : C extends {
41
+ 'multipart/form-data': unknown;
42
+ } ? FormData : unknown : unknown;
43
+ type BodyPart<O> = O extends {
44
+ requestBody: infer R;
45
+ } ? {
46
+ body: BodyContent<R>;
47
+ } : O extends {
48
+ requestBody?: infer R;
49
+ } ? [NonNullable<R>] extends [never] ? {
50
+ body?: undefined;
51
+ } : {
52
+ body?: BodyContent<NonNullable<R>>;
53
+ } : {
54
+ body?: undefined;
55
+ };
56
+ /** The one argument of a namespace method / `request(operationId, args)`. */
57
+ export type OperationArgs<K extends OperationId> = Simplify<PathPart<Op<K>> & QueryPart<Op<K>> & BodyPart<Op<K>> & CallOptions>;
58
+ /** `{}` is assignable exactly when every key of the argument is optional. */
59
+ type NoneRequired<T> = {} extends T ? true : false;
60
+ export type OperationArgsTuple<K extends OperationId> = NoneRequired<OperationArgs<K>> extends true ? [args?: OperationArgs<K>] : [args: OperationArgs<K>];
61
+ /** Operations whose argument may be left out entirely (no path params, nothing required). */
62
+ export type OptionalArgsOperationId = {
63
+ [K in OperationId]: NoneRequired<OperationArgs<K>> extends true ? K : never;
64
+ }[OperationId];
65
+ type Success<O> = O extends {
66
+ responses: infer R;
67
+ } ? 200 extends keyof R ? R[200] : 201 extends keyof R ? R[201] : 202 extends keyof R ? R[202] : 204 extends keyof R ? {
68
+ content?: never;
69
+ } : unknown : unknown;
70
+ type Payload<E> = E extends {
71
+ content: infer C;
72
+ } ? C extends {
73
+ 'application/json': infer D;
74
+ } ? D : C extends {
75
+ 'text/event-stream': unknown;
76
+ } | {
77
+ '*/*': unknown;
78
+ } ? Response : keyof C extends `text/${string}` ? string : Response : E extends {
79
+ content?: never;
80
+ } ? undefined : unknown;
81
+ /** What a call resolves to: the JSON body, a `string` for text, a `Response` for binary. */
82
+ export type OperationResult<K extends OperationId> = Payload<Success<Op<K>>>;
83
+ type IsPaged<O> = O extends {
84
+ parameters: {
85
+ query?: infer Q;
86
+ };
87
+ } ? [NonNullable<Q>] extends [never] ? false : 'cursor' extends keyof NonNullable<Q> ? true : false : false;
88
+ type RowOf<P> = [Extract<P, {
89
+ data: readonly unknown[];
90
+ }>] extends [never] ? unknown : Extract<P, {
91
+ data: readonly unknown[];
92
+ }>['data'][number];
93
+ /** The element a `for await` over this operation yields. */
94
+ export type OperationRow<K extends OperationId> = RowOf<OperationResult<K>>;
95
+ export type PagedOperationId = {
96
+ [K in OperationId]: IsPaged<Op<K>> extends true ? K : never;
97
+ }[OperationId];
98
+ /**
99
+ * A paged list answers either the pre-envelope body or the envelope, by the
100
+ * version stated. The generated door always states one, so it always gets the
101
+ * envelope: narrow to it rather than make every caller test for `page`.
102
+ */
103
+ type Enveloped<P> = [Extract<P, {
104
+ page: unknown;
105
+ }>] extends [never] ? P : Extract<P, {
106
+ page: unknown;
107
+ }>;
108
+ export type OperationReturn<K extends OperationId> = IsPaged<Op<K>> extends true ? PagePromise<Enveloped<OperationResult<K>>, OperationRow<K>> : Promise<OperationResult<K>>;
109
+ export type OperationFn<K extends OperationId> = ((...args: OperationArgsTuple<K>) => OperationReturn<K>) & {
110
+ readonly operationId: K;
111
+ };
112
+ /** Twin of `camel` in scripts/generate-operations.mjs. */
113
+ type IdFor<T extends string, M extends string, V> = V extends `=${infer S} ${string}` ? `${T}_${S}` : `${T}_${M}`;
114
+ type TagMethods<T extends OperationTag> = {
115
+ -readonly [M in keyof Ops[T][1] & string]: OperationFn<IdFor<T, M, Ops[T][1][M]> & OperationId>;
116
+ };
117
+ type Placement = {
118
+ [T in OperationTag]: Ops[T][0];
119
+ }[OperationTag];
120
+ type TagAt<P extends string> = {
121
+ [T in OperationTag]: Ops[T][0] extends P ? T : never;
122
+ }[OperationTag];
123
+ type MethodsAt<P extends string> = [TagAt<P>] extends [never] ? Record<never, never> : TagMethods<TagAt<P>>;
124
+ type SubsOf<N extends string> = Placement extends infer P ? P extends `${N}.${infer S}` ? S : never : never;
125
+ export type NamespaceName = Placement extends infer P ? P extends `${infer N}.${string}` ? N : P : never;
126
+ export type Namespace<N extends NamespaceName> = MethodsAt<N> & {
127
+ readonly [S in SubsOf<N>]: MethodsAt<`${N}.${S}`>;
128
+ };
129
+ export type Resources = {
130
+ readonly [N in NamespaceName]: Namespace<N>;
131
+ };
132
+ /**
133
+ * A generated namespace with the 0.5 positional helpers laid over it.
134
+ *
135
+ * Where a helper and a generated method share a name they are the same
136
+ * operation (`coverage.test.ts` holds that), and the member answers BOTH call
137
+ * shapes: `spaces.list('acme')` as in 0.5, `spaces.list({ orgId: 'acme' })` as
138
+ * every generated method — so the new style is uniform without breaking a
139
+ * single 0.5 call. A helper with no generated twin keeps its name; a helper
140
+ * object (`content.model`) is merged one level down.
141
+ */
142
+ type AnyFn = (...a: never[]) => unknown;
143
+ type MergeMember<G, H> = H extends AnyFn ? G extends AnyFn ? G & H : H : G extends AnyFn ? H : Omit<G, keyof H> & {
144
+ [K in keyof H]: K extends keyof G ? MergeMember<G[K], H[K]> : H[K];
145
+ };
146
+ export type Merged<G, H> = Omit<G, keyof H> & {
147
+ [K in keyof H]: K extends keyof G ? MergeMember<G[K], H[K]> : H[K];
148
+ };
149
+ export interface Route {
150
+ id: string;
151
+ method: string;
152
+ path: string;
153
+ /** p = paged, b = binary, t = text, m = multipart (see operations.gen.ts). */
154
+ flags: string;
155
+ }
156
+ export type Invoke = (route: Route, args: Record<string, unknown> | undefined) => unknown;
157
+ interface Table {
158
+ /** operationId → route. */
159
+ byId: Map<string, Route>;
160
+ /** placement (`forum`, `forum.topics`) → method name → route. */
161
+ byPlacement: Map<string, Map<string, Route>>;
162
+ }
163
+ /** Parsed once per process, on first use; the generated table is data only. */
164
+ export declare function routeTable(): Table;
165
+ export declare function namespaceNames(): string[];
166
+ /** One namespace's methods and sub-namespaces, as plain objects of closures. */
167
+ export declare function buildNamespace(name: string, invoke: Invoke): Record<string, unknown>;
168
+ /** Lay hand-written helpers over a generated namespace; see `Merged`. */
169
+ export declare function mergeNamespace(gen: Record<string, unknown>, hw: Record<string, unknown>): Record<string, unknown>;
170
+ /** Split a call's argument into the filled path and the request options. */
171
+ export declare function resolveArgs(route: Route, args?: Record<string, unknown>): {
172
+ path: string;
173
+ query: Record<string, unknown> | undefined;
174
+ body: unknown;
175
+ headers: Record<string, string> | undefined;
176
+ idempotencyKey: string | undefined;
177
+ signal: AbortSignal | undefined;
178
+ maxRetries: number | undefined;
179
+ };
180
+ export {};
181
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.routeTable = routeTable;
4
+ exports.namespaceNames = namespaceNames;
5
+ exports.buildNamespace = buildNamespace;
6
+ exports.mergeNamespace = mergeNamespace;
7
+ exports.resolveArgs = resolveArgs;
8
+ const operations_gen_1 = require("./operations.gen");
9
+ let table;
10
+ /** Parsed once per process, on first use; the generated table is data only. */
11
+ function routeTable() {
12
+ if (table)
13
+ return table;
14
+ const byId = new Map();
15
+ const byPlacement = new Map();
16
+ for (const [tag, [placement, methods]] of Object.entries(operations_gen_1.OPERATIONS)) {
17
+ const at = new Map();
18
+ for (const [name, spec] of Object.entries(methods)) {
19
+ let rest = spec;
20
+ let suffix = name;
21
+ if (rest.startsWith('=')) {
22
+ const cut = rest.indexOf(' ');
23
+ suffix = rest.slice(1, cut);
24
+ rest = rest.slice(cut + 1);
25
+ }
26
+ const [method = 'GET', short = '', flags = ''] = rest.split(' ');
27
+ const pre = short.slice(0, 2);
28
+ const path = short.startsWith('~') ? operations_gen_1.PATH_PREFIXES[pre] + short.slice(2) : short;
29
+ const id = `${tag}_${suffix}`;
30
+ const route = byId.get(id) ?? { id, method, path, flags };
31
+ byId.set(id, route);
32
+ at.set(name, route);
33
+ }
34
+ byPlacement.set(placement, at);
35
+ }
36
+ return (table = { byId, byPlacement });
37
+ }
38
+ function namespaceNames() {
39
+ return [...new Set([...routeTable().byPlacement.keys()].map((p) => p.split('.')[0]))];
40
+ }
41
+ function methodsOf(placement, invoke) {
42
+ const out = {};
43
+ for (const [name, route] of routeTable().byPlacement.get(placement) ?? []) {
44
+ const fn = (args) => invoke(route, args);
45
+ // Which operation a method calls, readable at runtime (`client.blog.list.operationId`):
46
+ // for logs, and for the coverage test that proves every operation is reachable.
47
+ Object.defineProperty(fn, 'operationId', { value: route.id });
48
+ out[name] = fn;
49
+ }
50
+ return out;
51
+ }
52
+ /** One namespace's methods and sub-namespaces, as plain objects of closures. */
53
+ function buildNamespace(name, invoke) {
54
+ const ns = methodsOf(name, invoke);
55
+ for (const placement of routeTable().byPlacement.keys()) {
56
+ if (placement.startsWith(name + '.'))
57
+ ns[placement.slice(name.length + 1)] = methodsOf(placement, invoke);
58
+ }
59
+ return ns;
60
+ }
61
+ const OPTION_KEYS = new Set(['query', 'body', 'headers', 'idempotencyKey', 'signal', 'maxRetries']);
62
+ /**
63
+ * One member, two call shapes. A call with no arguments, or with one object
64
+ * whose keys are all path params / call options of the operation, is the
65
+ * generated shape; anything else (a string first, a 0.5 input object such as
66
+ * `orgs.create({ name, slug })`) goes to the helper.
67
+ */
68
+ function dual(gen, helper) {
69
+ const route = routeTable().byId.get(gen.operationId);
70
+ const keys = new Set([
71
+ ...(route.path.match(/\{([^}]+)\}/g) ?? []).map((p) => p.slice(1, -1)),
72
+ ...OPTION_KEYS,
73
+ ]);
74
+ const fn = (...args) => {
75
+ const [a] = args;
76
+ const generated = args.length === 0 ||
77
+ (args.length === 1 &&
78
+ !!a &&
79
+ typeof a === 'object' &&
80
+ Object.keys(a).every((k) => keys.has(k)));
81
+ return generated ? gen(a) : helper(...args);
82
+ };
83
+ Object.defineProperty(fn, 'operationId', { value: gen.operationId });
84
+ // The 0.5 helper itself, so the coverage walk can prove both halves call the same route.
85
+ Object.defineProperty(fn, 'positional', { value: helper });
86
+ return fn;
87
+ }
88
+ /** Lay hand-written helpers over a generated namespace; see `Merged`. */
89
+ function mergeNamespace(gen, hw) {
90
+ const out = { ...gen };
91
+ for (const [k, v] of Object.entries(hw)) {
92
+ const g = out[k];
93
+ if (typeof g === 'function' && typeof v === 'function')
94
+ out[k] = dual(g, v);
95
+ else if (g && typeof g === 'object' && v && typeof v === 'object') {
96
+ out[k] = mergeNamespace(g, v);
97
+ }
98
+ else
99
+ out[k] = v;
100
+ }
101
+ return out;
102
+ }
103
+ /** Split a call's argument into the filled path and the request options. */
104
+ function resolveArgs(route, args = {}) {
105
+ const used = new Set();
106
+ const path = route.path.replace(/\{([^}]+)\}/g, (_, name) => {
107
+ const v = args[name];
108
+ if (v === undefined || v === null || v === '') {
109
+ throw new TypeError(`${route.id}: missing path parameter "${name}"`);
110
+ }
111
+ used.add(name);
112
+ return encodeURIComponent(String(v));
113
+ });
114
+ for (const k of Object.keys(args)) {
115
+ if (!used.has(k) && !OPTION_KEYS.has(k)) {
116
+ throw new TypeError(`${route.id}: unknown argument "${k}" — query parameters go under \`query\`, the request body under \`body\``);
117
+ }
118
+ }
119
+ return {
120
+ path,
121
+ query: args.query,
122
+ body: args.body,
123
+ headers: args.headers,
124
+ idempotencyKey: args.idempotencyKey,
125
+ signal: args.signal,
126
+ maxRetries: args.maxRetries,
127
+ };
128
+ }
129
+ //# sourceMappingURL=resources.js.map
package/dist/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/schema.d.ts
3
- size: 41197295 bytes
4
- sha256: 4b7229f500f88ffae386167d2d8d14491e8afd8734cc89cde35ce316308ade7d
3
+ size: 41339026 bytes
4
+ sha256: c747558055875df3c7006a060a53b57e9df6bc42e11b34c60ea3ee68ec437a0d