@mx-space/byline-api-client 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,573 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/case.ts
3
+ const isPlainObject = (value) => Object.prototype.toString.call(value) === "[object Object]";
4
+ const camelcase = (key) => key.replaceAll(/_([\da-z])/gi, (_, char) => char.toUpperCase());
5
+ const splitPath = (path) => path.replaceAll("[]", ".[]").split(".").filter((segment) => segment.length > 0);
6
+ const buildTree = (paths) => {
7
+ const root = /* @__PURE__ */ new Map();
8
+ for (const path of paths) {
9
+ const segments = splitPath(path);
10
+ let node = root;
11
+ segments.forEach((segment, index) => {
12
+ if (index === segments.length - 1) {
13
+ node.set(segment, true);
14
+ return;
15
+ }
16
+ const next = node.get(segment);
17
+ if (next === true) return;
18
+ const child = next ?? /* @__PURE__ */ new Map();
19
+ node.set(segment, child);
20
+ node = child;
21
+ });
22
+ }
23
+ return root;
24
+ };
25
+ const walk = (value, tree) => {
26
+ if (Array.isArray(value)) {
27
+ const element = tree?.get("[]");
28
+ if (element === true) return value;
29
+ return value.map((item) => walk(item, element));
30
+ }
31
+ if (!isPlainObject(value)) return value;
32
+ const out = {};
33
+ for (const [key, item] of Object.entries(value)) {
34
+ const nextKey = camelcase(key);
35
+ const branch = tree?.get(nextKey);
36
+ out[nextKey] = branch === true ? item : walk(item, branch);
37
+ }
38
+ return out;
39
+ };
40
+ const camelcaseKeys = (value, preservePaths = []) => walk(value, buildTree(preservePaths));
41
+ //#endregion
42
+ //#region src/error.ts
43
+ var BylineApiError = class extends Error {
44
+ status;
45
+ code;
46
+ path;
47
+ details;
48
+ name = "BylineApiError";
49
+ constructor(status, code, message, path, details) {
50
+ super(message);
51
+ this.status = status;
52
+ this.code = code;
53
+ this.path = path;
54
+ this.details = details;
55
+ }
56
+ };
57
+ const isBylineApiError = (error, code) => error instanceof BylineApiError && (code === void 0 || error.code === code);
58
+ //#endregion
59
+ //#region src/http.ts
60
+ const ENVELOPE_KEYS = /* @__PURE__ */ new Set(["data", "meta"]);
61
+ const isEnvelope = (body) => !!body && typeof body === "object" && !Array.isArray(body) && "data" in body && Object.keys(body).every((key) => ENVELOPE_KEYS.has(key));
62
+ const isBinaryBody = (body) => typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams || typeof body === "string";
63
+ var HttpCore = class {
64
+ options;
65
+ endpoint;
66
+ fetchImpl;
67
+ constructor(options) {
68
+ this.options = options;
69
+ if (!options.endpoint) throw new TypeError("endpoint is required");
70
+ this.endpoint = options.endpoint.replace(/\/+$/, "");
71
+ const fetchImpl = options.fetch ?? globalThis.fetch;
72
+ if (!fetchImpl) throw new TypeError("No fetch implementation available");
73
+ this.fetchImpl = fetchImpl;
74
+ }
75
+ get lang() {
76
+ return this.options.lang;
77
+ }
78
+ buildUrl(path, query) {
79
+ const url = new URL(`${this.endpoint}/${path.replace(/^\/+/, "")}`);
80
+ for (const [key, value] of Object.entries(query ?? {})) {
81
+ if (value === void 0 || value === null) continue;
82
+ url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
83
+ }
84
+ return url.toString();
85
+ }
86
+ async request(method, path, options = {}) {
87
+ const headers = {
88
+ accept: "application/json",
89
+ ...this.options.headers,
90
+ ...options.headers
91
+ };
92
+ if (this.options.apiKey) headers["x-api-key"] = this.options.apiKey;
93
+ if (this.options.token) headers.authorization = `Bearer ${this.options.token}`;
94
+ let body;
95
+ if (options.body !== void 0) {
96
+ if (isBinaryBody(options.body)) body = options.body;
97
+ else {
98
+ headers["content-type"] = "application/json";
99
+ body = JSON.stringify(options.body);
100
+ }
101
+ }
102
+ const url = this.buildUrl(path, options.query);
103
+ const response = await this.fetchImpl(url, {
104
+ method,
105
+ headers,
106
+ body,
107
+ signal: options.signal
108
+ });
109
+ if (response.status === 204) return { data: void 0 };
110
+ const payload = (response.headers.get("content-type") ?? "").includes("json") ? await response.json().catch(() => null) : await response.text();
111
+ if (!response.ok) throw this.toError(response.status, path, payload);
112
+ if (!isEnvelope(payload)) return { data: this.transform(payload, options.preserve) };
113
+ return {
114
+ data: this.transform(payload.data, options.preserve),
115
+ ...payload.meta === void 0 ? {} : { meta: this.transform(payload.meta) }
116
+ };
117
+ }
118
+ transform(value, preserve) {
119
+ if (this.options.camelCase === false) return value;
120
+ return camelcaseKeys(value, preserve);
121
+ }
122
+ toError(status, path, payload) {
123
+ const error = payload && typeof payload === "object" && "error" in payload ? payload.error : void 0;
124
+ if (error && typeof error === "object") return new BylineApiError(status, String(error.code ?? "HTTP_ERROR"), String(error.message ?? `Request failed with status ${status}`), path, error.details === void 0 ? void 0 : camelcaseKeys(error.details));
125
+ return new BylineApiError(status, "HTTP_ERROR", typeof payload === "string" && payload ? payload.slice(0, 500) : `Request failed with status ${status}`, path);
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region src/resources/authors.ts
130
+ var AuthorsResource = class {
131
+ http;
132
+ constructor(http) {
133
+ this.http = http;
134
+ }
135
+ list(query = {}) {
136
+ return this.http.request("GET", "/authors", { query: { ...query } });
137
+ }
138
+ async listAll() {
139
+ const authors = [];
140
+ for (let page = 1;; page++) {
141
+ const { data, meta } = await this.list({
142
+ page,
143
+ size: 100
144
+ });
145
+ authors.push(...data);
146
+ if (!meta || page >= meta.pagination.totalPages || !data.length) break;
147
+ }
148
+ return authors;
149
+ }
150
+ get(idOrUsername) {
151
+ return this.http.request("GET", `/authors/${encodeURIComponent(idOrUsername)}`);
152
+ }
153
+ };
154
+ //#endregion
155
+ //#region src/resources/content.ts
156
+ const CATALOG_PRESERVE = ["components[].propsSchema"];
157
+ var CategoriesResource = class {
158
+ http;
159
+ constructor(http) {
160
+ this.http = http;
161
+ }
162
+ list(options = {}) {
163
+ return this.http.request("GET", "/categories", { query: { lang: options.lang ?? this.http.lang } });
164
+ }
165
+ get(slugOrId, options = {}) {
166
+ return this.http.request("GET", `/categories/${encodeURIComponent(slugOrId)}`, { query: { lang: options.lang ?? this.http.lang } });
167
+ }
168
+ create(input) {
169
+ return this.http.request("POST", "/categories", { body: input });
170
+ }
171
+ update(id, input) {
172
+ return this.http.request("PATCH", `/categories/${id}`, { body: input });
173
+ }
174
+ delete(id) {
175
+ return this.http.request("DELETE", `/categories/${id}`);
176
+ }
177
+ };
178
+ var TranslationsResource = class {
179
+ http;
180
+ constructor(http) {
181
+ this.http = http;
182
+ }
183
+ getForArticle(articleId, lang) {
184
+ return this.http.request("GET", `/ai/translations/article/${articleId}`, { query: { lang } });
185
+ }
186
+ languages(articleId) {
187
+ return this.http.request("GET", `/ai/translations/article/${articleId}/languages`);
188
+ }
189
+ listByRef(articleId) {
190
+ return this.http.request("GET", `/ai/translations/ref/${articleId}`, { preserve: ["article.document.meta"] });
191
+ }
192
+ writeManual(articleId, lang, input) {
193
+ return this.http.request("PUT", `/ai/translations/article/${articleId}/${encodeURIComponent(lang)}`, { body: {
194
+ ...input,
195
+ content: typeof input.content === "string" ? input.content : JSON.stringify(input.content)
196
+ } });
197
+ }
198
+ update(id, patch) {
199
+ return this.http.request("PATCH", `/ai/translations/${id}`, { body: patch });
200
+ }
201
+ delete(id) {
202
+ return this.http.request("DELETE", `/ai/translations/${id}`);
203
+ }
204
+ createTask(input) {
205
+ return this.http.request("POST", "/ai/translations/task", { body: input });
206
+ }
207
+ };
208
+ var DynamicComponentsResource = class {
209
+ http;
210
+ constructor(http) {
211
+ this.http = http;
212
+ }
213
+ list() {
214
+ return this.http.request("GET", "/dynamic-components", { preserve: CATALOG_PRESERVE });
215
+ }
216
+ register(input) {
217
+ return this.http.request("POST", "/dynamic-components", {
218
+ body: input,
219
+ preserve: CATALOG_PRESERVE
220
+ });
221
+ }
222
+ };
223
+ var FilesResource = class {
224
+ http;
225
+ constructor(http) {
226
+ this.http = http;
227
+ }
228
+ upload(file, options = {}) {
229
+ const form = new FormData();
230
+ const filename = options.filename ?? (typeof File !== "undefined" && file instanceof File ? file.name : "upload.bin");
231
+ form.append("file", file, filename);
232
+ return this.http.request("POST", "/files/upload", {
233
+ query: {
234
+ type: options.type,
235
+ immutable: options.immutable ? "true" : void 0
236
+ },
237
+ body: form
238
+ });
239
+ }
240
+ };
241
+ //#endregion
242
+ //#region src/resources/drafts.ts
243
+ var DraftsResource = class {
244
+ http;
245
+ constructor(http) {
246
+ this.http = http;
247
+ }
248
+ list(query = {}) {
249
+ return this.http.request("GET", "/drafts", { query });
250
+ }
251
+ get(id) {
252
+ return this.http.request("GET", `/drafts/${id}`);
253
+ }
254
+ context(refType, refId) {
255
+ return this.http.request("GET", `/drafts/context/${refType}/${refId}`);
256
+ }
257
+ create(input) {
258
+ return this.http.request("POST", "/drafts", { body: input });
259
+ }
260
+ update(id, input) {
261
+ return this.http.request("PUT", `/drafts/${id}`, { body: input });
262
+ }
263
+ delete(id) {
264
+ return this.http.request("DELETE", `/drafts/${id}`);
265
+ }
266
+ };
267
+ //#endregion
268
+ //#region src/resources/posts.ts
269
+ const POST_PRESERVE = ["meta"];
270
+ const POST_LIST_PRESERVE = ["[].meta"];
271
+ const DRAFT_CORE_KEYS = /* @__PURE__ */ new Set([
272
+ "title",
273
+ "text",
274
+ "content",
275
+ "images",
276
+ "meta"
277
+ ]);
278
+ const toDraftData = (input) => {
279
+ const data = { contentFormat: "lexical" };
280
+ const typeSpecificData = {};
281
+ for (const [key, value] of Object.entries(input)) {
282
+ if (value === void 0) continue;
283
+ if (key === "content") data.content = typeof value === "string" ? value : JSON.stringify(value);
284
+ else if (DRAFT_CORE_KEYS.has(key)) data[key] = value;
285
+ else typeSpecificData[key] = value;
286
+ }
287
+ if (data.content !== void 0 && data.text === void 0) data.text = "";
288
+ if (Object.keys(typeSpecificData).length > 0) data.typeSpecificData = typeSpecificData;
289
+ return data;
290
+ };
291
+ const mergeSnapshot = (base, patch) => ({
292
+ title: base.title,
293
+ text: base.text,
294
+ content: base.content ?? void 0,
295
+ images: base.images,
296
+ meta: base.meta,
297
+ ...patch,
298
+ contentFormat: "lexical",
299
+ typeSpecificData: {
300
+ ...base.typeSpecificData,
301
+ ...patch.typeSpecificData
302
+ }
303
+ });
304
+ var PostsResource = class {
305
+ http;
306
+ drafts;
307
+ publishJobs;
308
+ constructor(http, drafts, publishJobs) {
309
+ this.http = http;
310
+ this.drafts = drafts;
311
+ this.publishJobs = publishJobs;
312
+ }
313
+ list(query = {}) {
314
+ return this.http.request("GET", "/posts", {
315
+ query: {
316
+ ...query,
317
+ lang: query.lang ?? this.http.lang
318
+ },
319
+ preserve: POST_LIST_PRESERVE
320
+ });
321
+ }
322
+ get(id, options = {}) {
323
+ return this.http.request("GET", `/posts/${id}`, {
324
+ query: { lang: options.lang ?? this.http.lang },
325
+ preserve: POST_PRESERVE
326
+ });
327
+ }
328
+ getBySlug(categorySlug, slug, options = {}) {
329
+ return this.http.request("GET", `/posts/${encodeURIComponent(categorySlug)}/${encodeURIComponent(slug)}`, {
330
+ query: { lang: options.lang ?? this.http.lang },
331
+ preserve: POST_PRESERVE
332
+ });
333
+ }
334
+ getPath(slug) {
335
+ return this.http.request("GET", `/posts/get-url/${encodeURIComponent(slug)}`);
336
+ }
337
+ async findBySlug(slug, options = {}) {
338
+ try {
339
+ const { data } = await this.getPath(slug);
340
+ const [category, postSlug] = data.path.replace(/^\/+/, "").split("/");
341
+ if (!category || !postSlug) return null;
342
+ return await this.getBySlug(category, decodeURIComponent(postSlug), options);
343
+ } catch (error) {
344
+ if (isBylineApiError(error) && error.status === 404) return null;
345
+ throw error;
346
+ }
347
+ }
348
+ latest(options = {}) {
349
+ return this.http.request("GET", "/posts/latest", {
350
+ query: { lang: options.lang ?? this.http.lang },
351
+ preserve: POST_PRESERVE
352
+ });
353
+ }
354
+ update(id, patch) {
355
+ return this.http.request("PATCH", `/posts/${id}`, { body: patch });
356
+ }
357
+ setPublished(id, isPublished) {
358
+ return this.http.request("PATCH", `/posts/${id}/publish`, { body: { isPublished } });
359
+ }
360
+ delete(id) {
361
+ return this.http.request("DELETE", `/posts/${id}`);
362
+ }
363
+ async create(input, options = {}) {
364
+ const { data: draft } = await this.drafts.create({
365
+ refType: "post",
366
+ data: toDraftData(input)
367
+ });
368
+ return this.finish(draft, options);
369
+ }
370
+ async revise(id, input, options = {}) {
371
+ const { data: context } = await this.drafts.context("post", id);
372
+ const base = context.publishedRevision;
373
+ if (!base) throw new BylineApiError(404, "CONTENT_REVISION_NOT_FOUND", `Post ${id} has no published revision to branch from`, `/drafts/context/post/${id}`);
374
+ const { data: draft } = await this.drafts.create({
375
+ refType: "post",
376
+ refId: id,
377
+ baseRevisionId: base.id,
378
+ data: mergeSnapshot(base, toDraftData(input))
379
+ });
380
+ return this.finish(draft, options);
381
+ }
382
+ async finish(draft, options) {
383
+ if (options.publish === false) return {
384
+ draft,
385
+ postId: draft.document.refId,
386
+ taskId: null
387
+ };
388
+ const task = await this.publishJobs.run({
389
+ branchId: draft.id,
390
+ revisionId: draft.headRevisionId,
391
+ expectedPublishedRevisionId: draft.document.publishedRevisionId,
392
+ confirmDiverged: draft.relationToPublished === "descendant" || draft.relationToPublished === "diverged",
393
+ aiResources: options.aiResources,
394
+ regenerateLockedTranslations: options.regenerateLockedTranslations
395
+ }, options);
396
+ return {
397
+ draft,
398
+ postId: task.result.articleId,
399
+ taskId: task.id
400
+ };
401
+ }
402
+ };
403
+ //#endregion
404
+ //#region src/resources/tasks.ts
405
+ const TERMINAL = /* @__PURE__ */ new Set([
406
+ "completed",
407
+ "partial_failed",
408
+ "failed",
409
+ "cancelled"
410
+ ]);
411
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
412
+ const timer = setTimeout(resolve, ms);
413
+ signal?.addEventListener("abort", () => {
414
+ clearTimeout(timer);
415
+ reject(signal.reason);
416
+ }, { once: true });
417
+ });
418
+ var TasksResource = class {
419
+ http;
420
+ constructor(http) {
421
+ this.http = http;
422
+ }
423
+ get(id) {
424
+ return this.http.request("GET", `/tasks/${id}`);
425
+ }
426
+ cancel(id) {
427
+ return this.http.request("POST", `/tasks/${id}/cancel`);
428
+ }
429
+ async wait(id, options = {}) {
430
+ const intervalMs = options.intervalMs ?? 500;
431
+ const deadline = Date.now() + (options.timeoutMs ?? 12e4);
432
+ for (;;) {
433
+ const { data } = await this.get(id);
434
+ if (TERMINAL.has(data.status)) return data;
435
+ if (Date.now() > deadline) throw new BylineApiError(408, "TASK_TIMEOUT", `Task ${id} did not finish in time (last status: ${data.status})`, `/tasks/${id}`);
436
+ await sleep(intervalMs, options.signal);
437
+ }
438
+ }
439
+ };
440
+ var PublishJobsResource = class {
441
+ http;
442
+ tasks;
443
+ constructor(http, tasks) {
444
+ this.http = http;
445
+ this.tasks = tasks;
446
+ }
447
+ create(input) {
448
+ return this.http.request("POST", "/publish-jobs", { body: input });
449
+ }
450
+ async run(input, options) {
451
+ const { data: ref } = await this.create(input);
452
+ const task = await this.tasks.wait(ref.taskId, options);
453
+ if (task.status !== "completed" || !task.result?.articleId) throw new BylineApiError(500, "PUBLISH_FAILED", `Publish job ${ref.taskId} ended with ${task.status}${task.error ? `: ${task.error}` : ""}`, "/publish-jobs", { task });
454
+ return task;
455
+ }
456
+ };
457
+ //#endregion
458
+ //#region src/resources/users.ts
459
+ var UsersResource = class {
460
+ http;
461
+ constructor(http) {
462
+ this.http = http;
463
+ }
464
+ allowLogin() {
465
+ return this.http.request("GET", "/users/allow-login");
466
+ }
467
+ checkLogged() {
468
+ return this.http.request("GET", "/users/check-logged");
469
+ }
470
+ me() {
471
+ return this.http.request("GET", "/users/me");
472
+ }
473
+ updateMe(input) {
474
+ return this.http.request("PATCH", "/users/me", { body: input });
475
+ }
476
+ list(query = {}) {
477
+ return this.http.request("GET", "/users", { query: { ...query } });
478
+ }
479
+ async listAll(query = {}) {
480
+ const users = [];
481
+ for (let page = 1;; page++) {
482
+ const { data, meta } = await this.list({
483
+ ...query,
484
+ page,
485
+ size: 100
486
+ });
487
+ users.push(...data);
488
+ if (!meta || page >= meta.pagination.totalPages || !data.length) break;
489
+ }
490
+ return users;
491
+ }
492
+ get(id) {
493
+ return this.http.request("GET", `/users/${id}`);
494
+ }
495
+ update(id, input) {
496
+ return this.http.request("PATCH", `/users/${id}`, { body: input });
497
+ }
498
+ createProfileOnly(input) {
499
+ return this.http.request("POST", "/users/profile-only", { body: input });
500
+ }
501
+ deactivate(id) {
502
+ return this.http.request("POST", `/users/${id}/deactivate`);
503
+ }
504
+ reactivate(id) {
505
+ return this.http.request("POST", `/users/${id}/reactivate`);
506
+ }
507
+ invite(id, input = {}) {
508
+ return this.http.request("POST", `/users/${id}/invite`, { body: input });
509
+ }
510
+ initFirstUser(input) {
511
+ return this.http.request("POST", "/init/user", { body: input });
512
+ }
513
+ };
514
+ var InvitesResource = class {
515
+ http;
516
+ constructor(http) {
517
+ this.http = http;
518
+ }
519
+ list() {
520
+ return this.http.request("GET", "/users/invites");
521
+ }
522
+ create(input) {
523
+ return this.http.request("POST", "/users/invites", { body: input });
524
+ }
525
+ revoke(id) {
526
+ return this.http.request("DELETE", `/users/invites/${id}`);
527
+ }
528
+ get(token) {
529
+ return this.http.request("GET", `/invites/${encodeURIComponent(token)}`);
530
+ }
531
+ accept(token, input) {
532
+ return this.http.request("POST", `/invites/${encodeURIComponent(token)}/accept`, { body: input });
533
+ }
534
+ };
535
+ //#endregion
536
+ //#region src/client.ts
537
+ var BylineClient = class {
538
+ http;
539
+ posts;
540
+ drafts;
541
+ publishJobs;
542
+ tasks;
543
+ categories;
544
+ translations;
545
+ users;
546
+ authors;
547
+ invites;
548
+ dynamicComponents;
549
+ files;
550
+ constructor(options) {
551
+ this.http = new HttpCore(options);
552
+ this.drafts = new DraftsResource(this.http);
553
+ this.tasks = new TasksResource(this.http);
554
+ this.publishJobs = new PublishJobsResource(this.http, this.tasks);
555
+ this.posts = new PostsResource(this.http, this.drafts, this.publishJobs);
556
+ this.categories = new CategoriesResource(this.http);
557
+ this.translations = new TranslationsResource(this.http);
558
+ this.users = new UsersResource(this.http);
559
+ this.authors = new AuthorsResource(this.http);
560
+ this.invites = new InvitesResource(this.http);
561
+ this.dynamicComponents = new DynamicComponentsResource(this.http);
562
+ this.files = new FilesResource(this.http);
563
+ }
564
+ };
565
+ const createClient = (options) => new BylineClient(options);
566
+ //#endregion
567
+ exports.BylineApiError = BylineApiError;
568
+ exports.BylineClient = BylineClient;
569
+ exports.HttpCore = HttpCore;
570
+ exports.camelcaseKeys = camelcaseKeys;
571
+ exports.createClient = createClient;
572
+ exports.isBylineApiError = isBylineApiError;
573
+ exports.toDraftData = toDraftData;