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

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