@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.
package/dist/index.js CHANGED
@@ -14,50 +14,66 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.ForumulateClient = exports.ForumulateError = exports.verifyWebhookSignature = void 0;
17
+ exports.ForumulateClient = exports.API_VERSION = exports.rowsOf = exports.retryDelayMs = exports.SDK_VERSION = exports.ForumulateError = exports.constructWebhookEvent = exports.verifyWebhookSignature = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  var webhooks_1 = require("./webhooks");
20
20
  Object.defineProperty(exports, "verifyWebhookSignature", { enumerable: true, get: function () { return webhooks_1.verifyWebhookSignature; } });
21
+ Object.defineProperty(exports, "constructWebhookEvent", { enumerable: true, get: function () { return webhooks_1.constructWebhookEvent; } });
21
22
  __exportStar(require("./content"), exports);
23
+ __exportStar(require("./translations"), exports);
24
+ var http_1 = require("./http");
25
+ Object.defineProperty(exports, "ForumulateError", { enumerable: true, get: function () { return http_1.ForumulateError; } });
26
+ Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return http_1.SDK_VERSION; } });
27
+ Object.defineProperty(exports, "retryDelayMs", { enumerable: true, get: function () { return http_1.retryDelayMs; } });
28
+ var pagination_1 = require("./pagination");
29
+ Object.defineProperty(exports, "rowsOf", { enumerable: true, get: function () { return pagination_1.rowsOf; } });
30
+ var operations_gen_1 = require("./operations.gen");
31
+ Object.defineProperty(exports, "API_VERSION", { enumerable: true, get: function () { return operations_gen_1.API_VERSION; } });
22
32
  const content_1 = require("./content");
23
- /** Thrown for any non-2xx API response. Mirrors the API's error envelope. */
24
- class ForumulateError extends Error {
25
- status;
26
- code;
27
- constructor(status, code, message) {
28
- super(message);
29
- this.status = status;
30
- this.code = code;
31
- this.name = 'ForumulateError';
32
- }
33
- }
34
- exports.ForumulateError = ForumulateError;
35
- /**
36
- * Typed client for the Forumulate REST API.
37
- *
38
- * ```ts
39
- * const forumulate = new ForumulateClient({ baseUrl: 'https://forumulate.com/api', token: pat });
40
- * const { orgs } = await forumulate.orgs.list();
41
- *
42
- * // or, from FORUMULATE_TOKEN / FORUMULATE_URL:
43
- * const fromEnv = ForumulateClient.fromEnv();
44
- * ```
45
- */
33
+ const translations_1 = require("./translations");
34
+ const http_2 = require("./http");
35
+ const pagination_2 = require("./pagination");
36
+ const helpers_1 = require("./helpers");
37
+ const operations_gen_2 = require("./operations.gen");
38
+ const resources_1 = require("./resources");
39
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
46
40
  class ForumulateClient {
47
- baseUrl;
48
- token;
49
- credentials;
50
- defaultHeaders;
51
- _fetch;
41
+ transport;
42
+ apiVersion;
52
43
  constructor(opts) {
53
- this.baseUrl = opts.baseUrl.replace(/\/$/, '');
54
- this.token = opts.token;
55
- this.credentials = opts.credentials ?? false;
56
- this.defaultHeaders = { ...(opts.headers ?? {}) };
57
44
  const f = opts.fetch ?? globalThis.fetch;
58
45
  if (!f)
59
46
  throw new Error('No fetch implementation available — pass one via options.fetch.');
60
- this._fetch = f;
47
+ const headers = { ...(opts.headers ?? {}) };
48
+ const inBrowser = typeof globalThis.document !== 'undefined';
49
+ if (!inBrowser && !Object.keys(headers).some((k) => k.toLowerCase() === 'user-agent')) {
50
+ headers['User-Agent'] = opts.userAgent ?? `forumulate-sdk-js/${http_2.SDK_VERSION}`;
51
+ }
52
+ this.transport = {
53
+ baseUrl: opts.baseUrl.replace(/\/$/, ''),
54
+ token: opts.token,
55
+ credentials: opts.credentials ?? false,
56
+ headers,
57
+ fetch: f,
58
+ maxRetries: opts.maxRetries ?? 2,
59
+ autoIdempotencyKey: opts.autoIdempotencyKey ?? true,
60
+ };
61
+ this.apiVersion = opts.apiVersion;
62
+ // Generated namespaces are built on first touch: a client that only ever
63
+ // calls `blog` never allocates the other 1,800 closures.
64
+ for (const name of (0, resources_1.namespaceNames)()) {
65
+ if (Object.prototype.hasOwnProperty.call(this, name))
66
+ continue;
67
+ Object.defineProperty(this, name, {
68
+ configurable: true,
69
+ enumerable: true,
70
+ get: () => {
71
+ const value = (0, resources_1.buildNamespace)(name, this.invoke);
72
+ Object.defineProperty(this, name, { value, enumerable: true });
73
+ return value;
74
+ },
75
+ });
76
+ }
61
77
  }
62
78
  /**
63
79
  * A client configured from the environment, by the same names the CLI and
@@ -85,74 +101,34 @@ class ForumulateClient {
85
101
  ...opts,
86
102
  });
87
103
  }
88
- /** Low-level request. Exposed so callers can reach endpoints the typed helpers don't cover yet. */
89
- async request(method, path, opts = {}) {
90
- const url = new URL(this.baseUrl + path);
91
- for (const [k, v] of Object.entries(opts.query ?? {})) {
92
- if (v !== undefined)
93
- url.searchParams.set(k, String(v));
94
- }
95
- /*
96
- * Ours first, the caller's last, so a per-request header wins.
97
- *
98
- * `Idempotency-Key` has to be per-REQUEST (a key reused across two
99
- * different writes is worse than none), and `Forumulate-Version` is
100
- * naturally per-client — so both layers exist and the narrower one
101
- * overrides. `Content-Type` is set before the caller's so a multipart or
102
- * ndjson body can correct it rather than be forced to JSON.
103
- */
104
- const headers = { ...this.defaultHeaders };
105
- if (this.token)
106
- headers.Authorization = `Bearer ${this.token}`;
107
- if (opts.body !== undefined)
108
- headers['Content-Type'] = 'application/json';
109
- Object.assign(headers, opts.headers ?? {});
110
- const res = await this._fetch(url.toString(), {
111
- method,
112
- headers,
113
- credentials: this.credentials ? 'include' : 'same-origin',
114
- body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
115
- });
116
- if (opts.raw) {
117
- if (!res.ok)
118
- throw await this.toError(res);
119
- return res;
120
- }
121
- const text = await res.text();
122
- const json = text ? JSON.parse(text) : undefined;
123
- if (!res.ok) {
124
- const err = (json?.error ?? {});
125
- throw new ForumulateError(res.status, err.code ?? 'error', err.message ?? res.statusText);
104
+ request(a, b, c) {
105
+ if (typeof b === 'string') {
106
+ return this.http(a, b, (c ?? {}));
126
107
  }
127
- return json;
108
+ const route = (0, resources_1.routeTable)().byId.get(a);
109
+ if (!route)
110
+ return Promise.reject(new TypeError(`Unknown operationId "${a}"`));
111
+ return this.invoke(route, b);
112
+ }
113
+ paginate(operationId, args) {
114
+ const route = (0, resources_1.routeTable)().byId.get(operationId);
115
+ if (!route?.flags.includes('p'))
116
+ throw new TypeError(`"${operationId}" is not a paged operation`);
117
+ return this.invoke(route, args);
128
118
  }
129
119
  /**
130
- * Fully-typed request against ANY endpoint in the OpenAPI spec.
120
+ * Fully-typed request against ANY endpoint in the OpenAPI spec, by path.
131
121
  *
132
- * Where the ~30 hand-written helpers above cover the stable, ergonomic
133
- * surface, `typed` covers the *whole* API: the generated `paths` interface
134
- * (`src/schema.ts`, produced from `openapi.json`) infers the path params,
135
- * query params, JSON body and success response for the `(method, path)` you
136
- * pass, so every one of the 600+ routes gets types and autocomplete without a
137
- * bespoke wrapper. Path templates like `/orgs/{orgId}/spaces` are filled from
138
- * `init.params`.
122
+ * The generated `paths` interface (`src/schema.ts`, produced from
123
+ * `openapi.json`) infers the path params, query params, JSON body and success
124
+ * response for the `(method, path)` you pass. Path templates like
125
+ * `/orgs/{orgId}/spaces` are filled from `init.params`.
139
126
  *
140
127
  * ```ts
141
- * // GET a list — path param is required and checked; response is typed.
142
128
  * const { spaces } = await forumulate.typed('get', '/orgs/{orgId}/spaces', {
143
- * params: { orgId }, query: { deleted: 'false', limit: '50', cursor: '' },
144
- * });
145
- *
146
- * // POST with a body the spec knows the shape of.
147
- * await forumulate.typed('post', '/orgs/{orgId}/spaces', {
148
- * params: { orgId },
149
- * body: { name: 'Docs', slug: 'docs', type: 'DOCS', visibility: 'PUBLIC' },
129
+ * params: { orgId }, query: { limit: 50 },
150
130
  * });
151
131
  * ```
152
- *
153
- * Regenerate the types after an API change with
154
- * `pnpm --filter @forumulate/sdk dump-spec` then
155
- * `npx openapi-typescript packages/sdk/openapi.json -o src/schema.ts`.
156
132
  */
157
133
  typed(method, path, init) {
158
134
  const opts = (init ?? {});
@@ -162,7 +138,7 @@ class ForumulateClient {
162
138
  concrete = concrete.replace(`{${k}}`, encodeURIComponent(String(v)));
163
139
  }
164
140
  }
165
- return this.request(String(method).toUpperCase(), concrete, {
141
+ return this.http(String(method).toUpperCase(), concrete, {
166
142
  body: opts.body,
167
143
  query: opts.query,
168
144
  raw: opts.raw,
@@ -171,187 +147,52 @@ class ForumulateClient {
171
147
  headers: opts.headers,
172
148
  });
173
149
  }
174
- async toError(res) {
175
- let code = 'error';
176
- let message = res.statusText;
150
+ /** The 0.5 door: no version stated unless the client pins one. */
151
+ http(method, path, opts = {}) {
152
+ return (0, http_2.send)(this.transport, { method, path, ...opts, version: this.apiVersion });
153
+ }
154
+ /** The generated door: path filled from the args, version always stated, lists iterable. */
155
+ invoke = (route, args) => {
156
+ let resolved;
177
157
  try {
178
- const j = await res.json();
179
- code = j?.error?.code ?? code;
180
- message = j?.error?.message ?? message;
158
+ resolved = (0, resources_1.resolveArgs)(route, args);
181
159
  }
182
- catch {
183
- /* non-JSON body */
160
+ catch (err) {
161
+ return Promise.reject(err);
184
162
  }
185
- return new ForumulateError(res.status, code, message);
186
- }
187
- // ── Identity ──────────────────────────────────────────────────────────────
188
- auth = {
189
- register: (input) => this.request('POST', '/auth/register', {
190
- body: input,
191
- }),
192
- login: (input) => this.request('POST', '/auth/login', { body: input }),
193
- me: () => this.request('GET', '/auth/me'),
194
- /** Mint a Personal Access Token (requires an authenticated session). */
195
- createToken: (input) => this.request('POST', '/pats', {
196
- body: input,
197
- }),
198
- };
199
- // ── Content libraries (headless CMS) — see ./content ───────────────────────
200
- content = (0, content_1.contentApi)((method, path, opts) => this.request(method, path, opts));
201
- // ── Organizations ─────────────────────────────────────────────────────────
202
- orgs = {
203
- list: () => this.request('GET', '/orgs'),
204
- create: (input) => this.request('POST', '/orgs', { body: input }),
205
- };
206
- // ── Spaces & content ───────────────────────────────────────────────────────
207
- spaces = {
208
- list: (orgId) => this.request('GET', `/orgs/${orgId}/spaces`),
209
- create: (orgId, input) => this.request('POST', `/orgs/${orgId}/spaces`, { body: input }),
210
- createCollection: (orgId, spaceId, input) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/collections`, { body: input }),
211
- createPage: (orgId, spaceId, collectionId, input) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/collections/${collectionId}/pages`, { body: input }),
212
- setPageContent: (orgId, spaceId, pageId, input) => this.request('PUT', `/orgs/${orgId}/spaces/${spaceId}/pages/${pageId}/content`, { body: input }),
213
- publishPage: (orgId, spaceId, pageId) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/pages/${pageId}/publish`, { body: {} }),
214
- };
215
- /**
216
- * Shapes and pieces — the surface an agent works through.
217
- *
218
- * `shape` says what one piece of a space is made of and hands back JSON
219
- * Schema; `items` writes one, whatever a piece happens to be in that space:
220
- * a documentation page, a blog post, a release, an incident. The reply says
221
- * which required sections are still empty, so an agent can create a piece,
222
- * ask what it forgot, and close the gap.
223
- */
224
- shapes = {
225
- get: (orgId, spaceId) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/shape`),
226
- set: (orgId, spaceId, patch) => this.request('PUT', `/orgs/${orgId}/spaces/${spaceId}/shape`, {
227
- body: patch,
228
- }),
229
- applyPreset: (orgId, spaceId, name) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/shape/presets/${name}`, {
230
- body: {},
231
- }),
232
- };
233
- items = {
234
- create: (orgId, spaceId, input) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/items`, {
235
- body: input,
236
- }),
237
- /**
238
- * What is in a space: how something finds a piece it has no words for.
239
- *
240
- * Pass `include: ['body']` to get content. Asking for `sections` ALONE is
241
- * the trap: it is populated only for pieces written against a shape, so a
242
- * space of free-form pages answers with empty objects and a sync carries
243
- * nothing while reporting success. Every row says which field holds its
244
- * content in `contentIn`.
245
- *
246
- * Any option here puts the response in its paged form, so `page` and
247
- * `sync` come back — `page.total` is what a walk should check its own
248
- * count against, and `sync.checkpoint` is what an incremental sync stores.
249
- */
250
- list: (orgId, spaceId, opts = {}) => {
251
- // A bare number used to be the whole signature; keep it working.
252
- const o = typeof opts === 'number' ? { limit: opts } : opts;
253
- const query = {};
254
- if (o.limit)
255
- query.limit = o.limit;
256
- if (o.include?.length)
257
- query.include = o.include.join(',');
258
- if (o.cursor)
259
- query.cursor = o.cursor;
260
- if (o.updatedSince)
261
- query.updated_since = o.updatedSince;
262
- if (o.includeDeleted)
263
- query.include_deleted = 'true';
264
- if (o.fields?.length)
265
- query.fields = o.fields.join(',');
266
- return this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/items`, { query });
267
- },
268
- get: (orgId, spaceId, itemId) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/items/${itemId}`),
269
- /**
270
- * Make it readable. Kinds that are live on creation say so and change nothing.
271
- *
272
- * `conformance` describes the piece AFTER publishing — which declared slots
273
- * are filled and which are still missing — so an agent that has just made
274
- * its work readable can tell a complete piece from one with holes in it
275
- * without fetching it again. The server has always sent it and this type
276
- * omitted it, so a TypeScript caller could not see the field the docs
277
- * promise (E-11).
278
- */
279
- publish: (orgId, spaceId, itemId) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/items/${itemId}/publish`, { body: {} }),
280
- /**
281
- * Replace a piece's whole body — as markdown, or as blocks.
282
- *
283
- * The create door has always taken markdown and this one took blocks only,
284
- * so "make it from this file" worked and "update it from the same file" did
285
- * not. Both now; blocks win if a caller sends both.
286
- */
287
- setContent: (orgId, spaceId, itemId, input) => this.request('PUT', `/orgs/${orgId}/spaces/${spaceId}/items/${itemId}/content`, { body: input }),
288
- /** Replace one named section. Idempotent — sending it twice writes it once. */
289
- setSection: (orgId, spaceId, itemId, key, markdown) => this.request('PUT', `/orgs/${orgId}/spaces/${spaceId}/items/${itemId}/sections/${key}`, { body: { markdown } }),
290
- };
291
- /**
292
- * The addresses a space answers at.
293
- *
294
- * Published because the question a customer asks about a custom domain —
295
- * "why is it not live yet" — is answerable only from `verified`, `active` and
296
- * the setup records, and the SDK could not reach any of them (DX-2). A failed
297
- * verification is a 200 with `verified: false`: read the body, not the status.
298
- */
299
- domains = {
300
- list: (orgId, spaceId) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/domains`),
301
- add: (orgId, spaceId, input) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/domains`, {
302
- body: input,
303
- }),
304
- /** Re-run the ownership check. `activate: false` proves it without switching traffic. */
305
- verify: (orgId, spaceId, domainId, activate = true) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/domains/${domainId}/verify`, { body: { activate } }),
306
- /** Switch traffic to an address whose ownership is already proven. */
307
- activate: (orgId, spaceId, domainId) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/domains/${domainId}/activate`, { body: {} }),
308
- /** What to configure, and what is still missing. */
309
- setup: (orgId, spaceId, domainId) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/domains/${domainId}/setup`),
310
- };
311
- // ── Webhooks ───────────────────────────────────────────────────────────────
312
- webhooks = {
313
- list: (orgId) => this.request('GET', `/orgs/${orgId}/webhooks`),
314
- create: (orgId, input) => this.request('POST', `/orgs/${orgId}/webhooks`, { body: input }),
315
- };
316
- // ── Space snapshots (version history) ───────────────────────────────────────
317
- snapshots = {
318
- list: (orgId, spaceId) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/snapshots`),
319
- create: (orgId, spaceId, label) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/snapshots`, {
320
- body: { label },
321
- }),
322
- restore: (orgId, spaceId, id) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/snapshots/${id}/restore`, { body: {} }),
323
- };
324
- // ── Community (forum topics) ────────────────────────────────────────────────
325
- forum = {
326
- listTopics: (orgId, spaceId, query = {}) => this.request('GET', `/orgs/${orgId}/spaces/${spaceId}/forum`, { query }),
327
- createTopic: (orgId, spaceId, input) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/forum`, {
328
- body: input,
329
- }),
330
- reply: (orgId, spaceId, topicId, content) => this.request('POST', `/orgs/${orgId}/spaces/${spaceId}/forum/${topicId}/replies`, { body: { content } }),
331
- };
332
- // ── Search & analytics (org-level) ──────────────────────────────────────────
333
- search = {
334
- unified: (orgId, q, opts = {}) => this.request('GET', `/orgs/${orgId}/search`, {
335
- query: { q, ...opts },
336
- }),
337
- };
338
- analytics = {
339
- orgEvents: (orgId, days) => this.request('GET', `/orgs/${orgId}/analytics/events`, {
340
- query: days ? { days } : {},
341
- }),
342
- };
343
- // ── Public reader (no auth required) ────────────────────────────────────────
344
- public = {
345
- getPage: (orgId, spaceId, pageId) => this.request('GET', `/public/orgs/${orgId}/spaces/${spaceId}/pages/${pageId}`),
346
- search: (orgId, spaceId, query) => this.request('GET', `/public/orgs/${orgId}/spaces/${spaceId}/search`, {
347
- query: { q: query },
348
- }),
349
- listComments: (orgId, spaceId, pageId) => this.request('GET', `/public/orgs/${orgId}/spaces/${spaceId}/pages/${pageId}/comments`),
350
- postComment: (orgId, spaceId, pageId, input) => this.request('POST', `/public/orgs/${orgId}/spaces/${spaceId}/pages/${pageId}/comments`, { body: input }),
351
- reactToComment: (orgId, spaceId, commentId, emoji) => this.request('POST', `/public/orgs/${orgId}/spaces/${spaceId}/comments/${commentId}/react`, { body: { emoji } }),
352
- /** Download the whole space as a PDF. Returns the raw `Response` (read `.blob()` / `.arrayBuffer()`). */
353
- pdf: (orgId, spaceId) => this.request('GET', `/public/orgs/${orgId}/spaces/${spaceId}/pdf`, { raw: true }),
163
+ const { query, ...rest } = resolved;
164
+ const kind = route.flags.includes('b') ? 'binary' : route.flags.includes('t') ? 'text' : 'json';
165
+ const fetchPage = (q) => (0, http_2.send)(this.transport, {
166
+ ...rest,
167
+ method: route.method,
168
+ query: q,
169
+ kind,
170
+ version: this.apiVersion ?? operations_gen_2.API_VERSION,
171
+ });
172
+ if (route.flags.includes('p'))
173
+ return (0, pagination_2.pagePromise)(fetchPage, query ?? {});
174
+ return fetchPage(query ?? {});
354
175
  };
176
+ ns(name, hw) {
177
+ return (0, resources_1.mergeNamespace)((0, resources_1.buildNamespace)(name, this.invoke), hw);
178
+ }
179
+ helpers = (0, helpers_1.positionalHelpers)((method, path, opts) => this.http(method, path, opts));
180
+ // ── Namespaces with 0.5 helpers laid over the generated methods ─────────────
181
+ auth = this.ns('auth', this.helpers.auth);
182
+ content = this.ns('content', (0, content_1.contentApi)((method, path, opts) => this.http(method, path, opts)));
183
+ /** Translations of every kind of piece, routed by space type — see ./translations. */
184
+ translations = (0, translations_1.translationsApi)((method, path, opts) => this.http(method, path, opts));
185
+ orgs = this.ns('orgs', this.helpers.orgs);
186
+ spaces = this.ns('spaces', this.helpers.spaces);
187
+ shapes = this.ns('shapes', this.helpers.shapes);
188
+ items = this.ns('items', this.helpers.items);
189
+ domains = this.ns('domains', this.helpers.domains);
190
+ webhooks = this.ns('webhooks', this.helpers.webhooks);
191
+ snapshots = this.ns('snapshots', this.helpers.snapshots);
192
+ forum = this.ns('forum', this.helpers.forum);
193
+ search = this.ns('search', this.helpers.search);
194
+ analytics = this.ns('analytics', this.helpers.analytics);
195
+ public = this.ns('public', this.helpers.public);
355
196
  }
356
197
  exports.ForumulateClient = ForumulateClient;
357
198
  //# sourceMappingURL=index.js.map