@byline/core 3.20.4 → 4.0.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 (103) hide show
  1. package/dist/@types/collection-types.d.ts +72 -31
  2. package/dist/@types/db-types.d.ts +93 -47
  3. package/dist/@types/field-data-types.d.ts +8 -10
  4. package/dist/@types/field-data-types.js +1 -1
  5. package/dist/@types/field-data-types.test.node.d.ts +1 -0
  6. package/dist/@types/field-data-types.test.node.js +70 -0
  7. package/dist/@types/field-types.d.ts +25 -1
  8. package/dist/@types/index.d.ts +1 -0
  9. package/dist/@types/index.js +1 -0
  10. package/dist/@types/query-predicate.d.ts +3 -3
  11. package/dist/@types/relation-types.d.ts +38 -0
  12. package/dist/@types/relation-types.js +8 -0
  13. package/dist/@types/search-types.d.ts +3 -4
  14. package/dist/@types/site-config.d.ts +52 -13
  15. package/dist/auth/apply-before-read.d.ts +23 -11
  16. package/dist/auth/apply-before-read.js +139 -33
  17. package/dist/auth/apply-before-read.test.node.js +241 -3
  18. package/dist/auth/index.d.ts +1 -1
  19. package/dist/auth/index.js +1 -1
  20. package/dist/auth/read-context-scope.d.ts +20 -0
  21. package/dist/auth/read-context-scope.js +48 -0
  22. package/dist/codegen/fixtures/all-fields.d.ts +342 -0
  23. package/dist/codegen/fixtures/all-fields.expected.d.ts +120 -0
  24. package/dist/codegen/fixtures/all-fields.expected.js +1 -0
  25. package/dist/codegen/fixtures/all-fields.js +94 -0
  26. package/dist/codegen/index.d.ts +16 -0
  27. package/dist/codegen/index.js +431 -0
  28. package/dist/codegen/index.test.node.d.ts +1 -0
  29. package/dist/codegen/index.test.node.js +230 -0
  30. package/dist/config/attach-hooks.d.ts +25 -0
  31. package/dist/config/attach-hooks.js +130 -0
  32. package/dist/config/attach-hooks.test.node.d.ts +1 -0
  33. package/dist/config/attach-hooks.test.node.js +173 -0
  34. package/dist/config/config-hooks.test.node.d.ts +1 -0
  35. package/dist/config/config-hooks.test.node.js +56 -0
  36. package/dist/config/config.d.ts +9 -5
  37. package/dist/config/config.js +20 -2
  38. package/dist/config/routes.d.ts +5 -5
  39. package/dist/config/routes.js +42 -9
  40. package/dist/config/routes.test.node.d.ts +1 -0
  41. package/dist/config/routes.test.node.js +152 -0
  42. package/dist/core.d.ts +3 -3
  43. package/dist/core.js +20 -14
  44. package/dist/core.test.node.d.ts +1 -0
  45. package/dist/core.test.node.js +28 -0
  46. package/dist/index.d.ts +4 -3
  47. package/dist/index.js +4 -3
  48. package/dist/lib/errors.d.ts +13 -0
  49. package/dist/lib/errors.js +14 -0
  50. package/dist/query/parse-where.d.ts +10 -1
  51. package/dist/query/parse-where.js +146 -2
  52. package/dist/query/parse-where.test.node.js +60 -1
  53. package/dist/schemas/zod/builder.js +7 -3
  54. package/dist/schemas/zod/builder.test.node.d.ts +1 -0
  55. package/dist/schemas/zod/builder.test.node.js +49 -0
  56. package/dist/services/collection-bootstrap.d.ts +1 -1
  57. package/dist/services/collection-bootstrap.test.node.js +30 -59
  58. package/dist/services/discover-counter-groups.d.ts +1 -1
  59. package/dist/services/discover-counter-groups.test.node.js +23 -0
  60. package/dist/services/document-lifecycle/audit.d.ts +22 -1
  61. package/dist/services/document-lifecycle/audit.js +32 -1
  62. package/dist/services/document-lifecycle/create.d.ts +2 -2
  63. package/dist/services/document-lifecycle/create.js +18 -8
  64. package/dist/services/document-lifecycle/delete.d.ts +17 -1
  65. package/dist/services/document-lifecycle/delete.js +91 -26
  66. package/dist/services/document-lifecycle/index.d.ts +1 -1
  67. package/dist/services/document-lifecycle/internals.d.ts +0 -20
  68. package/dist/services/document-lifecycle/internals.js +22 -46
  69. package/dist/services/document-lifecycle/status.d.ts +1 -1
  70. package/dist/services/document-lifecycle/status.js +20 -3
  71. package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
  72. package/dist/services/document-lifecycle/system-fields.js +112 -74
  73. package/dist/services/document-lifecycle/tree.d.ts +22 -24
  74. package/dist/services/document-lifecycle/tree.js +244 -123
  75. package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
  76. package/dist/services/document-lifecycle/tree.test.node.js +663 -0
  77. package/dist/services/document-lifecycle/update.d.ts +4 -4
  78. package/dist/services/document-lifecycle/update.js +11 -5
  79. package/dist/services/document-lifecycle.test.node.js +426 -16
  80. package/dist/services/document-read.d.ts +14 -6
  81. package/dist/services/document-read.js +47 -9
  82. package/dist/services/field-upload.test.node.js +70 -0
  83. package/dist/services/index.d.ts +3 -2
  84. package/dist/services/index.js +3 -2
  85. package/dist/services/normalize-numeric-fields.d.ts +23 -0
  86. package/dist/services/normalize-numeric-fields.js +89 -0
  87. package/dist/services/normalize-numeric-fields.test.node.d.ts +8 -0
  88. package/dist/services/normalize-numeric-fields.test.node.js +99 -0
  89. package/dist/services/populate.d.ts +9 -23
  90. package/dist/services/populate.js +180 -28
  91. package/dist/services/populate.test.node.js +59 -0
  92. package/dist/services/richtext-embed.d.ts +39 -2
  93. package/dist/services/richtext-embed.js +4 -34
  94. package/dist/services/richtext-embed.test.node.js +15 -0
  95. package/dist/services/richtext-populate.d.ts +21 -3
  96. package/dist/services/richtext-populate.js +160 -19
  97. package/dist/services/richtext-populate.test.node.js +523 -2
  98. package/dist/services/validate-search-config.d.ts +1 -1
  99. package/dist/storage/collection-fingerprint.js +6 -0
  100. package/dist/storage/collection-fingerprint.test.node.js +21 -0
  101. package/dist/utils/root-relative-redirect.d.ts +6 -0
  102. package/dist/utils/root-relative-redirect.js +37 -0
  103. package/package.json +7 -2
@@ -124,10 +124,10 @@ export interface UploadConfig {
124
124
  * have been written to the storage provider, before the document
125
125
  * version is created.
126
126
  *
127
- * Accepts an inline object, or because the schema is isomorphic a
128
- * **loader** (`hooks: () => import('./media.hooks.js')`) that defers the
129
- * hooks module so server-only code (storage SDKs, `sharp`, AV scanners)
130
- * never enters the client bundle. See {@link UploadHooksLoader}.
127
+ * Accepts an inline object or a lazy loader. Both remain reachable from an
128
+ * isomorphic schema, so use them only for isomorphic-safe hooks. Hooks that
129
+ * import server-only code belong in `ServerConfig.hooks.uploads`, registered
130
+ * from the server entry. See {@link UploadHooksLoader}.
131
131
  *
132
132
  * @see UploadHooks
133
133
  */
@@ -461,6 +461,32 @@ export interface AfterUpdateContext {
461
461
  locale: string;
462
462
  };
463
463
  }
464
+ /**
465
+ * Context passed to `afterSystemFieldsChange` after a non-versioned document
466
+ * path and/or advertised-locale change commits successfully.
467
+ *
468
+ * Both snapshots are included so cache and search consumers can remove stale
469
+ * paths and reconcile locale-specific output without re-reading the old state.
470
+ */
471
+ export interface SystemFieldsChangeContext {
472
+ documentId: string;
473
+ collectionPath: string;
474
+ /** Fields explicitly supplied by the caller, including a no-op retry. */
475
+ requested: {
476
+ path: boolean;
477
+ availableLocales: boolean;
478
+ };
479
+ changed: {
480
+ path: boolean;
481
+ availableLocales: boolean;
482
+ };
483
+ /** True when a no-op request deliberately re-runs post-commit reconciliation. */
484
+ reconciliation: boolean;
485
+ previousPath?: string;
486
+ currentPath?: string;
487
+ previousAvailableLocales: string[];
488
+ currentAvailableLocales: string[];
489
+ }
464
490
  /**
465
491
  * Context passed to `beforeStatusChange` / `afterStatusChange` hooks.
466
492
  *
@@ -700,16 +726,13 @@ export interface UploadHooks {
700
726
  * `() => import('./media.hooks.js')` works directly against an
701
727
  * `export default { … } satisfies UploadHooks`).
702
728
  *
703
- * Same rationale as {@link CollectionHooksLoader}: upload hooks are declared
704
- * on a field *inside the collection schema*, which is **isomorphic** (bundled
705
- * into the client admin). `beforeStore` / `afterStore` bodies typically reach
706
- * for server-only code — storage SDKs, `sharp`, AV scanners, `node:crypto`
707
- * so declaring them inline drags that graph into the client bundle. The
708
- * loader form defers the hooks module behind a dynamic `import()`, keeping it
709
- * structurally absent from the client.
729
+ * For hooks that are safe in both browser and server graphs, schemas may use
730
+ * this loader form directly. A dynamic import in an isomorphic schema is not
731
+ * by itself a client boundary: server-only hook modules must instead be
732
+ * registered through `ServerConfig.hooks.uploads`, from a server-only entry.
710
733
  *
711
734
  * @example
712
- * // media.schema.ts — isomorphic, client-safe by construction
735
+ * // media.schema.ts — only for isomorphic-safe hooks
713
736
  * {
714
737
  * name: 'image',
715
738
  * type: 'image',
@@ -719,7 +742,7 @@ export interface UploadHooks {
719
742
  * },
720
743
  * }
721
744
  *
722
- * // media.hooks.ts — server-only; may import any server-only module freely
745
+ * // media.hooks.ts — must remain safe for every graph that imports the schema
723
746
  * export default { afterStore: (ctx) => { … } } satisfies UploadHooks
724
747
  */
725
748
  export type UploadHooksLoader = () => Promise<UploadHooks | {
@@ -747,12 +770,15 @@ export declare function resolveUploadHooks(hooks: UploadHooks | UploadHooksLoade
747
770
  * - `find`, `findOne`, `findById`, `findByPath` on `CollectionHandle`
748
771
  * (once per returned source document)
749
772
  * - Each populated relation target across every depth level
773
+ * - Rich-text targets, tree/search hydration, and historical versions
750
774
  *
751
775
  * The hook receives the **raw storage shape** (`{document_version_id,
752
776
  * document_id, path, status, created_at, updated_at, fields, …}`), not
753
777
  * the camelCase `ClientDocument` — afterRead runs *before* the client's
754
778
  * response shaping pass so mutations to `fields` propagate cleanly.
755
779
  * Mutations persist in place; there is no return value.
780
+ * `requestContext` is the immutable operation-scoped identity and effective
781
+ * read mode, enabling actor-dependent field redaction on every path.
756
782
  *
757
783
  * Fires **after** populate on the source document, so hooks can observe
758
784
  * (and mutate) the fully populated tree.
@@ -767,6 +793,8 @@ export interface AfterReadContext {
767
793
  /** The raw reconstructed document. Mutate in place — changes persist. */
768
794
  doc: Record<string, any>;
769
795
  collectionPath: string;
796
+ /** Authenticated identity cloned for this operation's effective read mode. */
797
+ requestContext: RequestContext;
770
798
  /** Thread this into any nested reads the hook performs. */
771
799
  readContext: ReadContext;
772
800
  }
@@ -780,10 +808,10 @@ export interface AfterReadContext {
780
808
  * emits, then ANDs onto whatever the caller passed in `where`.
781
809
  *
782
810
  * Returning `undefined` (or simply `void`) means "no scoping" — typically
783
- * the superuser / unconditional-read branch. Use a sentinel predicate that
784
- * yields no rows (e.g. `{ id: '__none__' }`) when the actor cannot read
785
- * anything; do not throw, because callers expect empty list results rather
786
- * than collapsed endpoints.
811
+ * the superuser / unconditional-read branch. Return `{ id: { $in: [] } }`
812
+ * when the actor cannot read anything; it compiles to an always-false SQL
813
+ * predicate without passing an invalid UUID to Postgres. Do not throw, because
814
+ * callers expect empty list results rather than collapsed endpoints.
787
815
  *
788
816
  * The hook receives:
789
817
  * - `requestContext` — the authenticated request, including `actor`. The
@@ -853,6 +881,13 @@ export interface CollectionHooks {
853
881
  beforeUpdate?: CollectionHookSlot<BeforeUpdateContext>;
854
882
  /** Runs after an existing document is updated. */
855
883
  afterUpdate?: CollectionHookSlot<AfterUpdateContext>;
884
+ /**
885
+ * Runs after an actual path and/or advertised-locale change commits, or for
886
+ * an explicit no-op reconciliation retry. A failure rejects the lifecycle
887
+ * call but cannot roll back the already-committed system-field write/audit;
888
+ * retry with reconciliation enabled to run post-commit side effects again.
889
+ */
890
+ afterSystemFieldsChange?: CollectionHookSlot<SystemFieldsChangeContext>;
856
891
  /** Runs before a document's workflow status is changed. */
857
892
  beforeStatusChange?: CollectionHookSlot<StatusChangeContext>;
858
893
  /** Runs after a document's workflow status has been changed. */
@@ -902,23 +937,20 @@ export interface CollectionHooks {
902
937
  * (so `() => import('./docs.hooks.js')` works directly against an
903
938
  * `export default { … } satisfies CollectionHooks`).
904
939
  *
905
- * Why this exists: a `CollectionDefinition` is **isomorphic** the same
906
- * schema module is bundled into the *client* admin as well as the server.
907
- * Any module the schema *statically imports* is dragged into the client
908
- * bundle, so a hook body that imports server-only code (cache invalidation,
909
- * queue clients, Node built-ins) leaks that entire graph into the browser.
910
- * The loader form defers the hooks module behind a dynamic `import()`, so
911
- * the hooks module and its server-only graph are *structurally absent* from
912
- * the client — no per-call-site SSR guards required.
940
+ * A loader attached directly to an isomorphic schema is suitable only when
941
+ * its hook module is safe in every graph that imports that schema. Dynamic
942
+ * imports may still become browser chunks. Register hooks that import
943
+ * server-only code through `ServerConfig.hooks.collections` from a server-only
944
+ * entry instead.
913
945
  *
914
946
  * @example
915
- * // docs.schema.ts — isomorphic, client-safe by construction
947
+ * // docs.schema.ts — only for isomorphic-safe hooks
916
948
  * export const Docs = defineCollection({
917
949
  * // …declarative field config…
918
950
  * hooks: () => import('./docs.hooks.js'),
919
951
  * })
920
952
  *
921
- * // docs.hooks.ts — server-only; may import any server-only module freely
953
+ * // docs.hooks.ts — must remain safe for every graph that imports the schema
922
954
  * import { invalidateDocument } from '@/lib/cache/with-cache'
923
955
  * export default {
924
956
  * afterCreate: ({ collectionPath, path }) => invalidateDocument(collectionPath, path),
@@ -972,12 +1004,15 @@ export interface CollectionDefinition {
972
1004
  /**
973
1005
  * Lifecycle hooks for server-side document operations.
974
1006
  *
975
- * Two forms:
1007
+ * Two definition-attached forms:
976
1008
  * - **Inline** (`hooks: { afterCreate, … }`) — valid for hooks whose
977
1009
  * bodies only touch isomorphic / declarative code.
978
- * - **Loader** (`hooks: () => import('./docs.hooks.js')`) — defers the
979
- * hooks module behind a dynamic `import()` so server-only code never
980
- * enters the client bundle. See {@link CollectionHooksLoader}.
1010
+ * - **Loader** (`hooks: () => import('./docs.hooks.js')`) — lazy, but still
1011
+ * part of the schema's reachable module graph. See
1012
+ * {@link CollectionHooksLoader}.
1013
+ *
1014
+ * Hooks that import server-only modules belong in
1015
+ * `ServerConfig.hooks.collections`, registered from the server entry.
981
1016
  */
982
1017
  hooks?: CollectionHooks | CollectionHooksLoader;
983
1018
  /**
@@ -1187,6 +1222,12 @@ export interface CollectionDefinition {
1187
1222
  export declare function defineCollection<const C extends CollectionDefinition>(definition: C & CollectionDefinition): C;
1188
1223
  export type CollectionFieldData<C extends CollectionDefinition> = FieldSetData<C['fields']>;
1189
1224
  export type CollectionFieldDataAllLocales<C extends CollectionDefinition> = FieldSetDataAllLocales<C['fields']>;
1225
+ /** Broad collection registry used when an application has not supplied its schema types. */
1226
+ export type CollectionRegistry = Record<string, Record<string, any>>;
1227
+ /** Infer a path-to-fields registry from a shared readonly collection-definition tuple. */
1228
+ export type InferCollectionRegistry<TCollections extends readonly CollectionDefinition[]> = {
1229
+ [TCollection in TCollections[number] as TCollection['path']]: CollectionFieldData<TCollection>;
1230
+ };
1190
1231
  /**
1191
1232
  * Type-safe factory for creating a Block. Returns the definition as-is,
1192
1233
  * but locks in literal types for `blockType`, field names, and select
@@ -61,24 +61,10 @@ export interface ReadContext {
61
61
  */
62
62
  visited: Set<string>;
63
63
  /**
64
- * Composite keys (`${collection_path}:${document_id}`) for every
65
- * document whose `afterRead` hook has fired during this request. Used
66
- * to enforce "each document runs through `afterRead` at most once per
67
- * logical request" — the rule that forecloses the A→B→A loop when a
68
- * hook performs its own reads.
64
+ * @deprecated Ignored. `beforeRead` authorization state is module-private
65
+ * and authority-bound; this optional slot remains only for source compatibility.
69
66
  */
70
- afterReadFired: Set<string>;
71
- /**
72
- * Per-request memoisation of `beforeRead` hook results, keyed by
73
- * `collectionPath`. Populate fans out across many source documents and
74
- * many target-collection batches; without a cache, an async hook
75
- * (e.g. resolving the actor's tenant id) would re-run on every batch.
76
- * Keyed by collection path because the actor is invariant for the
77
- * lifetime of one `ReadContext`. `null` records "hook ran and returned
78
- * void" (i.e. no scoping applies); absence records "hook has not been
79
- * run yet for this collection".
80
- */
81
- beforeReadCache: Map<string, QueryPredicate | null>;
67
+ beforeReadCache?: Map<string, QueryPredicate | null>;
82
68
  /** Monotonic count of document materialisations; compared against `maxReads`. */
83
69
  readCount: number;
84
70
  /** Hard ceiling on materialisations per request. Default 500. */
@@ -225,39 +211,28 @@ export interface IDbAdapter {
225
211
  collections: ICollectionCommands;
226
212
  documents: IDocumentCommands;
227
213
  counters: ICounterCommands;
228
- /**
229
- * Append-only audit-log writes (docs/06-auth-and-security/02-auditability.md — Workstream 2). Optional
230
- * capability, paired with `withTransaction`: a consumer that records audit
231
- * entries asserts both are present and throws otherwise (it must never
232
- * silently skip the audit row). Adapters that model the audit log
233
- * implement it; others omit it.
234
- */
235
- audit?: IAuditCommands;
214
+ /** Append-only audit-log writes, committed atomically via `withTransaction`. */
215
+ audit: IAuditCommands;
236
216
  };
237
217
  queries: {
238
218
  collections: ICollectionQueries;
239
219
  documents: IDocumentQueries;
240
- /** Audit-log reads — per-document history, system-wide report. See docs/06-auth-and-security/02-auditability.md. */
241
- audit?: IAuditQueries;
220
+ /** Audit-log reads — per-document history, system-wide report. */
221
+ audit: IAuditQueries;
242
222
  };
243
223
  /**
244
- * Optional capability: run `fn` inside a single database transaction so the
224
+ * Run `fn` inside a single database transaction so the
245
225
  * writes it performs commit or roll back atomically. The adapter propagates
246
226
  * the transaction to every `commands.*` call made within `fn` (see
247
227
  * docs/03-architecture/03-transactions.md — AsyncLocalStorage propagation), so a service can
248
228
  * compose multiple commands into one unit of work without threading a
249
229
  * transaction handle through their signatures.
250
230
  *
251
- * **Loud-failure contract.** Optional because not every adapter can provide
252
- * interactive transactions a pure HTTP-gateway serverless driver (Neon
253
- * HTTP, Cloudflare D1, …) cannot. An adapter that cannot **must omit this
254
- * method** (or implement it to throw); a consumer that requires atomicity
255
- * (e.g. the audit log) MUST assert its presence and throw — never silently
256
- * run non-atomically, which would defeat the very guarantee it provides. See
257
- * docs/03-architecture/03-transactions.md ("Serverless / HTTP-gateway databases — the contract
258
- * seam").
231
+ * Canonical adapters must provide interactive transactions because audited
232
+ * lifecycle mutations cannot safely degrade to non-atomic writes. Runtime
233
+ * lifecycle guards remain in place for untyped JavaScript adapters.
259
234
  */
260
- withTransaction?: <T>(fn: () => Promise<T>) => Promise<T>;
235
+ withTransaction: <T>(fn: () => Promise<T>) => Promise<T>;
261
236
  /**
262
237
  * Optional maintenance: stamp `source_locale` (the per-document content
263
238
  * anchor) on documents created before the column existed, setting NULL rows
@@ -474,6 +449,34 @@ export interface ICollectionCommands {
474
449
  }): Promise<any>;
475
450
  delete(id: string): Promise<any>;
476
451
  }
452
+ /** Locked structural state returned by tree mutation commands. */
453
+ export interface TreePlacementState {
454
+ placed: boolean;
455
+ parentDocumentId: string | null;
456
+ orderKey: string | null;
457
+ index: number | null;
458
+ }
459
+ /** Result of an atomic place/reorder/remove tree command. */
460
+ export interface TreeMutationResult {
461
+ changed: boolean;
462
+ before: TreePlacementState;
463
+ after: TreePlacementState;
464
+ /** The pre-mutation sibling group, used only for post-commit hook fan-out. */
465
+ beforeSiblingDocumentIds: string[];
466
+ /** Locked pre-mutation node + descendants, populated when requested for hooks. */
467
+ beforeSubtreeDocumentIds: string[];
468
+ }
469
+ /** One child promoted to root while deleting its parent. */
470
+ export interface TreePromotionChange {
471
+ documentId: string;
472
+ before: TreePlacementState;
473
+ after: TreePlacementState;
474
+ }
475
+ /** Atomic edge reconciliation performed as part of document deletion. */
476
+ export interface TreeDeleteMutationResult {
477
+ removed: TreeMutationResult;
478
+ promoted: TreePromotionChange[];
479
+ }
477
480
  export interface IDocumentCommands {
478
481
  createDocumentVersion(params: {
479
482
  documentId?: string;
@@ -654,9 +657,9 @@ export interface IDocumentCommands {
654
657
  parentDocumentId: string | null;
655
658
  beforeDocumentId?: string | null;
656
659
  afterDocumentId?: string | null;
657
- }): Promise<{
658
- orderKey: string;
659
- }>;
660
+ /** Internal create/self-heal guard: leave an already-placed node untouched. */
661
+ ifUnplaced?: boolean;
662
+ }): Promise<TreeMutationResult>;
660
663
  /**
661
664
  * Remove a document's edge row, returning it to the *unplaced* state (in the
662
665
  * collection, but not in the tree). Distinct from document deletion — the
@@ -664,8 +667,21 @@ export interface IDocumentCommands {
664
667
  * unplaced. See docs/04-collections/03-document-trees.md.
665
668
  */
666
669
  removeFromTree(params: {
670
+ collectionId: string;
667
671
  documentId: string;
668
- }): Promise<void>;
672
+ /** Capture the authoritative pre-removal subtree under the tree lock. */
673
+ includeSubtree?: boolean;
674
+ }): Promise<TreeMutationResult>;
675
+ /**
676
+ * Atomically promote a node's direct children to roots and remove its edge,
677
+ * returning locked before/after states for parent and child audit rows. This
678
+ * capability is required by the canonical adapter contract and is also
679
+ * validated at runtime for untyped JavaScript adapters.
680
+ */
681
+ promoteChildrenAndRemoveFromTree(params: {
682
+ collectionId: string;
683
+ documentId: string;
684
+ }): Promise<TreeDeleteMutationResult>;
669
685
  }
670
686
  export interface ICollectionQueries {
671
687
  getAllCollections(): Promise<any[]>;
@@ -673,6 +689,20 @@ export interface ICollectionQueries {
673
689
  getCollectionById(id: string): Promise<any>;
674
690
  }
675
691
  export interface IDocumentQueries {
692
+ /**
693
+ * Lock a logical document as the transaction mutex for its non-versioned
694
+ * system fields, then return their authoritative current values. Must be
695
+ * called inside `IDbAdapter.withTransaction`. Canonical adapters must support
696
+ * this locked read so system-field audit rows use an authoritative snapshot.
697
+ */
698
+ getDocumentSystemFieldsForUpdate(params: {
699
+ collection_id: string;
700
+ document_id: string;
701
+ }): Promise<{
702
+ source_locale: string;
703
+ path: string | null;
704
+ availableLocales: string[];
705
+ } | null>;
676
706
  getDocumentById(params: {
677
707
  collection_id: string;
678
708
  document_id: string;
@@ -771,7 +801,11 @@ export interface IDocumentQueries {
771
801
  getDocumentByVersion(params: {
772
802
  document_version_id: string;
773
803
  locale?: string;
774
- }): Promise<any>;
804
+ /** Optional collection ownership gate for client-facing historical reads. */
805
+ collection_id?: string;
806
+ /** Historical-row `beforeRead` predicates compiled against this version. */
807
+ filters?: DocumentFilter[];
808
+ }): Promise<any | null>;
775
809
  getDocumentsByVersionIds(params: {
776
810
  document_version_ids: string[];
777
811
  locale?: string;
@@ -809,6 +843,8 @@ export interface IDocumentQueries {
809
843
  order?: string;
810
844
  desc?: boolean;
811
845
  query?: string;
846
+ /** Historical-row `beforeRead` predicates applied before count/pagination. */
847
+ filters?: DocumentFilter[];
812
848
  }): Promise<{
813
849
  documents: any[];
814
850
  meta: {
@@ -981,14 +1017,16 @@ export interface IDocumentQueries {
981
1017
  * unpublished ancestor** — it does *not* skip past it. So an unpublished
982
1018
  * mid-spine node truncates the returned chain, which the hierarchical-URL
983
1019
  * splat handler turns into a 404 (the "unpublished node hides its subtree"
984
- * decision, enforced at the URL layer). `readMode: 'any'` (the default) walks
985
- * the raw edges, matching the breadcrumb/lifecycle use that wants every
986
- * ancestor regardless of status.
1020
+ * decision, enforced at the URL layer). Optional `filters` apply the same
1021
+ * edge rule for `beforeRead`, including visibility of the queried node.
1022
+ * `readMode: 'any'` (the default) otherwise walks current non-deleted rows.
987
1023
  */
988
1024
  getTreeAncestors(params: {
989
1025
  document_id: string;
990
1026
  maxDepth?: number;
991
1027
  readMode?: ReadMode;
1028
+ locale?: string;
1029
+ filters?: DocumentFilter[];
992
1030
  }): Promise<Array<{
993
1031
  document_id: string;
994
1032
  depth: number;
@@ -1018,13 +1056,18 @@ export interface IDocumentQueries {
1018
1056
  * `getTreeAncestors` returns `[]` for *both* a root and an unplaced node, so it
1019
1057
  * cannot tell them apart; this primitive can. Backs the update-time self-heal
1020
1058
  * (re-root a stray doc) and the authoring widget's "Add to tree" vs "Top level"
1021
- * distinction.
1059
+ * distinction. With visibility filters, a hidden queried node reports
1060
+ * unplaced; a hidden parent is redacted to null while `placed` remains true.
1022
1061
  */
1023
1062
  getTreeParent(params: {
1024
1063
  document_id: string;
1064
+ readMode?: ReadMode;
1065
+ locale?: string;
1066
+ filters?: DocumentFilter[];
1025
1067
  }): Promise<{
1026
1068
  placed: boolean;
1027
1069
  parentDocumentId: string | null;
1070
+ parentRedacted?: true;
1028
1071
  }>;
1029
1072
  /**
1030
1073
  * Read a node's subtree as a flat, **pre-order (depth-first)** list — each
@@ -1043,7 +1086,8 @@ export interface IDocumentQueries {
1043
1086
  * nodes, an unpublished node's entire subtree is omitted — the spine is
1044
1087
  * broken and descendants are not promoted (see docs/04-collections/03-document-trees.md).
1045
1088
  * `readMode: 'any'` (the default) includes every current, non-deleted node.
1046
- * Depth-bounded by `maxDepth` as a backstop.
1089
+ * Optional `filters` enforce `beforeRead` at every edge with the same
1090
+ * no-promotion semantics. Depth-bounded by `maxDepth` as a backstop.
1047
1091
  *
1048
1092
  * Returns structure only; hydrate content via `getDocumentsByDocumentIds`.
1049
1093
  */
@@ -1052,6 +1096,8 @@ export interface IDocumentQueries {
1052
1096
  rootDocumentId?: string | null;
1053
1097
  maxDepth?: number;
1054
1098
  readMode?: ReadMode;
1099
+ locale?: string;
1100
+ filters?: DocumentFilter[];
1055
1101
  }): Promise<Array<{
1056
1102
  document_id: string;
1057
1103
  parent_document_id: string | null;
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { ArrayField, BlocksField, Field, FieldSet, GroupField, LocalizedField, OptionalField, SelectField } from './field-types.js';
8
+ import type { ArrayField, BlocksField, Field, FieldSet, GroupField, LocalizedField, OptionalField, RelationField, SelectField } from './field-types.js';
9
+ import type { RelatedDocumentValue } from './relation-types.js';
9
10
  import type { Prettify, ValueUnion } from './type-utils.js';
10
11
  /**
11
12
  * Structural type for any JSON-serializable value. Used as the type for
@@ -55,9 +56,12 @@ type BlocksFieldData<T extends BlocksField> = Array<Prettify<ValueUnion<{
55
56
  _type: K['blockType'];
56
57
  } & FieldSetData<K['fields']>>;
57
58
  }>>>;
59
+ type RelationFieldData<T extends RelationField> = T extends {
60
+ hasMany: true;
61
+ } ? RelatedDocumentValue[] : RelatedDocumentValue;
58
62
  type BaseFieldData<T extends Field> = T extends ArrayField ? Array<Prettify<{
59
63
  _id: string;
60
- } & FieldSetData<T['fields']>>> : T extends BlocksField ? Prettify<BlocksFieldData<T>> : T extends GroupField ? FieldSetData<T['fields']> : T extends SelectField ? T['options'][number]['value'] : BaseFieldDataTypes[T['type']];
64
+ } & FieldSetData<T['fields']>>> : T extends BlocksField ? Prettify<BlocksFieldData<T>> : T extends GroupField ? FieldSetData<T['fields']> : T extends SelectField ? T['options'][number]['value'] : T extends RelationField ? RelationFieldData<T> : BaseFieldDataTypes[T['type']];
61
65
  export type FieldData<T extends Field = Field> = T extends OptionalField ? BaseFieldData<T> | undefined : BaseFieldData<T>;
62
66
  export type FieldSetData<T extends FieldSet = FieldSet> = Prettify<{
63
67
  -readonly [F in T[number] as F extends OptionalField ? never : F['name']]: FieldData<F>;
@@ -75,7 +79,7 @@ type BlocksFieldDataAllLocales<T extends BlocksField> = Array<Prettify<ValueUnio
75
79
  }>>>;
76
80
  type BaseFieldDataAllLocales<T extends Field> = T extends ArrayField ? Array<Prettify<{
77
81
  _id: string;
78
- } & FieldSetDataAllLocales<T['fields']>>> : T extends BlocksField ? Prettify<BlocksFieldDataAllLocales<T>> : T extends GroupField ? FieldSetDataAllLocales<T['fields']> : T extends SelectField ? T['options'][number]['value'] : BaseFieldDataTypes[T['type']];
82
+ } & FieldSetDataAllLocales<T['fields']>>> : T extends BlocksField ? Prettify<BlocksFieldDataAllLocales<T>> : T extends GroupField ? FieldSetDataAllLocales<T['fields']> : T extends SelectField ? T['options'][number]['value'] : T extends RelationField ? RelationFieldData<T> : BaseFieldDataTypes[T['type']];
79
83
  type LocalizedFieldDataAllLocales<T extends Field> = T extends LocalizedField ? PerLocale<BaseFieldDataAllLocales<T>> : BaseFieldDataAllLocales<T>;
80
84
  export type FieldDataAllLocales<T extends Field = Field> = T extends OptionalField ? LocalizedFieldDataAllLocales<T> | undefined : LocalizedFieldDataAllLocales<T>;
81
85
  export type FieldSetDataAllLocales<T extends FieldSet = FieldSet> = Prettify<{
@@ -135,7 +139,7 @@ export interface PendingStoredFileValue {
135
139
  filename: string;
136
140
  originalFilename: string;
137
141
  mimeType: string;
138
- fileSize: string;
142
+ fileSize: number;
139
143
  storageProvider: 'pending';
140
144
  storagePath: '';
141
145
  storageUrl: string;
@@ -157,10 +161,4 @@ export declare function createPendingStoredFileValue(file: File, previewUrl: str
157
161
  width: number;
158
162
  height: number;
159
163
  }): PendingStoredFileValue;
160
- export interface RelatedDocumentValue {
161
- targetDocumentId: string;
162
- targetCollectionId: string;
163
- relationshipType?: string;
164
- cascadeDelete?: boolean;
165
- }
166
164
  export {};
@@ -23,7 +23,7 @@ export function createPendingStoredFileValue(file, previewUrl, dimensions) {
23
23
  filename: file.name,
24
24
  originalFilename: file.name,
25
25
  mimeType: file.type,
26
- fileSize: String(file.size),
26
+ fileSize: file.size,
27
27
  storageProvider: 'pending',
28
28
  storagePath: '',
29
29
  storageUrl: previewUrl,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { File } from 'node:buffer';
2
+ import { describe, expect, expectTypeOf, it } from 'vitest';
3
+ import { defineCollection } from './collection-types.js';
4
+ import { createPendingStoredFileValue } from './field-data-types.js';
5
+ const RelationShapes = defineCollection({
6
+ path: 'relation-shapes',
7
+ labels: { singular: 'Relation shape', plural: 'Relation shapes' },
8
+ fields: [
9
+ { name: 'single', type: 'relation', targetCollection: 'people' },
10
+ { name: 'many', type: 'relation', targetCollection: 'people', hasMany: true },
11
+ {
12
+ name: 'group',
13
+ type: 'group',
14
+ fields: [
15
+ { name: 'single', type: 'relation', targetCollection: 'people' },
16
+ { name: 'many', type: 'relation', targetCollection: 'people', hasMany: true },
17
+ ],
18
+ },
19
+ {
20
+ name: 'items',
21
+ type: 'array',
22
+ fields: [
23
+ { name: 'single', type: 'relation', targetCollection: 'people' },
24
+ { name: 'many', type: 'relation', targetCollection: 'people', hasMany: true },
25
+ ],
26
+ },
27
+ {
28
+ name: 'content',
29
+ type: 'blocks',
30
+ blocks: [
31
+ {
32
+ blockType: 'relations',
33
+ fields: [
34
+ { name: 'single', type: 'relation', targetCollection: 'people' },
35
+ { name: 'many', type: 'relation', targetCollection: 'people', hasMany: true },
36
+ ],
37
+ },
38
+ ],
39
+ },
40
+ { name: 'localizedTitle', type: 'text', localized: true },
41
+ ],
42
+ });
43
+ describe('field data contracts', () => {
44
+ it('infers single and hasMany relations through nested structures', () => {
45
+ expectTypeOf().toEqualTypeOf();
46
+ expectTypeOf().toEqualTypeOf();
47
+ expectTypeOf().toEqualTypeOf();
48
+ expectTypeOf().toEqualTypeOf();
49
+ expectTypeOf().toEqualTypeOf();
50
+ expectTypeOf().toEqualTypeOf();
51
+ expectTypeOf().toEqualTypeOf();
52
+ expectTypeOf().toEqualTypeOf();
53
+ expectTypeOf().toEqualTypeOf();
54
+ expectTypeOf().toEqualTypeOf();
55
+ expectTypeOf().toEqualTypeOf();
56
+ expectTypeOf().toEqualTypeOf();
57
+ expectTypeOf().toEqualTypeOf();
58
+ expectTypeOf().toEqualTypeOf();
59
+ });
60
+ it('keeps a pending file size numeric', () => {
61
+ const pending = createPendingStoredFileValue(new File(['content'], 'fixture.txt', { type: 'text/plain' }), 'blob:fixture');
62
+ expect(pending.fileSize).toBe(7);
63
+ expectTypeOf(pending.fileSize).toEqualTypeOf();
64
+ });
65
+ it('exposes generic single and hasMany relation read envelopes', () => {
66
+ expectTypeOf().toEqualTypeOf();
67
+ expectTypeOf().toEqualTypeOf();
68
+ expectTypeOf().toEqualTypeOf();
69
+ });
70
+ });
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
+ import type { RequestContext } from '@byline/auth';
8
9
  import type { UploadConfig } from './collection-types.js';
10
+ import type { ReadMode } from './db-types.js';
9
11
  import type { MaybePromise, NonEmptyArray } from './type-utils.js';
10
12
  /**
11
13
  * Context passed to functions that generate default values for fields,
@@ -703,7 +705,7 @@ export type RichTextEditorComponent = SlotComponent<RichTextEditorProps>;
703
705
  * Context passed to the richtext populate function for one rich-text field
704
706
  * value. Mirrors the shape consumed by the relation-field populate
705
707
  * primitive — `readContext` is the same request-scoped context, so the
706
- * visited set / read budget / `afterReadFired` machinery covers rich-text
708
+ * visited set / read budget / `afterRead` recursion machinery covers rich-text
707
709
  * fan-out automatically and any nested reads the adapter performs.
708
710
  *
709
711
  * The adapter mutates `value` in place — typically by walking the editor's
@@ -724,7 +726,23 @@ export interface RichTextPopulateContext {
724
726
  * read they perform via `_readContext`.
725
727
  */
726
728
  readContext: import('./db-types.js').ReadContext;
729
+ /** Authenticated context cloned for this operation's effective read mode. */
730
+ requestContext: RequestContext;
731
+ /** Effective source-view selector inherited from the containing read. */
732
+ readMode: ReadMode;
733
+ /**
734
+ * Framework-owned secure target reader. Editor adapters must use this
735
+ * instead of direct adapter access so target abilities and `beforeRead`
736
+ * predicates are enforced uniformly.
737
+ */
738
+ readDocuments: RichTextReadDocumentsFn;
739
+ }
740
+ export interface RichTextReadDocumentsInput {
741
+ collectionPath: string;
742
+ documentIds: string[];
743
+ fields?: string[];
727
744
  }
745
+ export type RichTextReadDocumentsFn = (input: RichTextReadDocumentsInput) => Promise<Array<Record<string, any>>>;
728
746
  /**
729
747
  * Server-side populate function contract. Editor adapters export an
730
748
  * implementation; installations register one via
@@ -760,6 +778,12 @@ export interface RichTextEmbedContext {
760
778
  * budget machinery with the rest of the framework.
761
779
  */
762
780
  readContext: import('./db-types.js').ReadContext;
781
+ /** Authenticated write-operation context used for target read checks. */
782
+ requestContext: RequestContext;
783
+ /** Embed refreshes resolve the public/published target view. */
784
+ readMode: ReadMode;
785
+ /** Framework-owned ability- and beforeRead-aware target reader. */
786
+ readDocuments: RichTextReadDocumentsFn;
763
787
  }
764
788
  /**
765
789
  * Server-side embed function contract. Editor adapters export an
@@ -12,6 +12,7 @@ export * from './field-data-types.js';
12
12
  export * from './field-types.js';
13
13
  export * from './populate-types.js';
14
14
  export * from './query-predicate.js';
15
+ export * from './relation-types.js';
15
16
  export * from './search-types.js';
16
17
  export * from './site-config.js';
17
18
  export * from './storage-types.js';
@@ -12,6 +12,7 @@ export * from './field-data-types.js';
12
12
  export * from './field-types.js';
13
13
  export * from './populate-types.js';
14
14
  export * from './query-predicate.js';
15
+ export * from './relation-types.js';
15
16
  export * from './search-types.js';
16
17
  export * from './site-config.js';
17
18
  export * from './storage-types.js';