@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/README.md +124 -15
- 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 +304 -179
- package/dist/index.js +112 -271
- 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
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translations, across every kind of piece: `client.translations`.
|
|
3
|
+
*
|
|
4
|
+
* Each kind grew its own doors — a docs page's language twin, a post's or a
|
|
5
|
+
* release's `…/translations/{locale}`, an incident's — and a script that
|
|
6
|
+
* translated a release had to learn a second vocabulary for a page. These
|
|
7
|
+
* helpers say it once and route by the space's type, exactly as the CLI's
|
|
8
|
+
* `forumulate translations` and the MCP translation tools do:
|
|
9
|
+
*
|
|
10
|
+
* locales declare / relabel / remove a space's or a library's languages
|
|
11
|
+
* status one space's coverage: every piece × language → missing / draft /
|
|
12
|
+
* published / outdated (`GET …/spaces/{spaceId}/translations`)
|
|
13
|
+
* get/put/remove/auto one piece's translation, whatever the piece is
|
|
14
|
+
* labels the site/space chrome dictionary (`…/i18n/labels`, `…/i18n/strings`)
|
|
15
|
+
* overrides the org's UI string overrides and the public catalog
|
|
16
|
+
*
|
|
17
|
+
* Machine translation always uses the organization's OWN provider (its DeepL
|
|
18
|
+
* key; its AI provider for a library entry). There is no platform key, so a
|
|
19
|
+
* call without one is a 409 `ai.unavailable` — connect one with `deepl.connect`.
|
|
20
|
+
*
|
|
21
|
+
* Hand-written over `request`, like `./content`, because the generated
|
|
22
|
+
* `schema.ts` is refreshed from the DEPLOYED spec.
|
|
23
|
+
*/
|
|
24
|
+
type Request = <T>(method: string, path: string, opts?: {
|
|
25
|
+
body?: unknown;
|
|
26
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
27
|
+
}) => Promise<T>;
|
|
28
|
+
/** A space or a content library — the two things that declare languages. */
|
|
29
|
+
export type LocaleTarget = {
|
|
30
|
+
spaceId: string;
|
|
31
|
+
} | {
|
|
32
|
+
libraryId: string;
|
|
33
|
+
};
|
|
34
|
+
export interface TranslationLocale {
|
|
35
|
+
code: string;
|
|
36
|
+
label: string;
|
|
37
|
+
/** The original language (`default` in a space; the library's `sourceLocale`). */
|
|
38
|
+
isSource: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type TranslationState = 'missing' | 'draft' | 'published' | 'outdated';
|
|
41
|
+
export type TranslationKind = 'page' | 'topic' | 'incident';
|
|
42
|
+
/** Which door a piece's translation is behind (from the space's type). */
|
|
43
|
+
export type TranslationDoor = 'page' | 'blog' | 'forum' | 'changelog' | 'incident';
|
|
44
|
+
export interface TranslationCell {
|
|
45
|
+
state: TranslationState;
|
|
46
|
+
/** A docs page's language twin id (an item of its own); null for topics and incidents. */
|
|
47
|
+
id: string | null;
|
|
48
|
+
origin: 'human' | 'machine' | null;
|
|
49
|
+
updatedAt: string | null;
|
|
50
|
+
}
|
|
51
|
+
export interface TranslationCoverage {
|
|
52
|
+
spaceId: string;
|
|
53
|
+
spaceType: string;
|
|
54
|
+
kind: TranslationKind;
|
|
55
|
+
locales: {
|
|
56
|
+
code: string;
|
|
57
|
+
label: string;
|
|
58
|
+
}[];
|
|
59
|
+
items: {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
slug: string | null;
|
|
63
|
+
kind: TranslationKind;
|
|
64
|
+
locales: Record<string, TranslationCell>;
|
|
65
|
+
}[];
|
|
66
|
+
totals: Record<string, Record<TranslationState, number>>;
|
|
67
|
+
truncated: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** One piece's translation in one language, the same shape for every kind. */
|
|
70
|
+
export interface PieceTranslation {
|
|
71
|
+
door: TranslationDoor;
|
|
72
|
+
locale: string;
|
|
73
|
+
state: TranslationState;
|
|
74
|
+
/** A page twin's own id; null otherwise. */
|
|
75
|
+
id: string | null;
|
|
76
|
+
title: string | null;
|
|
77
|
+
markdown: string | null;
|
|
78
|
+
/** A post's / release's summary line. */
|
|
79
|
+
excerpt?: string | null;
|
|
80
|
+
/** An incident's update texts, by update id. */
|
|
81
|
+
updates?: Record<string, string>;
|
|
82
|
+
origin: 'human' | 'machine' | null;
|
|
83
|
+
}
|
|
84
|
+
export interface PieceTranslationInput {
|
|
85
|
+
title?: string;
|
|
86
|
+
markdown?: string;
|
|
87
|
+
excerpt?: string;
|
|
88
|
+
updates?: Record<string, string>;
|
|
89
|
+
/** Serve it to readers. A page twin is published; a post's translation flagged. */
|
|
90
|
+
published?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface AutoTranslateOptions {
|
|
93
|
+
/** Replace a translation a person wrote. Unset = 409. */
|
|
94
|
+
force?: boolean;
|
|
95
|
+
/** Publish what the machine wrote at once. Unset = a draft a person reads first. */
|
|
96
|
+
published?: boolean;
|
|
97
|
+
}
|
|
98
|
+
export type LabelScope = {
|
|
99
|
+
siteId: string;
|
|
100
|
+
} | {
|
|
101
|
+
spaceId: string;
|
|
102
|
+
};
|
|
103
|
+
/** locale → original text → that language's words (plus `href` per link where set). */
|
|
104
|
+
export type LabelDictionary = Record<string, Record<string, unknown>>;
|
|
105
|
+
export type NamedTranslations = Record<string, {
|
|
106
|
+
name?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
}>;
|
|
109
|
+
export interface TranslationOverride {
|
|
110
|
+
locale: string;
|
|
111
|
+
key: string;
|
|
112
|
+
value: string;
|
|
113
|
+
updatedAt: string;
|
|
114
|
+
}
|
|
115
|
+
/** The door a space type keeps its pieces' translations behind. */
|
|
116
|
+
export declare function translationDoorOf(spaceType: string): TranslationDoor;
|
|
117
|
+
export declare function translationsApi(request: Request): {
|
|
118
|
+
locales: {
|
|
119
|
+
list: (orgId: string, target: LocaleTarget) => Promise<TranslationLocale[]>;
|
|
120
|
+
add: (orgId: string, target: LocaleTarget, input: {
|
|
121
|
+
code: string;
|
|
122
|
+
label: string;
|
|
123
|
+
}) => Promise<{
|
|
124
|
+
code: string;
|
|
125
|
+
label: string;
|
|
126
|
+
isSource: boolean;
|
|
127
|
+
}[]>;
|
|
128
|
+
/** Rename a language as the reader's switcher shows it. */
|
|
129
|
+
relabel: (orgId: string, target: LocaleTarget, code: string, label: string) => Promise<{
|
|
130
|
+
code: string;
|
|
131
|
+
label: string;
|
|
132
|
+
isSource: boolean;
|
|
133
|
+
}[]>;
|
|
134
|
+
remove: (orgId: string, target: LocaleTarget, code: string) => Promise<{
|
|
135
|
+
code: string;
|
|
136
|
+
label: string;
|
|
137
|
+
isSource: boolean;
|
|
138
|
+
}[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Make the declared languages exactly `want` (the source excluded):
|
|
141
|
+
* adds what is missing, relabels what changed, removes what is not
|
|
142
|
+
* listed only when `prune` — removing a language trashes its
|
|
143
|
+
* translations, so it is never a side effect. Idempotent.
|
|
144
|
+
*/
|
|
145
|
+
set: (orgId: string, target: LocaleTarget, want: {
|
|
146
|
+
code: string;
|
|
147
|
+
label: string;
|
|
148
|
+
}[], opts?: {
|
|
149
|
+
prune?: boolean;
|
|
150
|
+
}) => Promise<{
|
|
151
|
+
locales: TranslationLocale[];
|
|
152
|
+
added: string[];
|
|
153
|
+
relabeled: string[];
|
|
154
|
+
removed: string[];
|
|
155
|
+
}>;
|
|
156
|
+
};
|
|
157
|
+
/** Every piece × declared language of a space, in the four states. */
|
|
158
|
+
status: (orgId: string, spaceId: string) => Promise<TranslationCoverage>;
|
|
159
|
+
/** Each declared language and one piece's state in it (any kind). */
|
|
160
|
+
list: (orgId: string, spaceId: string, pieceId: string, opts?: {
|
|
161
|
+
door?: TranslationDoor;
|
|
162
|
+
}) => Promise<{
|
|
163
|
+
translations: unknown[];
|
|
164
|
+
}>;
|
|
165
|
+
/** One piece's translation, as Markdown, whatever the piece is. */
|
|
166
|
+
get: (orgId: string, spaceId: string, pieceId: string, locale: string, opts?: {
|
|
167
|
+
door?: TranslationDoor;
|
|
168
|
+
}) => Promise<PieceTranslation>;
|
|
169
|
+
/** Write or correct one piece's translation by hand (it becomes a person's). */
|
|
170
|
+
put: (orgId: string, spaceId: string, pieceId: string, locale: string, input: PieceTranslationInput, opts?: {
|
|
171
|
+
door?: TranslationDoor;
|
|
172
|
+
}) => Promise<PieceTranslation>;
|
|
173
|
+
/** Remove one piece's translation (a page twin goes to the trash). */
|
|
174
|
+
remove: (orgId: string, spaceId: string, pieceId: string, locale: string, opts?: {
|
|
175
|
+
door?: TranslationDoor;
|
|
176
|
+
}) => Promise<void>;
|
|
177
|
+
/** Machine-translate one piece with the org's own provider: `…/translations/{locale}/auto`. */
|
|
178
|
+
auto: (orgId: string, spaceId: string, pieceId: string, locale: string, opts?: AutoTranslateOptions & {
|
|
179
|
+
door?: TranslationDoor;
|
|
180
|
+
}) => Promise<{
|
|
181
|
+
translation: unknown;
|
|
182
|
+
}>;
|
|
183
|
+
/** A content library entry's machine translation (always a draft twin). */
|
|
184
|
+
autoEntry: (orgId: string, libraryId: string, entryId: string, locale: string, opts?: {
|
|
185
|
+
force?: boolean;
|
|
186
|
+
}) => Promise<{
|
|
187
|
+
locale: string;
|
|
188
|
+
results: {
|
|
189
|
+
entryId: string;
|
|
190
|
+
ok: boolean;
|
|
191
|
+
translationEntryId: string | null;
|
|
192
|
+
error: string | null;
|
|
193
|
+
}[];
|
|
194
|
+
}>;
|
|
195
|
+
/** Correct the language a piece is DECLARED to be written in (`PATCH …/items/{id}`). */
|
|
196
|
+
setItemLocale: (orgId: string, spaceId: string, itemId: string, locale: string | null) => Promise<{
|
|
197
|
+
item: {
|
|
198
|
+
id: string;
|
|
199
|
+
locale?: string | null;
|
|
200
|
+
};
|
|
201
|
+
}>;
|
|
202
|
+
/**
|
|
203
|
+
* Name translations: a space's (`settings.translations`) and a
|
|
204
|
+
* collection's. The API replaces the whole map on write, so `set` reads
|
|
205
|
+
* it first and changes one language — the other languages survive.
|
|
206
|
+
*/
|
|
207
|
+
names: {
|
|
208
|
+
get: (orgId: string, spaceId: string, opts?: {
|
|
209
|
+
collectionId?: string;
|
|
210
|
+
}) => Promise<NamedTranslations>;
|
|
211
|
+
/** One language's name (and description); `null` removes that language. */
|
|
212
|
+
set: (orgId: string, spaceId: string, locale: string, words: {
|
|
213
|
+
name?: string;
|
|
214
|
+
description?: string;
|
|
215
|
+
} | null, opts?: {
|
|
216
|
+
collectionId?: string;
|
|
217
|
+
}) => Promise<NamedTranslations>;
|
|
218
|
+
};
|
|
219
|
+
/** A library entry's locale twin: its localized fields in one language. */
|
|
220
|
+
entryLocale: {
|
|
221
|
+
get: (orgId: string, libraryId: string, entryId: string, locale: string) => Promise<{
|
|
222
|
+
entryId: string;
|
|
223
|
+
locale: string;
|
|
224
|
+
fields: Record<string, unknown>;
|
|
225
|
+
}>;
|
|
226
|
+
/** Write the twin's DRAFT (made from the source when missing). */
|
|
227
|
+
put: (orgId: string, libraryId: string, entryId: string, locale: string, fields: Record<string, unknown>) => Promise<{
|
|
228
|
+
locale: string;
|
|
229
|
+
sourceEntryId: string;
|
|
230
|
+
entryId: string;
|
|
231
|
+
created: boolean;
|
|
232
|
+
}>;
|
|
233
|
+
};
|
|
234
|
+
/** The site/space chrome dictionary: every owner-written word, per language. */
|
|
235
|
+
labels: {
|
|
236
|
+
strings: (orgId: string, scope: LabelScope) => Promise<{
|
|
237
|
+
languages: string[];
|
|
238
|
+
labels: LabelDictionary;
|
|
239
|
+
strings: unknown[];
|
|
240
|
+
coverage: Record<string, {
|
|
241
|
+
translated: number;
|
|
242
|
+
total: number;
|
|
243
|
+
}>;
|
|
244
|
+
}>;
|
|
245
|
+
/** Replace the whole dictionary. */
|
|
246
|
+
replace: (orgId: string, scope: LabelScope, labels: LabelDictionary) => Promise<{
|
|
247
|
+
labels: LabelDictionary;
|
|
248
|
+
}>;
|
|
249
|
+
/** Merge word by word; `null` removes a word or a language. */
|
|
250
|
+
merge: (orgId: string, scope: LabelScope, labels: Record<string, Record<string, unknown> | null>) => Promise<{
|
|
251
|
+
labels: LabelDictionary;
|
|
252
|
+
}>;
|
|
253
|
+
};
|
|
254
|
+
/** The organization's own wording for our interface strings, per locale. */
|
|
255
|
+
overrides: {
|
|
256
|
+
list: (orgId: string, locale?: string) => Promise<{
|
|
257
|
+
overrides: TranslationOverride[];
|
|
258
|
+
}>;
|
|
259
|
+
set: (orgId: string, input: {
|
|
260
|
+
locale: string;
|
|
261
|
+
key: string;
|
|
262
|
+
value: string;
|
|
263
|
+
}) => Promise<{
|
|
264
|
+
override: TranslationOverride;
|
|
265
|
+
}>;
|
|
266
|
+
remove: (orgId: string, input: {
|
|
267
|
+
locale: string;
|
|
268
|
+
key: string;
|
|
269
|
+
}) => Promise<{
|
|
270
|
+
ok: true;
|
|
271
|
+
}>;
|
|
272
|
+
/** Every key a reader page can show and the English it replaces (public). */
|
|
273
|
+
catalog: (orgId: string) => Promise<{
|
|
274
|
+
catalog: Record<string, string>;
|
|
275
|
+
}>;
|
|
276
|
+
/** The overrides readers in one locale are served (public). */
|
|
277
|
+
forLocale: (orgId: string, locale: string) => Promise<{
|
|
278
|
+
overrides: Record<string, string>;
|
|
279
|
+
}>;
|
|
280
|
+
};
|
|
281
|
+
/** The org-wide default for the reader's language switcher. */
|
|
282
|
+
switcher: {
|
|
283
|
+
get: (orgId: string) => Promise<Record<string, unknown>>;
|
|
284
|
+
set: (orgId: string, config: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
285
|
+
};
|
|
286
|
+
/** The org's own DeepL key — what every `auto` uses. */
|
|
287
|
+
deepl: {
|
|
288
|
+
status: (orgId: string) => Promise<{
|
|
289
|
+
configured: boolean;
|
|
290
|
+
}>;
|
|
291
|
+
connect: (orgId: string, apiKey: string) => Promise<{
|
|
292
|
+
ok: true;
|
|
293
|
+
}>;
|
|
294
|
+
disconnect: (orgId: string) => Promise<{
|
|
295
|
+
ok: true;
|
|
296
|
+
}>;
|
|
297
|
+
};
|
|
298
|
+
};
|
|
299
|
+
export type TranslationsApi = ReturnType<typeof translationsApi>;
|
|
300
|
+
export {};
|
|
301
|
+
//# sourceMappingURL=translations.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Translations, across every kind of piece: `client.translations`.
|
|
4
|
+
*
|
|
5
|
+
* Each kind grew its own doors — a docs page's language twin, a post's or a
|
|
6
|
+
* release's `…/translations/{locale}`, an incident's — and a script that
|
|
7
|
+
* translated a release had to learn a second vocabulary for a page. These
|
|
8
|
+
* helpers say it once and route by the space's type, exactly as the CLI's
|
|
9
|
+
* `forumulate translations` and the MCP translation tools do:
|
|
10
|
+
*
|
|
11
|
+
* locales declare / relabel / remove a space's or a library's languages
|
|
12
|
+
* status one space's coverage: every piece × language → missing / draft /
|
|
13
|
+
* published / outdated (`GET …/spaces/{spaceId}/translations`)
|
|
14
|
+
* get/put/remove/auto one piece's translation, whatever the piece is
|
|
15
|
+
* labels the site/space chrome dictionary (`…/i18n/labels`, `…/i18n/strings`)
|
|
16
|
+
* overrides the org's UI string overrides and the public catalog
|
|
17
|
+
*
|
|
18
|
+
* Machine translation always uses the organization's OWN provider (its DeepL
|
|
19
|
+
* key; its AI provider for a library entry). There is no platform key, so a
|
|
20
|
+
* call without one is a 409 `ai.unavailable` — connect one with `deepl.connect`.
|
|
21
|
+
*
|
|
22
|
+
* Hand-written over `request`, like `./content`, because the generated
|
|
23
|
+
* `schema.ts` is refreshed from the DEPLOYED spec.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.translationDoorOf = translationDoorOf;
|
|
27
|
+
exports.translationsApi = translationsApi;
|
|
28
|
+
const enc = encodeURIComponent;
|
|
29
|
+
/** The door a space type keeps its pieces' translations behind. */
|
|
30
|
+
function translationDoorOf(spaceType) {
|
|
31
|
+
switch (spaceType) {
|
|
32
|
+
case 'BLOG':
|
|
33
|
+
return 'blog';
|
|
34
|
+
case 'FORUM':
|
|
35
|
+
return 'forum';
|
|
36
|
+
case 'CHANGELOG':
|
|
37
|
+
return 'changelog';
|
|
38
|
+
case 'STATUS':
|
|
39
|
+
return 'incident';
|
|
40
|
+
default:
|
|
41
|
+
return 'page';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function translationsApi(request) {
|
|
45
|
+
const space = (orgId, spaceId) => `/orgs/${orgId}/spaces/${spaceId}`;
|
|
46
|
+
const library = (orgId, libraryId) => `/orgs/${orgId}/content/libraries/${libraryId}`;
|
|
47
|
+
const localesAt = (orgId, t) => 'libraryId' in t ? `${library(orgId, t.libraryId)}/locales` : `${space(orgId, t.spaceId)}/docs/locales`;
|
|
48
|
+
const labelsAt = (orgId, s) => 'siteId' in s ? `/orgs/${orgId}/sites/${s.siteId}/i18n` : `${space(orgId, s.spaceId)}/i18n`;
|
|
49
|
+
/** Both answers as one list: a space's has `default`, a library's `source`. */
|
|
50
|
+
const normalize = (r) => r.locales.map((l) => ({ code: l.code, label: l.label, isSource: Boolean(l.isDefault ?? l.source) }));
|
|
51
|
+
const doorOf = async (orgId, spaceId, door) => {
|
|
52
|
+
if (door)
|
|
53
|
+
return door;
|
|
54
|
+
const { space: s } = await request('GET', space(orgId, spaceId));
|
|
55
|
+
return translationDoorOf(s.type);
|
|
56
|
+
};
|
|
57
|
+
const collection = (orgId, spaceId, door, pieceId) => door === 'incident'
|
|
58
|
+
? `${space(orgId, spaceId)}/status/incidents/${pieceId}/translations`
|
|
59
|
+
: door === 'page'
|
|
60
|
+
? `${space(orgId, spaceId)}/pages/${pieceId}/translations`
|
|
61
|
+
: `${space(orgId, spaceId)}/${door}/${pieceId}/translations`;
|
|
62
|
+
const pageRow = async (orgId, spaceId, pageId, locale) => {
|
|
63
|
+
const { translations } = await request('GET', collection(orgId, spaceId, 'page', pageId));
|
|
64
|
+
const row = translations.find((r) => r.code.toLowerCase() === locale.toLowerCase());
|
|
65
|
+
if (!row)
|
|
66
|
+
throw Object.assign(new Error(`"${locale}" is not a language of this space.`), { status: 404 });
|
|
67
|
+
return row;
|
|
68
|
+
};
|
|
69
|
+
const topicState = (t) => !t.exists ? 'missing' : t.stale ? 'outdated' : t.published ? 'published' : 'draft';
|
|
70
|
+
const api = {
|
|
71
|
+
locales: {
|
|
72
|
+
list: async (orgId, target) => normalize(await request('GET', localesAt(orgId, target))),
|
|
73
|
+
add: async (orgId, target, input) => normalize(await request('POST', localesAt(orgId, target), { body: input })),
|
|
74
|
+
/** Rename a language as the reader's switcher shows it. */
|
|
75
|
+
relabel: async (orgId, target, code, label) => normalize(await request('PATCH', `${localesAt(orgId, target)}/${enc(code)}`, { body: { label } })),
|
|
76
|
+
remove: async (orgId, target, code) => normalize(await request('DELETE', `${localesAt(orgId, target)}/${enc(code)}`)),
|
|
77
|
+
/**
|
|
78
|
+
* Make the declared languages exactly `want` (the source excluded):
|
|
79
|
+
* adds what is missing, relabels what changed, removes what is not
|
|
80
|
+
* listed only when `prune` — removing a language trashes its
|
|
81
|
+
* translations, so it is never a side effect. Idempotent.
|
|
82
|
+
*/
|
|
83
|
+
set: async (orgId, target, want, opts = {}) => {
|
|
84
|
+
let current = await api.locales.list(orgId, target);
|
|
85
|
+
const added = [];
|
|
86
|
+
const relabeled = [];
|
|
87
|
+
const removed = [];
|
|
88
|
+
for (const w of want) {
|
|
89
|
+
const have = current.find((l) => !l.isSource && l.code.toLowerCase() === w.code.toLowerCase());
|
|
90
|
+
if (!have) {
|
|
91
|
+
current = await api.locales.add(orgId, target, w);
|
|
92
|
+
added.push(w.code);
|
|
93
|
+
}
|
|
94
|
+
else if (have.label !== w.label) {
|
|
95
|
+
current = await api.locales.relabel(orgId, target, have.code, w.label);
|
|
96
|
+
relabeled.push(have.code);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (opts.prune)
|
|
100
|
+
for (const l of current.filter((c) => !c.isSource && !want.some((w) => w.code.toLowerCase() === c.code.toLowerCase()))) {
|
|
101
|
+
current = await api.locales.remove(orgId, target, l.code);
|
|
102
|
+
removed.push(l.code);
|
|
103
|
+
}
|
|
104
|
+
return { locales: current, added, relabeled, removed };
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
/** Every piece × declared language of a space, in the four states. */
|
|
108
|
+
status: (orgId, spaceId) => request('GET', `${space(orgId, spaceId)}/translations`),
|
|
109
|
+
/** Each declared language and one piece's state in it (any kind). */
|
|
110
|
+
list: async (orgId, spaceId, pieceId, opts = {}) => {
|
|
111
|
+
const door = await doorOf(orgId, spaceId, opts.door);
|
|
112
|
+
return request('GET', collection(orgId, spaceId, door, pieceId));
|
|
113
|
+
},
|
|
114
|
+
/** One piece's translation, as Markdown, whatever the piece is. */
|
|
115
|
+
get: async (orgId, spaceId, pieceId, locale, opts = {}) => {
|
|
116
|
+
const door = await doorOf(orgId, spaceId, opts.door);
|
|
117
|
+
if (door === 'page') {
|
|
118
|
+
const row = await pageRow(orgId, spaceId, pieceId, locale);
|
|
119
|
+
if (!row.id)
|
|
120
|
+
return { door, locale: row.code, state: 'missing', id: null, title: null, markdown: null, origin: null };
|
|
121
|
+
const { item } = await request('GET', `${space(orgId, spaceId)}/items/${row.id}`);
|
|
122
|
+
return { door, locale: row.code, state: row.state, id: row.id, title: item.title, markdown: item.markdown, origin: row.origin };
|
|
123
|
+
}
|
|
124
|
+
if (door === 'incident') {
|
|
125
|
+
const { translations } = await request('GET', collection(orgId, spaceId, door, pieceId));
|
|
126
|
+
const t = translations.find((x) => x.locale.toLowerCase() === locale.toLowerCase());
|
|
127
|
+
if (!t)
|
|
128
|
+
return { door, locale, state: 'missing', id: null, title: null, markdown: null, updates: {}, origin: null };
|
|
129
|
+
return {
|
|
130
|
+
door,
|
|
131
|
+
locale: t.locale,
|
|
132
|
+
state: t.stale ? 'outdated' : 'published',
|
|
133
|
+
id: null,
|
|
134
|
+
title: t.title,
|
|
135
|
+
markdown: t.markdown ?? null,
|
|
136
|
+
updates: t.updates,
|
|
137
|
+
origin: t.origin === 'machine' ? 'machine' : 'human',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const { translation: t } = await request('GET', `${collection(orgId, spaceId, door, pieceId)}/${enc(locale)}`);
|
|
141
|
+
return { door, locale: t.code, state: topicState(t), id: null, title: t.title, markdown: t.markdown ?? null, excerpt: t.excerpt, origin: t.origin };
|
|
142
|
+
},
|
|
143
|
+
/** Write or correct one piece's translation by hand (it becomes a person's). */
|
|
144
|
+
put: async (orgId, spaceId, pieceId, locale, input, opts = {}) => {
|
|
145
|
+
const door = await doorOf(orgId, spaceId, opts.door);
|
|
146
|
+
if (door === 'page') {
|
|
147
|
+
const row = await pageRow(orgId, spaceId, pieceId, locale);
|
|
148
|
+
if (!row.id)
|
|
149
|
+
throw Object.assign(new Error(`This page has no ${locale} twin.`), { status: 404 });
|
|
150
|
+
const item = `${space(orgId, spaceId)}/items/${row.id}`;
|
|
151
|
+
if (input.title)
|
|
152
|
+
await request('PATCH', item, { body: { title: input.title } });
|
|
153
|
+
if (input.markdown !== undefined)
|
|
154
|
+
await request('PUT', `${item}/content`, { body: { body: input.markdown } });
|
|
155
|
+
if (input.published)
|
|
156
|
+
await request('POST', `${item}/publish`, { body: {} });
|
|
157
|
+
}
|
|
158
|
+
else if (door === 'incident') {
|
|
159
|
+
const { title, markdown, updates } = input;
|
|
160
|
+
await request('PUT', `${collection(orgId, spaceId, door, pieceId)}/${enc(locale)}`, { body: { title, markdown, updates } });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const { title, markdown, excerpt, published } = input;
|
|
164
|
+
await request('PUT', `${collection(orgId, spaceId, door, pieceId)}/${enc(locale)}`, { body: { title, markdown, excerpt, published } });
|
|
165
|
+
}
|
|
166
|
+
return api.get(orgId, spaceId, pieceId, locale, { door });
|
|
167
|
+
},
|
|
168
|
+
/** Remove one piece's translation (a page twin goes to the trash). */
|
|
169
|
+
remove: async (orgId, spaceId, pieceId, locale, opts = {}) => {
|
|
170
|
+
const door = await doorOf(orgId, spaceId, opts.door);
|
|
171
|
+
if (door === 'page') {
|
|
172
|
+
const row = await pageRow(orgId, spaceId, pieceId, locale);
|
|
173
|
+
if (row.id)
|
|
174
|
+
await request('DELETE', `${space(orgId, spaceId)}/items/${row.id}`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
await request('DELETE', `${collection(orgId, spaceId, door, pieceId)}/${enc(locale)}`);
|
|
178
|
+
},
|
|
179
|
+
/** Machine-translate one piece with the org's own provider: `…/translations/{locale}/auto`. */
|
|
180
|
+
auto: async (orgId, spaceId, pieceId, locale, opts = {}) => {
|
|
181
|
+
const door = await doorOf(orgId, spaceId, opts.door);
|
|
182
|
+
const body = { force: opts.force };
|
|
183
|
+
if (door !== 'incident')
|
|
184
|
+
body.published = opts.published;
|
|
185
|
+
return request('POST', `${collection(orgId, spaceId, door, pieceId)}/${enc(locale)}/auto`, { body });
|
|
186
|
+
},
|
|
187
|
+
/** A content library entry's machine translation (always a draft twin). */
|
|
188
|
+
autoEntry: (orgId, libraryId, entryId, locale, opts = {}) => request('POST', `${library(orgId, libraryId)}/entries/${entryId}/translations/${enc(locale)}/auto`, { body: opts }),
|
|
189
|
+
/** Correct the language a piece is DECLARED to be written in (`PATCH …/items/{id}`). */
|
|
190
|
+
setItemLocale: (orgId, spaceId, itemId, locale) => request('PATCH', `${space(orgId, spaceId)}/items/${itemId}`, { body: { locale } }),
|
|
191
|
+
/**
|
|
192
|
+
* Name translations: a space's (`settings.translations`) and a
|
|
193
|
+
* collection's. The API replaces the whole map on write, so `set` reads
|
|
194
|
+
* it first and changes one language — the other languages survive.
|
|
195
|
+
*/
|
|
196
|
+
names: {
|
|
197
|
+
get: async (orgId, spaceId, opts = {}) => {
|
|
198
|
+
if (opts.collectionId) {
|
|
199
|
+
const { collection } = await request('GET', `${space(orgId, spaceId)}/collections/${opts.collectionId}`);
|
|
200
|
+
return collection.translations ?? {};
|
|
201
|
+
}
|
|
202
|
+
const { space: s } = await request('GET', space(orgId, spaceId));
|
|
203
|
+
return s.settings?.translations ?? {};
|
|
204
|
+
},
|
|
205
|
+
/** One language's name (and description); `null` removes that language. */
|
|
206
|
+
set: async (orgId, spaceId, locale, words, opts = {}) => {
|
|
207
|
+
const current = { ...(await api.names.get(orgId, spaceId, opts)) };
|
|
208
|
+
if (words === null)
|
|
209
|
+
delete current[locale];
|
|
210
|
+
else
|
|
211
|
+
current[locale] = { ...(current[locale] ?? {}), ...words };
|
|
212
|
+
const at = opts.collectionId ? `${space(orgId, spaceId)}/collections/${opts.collectionId}` : space(orgId, spaceId);
|
|
213
|
+
await request('PATCH', at, { body: { translations: Object.keys(current).length ? current : null } });
|
|
214
|
+
return current;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
/** A library entry's locale twin: its localized fields in one language. */
|
|
218
|
+
entryLocale: {
|
|
219
|
+
get: (orgId, libraryId, entryId, locale) => request('GET', `${library(orgId, libraryId)}/entries/${entryId}/locales`, { query: { locale } }),
|
|
220
|
+
/** Write the twin's DRAFT (made from the source when missing). */
|
|
221
|
+
put: (orgId, libraryId, entryId, locale, fields) => request('PUT', `${library(orgId, libraryId)}/entries/${entryId}/locales/${enc(locale)}`, { body: { fields } }),
|
|
222
|
+
},
|
|
223
|
+
/** The site/space chrome dictionary: every owner-written word, per language. */
|
|
224
|
+
labels: {
|
|
225
|
+
strings: (orgId, scope) => request('GET', `${labelsAt(orgId, scope)}/strings`),
|
|
226
|
+
/** Replace the whole dictionary. */
|
|
227
|
+
replace: (orgId, scope, labels) => request('PUT', `${labelsAt(orgId, scope)}/labels`, { body: { labels } }),
|
|
228
|
+
/** Merge word by word; `null` removes a word or a language. */
|
|
229
|
+
merge: (orgId, scope, labels) => request('PATCH', `${labelsAt(orgId, scope)}/labels`, { body: { labels } }),
|
|
230
|
+
},
|
|
231
|
+
/** The organization's own wording for our interface strings, per locale. */
|
|
232
|
+
overrides: {
|
|
233
|
+
list: (orgId, locale) => request('GET', `/orgs/${orgId}/translation-overrides`, { query: { locale } }),
|
|
234
|
+
set: (orgId, input) => request('PUT', `/orgs/${orgId}/translation-overrides`, { body: input }),
|
|
235
|
+
remove: (orgId, input) => request('DELETE', `/orgs/${orgId}/translation-overrides`, { query: input }),
|
|
236
|
+
/** Every key a reader page can show and the English it replaces (public). */
|
|
237
|
+
catalog: (orgId) => request('GET', `/public/orgs/${orgId}/i18n/catalog`),
|
|
238
|
+
/** The overrides readers in one locale are served (public). */
|
|
239
|
+
forLocale: (orgId, locale) => request('GET', `/public/orgs/${orgId}/i18n/${enc(locale)}`),
|
|
240
|
+
},
|
|
241
|
+
/** The org-wide default for the reader's language switcher. */
|
|
242
|
+
switcher: {
|
|
243
|
+
get: (orgId) => request('GET', `/orgs/${orgId}/language-switcher`),
|
|
244
|
+
set: (orgId, config) => request('PUT', `/orgs/${orgId}/language-switcher`, { body: config }),
|
|
245
|
+
},
|
|
246
|
+
/** The org's own DeepL key — what every `auto` uses. */
|
|
247
|
+
deepl: {
|
|
248
|
+
status: (orgId) => request('GET', `/orgs/${orgId}/deepl`),
|
|
249
|
+
connect: (orgId, apiKey) => request('PUT', `/orgs/${orgId}/deepl`, { body: { apiKey } }),
|
|
250
|
+
disconnect: (orgId) => request('DELETE', `/orgs/${orgId}/deepl`),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
return api;
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=translations.js.map
|
package/dist/typed.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ type ParamsPart<Op> = Op extends {
|
|
|
49
49
|
* property adds, so `HasKeys` sees the object rather than a union with
|
|
50
50
|
* `undefined` (which would otherwise always look like it has keys).
|
|
51
51
|
*/
|
|
52
|
-
type QueryPart<Op> = Op extends {
|
|
52
|
+
export type QueryPart<Op> = Op extends {
|
|
53
53
|
parameters: {
|
|
54
54
|
query: infer Q;
|
|
55
55
|
};
|
|
@@ -93,7 +93,7 @@ type BodyPart<Op> = Op extends {
|
|
|
93
93
|
body?: undefined;
|
|
94
94
|
};
|
|
95
95
|
/** Flatten an intersection into a single object literal for readable hovers. */
|
|
96
|
-
type Simplify<T> = {
|
|
96
|
+
export type Simplify<T> = {
|
|
97
97
|
[K in keyof T]: T[K];
|
|
98
98
|
} & {};
|
|
99
99
|
/**
|
package/dist/webhooks.d.ts
CHANGED
|
@@ -75,4 +75,12 @@ export interface VerifyWebhookInput {
|
|
|
75
75
|
now?: () => number;
|
|
76
76
|
}
|
|
77
77
|
export declare function verifyWebhookSignature(input: VerifyWebhookInput): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Verify a delivery, then parse it — the one call a webhook handler needs.
|
|
80
|
+
*
|
|
81
|
+
* Throws instead of returning `false` so the parse can never run ahead of the
|
|
82
|
+
* check: a handler that did `JSON.parse(body)` first and verified second has
|
|
83
|
+
* already acted on whatever it was sent.
|
|
84
|
+
*/
|
|
85
|
+
export declare function constructWebhookEvent<T = Record<string, unknown>>(input: VerifyWebhookInput): Promise<T>;
|
|
78
86
|
//# sourceMappingURL=webhooks.d.ts.map
|
package/dist/webhooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
4
|
+
exports.constructWebhookEvent = constructWebhookEvent;
|
|
4
5
|
async function verifyWebhookSignature(input) {
|
|
5
6
|
const { secret, signature, acceptLegacy = false, toleranceSeconds = 300 } = input;
|
|
6
7
|
if (!secret || !signature)
|
|
@@ -37,6 +38,20 @@ async function verifyWebhookSignature(input) {
|
|
|
37
38
|
}
|
|
38
39
|
return false;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Verify a delivery, then parse it — the one call a webhook handler needs.
|
|
43
|
+
*
|
|
44
|
+
* Throws instead of returning `false` so the parse can never run ahead of the
|
|
45
|
+
* check: a handler that did `JSON.parse(body)` first and verified second has
|
|
46
|
+
* already acted on whatever it was sent.
|
|
47
|
+
*/
|
|
48
|
+
async function constructWebhookEvent(input) {
|
|
49
|
+
if (!(await verifyWebhookSignature(input))) {
|
|
50
|
+
throw new Error('Forumulate webhook signature did not verify (wrong secret, altered body, or too old)');
|
|
51
|
+
}
|
|
52
|
+
const body = typeof input.body === 'string' ? input.body : new TextDecoder().decode(input.body);
|
|
53
|
+
return JSON.parse(body);
|
|
54
|
+
}
|
|
40
55
|
/** The substring of `haystack` at `needle`'s position, for a constant-time compare. */
|
|
41
56
|
function extractMatch(haystack, needle) {
|
|
42
57
|
const at = haystack.indexOf(needle);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forumulate/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Typed TypeScript client for the Forumulate REST API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://forumulate.com/reference",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"main": "dist/index.js",
|
|
22
22
|
"types": "dist/index.d.ts",
|
|
23
|
+
"sideEffects": false,
|
|
23
24
|
"files": [
|
|
24
25
|
"dist",
|
|
25
26
|
"!dist/**/*.map",
|
|
@@ -45,9 +46,12 @@
|
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"build": "tsc -p tsconfig.json",
|
|
48
|
-
"typecheck": "
|
|
49
|
+
"typecheck": "node ../../scripts/tsgo.mjs -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsgo-src.tsbuildinfo && node ../../scripts/tsgo.mjs -p tsconfig.test.json --incremental --tsBuildInfoFile node_modules/.cache/tsgo-test.tsbuildinfo",
|
|
50
|
+
"typecheck:tsc": "tsc -p tsconfig.json --noEmit --incremental --tsBuildInfoFile node_modules/.cache/tsc-src.tsbuildinfo && tsc -p tsconfig.test.json --incremental --tsBuildInfoFile node_modules/.cache/tsc-test.tsbuildinfo",
|
|
49
51
|
"test": "vitest run",
|
|
50
|
-
"dump-spec": "
|
|
51
|
-
"generate-schema": "npx -y openapi-typescript@7 openapi.json -o src/schema.ts"
|
|
52
|
+
"dump-spec": "pnpm --filter @forumulate/api run dump-spec",
|
|
53
|
+
"generate-schema": "npx -y openapi-typescript@7 openapi.json -o src/schema.ts && node scripts/generate-operations.mjs",
|
|
54
|
+
"generate-operations": "node scripts/generate-operations.mjs",
|
|
55
|
+
"spec:refresh": "node ../../scripts/spec-refresh.mjs"
|
|
52
56
|
}
|
|
53
57
|
}
|