@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 +124 -15
- package/dist/content.d.ts +318 -0
- package/dist/content.js +81 -0
- package/dist/helpers.d.ts +303 -0
- package/dist/helpers.js +180 -0
- package/dist/http.d.ts +86 -0
- package/dist/http.js +200 -0
- package/dist/index.d.ts +471 -177
- package/dist/index.js +114 -269
- package/dist/operations.gen.d.ts +2597 -0
- package/dist/operations.gen.js +360 -0
- package/dist/pagination.d.ts +27 -0
- package/dist/pagination.js +79 -0
- package/dist/resources.d.ts +181 -0
- package/dist/resources.js +129 -0
- package/dist/schema.d.ts +2 -2
- package/dist/translations.d.ts +301 -0
- package/dist/translations.js +255 -0
- package/dist/typed.d.ts +2 -2
- package/dist/webhooks.d.ts +8 -0
- package/dist/webhooks.js +15 -0
- package/package.json +8 -4
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 {
|
|
168
|
+
import { constructWebhookEvent } from '@forumulate/sdk';
|
|
108
169
|
|
|
109
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/content.js
ADDED
|
@@ -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
|