@thebes/cadmus 0.2.1 → 0.3.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.
Files changed (41) hide show
  1. package/dist/cms/index.cjs +458 -3
  2. package/dist/cms/index.cjs.map +1 -1
  3. package/dist/cms/index.d.cts +2 -2
  4. package/dist/cms/index.d.ts +2 -2
  5. package/dist/cms/index.js +451 -5
  6. package/dist/cms/index.js.map +1 -1
  7. package/dist/email/index.cjs +1 -1
  8. package/dist/email/index.js +1 -1
  9. package/dist/{errors-CW6Lz0AQ.cjs → errors-BhoibM6Z.cjs} +24 -1
  10. package/dist/{errors-CW6Lz0AQ.cjs.map → errors-BhoibM6Z.cjs.map} +1 -1
  11. package/dist/{errors-mZIqZJO4.js → errors-C8SqkFjl.js} +19 -2
  12. package/dist/{errors-mZIqZJO4.js.map → errors-C8SqkFjl.js.map} +1 -1
  13. package/dist/hono/index.cjs +6 -1
  14. package/dist/hono/index.cjs.map +1 -1
  15. package/dist/hono/index.d.cts +1 -1
  16. package/dist/hono/index.d.cts.map +1 -1
  17. package/dist/hono/index.d.ts +1 -1
  18. package/dist/hono/index.d.ts.map +1 -1
  19. package/dist/hono/index.js +6 -1
  20. package/dist/hono/index.js.map +1 -1
  21. package/dist/{index-BUrCSGVb.d.ts → index-BRZrCTsN.d.cts} +641 -145
  22. package/dist/index-BRZrCTsN.d.cts.map +1 -0
  23. package/dist/{index-BUrCSGVb.d.cts → index-BRZrCTsN.d.ts} +641 -145
  24. package/dist/index-BRZrCTsN.d.ts.map +1 -0
  25. package/dist/index.cjs +11 -1
  26. package/dist/index.d.cts +2 -88
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +2 -88
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/queues/index.cjs +1 -1
  32. package/dist/queues/index.js +1 -1
  33. package/dist/rate-limit/index.cjs +1 -1
  34. package/dist/rate-limit/index.js +1 -1
  35. package/dist/session/index.cjs +1 -1
  36. package/dist/session/index.js +1 -1
  37. package/dist/storage/index.cjs +1 -1
  38. package/dist/storage/index.js +1 -1
  39. package/package.json +8 -8
  40. package/dist/index-BUrCSGVb.d.cts.map +0 -1
  41. package/dist/index-BUrCSGVb.d.ts.map +0 -1
@@ -1,2 +1,2 @@
1
- import { A as CollectionAccess, B as RelationshipFieldConfig, C as relationshipJoinTables, D as CadmeaPlugin, E as BaseFieldConfig, F as GroupFieldConfig, G as flattenDoc, H as SelectFieldConfig, I as JsonFieldConfig, K as flattenFields, L as JsonValue, M as CollectionHooks, N as DateFieldConfig, O as CheckboxFieldConfig, P as FieldConfig, R as NumberFieldConfig, S as extractSearchText, T as ArrayFieldConfig, U as TextFieldConfig, V as RichTextFieldConfig, W as UploadFieldConfig, _ as cmsConfigToSchema, a as generateSchemaSource, b as collectionToTable, c as CmsRegistry, d as can, f as createLocalApi, g as defineCollection, h as defineCmsConfig, i as deliverWebhookMessage, j as CollectionConfig, k as CmsConfig, l as LocalApi, m as getRegisteredApi, n as WebhookMessage, o as CollectionMeta, p as createVersionedLocalApi, q as nestDoc, r as createWebhookHook, s as getCollectionsMeta, t as WebhookConfig, u as VersionedLocalApi, v as collectionSearchTableName, w as AccessFn, x as collectionVersionsTable, y as collectionSearchTableSQL, z as RelationshipDepth } from "../index-BUrCSGVb.cjs";
2
- export { AccessFn, ArrayFieldConfig, BaseFieldConfig, CadmeaPlugin, CheckboxFieldConfig, CmsConfig, CmsRegistry, CollectionAccess, CollectionConfig, CollectionHooks, CollectionMeta, DateFieldConfig, FieldConfig, GroupFieldConfig, JsonFieldConfig, JsonValue, LocalApi, NumberFieldConfig, RelationshipDepth, RelationshipFieldConfig, RichTextFieldConfig, SelectFieldConfig, TextFieldConfig, UploadFieldConfig, VersionedLocalApi, WebhookConfig, WebhookMessage, can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables };
1
+ import { $ as ValidationSeverity, A as CollectionConfig, B as RichTextFieldConfig, C as ArrayFieldConfig, D as CmsConfig, Dt as StringBlockRenderer, E as CheckboxFieldConfig, Et as PortableBlockLike, F as JsonFieldConfig, G as flattenFields, H as TextFieldConfig, I as JsonValue, J as CustomValidatorResult, K as nestDoc, L as NumberFieldConfig, M as DateFieldConfig, N as FieldConfig, O as CollectionAccess, Ot as createBlockRegistry, P as GroupFieldConfig, Q as ValidationFieldContext, R as RelationshipDepth, S as AccessFn, T as CadmeaPlugin, Tt as BlockRegistry, U as UploadFieldConfig, V as SelectFieldConfig, W as flattenDoc, X as ValidateDocumentOptions, Y as Rule, Z as ValidationBuilder, _ as collectionSearchTableSQL, a as BuildStudioStructureOptions, at as LocalApi, b as extractSearchText, c as StudioStructureItem, ct as createLocalApi, d as CollectionMeta, et as assertValid, f as getCollectionsMeta, g as collectionSearchTableName, h as cmsConfigToSchema, i as deliverWebhookMessage, it as CmsRegistry, j as CollectionHooks, k as CollectionAdminConfig, kt as renderBlocksToString, l as buildStudioStructure, lt as createVersionedLocalApi, m as defineCollection, n as WebhookMessage, nt as rule, o as DEFAULT_STUDIO_GROUP, ot as VersionedLocalApi, p as defineCmsConfig, q as CustomValidator, r as createWebhookHook, rt as validateDocument, s as StudioStructureGroup, st as can, t as WebhookConfig, tt as defineField, u as generateSchemaSource, ut as getRegisteredApi, v as collectionToTable, w as BaseFieldConfig, x as relationshipJoinTables, y as collectionVersionsTable, z as RelationshipFieldConfig } from "../index-BRZrCTsN.cjs";
2
+ export { AccessFn, ArrayFieldConfig, BaseFieldConfig, BlockRegistry, BuildStudioStructureOptions, CadmeaPlugin, CheckboxFieldConfig, CmsConfig, CmsRegistry, CollectionAccess, CollectionAdminConfig, CollectionConfig, CollectionHooks, CollectionMeta, CustomValidator, CustomValidatorResult, DEFAULT_STUDIO_GROUP, DateFieldConfig, FieldConfig, GroupFieldConfig, JsonFieldConfig, JsonValue, LocalApi, NumberFieldConfig, PortableBlockLike, RelationshipDepth, RelationshipFieldConfig, RichTextFieldConfig, Rule, SelectFieldConfig, StringBlockRenderer, StudioStructureGroup, StudioStructureItem, TextFieldConfig, UploadFieldConfig, ValidateDocumentOptions, ValidationBuilder, ValidationFieldContext, ValidationSeverity, VersionedLocalApi, WebhookConfig, WebhookMessage, assertValid, buildStudioStructure, can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createBlockRegistry, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, defineField, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables, renderBlocksToString, rule, validateDocument };
@@ -1,2 +1,2 @@
1
- import { A as CollectionAccess, B as RelationshipFieldConfig, C as relationshipJoinTables, D as CadmeaPlugin, E as BaseFieldConfig, F as GroupFieldConfig, G as flattenDoc, H as SelectFieldConfig, I as JsonFieldConfig, K as flattenFields, L as JsonValue, M as CollectionHooks, N as DateFieldConfig, O as CheckboxFieldConfig, P as FieldConfig, R as NumberFieldConfig, S as extractSearchText, T as ArrayFieldConfig, U as TextFieldConfig, V as RichTextFieldConfig, W as UploadFieldConfig, _ as cmsConfigToSchema, a as generateSchemaSource, b as collectionToTable, c as CmsRegistry, d as can, f as createLocalApi, g as defineCollection, h as defineCmsConfig, i as deliverWebhookMessage, j as CollectionConfig, k as CmsConfig, l as LocalApi, m as getRegisteredApi, n as WebhookMessage, o as CollectionMeta, p as createVersionedLocalApi, q as nestDoc, r as createWebhookHook, s as getCollectionsMeta, t as WebhookConfig, u as VersionedLocalApi, v as collectionSearchTableName, w as AccessFn, x as collectionVersionsTable, y as collectionSearchTableSQL, z as RelationshipDepth } from "../index-BUrCSGVb.js";
2
- export { AccessFn, ArrayFieldConfig, BaseFieldConfig, CadmeaPlugin, CheckboxFieldConfig, CmsConfig, CmsRegistry, CollectionAccess, CollectionConfig, CollectionHooks, CollectionMeta, DateFieldConfig, FieldConfig, GroupFieldConfig, JsonFieldConfig, JsonValue, LocalApi, NumberFieldConfig, RelationshipDepth, RelationshipFieldConfig, RichTextFieldConfig, SelectFieldConfig, TextFieldConfig, UploadFieldConfig, VersionedLocalApi, WebhookConfig, WebhookMessage, can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables };
1
+ import { $ as ValidationSeverity, A as CollectionConfig, B as RichTextFieldConfig, C as ArrayFieldConfig, D as CmsConfig, Dt as StringBlockRenderer, E as CheckboxFieldConfig, Et as PortableBlockLike, F as JsonFieldConfig, G as flattenFields, H as TextFieldConfig, I as JsonValue, J as CustomValidatorResult, K as nestDoc, L as NumberFieldConfig, M as DateFieldConfig, N as FieldConfig, O as CollectionAccess, Ot as createBlockRegistry, P as GroupFieldConfig, Q as ValidationFieldContext, R as RelationshipDepth, S as AccessFn, T as CadmeaPlugin, Tt as BlockRegistry, U as UploadFieldConfig, V as SelectFieldConfig, W as flattenDoc, X as ValidateDocumentOptions, Y as Rule, Z as ValidationBuilder, _ as collectionSearchTableSQL, a as BuildStudioStructureOptions, at as LocalApi, b as extractSearchText, c as StudioStructureItem, ct as createLocalApi, d as CollectionMeta, et as assertValid, f as getCollectionsMeta, g as collectionSearchTableName, h as cmsConfigToSchema, i as deliverWebhookMessage, it as CmsRegistry, j as CollectionHooks, k as CollectionAdminConfig, kt as renderBlocksToString, l as buildStudioStructure, lt as createVersionedLocalApi, m as defineCollection, n as WebhookMessage, nt as rule, o as DEFAULT_STUDIO_GROUP, ot as VersionedLocalApi, p as defineCmsConfig, q as CustomValidator, r as createWebhookHook, rt as validateDocument, s as StudioStructureGroup, st as can, t as WebhookConfig, tt as defineField, u as generateSchemaSource, ut as getRegisteredApi, v as collectionToTable, w as BaseFieldConfig, x as relationshipJoinTables, y as collectionVersionsTable, z as RelationshipFieldConfig } from "../index-BRZrCTsN.js";
2
+ export { AccessFn, ArrayFieldConfig, BaseFieldConfig, BlockRegistry, BuildStudioStructureOptions, CadmeaPlugin, CheckboxFieldConfig, CmsConfig, CmsRegistry, CollectionAccess, CollectionAdminConfig, CollectionConfig, CollectionHooks, CollectionMeta, CustomValidator, CustomValidatorResult, DEFAULT_STUDIO_GROUP, DateFieldConfig, FieldConfig, GroupFieldConfig, JsonFieldConfig, JsonValue, LocalApi, NumberFieldConfig, PortableBlockLike, RelationshipDepth, RelationshipFieldConfig, RichTextFieldConfig, Rule, SelectFieldConfig, StringBlockRenderer, StudioStructureGroup, StudioStructureItem, TextFieldConfig, UploadFieldConfig, ValidateDocumentOptions, ValidationBuilder, ValidationFieldContext, ValidationSeverity, VersionedLocalApi, WebhookConfig, WebhookMessage, assertValid, buildStudioStructure, can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createBlockRegistry, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, defineField, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables, renderBlocksToString, rule, validateDocument };
package/dist/cms/index.js CHANGED
@@ -1,7 +1,64 @@
1
- import { a as CadmusCmsError, l as CadmusQueueError, t as CadmusAccessDeniedError } from "../errors-mZIqZJO4.js";
1
+ import { a as CadmusCmsError, l as CadmusQueueError, p as CadmusValidationError, t as CadmusAccessDeniedError } from "../errors-C8SqkFjl.js";
2
2
  import { enqueue } from "../queues/index.js";
3
3
  import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core";
4
- import { count, desc, eq, inArray, sql } from "drizzle-orm";
4
+ import { and, count, desc, eq, inArray, ne, sql } from "drizzle-orm";
5
+ //#region src/cms/blocks.ts
6
+ /**
7
+ * Create a block renderer registry. Seed it with an initial `type → renderer`
8
+ * map and/or an `options.fallback` for unknown types.
9
+ *
10
+ * ```ts
11
+ * const registry = createBlockRegistry<StringBlockRenderer>({
12
+ * divider: () => "<hr>",
13
+ * });
14
+ * registry.register("hero", (b) => `<h1>${b.heading}</h1>`);
15
+ * renderBlocksToString(blocks, registry);
16
+ * ```
17
+ */
18
+ function createBlockRegistry(initial = {}, options = {}) {
19
+ const renderers = new Map(Object.entries(initial));
20
+ let fallback = options.fallback;
21
+ const registry = {
22
+ register(type, renderer) {
23
+ renderers.set(type, renderer);
24
+ return registry;
25
+ },
26
+ registerMany(map) {
27
+ for (const [type, renderer] of Object.entries(map)) renderers.set(type, renderer);
28
+ return registry;
29
+ },
30
+ get(type) {
31
+ return renderers.get(type);
32
+ },
33
+ has(type) {
34
+ return renderers.has(type);
35
+ },
36
+ types() {
37
+ return [...renderers.keys()];
38
+ },
39
+ setFallback(renderer) {
40
+ fallback = renderer;
41
+ return registry;
42
+ },
43
+ resolve(type) {
44
+ return renderers.get(type) ?? fallback;
45
+ }
46
+ };
47
+ return registry;
48
+ }
49
+ /**
50
+ * Render an array of blocks to a single HTML string via a registry of
51
+ * {@link StringBlockRenderer}s. Blocks whose type resolves to no renderer
52
+ * (and no fallback) contribute the empty string — the same forgiving
53
+ * behavior the old hand-rolled `switch` had for unknown types.
54
+ */
55
+ function renderBlocksToString(blocks, registry) {
56
+ return blocks.map((block) => {
57
+ const renderer = registry.resolve(block.type);
58
+ return renderer ? renderer(block) : "";
59
+ }).join("");
60
+ }
61
+ //#endregion
5
62
  //#region src/cms/types.ts
6
63
  /**
7
64
  * Expands every `group` field in `fields` into its flattened equivalents
@@ -249,6 +306,289 @@ function defineCmsConfig(config) {
249
306
  return resolved;
250
307
  }
251
308
  //#endregion
309
+ //#region src/cms/validation.ts
310
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
311
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
312
+ /**
313
+ * Immutable, chainable rule builder — the value a field's `validation`
314
+ * function receives and returns. Build a `Rule` with the module-level
315
+ * {@link rule} factory, or accept the one passed to your `validation`
316
+ * callback.
317
+ */
318
+ var Rule = class Rule {
319
+ checks;
320
+ constructor(checks = []) {
321
+ this.checks = checks;
322
+ }
323
+ add(check) {
324
+ return new Rule([...this.checks, check]);
325
+ }
326
+ /** Override the message of the most recently added check. */
327
+ error(message) {
328
+ return this.withLast({
329
+ message,
330
+ severity: "error"
331
+ });
332
+ }
333
+ /**
334
+ * Demote the most recently added check to a warning (non-blocking),
335
+ * optionally with a message. Sanity's `Rule.warning()` analogue.
336
+ */
337
+ warning(message) {
338
+ return this.withLast({
339
+ severity: "warning",
340
+ ...message ? { message } : {}
341
+ });
342
+ }
343
+ withLast(patch) {
344
+ if (this.checks.length === 0) return this;
345
+ const next = this.checks.slice();
346
+ next[next.length - 1] = {
347
+ ...next[next.length - 1],
348
+ ...patch
349
+ };
350
+ return new Rule(next);
351
+ }
352
+ required() {
353
+ return this.add({ kind: "required" });
354
+ }
355
+ /** Minimum string length / array length / numeric value. */
356
+ min(n) {
357
+ return this.add({
358
+ kind: "min",
359
+ n
360
+ });
361
+ }
362
+ /** Maximum string length / array length / numeric value. */
363
+ max(n) {
364
+ return this.add({
365
+ kind: "max",
366
+ n
367
+ });
368
+ }
369
+ /** Exact string/array length. */
370
+ length(n) {
371
+ return this.add({
372
+ kind: "length",
373
+ n
374
+ });
375
+ }
376
+ regex(re, label = "match the required format") {
377
+ return this.add({
378
+ kind: "regex",
379
+ re,
380
+ label
381
+ });
382
+ }
383
+ email() {
384
+ return this.add({
385
+ kind: "regex",
386
+ re: EMAIL_RE,
387
+ label: "be a valid email"
388
+ });
389
+ }
390
+ /** Lowercase kebab-case slug format. Pair with `.unique()` for slugs. */
391
+ slug() {
392
+ return this.add({
393
+ kind: "regex",
394
+ re: SLUG_RE,
395
+ label: "be a lowercase, hyphen-separated slug"
396
+ });
397
+ }
398
+ integer() {
399
+ return this.add({ kind: "integer" });
400
+ }
401
+ positive() {
402
+ return this.add({ kind: "positive" });
403
+ }
404
+ /**
405
+ * Value must be unique across the collection (DB-backed; skipped in a
406
+ * pure client-side pass). A first-class rule rather than the hand-rolled
407
+ * column `unique` flag, so the failure is a clear field message instead of
408
+ * a raw UNIQUE-constraint write error.
409
+ */
410
+ unique() {
411
+ return this.add({ kind: "unique" });
412
+ }
413
+ /**
414
+ * For a `relationship` field: the referenced id must exist in the related
415
+ * collection (DB-backed; skipped client-side).
416
+ */
417
+ reference() {
418
+ return this.add({ kind: "reference" });
419
+ }
420
+ custom(fn) {
421
+ return this.add({
422
+ kind: "custom",
423
+ fn
424
+ });
425
+ }
426
+ /** Internal: the accumulated checks, read by {@link validateDocument}. */
427
+ toChecks() {
428
+ return this.checks;
429
+ }
430
+ };
431
+ /** Fresh, empty rule — the root of a chain. */
432
+ function rule() {
433
+ return new Rule();
434
+ }
435
+ /**
436
+ * Identity helper mirroring Sanity's `defineField` — returns the field
437
+ * config unchanged but gives editors autocomplete and a single, greppable
438
+ * call site for field definitions. Optional: a plain object literal is still
439
+ * a valid field.
440
+ */
441
+ function defineField(field) {
442
+ return field;
443
+ }
444
+ function resolveChecks(field) {
445
+ if (!field.validation) return [];
446
+ const built = field.validation(new Rule());
447
+ return (Array.isArray(built) ? built : [built]).flatMap((r) => r.toChecks());
448
+ }
449
+ function isEmpty(value) {
450
+ return value === void 0 || value === null || typeof value === "string" && value.length === 0;
451
+ }
452
+ function sizeOf(value) {
453
+ if (typeof value === "string") return {
454
+ size: value.length,
455
+ unit: "character"
456
+ };
457
+ if (Array.isArray(value)) return {
458
+ size: value.length,
459
+ unit: "item"
460
+ };
461
+ if (typeof value === "number") return {
462
+ size: value,
463
+ unit: ""
464
+ };
465
+ return null;
466
+ }
467
+ /**
468
+ * Evaluate every field's validation rules against `doc`, returning all
469
+ * violations (both errors and warnings). `doc` is the nested document; field
470
+ * values are read from its flattened form so group subfields validate too.
471
+ */
472
+ async function validateDocument(config, doc, options) {
473
+ const flatFields = flattenFields(config.fields);
474
+ const flatDoc = flattenDocShallow(config, doc);
475
+ const violations = [];
476
+ for (const [path, field] of Object.entries(flatFields)) {
477
+ if (options.onlyFields && !options.onlyFields.has(path)) continue;
478
+ const checks = resolveChecks(field);
479
+ if (checks.length === 0) continue;
480
+ const value = flatDoc[path];
481
+ const ctx = {
482
+ document: doc,
483
+ path,
484
+ operation: options.operation,
485
+ ...options.id !== void 0 ? { id: options.id } : {}
486
+ };
487
+ for (const check of checks) {
488
+ const violation = await evaluateCheck(check, value, field, ctx, options);
489
+ if (violation) violations.push(violation);
490
+ }
491
+ }
492
+ return violations;
493
+ }
494
+ function flattenDocShallow(config, doc) {
495
+ return Object.values(config.fields).some((f) => f.type === "group") ? flattenDoc(config.fields, doc) : doc;
496
+ }
497
+ async function evaluateCheck(check, value, field, ctx, options) {
498
+ const fail = (defaultMessage) => ({
499
+ path: ctx.path,
500
+ message: check.message ?? `${ctx.path} must ${defaultMessage}`,
501
+ severity: check.severity ?? "error"
502
+ });
503
+ switch (check.kind) {
504
+ case "required": return isEmpty(value) ? fail("not be empty") : null;
505
+ case "min": {
506
+ if (isEmpty(value)) return null;
507
+ const s = sizeOf(value);
508
+ if (s && s.size < check.n) return fail(s.unit ? `have at least ${check.n} ${s.unit}${check.n === 1 ? "" : "s"}` : `be at least ${check.n}`);
509
+ return null;
510
+ }
511
+ case "max": {
512
+ if (isEmpty(value)) return null;
513
+ const s = sizeOf(value);
514
+ if (s && s.size > check.n) return fail(s.unit ? `have at most ${check.n} ${s.unit}${check.n === 1 ? "" : "s"}` : `be at most ${check.n}`);
515
+ return null;
516
+ }
517
+ case "length": {
518
+ if (isEmpty(value)) return null;
519
+ const s = sizeOf(value);
520
+ if (s?.unit && s.size !== check.n) return fail(`be exactly ${check.n} ${s.unit}${check.n === 1 ? "" : "s"}`);
521
+ return null;
522
+ }
523
+ case "regex":
524
+ if (isEmpty(value)) return null;
525
+ if (typeof value !== "string" || !check.re.test(value)) return fail(check.label);
526
+ return null;
527
+ case "integer":
528
+ if (isEmpty(value)) return null;
529
+ return typeof value === "number" && Number.isInteger(value) ? null : fail("be an integer");
530
+ case "positive":
531
+ if (isEmpty(value)) return null;
532
+ return typeof value === "number" && value > 0 ? null : fail("be a positive number");
533
+ case "unique": return evaluateUnique(value, ctx, options, check);
534
+ case "reference": return evaluateReference(value, field, ctx, options, check);
535
+ case "custom": {
536
+ const result = await check.fn(value, ctx);
537
+ if (result === true || result === void 0) return null;
538
+ if (result === false) return fail("be valid");
539
+ if (typeof result === "string") return {
540
+ path: ctx.path,
541
+ message: result,
542
+ severity: check.severity ?? "error"
543
+ };
544
+ return {
545
+ path: ctx.path,
546
+ message: result.message,
547
+ severity: result.severity ?? check.severity ?? "error"
548
+ };
549
+ }
550
+ }
551
+ }
552
+ async function evaluateUnique(value, ctx, options, check) {
553
+ if (isEmpty(value) || !options.db || !options.table) return null;
554
+ const column = options.table[ctx.path];
555
+ if (!column) return null;
556
+ const where = ctx.id !== void 0 ? and(eq(column, value), ne(options.table.id, ctx.id)) : eq(column, value);
557
+ if ((await options.db.select({ id: options.table.id }).from(options.table).where(where).limit(1)).length > 0) return {
558
+ path: ctx.path,
559
+ message: check.message ?? `${ctx.path} "${String(value)}" is already taken`,
560
+ severity: check.severity ?? "error"
561
+ };
562
+ return null;
563
+ }
564
+ async function evaluateReference(value, field, ctx, options, check) {
565
+ if (isEmpty(value) || !options.db || !options.registry) return null;
566
+ if (field.type !== "relationship") return null;
567
+ const target = options.registry.tables[field.relationTo];
568
+ if (!target) return null;
569
+ if ((await options.db.select({ id: target.id }).from(target).where(eq(target.id, value)).limit(1)).length === 0) return {
570
+ path: ctx.path,
571
+ message: check.message ?? `${ctx.path} references a "${field.relationTo}" that does not exist`,
572
+ severity: check.severity ?? "error"
573
+ };
574
+ return null;
575
+ }
576
+ /**
577
+ * Run {@link validateDocument} and throw {@link CadmusValidationError} if any
578
+ * `"error"`-severity violations are found. Warnings are returned (never
579
+ * thrown) so a caller can still surface them. The thrown error's message is
580
+ * a readable, joined summary of every blocking violation.
581
+ */
582
+ async function assertValid(config, doc, options) {
583
+ const violations = await validateDocument(config, doc, options);
584
+ const errors = violations.filter((v) => v.severity === "error");
585
+ if (errors.length > 0) {
586
+ const summary = errors.map((v) => v.message).join("; ");
587
+ throw new CadmusValidationError(`Validation failed for collection "${config.slug}": ${summary}`, violations);
588
+ }
589
+ return violations;
590
+ }
591
+ //#endregion
252
592
  //#region src/cms/localApi.ts
253
593
  function validateRequiredFields(config, input) {
254
594
  for (const [key, field] of Object.entries(flattenFields(config.fields))) {
@@ -429,9 +769,16 @@ function createLocalApi(db, table, config, registry) {
429
769
  },
430
770
  async create(context, input) {
431
771
  await checkAccess(config, "create", context);
432
- const flatData = toFlatDoc(await runBeforeChange(config, input));
772
+ const data = await runBeforeChange(config, input);
773
+ const flatData = toFlatDoc(data);
433
774
  validateRequiredFields(config, flatData);
434
775
  rejectUnknownFields(config, flatData);
776
+ await assertValid(config, data, {
777
+ operation: "create",
778
+ db,
779
+ table,
780
+ registry
781
+ });
435
782
  let row;
436
783
  try {
437
784
  const [inserted] = await db.insert(table).values(flatData).returning();
@@ -446,8 +793,17 @@ function createLocalApi(db, table, config, registry) {
446
793
  },
447
794
  async update(context, id, input) {
448
795
  await checkAccess(config, "update", context);
449
- const flatData = toFlatDoc(await runBeforeChange(config, input));
796
+ const data = await runBeforeChange(config, input);
797
+ const flatData = toFlatDoc(data);
450
798
  rejectUnknownFields(config, flatData);
799
+ await assertValid(config, data, {
800
+ operation: "update",
801
+ id,
802
+ onlyFields: new Set(Object.keys(flatData)),
803
+ db,
804
+ table,
805
+ registry
806
+ });
451
807
  let row;
452
808
  try {
453
809
  const [updated] = await db.update(table).set(flatData).where(eq(idColumn, id)).returning();
@@ -510,6 +866,13 @@ function createVersionedLocalApi(db, table, versionsTable, config, registry) {
510
866
  validateRequiredFields(config, data);
511
867
  rejectUnknownFields(config, data);
512
868
  const parentId = versionRecord.parentId;
869
+ await assertValid(config, data, {
870
+ operation: "update",
871
+ id: parentId,
872
+ db,
873
+ table,
874
+ registry
875
+ });
513
876
  let doc;
514
877
  try {
515
878
  const [row] = await db.update(table).set({
@@ -659,6 +1022,89 @@ function generateSchemaSource(config) {
659
1022
  ].join("\n");
660
1023
  }
661
1024
  //#endregion
1025
+ //#region src/cms/structure.ts
1026
+ /**
1027
+ * Cadmea's Structure Builder — the framework half of issue #12.
1028
+ *
1029
+ * Adopts Sanity's `sanity/structure` idea (pattern, not code): **decouple
1030
+ * the admin nav from the raw collection list.** Instead of mapping every
1031
+ * `config.collections` entry to an `/admin/<slug>` link — which surfaces
1032
+ * system/log tables as editable links and produces dead links — the sidebar
1033
+ * renders from an explicit, grouped structure derived here from each
1034
+ * collection's `admin` hints (see {@link CollectionAdminConfig}) plus
1035
+ * optional per-slug overrides supplied at the call site.
1036
+ *
1037
+ * Pure data in / pure data out: no SolidJS, no DOM, no server imports — so
1038
+ * it's safe to import from a client studio component (e.g. the site's
1039
+ * `PanelNav`) and trivially testable.
1040
+ */
1041
+ /** Default group heading for collections that don't declare `admin.group`. */
1042
+ const DEFAULT_STUDIO_GROUP = "Content";
1043
+ function capitalize(value) {
1044
+ return value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
1045
+ }
1046
+ function resolveAdmin(collection, overrides) {
1047
+ return {
1048
+ ...collection.admin,
1049
+ ...overrides?.[collection.slug]
1050
+ };
1051
+ }
1052
+ /**
1053
+ * Build the studio sidebar structure from a resolved CMS config.
1054
+ *
1055
+ * - Hidden collections (`admin.hidden`) are dropped entirely.
1056
+ * - Each remaining collection is placed in its `admin.group` (or
1057
+ * {@link DEFAULT_STUDIO_GROUP}).
1058
+ * - Within a group, items sort by `admin.order` (ascending; unset sorts
1059
+ * after set), then by their original position in `config.collections` —
1060
+ * so config order is the stable tiebreaker.
1061
+ * - Groups render in `options.groupOrder` first, then first-appearance
1062
+ * order for the rest.
1063
+ *
1064
+ * The input is expected to be the *resolved* config (post-plugins), since
1065
+ * that's what carries plugin-injected collections like `products`.
1066
+ */
1067
+ function buildStudioStructure(config, options = {}) {
1068
+ const basePath = options.basePath ?? "/admin";
1069
+ const groupOrder = options.groupOrder ?? [];
1070
+ const ranked = config.collections.map((collection, index) => ({
1071
+ collection,
1072
+ index,
1073
+ admin: resolveAdmin(collection, options.overrides)
1074
+ }));
1075
+ const groups = /* @__PURE__ */ new Map();
1076
+ const appearance = [];
1077
+ for (const { collection, admin } of ranked) {
1078
+ if (admin.hidden) continue;
1079
+ const title = admin.group ?? "Content";
1080
+ if (!groups.has(title)) {
1081
+ groups.set(title, []);
1082
+ appearance.push(title);
1083
+ }
1084
+ groups.get(title).push({
1085
+ slug: collection.slug,
1086
+ label: admin.label ?? capitalize(collection.slug),
1087
+ href: `${basePath}/${collection.slug}`,
1088
+ readOnly: admin.readOnly ?? false,
1089
+ singleton: admin.singleton ?? false,
1090
+ ...admin.icon ? { icon: admin.icon } : {}
1091
+ });
1092
+ }
1093
+ const meta = new Map(ranked.map(({ collection, index, admin }) => [collection.slug, {
1094
+ order: admin.order ?? Number.POSITIVE_INFINITY,
1095
+ index
1096
+ }]));
1097
+ for (const items of groups.values()) items.sort((a, b) => {
1098
+ const ma = meta.get(a.slug);
1099
+ const mb = meta.get(b.slug);
1100
+ return ma.order - mb.order || ma.index - mb.index;
1101
+ });
1102
+ return [...groupOrder.filter((title) => groups.has(title)), ...appearance.filter((title) => !groupOrder.includes(title))].map((title) => ({
1103
+ title,
1104
+ items: groups.get(title)
1105
+ }));
1106
+ }
1107
+ //#endregion
662
1108
  //#region src/cms/webhooks.ts
663
1109
  /**
664
1110
  * Builds an `afterChange` hook that enqueues a `WebhookMessage` for every
@@ -738,6 +1184,6 @@ async function deliverWebhookMessage(message) {
738
1184
  if (!response.ok) throw new CadmusQueueError(`Webhook delivery to "${message.url}" returned status ${response.status}`);
739
1185
  }
740
1186
  //#endregion
741
- export { can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables };
1187
+ export { DEFAULT_STUDIO_GROUP, Rule, assertValid, buildStudioStructure, can, cmsConfigToSchema, collectionSearchTableName, collectionSearchTableSQL, collectionToTable, collectionVersionsTable, createBlockRegistry, createLocalApi, createVersionedLocalApi, createWebhookHook, defineCmsConfig, defineCollection, defineField, deliverWebhookMessage, extractSearchText, flattenDoc, flattenFields, generateSchemaSource, getCollectionsMeta, getRegisteredApi, nestDoc, relationshipJoinTables, renderBlocksToString, rule, validateDocument };
742
1188
 
743
1189
  //# sourceMappingURL=index.js.map