@forumulate/sdk 0.4.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/README.md CHANGED
@@ -39,13 +39,74 @@ The [CLI](https://www.npmjs.com/package/@forumulate/cli) and the [MCP server](ht
39
39
  const { user } = await forumulate.auth.me();
40
40
  const { orgs } = await forumulate.orgs.list();
41
41
  const orgId = orgs[0]!.id; // ids and slugs are both accepted wherever an org id is
42
- const { spaces } = await forumulate.spaces.list(orgId);
43
- console.log(
44
- user.email,
45
- spaces.map((s) => `${s.slug} [${s.type}]`),
46
- );
42
+
43
+ // Every list is an async iterator over every row, across every page.
44
+ for await (const space of forumulate.spaces.list({ orgId })) {
45
+ console.log(space.slug, space.type);
46
+ }
47
+ ```
48
+
49
+ ## Every operation is a method
50
+
51
+ Each of the API's ~1,800 operations is a method named after its operationId:
52
+ `ForumTopics_get` is `forumulate.forum.topics.get`, `Webhook_rotate` is
53
+ `forumulate.webhooks.rotate`. Paths, query, body and response are typed from
54
+ the OpenAPI spec, and the namespaces are generated from it, so a route in the
55
+ [API reference](https://forumulate.com/reference) is a method here.
56
+
57
+ ```ts
58
+ // One argument: path params at the top level, `query` and `body` nested.
59
+ const { space } = await forumulate.spaces.get({ orgId, spaceId });
60
+ await forumulate.blog.create({ orgId, spaceId, body: { title: 'v2 is out', content } });
61
+ await forumulate.webhooks.update({ orgId, webhookId, body: { active: false } });
62
+ await forumulate.webhooks.delete({ orgId, webhookId }); // `delete` aliases the API's `remove`
63
+ await forumulate.tokens.rotate({ id: patId });
64
+ await forumulate.forum.taxonomy.listCategories({ orgId, spaceId }); // a sub-namespace per area of a namespace
47
65
  ```
48
66
 
67
+ | Area | Namespaces |
68
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
69
+ | Organization | `orgs`, `spaces`, `sites`, `members`, `invites`, `roles`, `grants`, `groups`, `tokens`, `users`, `billing`, `sso`, `scim` |
70
+ | Docs | `items`, `pages`, `collections`, `shapes`, `snapshots`, `changeRequests`, `docs`, `openapi`, `comments` |
71
+ | Community and publishing | `blog`, `forum`, `community`, `moderation`, `chat`, `messages`, `changelog`, `roadmap`, `newsletter` |
72
+ | Space types | `status`, `events`, `courses`, `faq`, `careers`, `legal`, `forms`, `questions` |
73
+ | Platform | `webhooks`, `domains`, `themes`, `extensions`, `integrations`, `notifications`, `mail`, `assets`, `embeds`, `privacy` |
74
+ | Search, AI and analytics | `search`, `ai`, `analytics` |
75
+ | Headless CMS | `content` (libraries, entries, model, locales, keys, assets…), `delivery` |
76
+ | Public reader (no token) | `public`, and the `public…` sub-namespace of each area: `forum.publicTopic`, `status.public`, `blog.public`… |
77
+
78
+ `method.operationId` tells you which operation a method calls. By id, with the same types:
79
+
80
+ ```ts
81
+ const { space } = await forumulate.request('Space_get', { orgId, spaceId });
82
+ for await (const hook of forumulate.paginate('Webhook_list', { orgId })) console.log(hook.url);
83
+ ```
84
+
85
+ The 0.5 positional helpers (`spaces.list(orgId)`, `items.create(orgId, spaceId, input)`…) still work
86
+ unchanged, except `auth.register` and `auth.login`, removed in 0.6: their routes went away when
87
+ sign-in moved to Atlas, so they failed for every caller. Authenticate with a Personal Access Token. Where one shares a name with a generated method it is the same operation, and the member
88
+ takes either call shape.
89
+
90
+ ### Pagination
91
+
92
+ A list returns a `PagePromise`: `await` it for the first page as the API answered it, `for await` it for every row.
93
+
94
+ ```ts
95
+ const first = await forumulate.blog.list({ orgId, spaceId, query: { limit: 50 } });
96
+ first.page.nextCursor; // the envelope: page.{limit, hasMore, nextCursor, total}, rows under data
97
+
98
+ for await (const post of forumulate.blog.list({ orgId, spaceId })) {
99
+ /* every post */
100
+ }
101
+ for await (const page of forumulate.blog.list({ orgId, spaceId }).pages()) {
102
+ /* every page */
103
+ }
104
+ ```
105
+
106
+ The walk follows `page.nextCursor`, and `?page=` for the ranked lists that have no cursor. The
107
+ generated methods send `Forumulate-Version` (the version the SDK was generated at, `API_VERSION`,
108
+ unless you set `apiVersion`) because only a versioned answer carries the envelope.
109
+
49
110
  ## Common tasks
50
111
 
51
112
  ### 1. Write and publish a page from markdown
@@ -104,9 +165,10 @@ await forumulate.forum.reply(orgId, forumSpaceId, topicId, doc('Upgrade notes ar
104
165
  ### 5. Verify a webhook
105
166
 
106
167
  ```ts
107
- import { verifyWebhookSignature } from '@forumulate/sdk';
168
+ import { constructWebhookEvent } from '@forumulate/sdk';
108
169
 
109
- const ok = await verifyWebhookSignature({
170
+ // Verifies the signature and its timestamp, THEN parses; throws if either fails.
171
+ const event = await constructWebhookEvent({
110
172
  secret: process.env.FORUMULATE_WEBHOOK_SECRET!,
111
173
  body: rawBody, // the exact bytes received, before JSON.parse
112
174
  signature: req.headers['x-forumulate-signature'] as string,
@@ -114,7 +176,21 @@ const ok = await verifyWebhookSignature({
114
176
  });
115
177
  ```
116
178
 
117
- Create the webhook with `forumulate.webhooks.create(orgId, { url, events })`.
179
+ `verifyWebhookSignature` (also `forumulate.webhooks.verifySignature`) is the same check returning a
180
+ boolean. Create the endpoint with `forumulate.webhooks.create({ orgId, body: { url, events } })`;
181
+ `webhooks.rotate`, `webhooks.ping`, `webhooks.deliveries` and `webhooks.replay` manage it after.
182
+
183
+ ### 6. Translations, any kind of piece
184
+
185
+ `client.translations` picks the right route from the space's type: a docs page's language twin, a post's, topic's or release's translation, or an incident's. You only name the original piece and the language. Machine translation uses your organization's own DeepL key or AI provider. See [Translations everywhere](https://forumulate.com/guides/translations-everywhere).
186
+
187
+ ```ts
188
+ const cov = await client.translations.status(orgId, spaceId); // piece × language → missing/draft/published/outdated
189
+ await client.translations.locales.set(orgId, { spaceId }, [{ code: 'es', label: 'Español' }]);
190
+ await client.translations.auto(orgId, spaceId, pageId, 'es');
191
+ const es = await client.translations.get(orgId, spaceId, pageId, 'es'); // { title, markdown, state }
192
+ await client.translations.put(orgId, spaceId, pageId, 'es', { markdown: '…', published: true });
193
+ ```
118
194
 
119
195
  ## Every endpoint: `typed()`
120
196
 
@@ -134,23 +210,43 @@ await forumulate.typed('post', '/orgs/{orgId}/spaces', {
134
210
 
135
211
  Pass `raw: true` for the untouched `Response` (binary downloads). For anything else, `request(method, path, { body, query, headers })` is the untyped escape hatch that `typed` and the helpers use.
136
212
 
213
+ Operations that answer a file (PDF, PNG, ZIP) or a stream resolve to the `Response`; text answers (CSV, Markdown, iCal) resolve to a `string`.
214
+
137
215
  ## Errors
138
216
 
139
- Every non-2xx response throws a `ForumulateError` carrying `status`, `code` and `message` from the API's error envelope.
217
+ Every non-2xx response throws a `ForumulateError` carrying the API's error envelope: `status`, `code`,
218
+ `message`, `details` (field-level, for `validation.failed`), `requestId` (quote it in a support request)
219
+ and `retryAfter` (seconds, when the API said).
140
220
 
141
221
  ```ts
142
222
  import { ForumulateError } from '@forumulate/sdk';
143
223
 
144
224
  try {
145
- await forumulate.orgs.create({ name: '', slug: '' });
225
+ await forumulate.orgs.create({ body: { name: '', slug: '' } });
146
226
  } catch (err) {
147
227
  if (err instanceof ForumulateError && err.code === 'validation.failed') {
148
- /* … */
228
+ console.log(err.details); // { name: ['Required'], … }
149
229
  }
150
230
  }
151
231
  ```
152
232
 
153
- A `401` means the token is missing or revoked. A `403` means it lacks a scope or capability; see [API tokens and scopes](https://forumulate.com/guides/api-tokens-and-scopes).
233
+ A `401` means the token is missing or revoked. A `403` means it lacks a scope or capability; see [API tokens and scopes](https://forumulate.com/guides/api-tokens-and-scopes). A network failure that outlives its retries is a `ForumulateError` with `status: 0` and `code: 'network.error'`.
234
+
235
+ ## Retries and idempotency
236
+
237
+ Requests that are safe to repeat are retried on `408`, `429`, `502`, `503`, `504` and network
238
+ failures: two retries by default, waiting what `Retry-After` says (a wait over a minute is surfaced
239
+ rather than slept through), else exponential backoff with jitter. A `500` is not retried.
240
+
241
+ Every POST and PATCH carries an `Idempotency-Key`, generated once per call and reused by its
242
+ retries, so a write that timed out is retried without running twice. Pass your own to dedupe
243
+ across processes:
244
+
245
+ ```ts
246
+ await forumulate.items.create({ orgId, spaceId, body, idempotencyKey: message.id });
247
+ ```
248
+
249
+ Each call also takes `headers`, `signal` (an `AbortSignal`) and `maxRetries`.
154
250
 
155
251
  ## Options
156
252
 
@@ -158,13 +254,20 @@ A `401` means the token is missing or revoked. A `403` means it lacks a scope or
158
254
  new ForumulateClient({
159
255
  baseUrl: 'https://forumulate.com/api',
160
256
  token,
161
- headers: { 'Forumulate-Version': '2026-09-01' }, // pin the API version for this client
257
+ apiVersion: '2026-12-01', // Forumulate-Version for every request (default: generated methods only)
258
+ maxRetries: 2, // 0 turns retries off
259
+ autoIdempotencyKey: true, // generated Idempotency-Key on POST/PATCH
260
+ userAgent: 'my-app/1.0', // default forumulate-sdk-js/<version> outside a browser
261
+ headers: { 'X-Trace': id }, // sent on every request
162
262
  fetch: customFetch, // any fetch-compatible function
163
263
  credentials: true, // browser on the same origin: send the session cookie instead of a token
164
264
  });
165
265
  ```
166
266
 
167
- Set a descriptive `User-Agent` when you run outside a browser: the edge firewall challenges default library User-Agents.
267
+ The default `User-Agent` matters outside a browser: the edge firewall challenges default library User-Agents.
268
+
269
+ The package has no runtime dependencies and is marked side-effect free; the route table behind the
270
+ namespaces is about 100 KB unminified (about 20 KB gzipped) and namespaces are built on first use.
168
271
 
169
272
  ## Links
170
273
 
@@ -179,9 +282,15 @@ Set a descriptive `User-Agent` when you run outside a browser: the edge firewall
179
282
 
180
283
  ```sh
181
284
  curl -s https://forumulate.com/api/openapi.json -o packages/sdk/openapi.json
182
- pnpm --filter @forumulate/sdk generate-schema # → src/schema.ts
285
+ pnpm --filter @forumulate/sdk generate-schema # → src/schema.ts, then src/operations.gen.ts
183
286
  ```
184
287
 
288
+ `src/operations.gen.ts` is the route table behind the namespaces (`generate-operations` rebuilds it
289
+ alone). Placement lives in `scripts/generate-operations.mjs`: a short map from operationId tag
290
+ prefixes to namespaces, so a new tag lands somewhere without an edit. `src/coverage.test.ts` fails
291
+ when the table is stale, when an operation has no method, or when a 0.5 helper and a generated
292
+ method share a name but not an operation.
293
+
185
294
  ## License
186
295
 
187
296
  MIT
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Content libraries (headless CMS): the management routes under
3
+ * `/orgs/{orgId}/content/libraries/{libraryId}/…`.
4
+ *
5
+ * Hand-written because the generated `schema.ts` is refreshed from the
6
+ * DEPLOYED spec, and these routes are newer than the last refresh; `typed()`
7
+ * covers them once it catches up. Everything is addressed by `libraryId`,
8
+ * never by the hidden space behind a library: that id is not part of the
9
+ * contract (spec 14).
10
+ */
11
+ type Request = <T>(method: string, path: string, opts?: {
12
+ body?: unknown;
13
+ query?: Record<string, string | number | boolean | undefined>;
14
+ headers?: Record<string, string>;
15
+ }) => Promise<T>;
16
+ export interface ContentLibrary {
17
+ id: string;
18
+ name: string;
19
+ slug: string;
20
+ siteId: string | null;
21
+ settings: Record<string, unknown>;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+ export type LibraryRole = 'viewer' | 'editor' | 'manager';
26
+ export interface LibraryMember {
27
+ userId: string;
28
+ email: string;
29
+ name: string;
30
+ role: LibraryRole | null;
31
+ customRoleId: string | null;
32
+ effect: 'ALLOW' | 'DENY';
33
+ }
34
+ /** A declared model change a diff cannot infer (a rename), or a removal. */
35
+ export type ModelOp = {
36
+ op: 'renameField';
37
+ type?: string;
38
+ blockType?: string;
39
+ from: string;
40
+ to: string;
41
+ } | {
42
+ op: 'removeField';
43
+ type?: string;
44
+ blockType?: string;
45
+ path: string;
46
+ } | {
47
+ op: 'restoreField';
48
+ type?: string;
49
+ blockType?: string;
50
+ path: string;
51
+ } | {
52
+ op: 'renameType';
53
+ from: string;
54
+ to: string;
55
+ } | {
56
+ op: 'renameBlockType';
57
+ from: string;
58
+ to: string;
59
+ };
60
+ export interface ContentModel {
61
+ shape: Record<string, unknown>;
62
+ types: Record<string, unknown>;
63
+ version: number;
64
+ }
65
+ export interface ContentModelVersion {
66
+ version: number;
67
+ ops: ModelOp[];
68
+ createdBy: string | null;
69
+ createdAt: string;
70
+ migratedAt: string | null;
71
+ }
72
+ export type ModelFileFormat = 'json' | 'payload';
73
+ export interface ModelFileIssue {
74
+ path: string;
75
+ kind: 'hook' | 'access' | 'component' | 'function' | 'lossy' | 'unsupported' | 'ignored' | 'rewritten';
76
+ message: string;
77
+ }
78
+ export interface ContentModelExport {
79
+ format: ModelFileFormat;
80
+ filename: string;
81
+ content: string;
82
+ version: number;
83
+ issues: ModelFileIssue[];
84
+ }
85
+ export interface ModelChangeLine {
86
+ op: 'add' | 'remove' | 'change';
87
+ target: 'type' | 'setting' | 'field' | 'blockType' | 'blockField';
88
+ type?: string;
89
+ blockType?: string;
90
+ path?: string;
91
+ property?: string;
92
+ before?: unknown;
93
+ after?: unknown;
94
+ }
95
+ export interface ContentModelImportResult {
96
+ changed: boolean;
97
+ applied: boolean;
98
+ diff: ModelChangeLine[];
99
+ storageOnly: boolean;
100
+ ops: ModelOp[];
101
+ issues: ModelFileIssue[];
102
+ version: number;
103
+ }
104
+ /** An entry as the management API answers a write: enough to address it again. */
105
+ export interface ContentEntryRef {
106
+ id: string;
107
+ title: string;
108
+ slug?: string;
109
+ preset?: string | null;
110
+ }
111
+ export interface ContentAssetSummary {
112
+ id: string;
113
+ url: string;
114
+ filename: string;
115
+ contentType?: string;
116
+ [extra: string]: unknown;
117
+ }
118
+ export type DeliveryKeyKind = 'delivery' | 'publishable' | 'preview';
119
+ export interface DeliveryKeySummary {
120
+ id: string;
121
+ name: string;
122
+ kind: DeliveryKeyKind;
123
+ prefix: string;
124
+ stage: 'published' | 'draft';
125
+ libraryId: string;
126
+ origins: string[] | null;
127
+ contentTypes: string[] | null;
128
+ locales: string[] | null;
129
+ lastUsedAt: string | null;
130
+ [extra: string]: unknown;
131
+ }
132
+ export interface IssuedDeliveryKey {
133
+ key: DeliveryKeySummary;
134
+ /** Shown once. */
135
+ secret: string;
136
+ }
137
+ export declare function contentApi(request: Request): {
138
+ libraries: {
139
+ list: (orgId: string) => Promise<{
140
+ libraries: ContentLibrary[];
141
+ }>;
142
+ get: (orgId: string, libraryId: string) => Promise<{
143
+ library: ContentLibrary;
144
+ }>;
145
+ create: (orgId: string, input: {
146
+ name: string;
147
+ slug?: string;
148
+ siteId?: string | null;
149
+ settings?: Record<string, unknown>;
150
+ template?: string;
151
+ }) => Promise<{
152
+ library: ContentLibrary;
153
+ }>;
154
+ update: (orgId: string, libraryId: string, input: {
155
+ name?: string;
156
+ slug?: string;
157
+ siteId?: string | null;
158
+ settings?: Record<string, unknown>;
159
+ }) => Promise<{
160
+ library: ContentLibrary;
161
+ }>;
162
+ remove: (orgId: string, libraryId: string) => Promise<{
163
+ ok: true;
164
+ }>;
165
+ };
166
+ members: {
167
+ list: (orgId: string, libraryId: string) => Promise<{
168
+ members: LibraryMember[];
169
+ }>;
170
+ put: (orgId: string, libraryId: string, input: {
171
+ userId: string;
172
+ role?: LibraryRole;
173
+ customRoleId?: string;
174
+ effect?: "ALLOW" | "DENY";
175
+ }) => Promise<{
176
+ member: LibraryMember;
177
+ }>;
178
+ remove: (orgId: string, libraryId: string, userId: string) => Promise<{
179
+ ok: true;
180
+ }>;
181
+ };
182
+ model: {
183
+ get: (orgId: string, libraryId: string) => Promise<{
184
+ model: ContentModel;
185
+ }>;
186
+ save: (orgId: string, libraryId: string, input: {
187
+ shape?: Record<string, unknown>;
188
+ types?: Record<string, unknown>;
189
+ ops?: ModelOp[];
190
+ }) => Promise<{
191
+ model: ContentModel;
192
+ }>;
193
+ versions: (orgId: string, libraryId: string) => Promise<{
194
+ versions: ContentModelVersion[];
195
+ }>;
196
+ /** The model as a file: `forumulate.content.json`, or a Payload config. */
197
+ export: (orgId: string, libraryId: string, format?: ModelFileFormat) => Promise<ContentModelExport>;
198
+ /** Push a model file: a diff always, a new version unless `dryRun` (or nothing changed). */
199
+ import: (orgId: string, libraryId: string, input: {
200
+ format: ModelFileFormat;
201
+ content: string;
202
+ dryRun?: boolean;
203
+ baseVersion?: number;
204
+ force?: boolean;
205
+ ops?: ModelOp[];
206
+ }) => Promise<ContentModelImportResult>;
207
+ /** The library's delivery JSON Schema (what `forumulate content types` generates from). */
208
+ schema: (orgId: string, libraryId: string, opts?: {
209
+ audience?: "public" | "preview";
210
+ }) => Promise<Record<string, unknown>>;
211
+ };
212
+ /**
213
+ * Entries by their management id. `create` takes an `Idempotency-Key` (the
214
+ * route requires one), so a retried import returns the entry the first call
215
+ * made. Later writes may carry the entry's `contentHash` as `If-Match`; an import is the only writer, so it sends none.
216
+ */
217
+ entries: {
218
+ create: (orgId: string, libraryId: string, input: {
219
+ title: string;
220
+ slug?: string;
221
+ preset: string;
222
+ params?: Record<string, unknown>;
223
+ }, idempotencyKey: string) => Promise<{
224
+ item: ContentEntryRef;
225
+ warnings?: string[];
226
+ }>;
227
+ /** Which of these `Idempotency-Key`s already made an entry, as key → entry id (an import's dry-run and its update path). */
228
+ lookup: (orgId: string, libraryId: string, keys: string[]) => Promise<{
229
+ found: Record<string, string>;
230
+ }>;
231
+ /** Rename, re-address or patch the fields (`params`) of an entry. */
232
+ update: (orgId: string, libraryId: string, entryId: string, input: {
233
+ title?: string;
234
+ slug?: string;
235
+ params?: Record<string, unknown>;
236
+ }) => Promise<{
237
+ item: ContentEntryRef;
238
+ warnings?: string[];
239
+ }>;
240
+ /** Write an entry's localized fields into the DRAFT of one locale's twin, making the twin first when needed. */
241
+ writeLocale: (orgId: string, libraryId: string, entryId: string, locale: string, fields: Record<string, unknown>) => Promise<{
242
+ locale: string;
243
+ sourceEntryId: string;
244
+ entryId: string;
245
+ created: boolean;
246
+ }>;
247
+ read: (orgId: string, libraryId: string, entryId: string) => Promise<{
248
+ item: ContentEntryRef & {
249
+ contentHash: string;
250
+ };
251
+ }>;
252
+ setFields: (orgId: string, libraryId: string, entryId: string, fields: Record<string, unknown>, ifMatch?: string) => Promise<{
253
+ fields: Record<string, unknown>;
254
+ warnings?: string[];
255
+ }>;
256
+ publish: (orgId: string, libraryId: string, entryId: string, ifMatch?: string) => Promise<{
257
+ published: boolean;
258
+ note?: string;
259
+ }>;
260
+ };
261
+ locales: {
262
+ list: (orgId: string, libraryId: string) => Promise<{
263
+ sourceLocale: string;
264
+ locales: {
265
+ code: string;
266
+ label: string;
267
+ }[];
268
+ }>;
269
+ add: (orgId: string, libraryId: string, input: {
270
+ code: string;
271
+ label: string;
272
+ }) => Promise<{
273
+ sourceLocale: string;
274
+ locales: {
275
+ code: string;
276
+ label: string;
277
+ }[];
278
+ }>;
279
+ };
280
+ assets: {
281
+ list: (orgId: string, libraryId: string, query?: {
282
+ search?: string;
283
+ limit?: number;
284
+ }) => Promise<{
285
+ assets: ContentAssetSummary[];
286
+ }>;
287
+ };
288
+ keys: {
289
+ list: (orgId: string, libraryId: string) => Promise<{
290
+ keys: DeliveryKeySummary[];
291
+ }>;
292
+ create: (orgId: string, libraryId: string, input: {
293
+ name: string;
294
+ kind: DeliveryKeyKind;
295
+ origins?: string[];
296
+ contentTypes?: string[];
297
+ locales?: string[];
298
+ expiresInDays?: number;
299
+ }) => Promise<IssuedDeliveryKey>;
300
+ update: (orgId: string, libraryId: string, keyId: string, input: {
301
+ name?: string;
302
+ origins?: string[] | null;
303
+ contentTypes?: string[] | null;
304
+ locales?: string[] | null;
305
+ }) => Promise<{
306
+ key: DeliveryKeySummary;
307
+ }>;
308
+ rotate: (orgId: string, libraryId: string, keyId: string, input?: {
309
+ graceMinutes?: number;
310
+ }) => Promise<IssuedDeliveryKey>;
311
+ revoke: (orgId: string, libraryId: string, keyId: string) => Promise<{
312
+ ok: true;
313
+ }>;
314
+ };
315
+ };
316
+ export type ContentApi = ReturnType<typeof contentApi>;
317
+ export {};
318
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ /**
3
+ * Content libraries (headless CMS): the management routes under
4
+ * `/orgs/{orgId}/content/libraries/{libraryId}/…`.
5
+ *
6
+ * Hand-written because the generated `schema.ts` is refreshed from the
7
+ * DEPLOYED spec, and these routes are newer than the last refresh; `typed()`
8
+ * covers them once it catches up. Everything is addressed by `libraryId`,
9
+ * never by the hidden space behind a library: that id is not part of the
10
+ * contract (spec 14).
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.contentApi = contentApi;
14
+ function contentApi(request) {
15
+ const lib = (orgId, libraryId) => `/orgs/${orgId}/content/libraries/${libraryId}`;
16
+ return {
17
+ libraries: {
18
+ list: (orgId) => request('GET', `/orgs/${orgId}/content/libraries`),
19
+ get: (orgId, libraryId) => request('GET', lib(orgId, libraryId)),
20
+ create: (orgId, input) => request('POST', `/orgs/${orgId}/content/libraries`, { body: input }),
21
+ update: (orgId, libraryId, input) => request('PATCH', lib(orgId, libraryId), { body: input }),
22
+ remove: (orgId, libraryId) => request('DELETE', lib(orgId, libraryId)),
23
+ },
24
+ members: {
25
+ list: (orgId, libraryId) => request('GET', `${lib(orgId, libraryId)}/members`),
26
+ put: (orgId, libraryId, input) => request('POST', `${lib(orgId, libraryId)}/members`, { body: input }),
27
+ remove: (orgId, libraryId, userId) => request('DELETE', `${lib(orgId, libraryId)}/members/${userId}`),
28
+ },
29
+ model: {
30
+ get: (orgId, libraryId) => request('GET', `${lib(orgId, libraryId)}/model`),
31
+ save: (orgId, libraryId, input) => request('PUT', `${lib(orgId, libraryId)}/model`, { body: input }),
32
+ versions: (orgId, libraryId) => request('GET', `${lib(orgId, libraryId)}/model/versions`),
33
+ /** The model as a file: `forumulate.content.json`, or a Payload config. */
34
+ export: (orgId, libraryId, format = 'json') => request('GET', `${lib(orgId, libraryId)}/model/export`, { query: { format } }),
35
+ /** Push a model file: a diff always, a new version unless `dryRun` (or nothing changed). */
36
+ import: (orgId, libraryId, input) => request('POST', `${lib(orgId, libraryId)}/model/import`, { body: input }),
37
+ /** The library's delivery JSON Schema (what `forumulate content types` generates from). */
38
+ schema: (orgId, libraryId, opts = {}) => request('GET', `${lib(orgId, libraryId)}/model/schema.json`, { query: { audience: opts.audience } }),
39
+ },
40
+ /**
41
+ * Entries by their management id. `create` takes an `Idempotency-Key` (the
42
+ * route requires one), so a retried import returns the entry the first call
43
+ * made. Later writes may carry the entry's `contentHash` as `If-Match`; an import is the only writer, so it sends none.
44
+ */
45
+ entries: {
46
+ create: (orgId, libraryId, input, idempotencyKey) => request('POST', `${lib(orgId, libraryId)}/items`, {
47
+ body: input,
48
+ headers: { 'Idempotency-Key': idempotencyKey },
49
+ }),
50
+ /** Which of these `Idempotency-Key`s already made an entry, as key → entry id (an import's dry-run and its update path). */
51
+ lookup: (orgId, libraryId, keys) => request('POST', `${lib(orgId, libraryId)}/items/lookup`, { body: { keys } }),
52
+ /** Rename, re-address or patch the fields (`params`) of an entry. */
53
+ update: (orgId, libraryId, entryId, input) => request('PATCH', `${lib(orgId, libraryId)}/items/${entryId}`, { body: input }),
54
+ /** Write an entry's localized fields into the DRAFT of one locale's twin, making the twin first when needed. */
55
+ writeLocale: (orgId, libraryId, entryId, locale, fields) => request('PUT', `${lib(orgId, libraryId)}/entries/${entryId}/locales/${encodeURIComponent(locale)}`, { body: { fields } }),
56
+ read: (orgId, libraryId, entryId) => request('GET', `${lib(orgId, libraryId)}/items/${entryId}`),
57
+ setFields: (orgId, libraryId, entryId, fields, ifMatch) => request('PATCH', `${lib(orgId, libraryId)}/items/${entryId}/fields`, {
58
+ body: { fields },
59
+ headers: ifMatch ? { 'If-Match': `"${ifMatch}"` } : undefined,
60
+ }),
61
+ publish: (orgId, libraryId, entryId, ifMatch) => request('POST', `${lib(orgId, libraryId)}/items/${entryId}/publish`, {
62
+ headers: ifMatch ? { 'If-Match': `"${ifMatch}"` } : undefined,
63
+ }),
64
+ },
65
+ locales: {
66
+ list: (orgId, libraryId) => request('GET', `${lib(orgId, libraryId)}/locales`),
67
+ add: (orgId, libraryId, input) => request('POST', `${lib(orgId, libraryId)}/locales`, { body: input }),
68
+ },
69
+ assets: {
70
+ list: (orgId, libraryId, query = {}) => request('GET', `${lib(orgId, libraryId)}/assets`, { query }),
71
+ },
72
+ keys: {
73
+ list: (orgId, libraryId) => request('GET', `${lib(orgId, libraryId)}/keys`),
74
+ create: (orgId, libraryId, input) => request('POST', `${lib(orgId, libraryId)}/keys`, { body: input }),
75
+ update: (orgId, libraryId, keyId, input) => request('PATCH', `${lib(orgId, libraryId)}/keys/${keyId}`, { body: input }),
76
+ rotate: (orgId, libraryId, keyId, input = {}) => request('POST', `${lib(orgId, libraryId)}/keys/${keyId}/rotate`, { body: input }),
77
+ revoke: (orgId, libraryId, keyId) => request('DELETE', `${lib(orgId, libraryId)}/keys/${keyId}`),
78
+ },
79
+ };
80
+ }
81
+ //# sourceMappingURL=content.js.map