busabase-cms-sdk 0.1.3

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.
@@ -0,0 +1,1175 @@
1
+ import { z } from "zod";
2
+ import { Busabase } from "busabase-sdk";
3
+ //#region src/errors.ts
4
+ var BusabaseCmsError = class extends Error {
5
+ constructor(message, options) {
6
+ super(message, options);
7
+ this.name = "BusabaseCmsError";
8
+ }
9
+ };
10
+ var BusabaseCmsSetupError = class extends BusabaseCmsError {
11
+ constructor(message, options) {
12
+ super(message, options);
13
+ this.name = "BusabaseCmsSetupError";
14
+ }
15
+ };
16
+ var BusabaseCmsSchemaDriftError = class extends BusabaseCmsSetupError {
17
+ constructor(message, options) {
18
+ super(message, options);
19
+ this.name = "BusabaseCmsSchemaDriftError";
20
+ }
21
+ };
22
+ //#endregion
23
+ //#region src/schema.ts
24
+ const BUSABASE_CMS_SCHEMA_VERSION = 1;
25
+ const BUSABASE_CMS_METADATA_KEY = "busabaseCms";
26
+ const BUSABASE_CMS_ROLES = [
27
+ "categories",
28
+ "tags",
29
+ "posts",
30
+ "pages"
31
+ ];
32
+ /** The only profile the SDK itself ships a field shape for. Any other non-empty string is a
33
+ * caller-owned profile label (see `fieldsOverride`/`legacyOptionalFields` on the provisioning
34
+ * options) — the SDK stores and round-trips it but never branches on its value. */
35
+ const BUSABASE_CMS_SCHEMA_PROFILES = ["standard"];
36
+ const i18nName = (en, zhCN) => ({
37
+ en,
38
+ "zh-CN": zhCN
39
+ });
40
+ const selectOptions = (values) => ({ choices: values.map(([id, en, zhCN]) => ({
41
+ id,
42
+ name: `${en} / ${zhCN}`
43
+ })) });
44
+ const localeOptions = selectOptions([
45
+ [
46
+ "en",
47
+ "English",
48
+ "英文"
49
+ ],
50
+ [
51
+ "zh-CN",
52
+ "Simplified Chinese",
53
+ "简体中文"
54
+ ],
55
+ [
56
+ "zh-TW",
57
+ "Traditional Chinese",
58
+ "繁體中文"
59
+ ],
60
+ [
61
+ "ja",
62
+ "Japanese",
63
+ "日文"
64
+ ],
65
+ [
66
+ "pt",
67
+ "Portuguese",
68
+ "葡萄牙文"
69
+ ]
70
+ ]);
71
+ const statusOptions = selectOptions([
72
+ [
73
+ "draft",
74
+ "Draft",
75
+ "草稿"
76
+ ],
77
+ [
78
+ "in-review",
79
+ "In review",
80
+ "审核中"
81
+ ],
82
+ [
83
+ "published",
84
+ "Published",
85
+ "已发布"
86
+ ],
87
+ [
88
+ "archived",
89
+ "Archived",
90
+ "已归档"
91
+ ]
92
+ ]);
93
+ const taxonomyFields = () => [
94
+ {
95
+ slug: "name",
96
+ name: i18nName("Name", "名称"),
97
+ type: "text",
98
+ required: true,
99
+ options: {}
100
+ },
101
+ {
102
+ slug: "slug",
103
+ name: i18nName("Slug", "标识"),
104
+ type: "text",
105
+ required: true,
106
+ options: {}
107
+ },
108
+ {
109
+ slug: "locale",
110
+ name: i18nName("Locale", "语言"),
111
+ type: "select",
112
+ required: true,
113
+ options: localeOptions
114
+ },
115
+ {
116
+ slug: "description",
117
+ name: i18nName("Description", "描述"),
118
+ type: "longtext",
119
+ required: false,
120
+ options: {}
121
+ },
122
+ {
123
+ slug: "updated-at",
124
+ name: i18nName("Updated at", "更新时间"),
125
+ type: "updated_time",
126
+ required: false,
127
+ options: {}
128
+ }
129
+ ];
130
+ const postFields = () => [
131
+ [
132
+ "path",
133
+ "Path",
134
+ "网址",
135
+ "text",
136
+ true
137
+ ],
138
+ [
139
+ "title",
140
+ "Title",
141
+ "标题",
142
+ "text",
143
+ true
144
+ ],
145
+ [
146
+ "slug",
147
+ "Slug",
148
+ "标识",
149
+ "text",
150
+ true
151
+ ]
152
+ ].map(([slug, en, zhCN, type, required]) => ({
153
+ slug,
154
+ name: i18nName(en, zhCN),
155
+ type,
156
+ required,
157
+ options: {}
158
+ }));
159
+ const buildPostFields = (baseIds) => [
160
+ ...postFields(),
161
+ {
162
+ slug: "locale",
163
+ name: i18nName("Locale", "语言"),
164
+ type: "select",
165
+ required: true,
166
+ options: localeOptions
167
+ },
168
+ {
169
+ slug: "status",
170
+ name: i18nName("Status", "状态"),
171
+ type: "select",
172
+ required: true,
173
+ options: statusOptions
174
+ },
175
+ {
176
+ slug: "description",
177
+ name: i18nName("Excerpt", "摘要"),
178
+ type: "longtext",
179
+ required: false,
180
+ options: {}
181
+ },
182
+ {
183
+ slug: "body",
184
+ name: i18nName("Body", "正文"),
185
+ type: "markdown",
186
+ required: true,
187
+ options: {}
188
+ },
189
+ {
190
+ slug: "cover-image",
191
+ name: i18nName("Cover image", "封面图片"),
192
+ type: "attachment",
193
+ required: false,
194
+ options: { attachment: {
195
+ maxFiles: 1,
196
+ allowedMimeTypes: ["image/*"],
197
+ maxFileSize: 10485760
198
+ } }
199
+ },
200
+ {
201
+ slug: "attachments",
202
+ name: i18nName("Attachments", "附件"),
203
+ type: "attachment",
204
+ required: false,
205
+ options: { attachment: {
206
+ maxFiles: 20,
207
+ allowedMimeTypes: ["image/*", "application/pdf"],
208
+ maxFileSize: 20971520
209
+ } }
210
+ },
211
+ {
212
+ slug: "author",
213
+ name: i18nName("Author", "作者"),
214
+ type: "text",
215
+ required: false,
216
+ options: {}
217
+ },
218
+ {
219
+ slug: "categories",
220
+ name: i18nName("Categories", "分类"),
221
+ type: "relation",
222
+ required: false,
223
+ options: {
224
+ ...baseIds.categories ? { targetBaseId: baseIds.categories } : {},
225
+ multiple: true
226
+ }
227
+ },
228
+ {
229
+ slug: "tags",
230
+ name: i18nName("Tags", "标签"),
231
+ type: "relation",
232
+ required: false,
233
+ options: {
234
+ ...baseIds.tags ? { targetBaseId: baseIds.tags } : {},
235
+ multiple: true
236
+ }
237
+ },
238
+ {
239
+ slug: "published-at",
240
+ name: i18nName("Published at", "发布时间"),
241
+ type: "date",
242
+ required: false,
243
+ options: {}
244
+ },
245
+ {
246
+ slug: "canonical-url",
247
+ name: i18nName("Canonical URL", "规范网址"),
248
+ type: "url",
249
+ required: false,
250
+ options: {}
251
+ },
252
+ {
253
+ slug: "legacy-paths",
254
+ name: i18nName("Legacy paths", "旧网址"),
255
+ type: "json",
256
+ required: false,
257
+ options: {}
258
+ },
259
+ {
260
+ slug: "seo-title",
261
+ name: i18nName("SEO title", "SEO 标题"),
262
+ type: "text",
263
+ required: false,
264
+ options: {}
265
+ },
266
+ {
267
+ slug: "seo-description",
268
+ name: i18nName("SEO description", "SEO 描述"),
269
+ type: "longtext",
270
+ required: false,
271
+ options: {}
272
+ },
273
+ {
274
+ slug: "schema-version",
275
+ name: i18nName("Schema version", "结构版本"),
276
+ type: "number",
277
+ required: true,
278
+ options: {}
279
+ },
280
+ {
281
+ slug: "updated-at",
282
+ name: i18nName("Updated at", "更新时间"),
283
+ type: "updated_time",
284
+ required: false,
285
+ options: {}
286
+ }
287
+ ];
288
+ const pageFields = () => [
289
+ {
290
+ slug: "path",
291
+ name: i18nName("Path", "网址"),
292
+ type: "text",
293
+ required: true,
294
+ options: {}
295
+ },
296
+ {
297
+ slug: "title",
298
+ name: i18nName("Title", "标题"),
299
+ type: "text",
300
+ required: true,
301
+ options: {}
302
+ },
303
+ {
304
+ slug: "slug",
305
+ name: i18nName("Slug", "标识"),
306
+ type: "text",
307
+ required: true,
308
+ options: {}
309
+ },
310
+ {
311
+ slug: "locale",
312
+ name: i18nName("Locale", "语言"),
313
+ type: "select",
314
+ required: true,
315
+ options: localeOptions
316
+ },
317
+ {
318
+ slug: "status",
319
+ name: i18nName("Status", "状态"),
320
+ type: "select",
321
+ required: true,
322
+ options: statusOptions
323
+ },
324
+ {
325
+ slug: "template",
326
+ name: i18nName("Template", "模板"),
327
+ type: "select",
328
+ required: true,
329
+ options: selectOptions([
330
+ [
331
+ "standard",
332
+ "Standard",
333
+ "标准"
334
+ ],
335
+ [
336
+ "landing",
337
+ "Landing",
338
+ "落地页"
339
+ ],
340
+ [
341
+ "product",
342
+ "Product",
343
+ "产品"
344
+ ],
345
+ [
346
+ "use-case",
347
+ "Use case",
348
+ "使用场景"
349
+ ]
350
+ ])
351
+ },
352
+ {
353
+ slug: "body",
354
+ name: i18nName("Body", "正文"),
355
+ type: "html",
356
+ required: true,
357
+ options: {}
358
+ },
359
+ {
360
+ slug: "hero",
361
+ name: i18nName("Hero", "首屏"),
362
+ type: "json",
363
+ required: false,
364
+ options: {}
365
+ },
366
+ {
367
+ slug: "features",
368
+ name: i18nName("Features", "功能"),
369
+ type: "json",
370
+ required: false,
371
+ options: {}
372
+ },
373
+ {
374
+ slug: "faqs",
375
+ name: i18nName("FAQs", "常见问题"),
376
+ type: "json",
377
+ required: false,
378
+ options: {}
379
+ },
380
+ {
381
+ slug: "canonical-url",
382
+ name: i18nName("Canonical URL", "规范网址"),
383
+ type: "url",
384
+ required: false,
385
+ options: {}
386
+ },
387
+ {
388
+ slug: "legacy-paths",
389
+ name: i18nName("Legacy paths", "旧网址"),
390
+ type: "json",
391
+ required: false,
392
+ options: {}
393
+ },
394
+ {
395
+ slug: "seo-title",
396
+ name: i18nName("SEO title", "SEO 标题"),
397
+ type: "text",
398
+ required: false,
399
+ options: {}
400
+ },
401
+ {
402
+ slug: "seo-description",
403
+ name: i18nName("SEO description", "SEO 描述"),
404
+ type: "longtext",
405
+ required: false,
406
+ options: {}
407
+ },
408
+ {
409
+ slug: "schema-version",
410
+ name: i18nName("Schema version", "结构版本"),
411
+ type: "number",
412
+ required: true,
413
+ options: {}
414
+ },
415
+ {
416
+ slug: "updated-at",
417
+ name: i18nName("Updated at", "更新时间"),
418
+ type: "updated_time",
419
+ required: false,
420
+ options: {}
421
+ }
422
+ ];
423
+ const replaceField = (fields, slug, replacement) => fields.map((field) => field.slug === slug ? replacement : field);
424
+ const getBusabaseCmsBaseDefinition = (role, baseIds = {}, fieldsOverride) => {
425
+ if (role === "categories" || role === "tags") return {
426
+ role,
427
+ name: role === "categories" ? "Categories / 分类" : "Tags / 标签",
428
+ description: role === "categories" ? "Reusable content categories / 可复用的内容分类" : "Reusable content tags / 可复用的内容标签",
429
+ fields: taxonomyFields()
430
+ };
431
+ if (role === "posts") {
432
+ const standard = buildPostFields(baseIds);
433
+ return {
434
+ role,
435
+ name: "Posts / 文章",
436
+ description: "Publishable Markdown posts / 可发布的 Markdown 文章",
437
+ fields: fieldsOverride ? fieldsOverride("posts", standard, baseIds) : standard
438
+ };
439
+ }
440
+ const standard = pageFields();
441
+ return {
442
+ role,
443
+ name: "Pages / 页面",
444
+ description: "Publishable HTML pages / 可发布的 HTML 页面",
445
+ fields: fieldsOverride ? fieldsOverride("pages", standard, baseIds) : standard
446
+ };
447
+ };
448
+ //#endregion
449
+ //#region src/provision.ts
450
+ const isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
451
+ const parseMetadata = (folder) => {
452
+ const raw = folder.metadata[BUSABASE_CMS_METADATA_KEY];
453
+ if (raw === void 0) return null;
454
+ if (!isRecord(raw) || raw.schemaVersion !== 1 || !isRecord(raw.bases)) throw new BusabaseCmsSchemaDriftError(`Folder "${folder.name}" has unsupported ${BUSABASE_CMS_METADATA_KEY} metadata`);
455
+ const rawBases = raw.bases;
456
+ const profile = raw.profile === void 0 ? "standard" : raw.profile;
457
+ if (typeof profile !== "string" || profile.length === 0) throw new BusabaseCmsSchemaDriftError(`Folder "${folder.name}" has unsupported Busabase CMS profile metadata`);
458
+ const bases = Object.fromEntries(BUSABASE_CMS_ROLES.map((role) => [role, rawBases[role]]));
459
+ for (const role of BUSABASE_CMS_ROLES) if (typeof bases[role] !== "string" || bases[role].length === 0) throw new BusabaseCmsSchemaDriftError(`Folder "${folder.name}" metadata is missing the ${role} Base ID`);
460
+ return {
461
+ schemaVersion: 1,
462
+ profile,
463
+ bases
464
+ };
465
+ };
466
+ const assertMetadataProfile = (folder, metadata, requested) => {
467
+ const stored = metadata.profile ?? "standard";
468
+ if (stored !== requested) throw new BusabaseCmsSchemaDriftError(`Folder "${folder.name}" is bound to the ${stored} CMS profile, not ${requested}`);
469
+ };
470
+ const requireProvisioningSource = (source) => {
471
+ if (!source.getBaseById || !source.getNode || !source.listDirectChildren || !source.updateNodeMetadata) throw new BusabaseCmsSetupError("folderId requires a Busabase client/config or a source with node, Base, and metadata discovery methods");
472
+ return source;
473
+ };
474
+ const requireLazyProvisioningSource = (source) => {
475
+ if (!source.createBase || !source.createField) throw new BusabaseCmsSetupError("lazyCreate requires direct Base and field creation methods from the Busabase SDK");
476
+ return source;
477
+ };
478
+ const getFolder = async (source, folderId) => {
479
+ const folder = await source.getNode(folderId);
480
+ if (!folder) throw new BusabaseCmsSetupError(`Busabase CMS Folder "${folderId}" was not found`);
481
+ if (folder.type !== "folder") throw new BusabaseCmsSetupError(`Busabase CMS node "${folderId}" is not a Folder`);
482
+ return folder;
483
+ };
484
+ const getDirectBaseNodes = async (source, folderId) => (await source.listDirectChildren(folderId)).filter((node) => node.parentId === folderId && node.type === "base" && Boolean(node.baseId));
485
+ const normalizeName = (value) => value.trim().toLocaleLowerCase();
486
+ const roleAliases = {
487
+ posts: [
488
+ "posts / 文章",
489
+ "posts",
490
+ "文章",
491
+ "blog posts",
492
+ "blog posts / 博客文章",
493
+ "博客文章"
494
+ ],
495
+ pages: [
496
+ "pages / 页面",
497
+ "pages",
498
+ "页面"
499
+ ],
500
+ categories: [
501
+ "categories / 分类",
502
+ "categories",
503
+ "分类"
504
+ ],
505
+ tags: [
506
+ "tags / 标签",
507
+ "tags",
508
+ "标签"
509
+ ]
510
+ };
511
+ const candidateScore = (role, folder, node) => {
512
+ const name = normalizeName(node.name);
513
+ const expectedSlug = `${folder.slug}-${role}`;
514
+ if (roleAliases[role].includes(name)) return 100;
515
+ if (role === "posts" && node.slug === "blog") return 90;
516
+ if (node.slug === expectedSlug) return 80;
517
+ if (node.slug === `busabase-cms-${role}`) return 70;
518
+ return 0;
519
+ };
520
+ const adoptExistingBases = async (source, folder, nodes) => {
521
+ const available = [];
522
+ for (const node of nodes) {
523
+ const base = await source.getBaseById(node.baseId);
524
+ if (base) available.push({
525
+ node,
526
+ base
527
+ });
528
+ }
529
+ const adopted = {};
530
+ const used = /* @__PURE__ */ new Set();
531
+ for (const role of BUSABASE_CMS_ROLES) {
532
+ const candidates = available.filter(({ base }) => !used.has(base.id)).map((candidate) => ({
533
+ ...candidate,
534
+ score: candidateScore(role, folder, candidate.node)
535
+ })).filter(({ score }) => score > 0).sort((a, b) => b.score - a.score);
536
+ if (!candidates[0]) continue;
537
+ if (candidates[1]?.score === candidates[0].score) throw new BusabaseCmsSetupError(`Could not uniquely identify the ${role} Base below Folder "${folder.name}"`);
538
+ adopted[role] = candidates[0].base.id;
539
+ used.add(candidates[0].base.id);
540
+ }
541
+ return adopted;
542
+ };
543
+ const validateMetadataBases = async (source, folder, nodes, baseIds) => {
544
+ const nodeByBaseId = new Map(nodes.map((node) => [node.baseId, node]));
545
+ for (const role of BUSABASE_CMS_ROLES) {
546
+ const baseId = baseIds[role];
547
+ const node = nodeByBaseId.get(baseId);
548
+ if (!node || node.parentId !== folder.id) throw new BusabaseCmsSchemaDriftError(`Folder metadata maps ${role} to Base "${baseId}", but it is not a direct child`);
549
+ if (!await source.getBaseById(baseId)) throw new BusabaseCmsSchemaDriftError(`Folder metadata maps ${role} to missing Base "${baseId}"`);
550
+ }
551
+ };
552
+ const sameStringSet = (actual, expected) => {
553
+ if (!expected) return true;
554
+ if (!actual) return false;
555
+ return [...actual].sort().join("\0") === [...expected].sort().join("\0");
556
+ };
557
+ const mimePatternIsWithin = (actual, expected) => {
558
+ actual = actual.trim().toLocaleLowerCase();
559
+ expected = expected.trim().toLocaleLowerCase();
560
+ if (actual === expected || expected === "*/*") return true;
561
+ if (!expected.endsWith("/*")) return false;
562
+ if (actual.endsWith("/*")) return actual === expected;
563
+ return actual.startsWith(expected.slice(0, -1));
564
+ };
565
+ /** An existing attachment policy may be stricter, but never broader, than the CMS contract. */
566
+ const mimePolicyIsCompatible = (actual, expected) => {
567
+ if (!expected || expected.length === 0) return true;
568
+ if (!actual || actual.length === 0) return false;
569
+ return actual.every((actualPattern) => expected.some((expectedPattern) => mimePatternIsWithin(actualPattern, expectedPattern)));
570
+ };
571
+ const upperBoundIsCompatible = (actual, expected) => {
572
+ if (expected === void 0) return true;
573
+ return actual !== void 0 && actual <= expected;
574
+ };
575
+ const allowsLegacyOptionalRequiredField = (expected, context) => context.allowLegacyExisting && (context.schema.legacyOptionalFields ?? []).some((legacy) => legacy.role === context.role && legacy.slug === expected.slug);
576
+ const fieldDrift = (actual, expected, context) => {
577
+ const drift = [];
578
+ if (actual.type !== expected.type) drift.push(`type is ${actual.type}, expected ${expected.type}`);
579
+ if (expected.required && !actual.required && !allowsLegacyOptionalRequiredField(expected, context)) drift.push(`required is ${actual.required}, expected ${expected.required}`);
580
+ if (expected.options.multiple !== void 0 && actual.options.multiple !== expected.options.multiple) drift.push(`multiple is ${String(actual.options.multiple)}, expected ${expected.options.multiple}`);
581
+ if (expected.options.targetBaseId && actual.options.targetBaseId !== expected.options.targetBaseId) drift.push(`targetBaseId is ${String(actual.options.targetBaseId)}, expected ${expected.options.targetBaseId}`);
582
+ if (expected.options.choices) {
583
+ const actualIds = actual.options.choices?.map((choice) => choice.id);
584
+ const expectedIds = expected.options.choices.map((choice) => choice.id);
585
+ if (!sameStringSet(actualIds, expectedIds)) drift.push("select choices do not match");
586
+ }
587
+ const attachment = expected.options.attachment;
588
+ if (attachment) {
589
+ const actualAttachment = actual.options.attachment;
590
+ if (!upperBoundIsCompatible(actualAttachment?.maxFiles, attachment.maxFiles)) drift.push("attachment maxFiles is broader than expected");
591
+ if (!upperBoundIsCompatible(actualAttachment?.maxFileSize, attachment.maxFileSize)) drift.push("attachment maxFileSize is broader than expected");
592
+ if (!mimePolicyIsCompatible(actualAttachment?.allowedMimeTypes, attachment.allowedMimeTypes)) drift.push("attachment allowedMimeTypes are broader than expected");
593
+ }
594
+ return drift;
595
+ };
596
+ const preflightBaseFields = async (source, role, baseIds, schema, allowLegacyExisting) => {
597
+ const expected = getBusabaseCmsBaseDefinition(role, baseIds, schema.fieldsOverride);
598
+ const baseId = baseIds[role];
599
+ if (!baseId) throw new BusabaseCmsSetupError(`Busabase CMS ${role} Base was not resolved`);
600
+ const base = await source.getBaseById(baseId);
601
+ if (!base) throw new BusabaseCmsSetupError(`Busabase CMS ${role} Base was not found`);
602
+ const missing = [];
603
+ for (const field of expected.fields) {
604
+ const actual = base.fields.find((candidate) => candidate.slug === field.slug);
605
+ if (!actual) {
606
+ missing.push({
607
+ role,
608
+ baseId: base.id,
609
+ field
610
+ });
611
+ continue;
612
+ }
613
+ const drift = fieldDrift(actual, field, {
614
+ role,
615
+ schema,
616
+ allowLegacyExisting
617
+ });
618
+ if (field.type === "relation" && !field.options.targetBaseId) drift.push("target Base cannot be validated before its CMS role is resolved");
619
+ if (drift.length > 0) throw new BusabaseCmsSchemaDriftError(`Busabase CMS ${role}.${field.slug} schema drift: ${drift.join("; ")}`);
620
+ }
621
+ return missing;
622
+ };
623
+ const preflightAllFields = async (source, baseIds, schema, allowLegacyExisting) => {
624
+ const missing = [];
625
+ for (const role of BUSABASE_CMS_ROLES) missing.push(...await preflightBaseFields(source, role, baseIds, schema, allowLegacyExisting));
626
+ return missing;
627
+ };
628
+ const preflightResolvedFields = async (source, baseIds, schema) => {
629
+ for (const role of BUSABASE_CMS_ROLES) if (baseIds[role]) await preflightBaseFields(source, role, baseIds, schema, true);
630
+ };
631
+ const createMissingFields = async (source, missing, schema) => {
632
+ for (const { role, baseId, field } of missing) try {
633
+ const actual = (await source.createField({
634
+ baseId,
635
+ ...field
636
+ })).fields.find((candidate) => candidate.slug === field.slug);
637
+ if (!actual) throw new BusabaseCmsSetupError(`Busabase did not materialize field "${field.slug}" in the ${role} Base`);
638
+ const drift = fieldDrift(actual, field, {
639
+ role,
640
+ schema,
641
+ allowLegacyExisting: false
642
+ });
643
+ if (drift.length > 0) throw new BusabaseCmsSchemaDriftError(`Busabase CMS ${role}.${field.slug} schema drift: ${drift.join("; ")}`);
644
+ } catch (cause) {
645
+ if (cause instanceof BusabaseCmsSchemaDriftError) throw cause;
646
+ const actual = (await source.getBaseById(baseId))?.fields.find((candidate) => candidate.slug === field.slug);
647
+ if (!actual) throw new BusabaseCmsSetupError(`Could not create field "${field.slug}" in the ${role} Base`, { cause });
648
+ const drift = fieldDrift(actual, field, {
649
+ role,
650
+ schema,
651
+ allowLegacyExisting: false
652
+ });
653
+ if (drift.length > 0) throw new BusabaseCmsSchemaDriftError(`Busabase CMS ${role}.${field.slug} schema drift: ${drift.join("; ")}`, { cause });
654
+ }
655
+ };
656
+ const findCreatedBase = async (source, folder, role) => {
657
+ const expectedName = roleAliases[role][0];
658
+ const expectedSlug = `${folder.slug}-${role}`;
659
+ const node = (await getDirectBaseNodes(source, folder.id)).find((candidate) => candidate.slug === expectedSlug || normalizeName(candidate.name) === expectedName);
660
+ return node?.baseId ? source.getBaseById(node.baseId) : null;
661
+ };
662
+ const createMissingBase = async (source, folder, role, baseIds, schema) => {
663
+ const definition = getBusabaseCmsBaseDefinition(role, baseIds, schema.fieldsOverride);
664
+ try {
665
+ const created = await source.createBase({
666
+ parentNodeId: folder.id,
667
+ slug: `${folder.slug}-${role}`,
668
+ name: definition.name,
669
+ description: definition.description,
670
+ fields: definition.fields,
671
+ autoMerge: true
672
+ });
673
+ if (!created.id || !created.nodeId) throw new BusabaseCmsSetupError(`Busabase did not immediately materialize the ${role} Base with autoMerge`);
674
+ return created;
675
+ } catch (cause) {
676
+ const concurrent = await findCreatedBase(source, folder, role);
677
+ if (concurrent) return concurrent;
678
+ if (cause instanceof BusabaseCmsSetupError) throw cause;
679
+ throw new BusabaseCmsSetupError(`Could not create the Busabase CMS ${role} Base`, { cause });
680
+ }
681
+ };
682
+ const saveMetadata = async (source, folder, baseIds, schema) => {
683
+ const value = {
684
+ schemaVersion: 1,
685
+ profile: schema.profile,
686
+ bases: baseIds
687
+ };
688
+ try {
689
+ await source.updateNodeMetadata({
690
+ nodeId: folder.id,
691
+ metadata: { [BUSABASE_CMS_METADATA_KEY]: value }
692
+ });
693
+ } catch (cause) {
694
+ const refreshed = await source.getNode(folder.id);
695
+ const concurrent = refreshed ? parseMetadata(refreshed) : null;
696
+ if (concurrent && (concurrent.profile ?? "standard") === schema.profile && BUSABASE_CMS_ROLES.every((role) => concurrent.bases[role] === baseIds[role])) return;
697
+ throw new BusabaseCmsSetupError("Could not persist the Busabase CMS Base ID mapping", { cause });
698
+ }
699
+ };
700
+ const resolveFolderBases = async ({ source: rawSource, folderId, lazyCreate, schema }) => {
701
+ const source = requireProvisioningSource(rawSource);
702
+ const folder = await getFolder(source, folderId);
703
+ let nodes = await getDirectBaseNodes(source, folderId);
704
+ const metadata = parseMetadata(folder);
705
+ if (metadata) {
706
+ assertMetadataProfile(folder, metadata, schema.profile);
707
+ await validateMetadataBases(source, folder, nodes, metadata.bases);
708
+ const missing = await preflightAllFields(source, metadata.bases, schema, true);
709
+ if (missing.length > 0 && !lazyCreate) {
710
+ const first = missing[0];
711
+ throw new BusabaseCmsSetupError(`Busabase CMS ${first.role} Base is missing required field "${first.field.slug}"; enable lazyCreate to add it`);
712
+ }
713
+ if (missing.length > 0) {
714
+ await createMissingFields(requireLazyProvisioningSource(source), missing, schema);
715
+ if ((await preflightAllFields(source, metadata.bases, schema, true)).length > 0) throw new BusabaseCmsSetupError("Busabase did not materialize the complete CMS schema");
716
+ }
717
+ return metadata.bases;
718
+ }
719
+ const resolved = await adoptExistingBases(source, folder, nodes);
720
+ const createdRoles = /* @__PURE__ */ new Set();
721
+ await preflightResolvedFields(source, resolved, schema);
722
+ for (const role of BUSABASE_CMS_ROLES) {
723
+ if (resolved[role]) continue;
724
+ if (!lazyCreate) throw new BusabaseCmsSetupError(`Folder "${folder.name}" is missing the ${role} Base; enable lazyCreate to create it`);
725
+ const writable = requireLazyProvisioningSource(source);
726
+ resolved[role] = (await createMissingBase(writable, folder, role, resolved, schema)).id;
727
+ createdRoles.add(role);
728
+ nodes = await getDirectBaseNodes(source, folderId);
729
+ }
730
+ const baseIds = resolved;
731
+ await validateMetadataBases(source, folder, nodes, baseIds);
732
+ for (const role of createdRoles) await preflightBaseFields(source, role, baseIds, schema, false);
733
+ const missing = await preflightAllFields(source, baseIds, schema, true);
734
+ if (missing.length > 0 && !lazyCreate) {
735
+ const first = missing[0];
736
+ throw new BusabaseCmsSetupError(`Busabase CMS ${first.role} Base is missing required field "${first.field.slug}"; enable lazyCreate to add it`);
737
+ }
738
+ if (missing.length > 0) {
739
+ await createMissingFields(requireLazyProvisioningSource(source), missing, schema);
740
+ if ((await preflightAllFields(source, baseIds, schema, true)).length > 0) throw new BusabaseCmsSetupError("Busabase did not materialize the complete CMS schema");
741
+ }
742
+ await saveMetadata(source, folder, baseIds, schema);
743
+ return baseIds;
744
+ };
745
+ const createBusabaseCmsBaseResolver = (options) => {
746
+ let resolution;
747
+ const resolve = () => {
748
+ resolution ??= resolveFolderBases(options).catch((error) => {
749
+ resolution = void 0;
750
+ throw error;
751
+ });
752
+ return resolution;
753
+ };
754
+ return async (role) => (await resolve())[role];
755
+ };
756
+ //#endregion
757
+ //#region src/source.ts
758
+ const findNode = (nodes, nodeId) => {
759
+ for (const node of nodes) {
760
+ if (node.id === nodeId) return node;
761
+ const child = findNode(node.children, nodeId);
762
+ if (child) return child;
763
+ }
764
+ return null;
765
+ };
766
+ const createBusabaseCmsSource = (client = new Busabase()) => ({
767
+ getBaseBySlug: async (slug) => client.bases.get({ baseId: slug }),
768
+ getBaseById: async (baseId) => client.bases.get({ baseId }),
769
+ getNode: async (nodeId) => findNode(await client.nodes.list(), nodeId),
770
+ listDirectChildren: async (parentNodeId) => client.nodes.list({
771
+ parentId: parentNodeId,
772
+ depth: 1
773
+ }),
774
+ createBase: async (input) => {
775
+ const result = await client.bases.create(input);
776
+ if (!("fields" in result)) throw new Error("Busabase did not materialize the Base despite autoMerge: true");
777
+ return result;
778
+ },
779
+ createField: async (input) => client.bases.createField(input),
780
+ updateNodeMetadata: async (input) => client.nodes.updateMetadata(input),
781
+ listRecordsPage: async (input) => client.records.list(input),
782
+ getRecordByField: client.records.getByField ? async (input) => client.records.getByField?.(input) ?? null : void 0
783
+ });
784
+ const createBusabaseCmsSourceFromConfig = (config) => createBusabaseCmsSource(new Busabase(config));
785
+ //#endregion
786
+ //#region src/types.ts
787
+ const contentPathSchema = z.string().startsWith("/");
788
+ const optionalTextSchema = z.string().min(1).nullable();
789
+ const rawFieldsSchema = z.record(z.string(), z.unknown());
790
+ const attachmentVOSchema = z.object({
791
+ id: z.string(),
792
+ attachmentId: z.string(),
793
+ assetId: z.string().nullable(),
794
+ url: z.url(),
795
+ fileName: z.string(),
796
+ mimeType: z.string(),
797
+ size: z.number().nonnegative()
798
+ });
799
+ const attachmentFieldsDTOSchema = z.object({
800
+ id: z.string(),
801
+ attachmentId: z.string().optional(),
802
+ assetId: z.string().optional(),
803
+ url: z.url(),
804
+ fileName: z.string().optional(),
805
+ mimeType: z.string().optional(),
806
+ size: z.number().nonnegative().optional()
807
+ }).passthrough();
808
+ const relationFieldsDTOSchema = z.union([z.string(), z.array(z.string())]);
809
+ const postFieldsDTOSchema = z.object({
810
+ path: contentPathSchema,
811
+ title: z.string().min(1),
812
+ slug: z.string().min(1),
813
+ locale: z.string().min(1),
814
+ status: z.literal("published"),
815
+ description: z.string().optional(),
816
+ body: z.string().min(1),
817
+ "cover-image": z.union([
818
+ attachmentFieldsDTOSchema,
819
+ z.array(attachmentFieldsDTOSchema),
820
+ z.url()
821
+ ]).optional(),
822
+ attachments: z.array(attachmentFieldsDTOSchema).optional().default([]),
823
+ author: z.string().optional(),
824
+ categories: relationFieldsDTOSchema.optional(),
825
+ tags: relationFieldsDTOSchema.optional(),
826
+ "published-at": z.string().optional(),
827
+ "canonical-url": z.url().optional(),
828
+ "legacy-paths": z.unknown().optional(),
829
+ "seo-title": z.string().optional(),
830
+ "seo-description": z.string().optional(),
831
+ "schema-version": z.number().int().positive().optional().default(1),
832
+ "updated-at": z.string().optional()
833
+ }).passthrough();
834
+ const postVOSchema = z.object({
835
+ id: z.string(),
836
+ path: contentPathSchema,
837
+ title: z.string(),
838
+ slug: z.string(),
839
+ locale: z.string(),
840
+ status: z.literal("published"),
841
+ description: optionalTextSchema,
842
+ body: z.string(),
843
+ coverImage: attachmentVOSchema.nullable(),
844
+ attachments: z.array(attachmentVOSchema),
845
+ author: optionalTextSchema,
846
+ categoryIds: z.array(z.string()),
847
+ tagIds: z.array(z.string()),
848
+ publishedAt: optionalTextSchema,
849
+ canonicalUrl: optionalTextSchema,
850
+ legacyPaths: z.array(z.string()),
851
+ seoTitle: optionalTextSchema,
852
+ seoDescription: optionalTextSchema,
853
+ schemaVersion: z.number().int().positive(),
854
+ updatedAt: z.string(),
855
+ rawFields: rawFieldsSchema
856
+ });
857
+ const pageFieldsDTOSchema = z.object({
858
+ path: contentPathSchema,
859
+ title: z.string().min(1),
860
+ slug: z.string().min(1),
861
+ locale: z.string().min(1),
862
+ status: z.literal("published"),
863
+ template: z.enum([
864
+ "standard",
865
+ "landing",
866
+ "product",
867
+ "use-case"
868
+ ]).optional(),
869
+ body: z.string().min(1),
870
+ hero: z.unknown().optional(),
871
+ features: z.unknown().optional(),
872
+ faqs: z.unknown().optional(),
873
+ "canonical-url": z.url().optional(),
874
+ "legacy-paths": z.unknown().optional(),
875
+ "seo-title": z.string().optional(),
876
+ "seo-description": z.string().optional(),
877
+ "schema-version": z.number().int().positive().optional().default(1),
878
+ "updated-at": z.string().optional()
879
+ }).passthrough();
880
+ const pageVOSchema = z.object({
881
+ id: z.string(),
882
+ path: contentPathSchema,
883
+ title: z.string(),
884
+ slug: z.string(),
885
+ locale: z.string(),
886
+ status: z.literal("published"),
887
+ template: z.enum([
888
+ "standard",
889
+ "landing",
890
+ "product",
891
+ "use-case"
892
+ ]).nullable(),
893
+ body: z.string(),
894
+ hero: z.unknown(),
895
+ features: z.unknown(),
896
+ faqs: z.unknown(),
897
+ canonicalUrl: optionalTextSchema,
898
+ legacyPaths: z.array(z.string()),
899
+ seoTitle: optionalTextSchema,
900
+ seoDescription: optionalTextSchema,
901
+ schemaVersion: z.number().int().positive(),
902
+ updatedAt: z.string(),
903
+ rawFields: rawFieldsSchema
904
+ });
905
+ const taxonomyFieldsDTOSchema = z.object({
906
+ name: z.string().min(1),
907
+ slug: z.string().min(1),
908
+ locale: z.string().min(1),
909
+ description: z.string().optional(),
910
+ "updated-at": z.string().optional()
911
+ }).passthrough();
912
+ const taxonomyVOSchema = z.object({
913
+ id: z.string(),
914
+ name: z.string(),
915
+ slug: z.string(),
916
+ locale: z.string(),
917
+ description: optionalTextSchema,
918
+ updatedAt: z.string(),
919
+ rawFields: rawFieldsSchema
920
+ });
921
+ const categoryFieldsDTOSchema = taxonomyFieldsDTOSchema;
922
+ const tagFieldsDTOSchema = taxonomyFieldsDTOSchema;
923
+ const categoryVOSchema = taxonomyVOSchema;
924
+ const tagVOSchema = taxonomyVOSchema;
925
+ //#endregion
926
+ //#region src/content.ts
927
+ const DEFAULT_POSTS_BASE_SLUG = "busabase-cms-posts";
928
+ const DEFAULT_PAGES_BASE_SLUG = "busabase-cms-pages";
929
+ const DEFAULT_CATEGORIES_BASE_SLUG = "busabase-cms-categories";
930
+ const DEFAULT_TAGS_BASE_SLUG = "busabase-cms-tags";
931
+ const DEFAULT_PAGE_SIZE = 100;
932
+ const optional = (value) => value ?? null;
933
+ const parseJsonField = (raw, fieldSlug, schema, fallback) => {
934
+ if (raw === void 0 || raw === null || raw === "") return fallback;
935
+ let decoded = raw;
936
+ if (typeof raw === "string") try {
937
+ decoded = JSON.parse(raw);
938
+ } catch (cause) {
939
+ throw new BusabaseCmsError(`Busabase field "${fieldSlug}" contains malformed JSON`, { cause });
940
+ }
941
+ const parsed = schema.safeParse(decoded);
942
+ if (!parsed.success) throw new BusabaseCmsError(`Busabase field "${fieldSlug}" has an invalid shape`, { cause: parsed.error });
943
+ return parsed.data;
944
+ };
945
+ const attachmentFromFields = (fields) => attachmentVOSchema.parse({
946
+ id: fields.id,
947
+ attachmentId: fields.attachmentId ?? fields.id,
948
+ assetId: fields.assetId ?? null,
949
+ url: fields.url,
950
+ fileName: fields.fileName ?? fields.url.split("/").at(-1) ?? "attachment",
951
+ mimeType: fields.mimeType ?? "application/octet-stream",
952
+ size: fields.size ?? 0
953
+ });
954
+ const attachmentFromLegacyUrl = (url) => attachmentVOSchema.parse({
955
+ id: url,
956
+ attachmentId: url,
957
+ assetId: null,
958
+ url,
959
+ fileName: url.split("/").at(-1) ?? "attachment",
960
+ mimeType: "application/octet-stream",
961
+ size: 0
962
+ });
963
+ const normalizeCoverImage = (raw) => {
964
+ if (!raw) return null;
965
+ if (typeof raw === "string") return attachmentFromLegacyUrl(raw);
966
+ const first = Array.isArray(raw) ? raw[0] : raw;
967
+ return first ? attachmentFromFields(first) : null;
968
+ };
969
+ const normalizeAttachments = (raw) => raw.map(attachmentFromFields);
970
+ const normalizeRelationIds = (raw) => {
971
+ if (!raw) return [];
972
+ return [...new Set(Array.isArray(raw) ? raw : [raw])];
973
+ };
974
+ const isPublishedRecord = (record) => record.status === "active" && record.headCommit.payload.status === "published";
975
+ const mapPublishedPostRecord = (record) => {
976
+ if (!isPublishedRecord(record)) return null;
977
+ const parsed = postFieldsDTOSchema.safeParse(record.headCommit.payload);
978
+ if (!parsed.success) throw new BusabaseCmsError(`Published Busabase Post record ${record.id} is invalid`, { cause: parsed.error });
979
+ const fields = parsed.data;
980
+ return postVOSchema.parse({
981
+ id: record.id,
982
+ path: fields.path,
983
+ title: fields.title,
984
+ slug: fields.slug,
985
+ locale: fields.locale,
986
+ status: fields.status,
987
+ description: optional(fields.description),
988
+ body: fields.body,
989
+ coverImage: normalizeCoverImage(fields["cover-image"]),
990
+ attachments: normalizeAttachments(fields.attachments),
991
+ author: optional(fields.author),
992
+ categoryIds: normalizeRelationIds(fields.categories),
993
+ tagIds: normalizeRelationIds(fields.tags),
994
+ publishedAt: optional(fields["published-at"]),
995
+ canonicalUrl: optional(fields["canonical-url"]),
996
+ legacyPaths: parseJsonField(fields["legacy-paths"], "legacy-paths", z.array(z.string()), []),
997
+ seoTitle: optional(fields["seo-title"]),
998
+ seoDescription: optional(fields["seo-description"]),
999
+ schemaVersion: fields["schema-version"],
1000
+ updatedAt: fields["updated-at"] ?? record.updatedAt,
1001
+ rawFields: record.headCommit.payload
1002
+ });
1003
+ };
1004
+ const mapPublishedPageRecord = (record) => {
1005
+ if (!isPublishedRecord(record)) return null;
1006
+ const parsed = pageFieldsDTOSchema.safeParse(record.headCommit.payload);
1007
+ if (!parsed.success) throw new BusabaseCmsError(`Published Busabase Page record ${record.id} is invalid`, { cause: parsed.error });
1008
+ const fields = parsed.data;
1009
+ return pageVOSchema.parse({
1010
+ id: record.id,
1011
+ path: fields.path,
1012
+ title: fields.title,
1013
+ slug: fields.slug,
1014
+ locale: fields.locale,
1015
+ status: fields.status,
1016
+ template: optional(fields.template),
1017
+ body: fields.body,
1018
+ hero: parseJsonField(fields.hero, "hero", z.unknown(), null),
1019
+ features: parseJsonField(fields.features, "features", z.unknown(), []),
1020
+ faqs: parseJsonField(fields.faqs, "faqs", z.unknown(), []),
1021
+ canonicalUrl: optional(fields["canonical-url"]),
1022
+ legacyPaths: parseJsonField(fields["legacy-paths"], "legacy-paths", z.array(z.string()), []),
1023
+ seoTitle: optional(fields["seo-title"]),
1024
+ seoDescription: optional(fields["seo-description"]),
1025
+ schemaVersion: fields["schema-version"],
1026
+ updatedAt: fields["updated-at"] ?? record.updatedAt,
1027
+ rawFields: record.headCommit.payload
1028
+ });
1029
+ };
1030
+ const mapTaxonomyRecord = (record, kind) => {
1031
+ if (record.status !== "active") return null;
1032
+ const parsed = (kind === "Category" ? categoryFieldsDTOSchema : tagFieldsDTOSchema).safeParse(record.headCommit.payload);
1033
+ if (!parsed.success) throw new BusabaseCmsError(`Active Busabase ${kind} record ${record.id} is invalid`, { cause: parsed.error });
1034
+ const fields = parsed.data;
1035
+ const value = {
1036
+ id: record.id,
1037
+ name: fields.name,
1038
+ slug: fields.slug,
1039
+ locale: fields.locale,
1040
+ description: optional(fields.description),
1041
+ updatedAt: fields["updated-at"] ?? record.updatedAt,
1042
+ rawFields: record.headCommit.payload
1043
+ };
1044
+ return kind === "Category" ? categoryVOSchema.parse(value) : tagVOSchema.parse(value);
1045
+ };
1046
+ const mapActiveCategoryRecord = (record) => mapTaxonomyRecord(record, "Category");
1047
+ const mapActiveTagRecord = (record) => mapTaxonomyRecord(record, "Tag");
1048
+ const resolveOptions = (options) => {
1049
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
1050
+ if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 100) throw new BusabaseCmsError("pageSize must be an integer between 1 and 100");
1051
+ if (options.source && (options.client || options.config)) throw new BusabaseCmsError("Provide source or client/config, not both");
1052
+ if (options.client && options.config) throw new BusabaseCmsError("Provide client or config, not both");
1053
+ if (options.lazyCreate && !options.folderId) throw new BusabaseCmsError("lazyCreate requires folderId");
1054
+ if (options.source && options.lazyCreate && (!options.source.getBaseById || !options.source.getNode || !options.source.listDirectChildren || !options.source.createBase || !options.source.createField || !options.source.updateNodeMetadata)) throw new BusabaseCmsError("lazyCreate with a custom source requires node, Base, field, and metadata provisioning methods");
1055
+ const source = options.source ?? (options.client ? createBusabaseCmsSource(options.client) : createBusabaseCmsSourceFromConfig(options.config));
1056
+ return {
1057
+ source,
1058
+ resolveBaseId: options.folderId ? createBusabaseCmsBaseResolver({
1059
+ source,
1060
+ folderId: options.folderId,
1061
+ lazyCreate: options.lazyCreate ?? false,
1062
+ schema: {
1063
+ profile: options.schemaProfile ?? "standard",
1064
+ fieldsOverride: options.fieldsOverride,
1065
+ legacyOptionalFields: options.legacyOptionalFields
1066
+ }
1067
+ }) : void 0,
1068
+ baseSlugs: {
1069
+ posts: options.baseSlugs?.posts ?? "busabase-cms-posts",
1070
+ pages: options.baseSlugs?.pages ?? "busabase-cms-pages",
1071
+ categories: options.baseSlugs?.categories ?? "busabase-cms-categories",
1072
+ tags: options.baseSlugs?.tags ?? "busabase-cms-tags"
1073
+ },
1074
+ pageSize,
1075
+ invalidRecords: options.invalidRecords ?? "skip",
1076
+ onInvalidRecord: options.onInvalidRecord
1077
+ };
1078
+ };
1079
+ const resolveRoleBaseId = async (options, role) => {
1080
+ const baseSlug = options.baseSlugs[role];
1081
+ const baseId = options.resolveBaseId ? await options.resolveBaseId(role) : (await options.source.getBaseBySlug(baseSlug))?.id;
1082
+ if (!baseId) throw new BusabaseCmsError(`Busabase Base "${baseSlug}" was not found`);
1083
+ return baseId;
1084
+ };
1085
+ /**
1086
+ * Point lookup by an exact field value (e.g. `path`, `slug`), scoped to one role's Base.
1087
+ * Returns `undefined` — not `null` — when the source has no `getRecordByField` capability,
1088
+ * so callers can tell "unsupported, fall back to list+find" apart from "supported, not found".
1089
+ */
1090
+ const findRecordByField = async (options, role, fieldSlug, valueText) => {
1091
+ if (!options.source.getRecordByField) return void 0;
1092
+ const baseId = await resolveRoleBaseId(options, role);
1093
+ return options.source.getRecordByField({
1094
+ baseId,
1095
+ fieldSlug,
1096
+ valueText
1097
+ });
1098
+ };
1099
+ const listAllRecords = async (options, role) => {
1100
+ const baseId = await resolveRoleBaseId(options, role);
1101
+ const records = [];
1102
+ const visitedCursors = /* @__PURE__ */ new Set();
1103
+ let cursor;
1104
+ do {
1105
+ const page = await options.source.listRecordsPage({
1106
+ baseId,
1107
+ limit: options.pageSize,
1108
+ cursor
1109
+ });
1110
+ records.push(...page.records);
1111
+ if (!page.nextCursor) break;
1112
+ if (visitedCursors.has(page.nextCursor)) throw new BusabaseCmsError(`Busabase returned a repeated cursor for Base "${options.baseSlugs[role]}"`);
1113
+ visitedCursors.add(page.nextCursor);
1114
+ cursor = page.nextCursor;
1115
+ } while (cursor);
1116
+ return records;
1117
+ };
1118
+ const mapValidRecords = (options, records, kind, mapper) => records.flatMap((record) => {
1119
+ try {
1120
+ const content = mapper(record);
1121
+ return content ? [content] : [];
1122
+ } catch (cause) {
1123
+ const error = cause instanceof BusabaseCmsError ? cause : new BusabaseCmsError(`Could not map Busabase record ${record.id}`, { cause });
1124
+ if (options.invalidRecords === "throw") throw error;
1125
+ const issue = {
1126
+ kind,
1127
+ recordId: record.id,
1128
+ error
1129
+ };
1130
+ if (options.onInvalidRecord) options.onInvalidRecord(issue);
1131
+ else console.warn(`[busabase-cms] Skipping invalid ${kind} record ${record.id}`, error);
1132
+ return [];
1133
+ }
1134
+ });
1135
+ /**
1136
+ * Resolve one record by an exact field value. Prefers the source's `getRecordByField`
1137
+ * point lookup (a single indexed query); falls back to `list()` + client-side `.find()`
1138
+ * when the source doesn't support it (e.g. a demo/test source).
1139
+ */
1140
+ const getSingleByField = async (options, role, fieldSlug, valueText, kind, mapper, fallbackList, matches) => {
1141
+ const found = await findRecordByField(options, role, fieldSlug, valueText);
1142
+ if (found !== void 0) {
1143
+ if (!found) return null;
1144
+ const [mapped] = mapValidRecords(options, [found], kind, mapper);
1145
+ return mapped ?? null;
1146
+ }
1147
+ return (await fallbackList()).find(matches) ?? null;
1148
+ };
1149
+ const createBusabaseCms = (options = {}) => {
1150
+ const resolved = resolveOptions(options);
1151
+ const listPosts = async () => mapValidRecords(resolved, await listAllRecords(resolved, "posts"), "post", mapPublishedPostRecord);
1152
+ const listPages = async () => mapValidRecords(resolved, await listAllRecords(resolved, "pages"), "page", mapPublishedPageRecord);
1153
+ const listCategories = async () => mapValidRecords(resolved, await listAllRecords(resolved, "categories"), "category", mapActiveCategoryRecord);
1154
+ const listTags = async () => mapValidRecords(resolved, await listAllRecords(resolved, "tags"), "tag", mapActiveTagRecord);
1155
+ return {
1156
+ posts: {
1157
+ list: listPosts,
1158
+ getByPath: (path) => getSingleByField(resolved, "posts", "path", path, "post", mapPublishedPostRecord, listPosts, (post) => post.path === path)
1159
+ },
1160
+ pages: {
1161
+ list: listPages,
1162
+ getByPath: (path) => getSingleByField(resolved, "pages", "path", path, "page", mapPublishedPageRecord, listPages, (page) => page.path === path)
1163
+ },
1164
+ categories: {
1165
+ list: listCategories,
1166
+ getBySlug: (slug) => getSingleByField(resolved, "categories", "slug", slug, "category", mapActiveCategoryRecord, listCategories, (category) => category.slug === slug)
1167
+ },
1168
+ tags: {
1169
+ list: listTags,
1170
+ getBySlug: (slug) => getSingleByField(resolved, "tags", "slug", slug, "tag", mapActiveTagRecord, listTags, (tag) => tag.slug === slug)
1171
+ }
1172
+ };
1173
+ };
1174
+ //#endregion
1175
+ export { replaceField as A, createBusabaseCmsSourceFromConfig as C, BUSABASE_CMS_SCHEMA_VERSION as D, BUSABASE_CMS_SCHEMA_PROFILES as E, BusabaseCmsSchemaDriftError as M, BusabaseCmsSetupError as N, getBusabaseCmsBaseDefinition as O, createBusabaseCmsSource as S, BUSABASE_CMS_ROLES as T, postVOSchema as _, createBusabaseCms as a, tagVOSchema as b, mapPublishedPageRecord as c, attachmentVOSchema as d, categoryFieldsDTOSchema as f, postFieldsDTOSchema as g, pageVOSchema as h, DEFAULT_TAGS_BASE_SLUG as i, BusabaseCmsError as j, i18nName as k, mapPublishedPostRecord as l, pageFieldsDTOSchema as m, DEFAULT_PAGES_BASE_SLUG as n, mapActiveCategoryRecord as o, categoryVOSchema as p, DEFAULT_POSTS_BASE_SLUG as r, mapActiveTagRecord as s, DEFAULT_CATEGORIES_BASE_SLUG as t, attachmentFieldsDTOSchema as u, relationFieldsDTOSchema as v, BUSABASE_CMS_METADATA_KEY as w, taxonomyFieldsDTOSchema as x, tagFieldsDTOSchema as y };