@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.
- package/dist/@types/collection-types.d.ts +72 -31
- package/dist/@types/db-types.d.ts +93 -47
- package/dist/@types/field-data-types.d.ts +8 -10
- package/dist/@types/field-data-types.js +1 -1
- package/dist/@types/field-data-types.test.node.d.ts +1 -0
- package/dist/@types/field-data-types.test.node.js +70 -0
- package/dist/@types/field-types.d.ts +25 -1
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/@types/query-predicate.d.ts +3 -3
- package/dist/@types/relation-types.d.ts +38 -0
- package/dist/@types/relation-types.js +8 -0
- package/dist/@types/search-types.d.ts +3 -4
- package/dist/@types/site-config.d.ts +52 -13
- package/dist/auth/apply-before-read.d.ts +23 -11
- package/dist/auth/apply-before-read.js +139 -33
- package/dist/auth/apply-before-read.test.node.js +241 -3
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/read-context-scope.d.ts +20 -0
- package/dist/auth/read-context-scope.js +48 -0
- package/dist/codegen/fixtures/all-fields.d.ts +342 -0
- package/dist/codegen/fixtures/all-fields.expected.d.ts +120 -0
- package/dist/codegen/fixtures/all-fields.expected.js +1 -0
- package/dist/codegen/fixtures/all-fields.js +94 -0
- package/dist/codegen/index.d.ts +16 -0
- package/dist/codegen/index.js +431 -0
- package/dist/codegen/index.test.node.d.ts +1 -0
- package/dist/codegen/index.test.node.js +230 -0
- package/dist/config/attach-hooks.d.ts +25 -0
- package/dist/config/attach-hooks.js +130 -0
- package/dist/config/attach-hooks.test.node.d.ts +1 -0
- package/dist/config/attach-hooks.test.node.js +173 -0
- package/dist/config/config-hooks.test.node.d.ts +1 -0
- package/dist/config/config-hooks.test.node.js +56 -0
- package/dist/config/config.d.ts +9 -5
- package/dist/config/config.js +20 -2
- package/dist/config/routes.d.ts +5 -5
- package/dist/config/routes.js +42 -9
- package/dist/config/routes.test.node.d.ts +1 -0
- package/dist/config/routes.test.node.js +152 -0
- package/dist/core.d.ts +3 -3
- package/dist/core.js +20 -14
- package/dist/core.test.node.d.ts +1 -0
- package/dist/core.test.node.js +28 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +14 -0
- package/dist/query/parse-where.d.ts +10 -1
- package/dist/query/parse-where.js +146 -2
- package/dist/query/parse-where.test.node.js +60 -1
- package/dist/schemas/zod/builder.js +7 -3
- package/dist/schemas/zod/builder.test.node.d.ts +1 -0
- package/dist/schemas/zod/builder.test.node.js +49 -0
- package/dist/services/collection-bootstrap.d.ts +1 -1
- package/dist/services/collection-bootstrap.test.node.js +30 -59
- package/dist/services/discover-counter-groups.d.ts +1 -1
- package/dist/services/discover-counter-groups.test.node.js +23 -0
- package/dist/services/document-lifecycle/audit.d.ts +22 -1
- package/dist/services/document-lifecycle/audit.js +32 -1
- package/dist/services/document-lifecycle/create.d.ts +2 -2
- package/dist/services/document-lifecycle/create.js +18 -8
- package/dist/services/document-lifecycle/delete.d.ts +17 -1
- package/dist/services/document-lifecycle/delete.js +91 -26
- package/dist/services/document-lifecycle/index.d.ts +1 -1
- package/dist/services/document-lifecycle/internals.d.ts +0 -20
- package/dist/services/document-lifecycle/internals.js +22 -46
- package/dist/services/document-lifecycle/status.d.ts +1 -1
- package/dist/services/document-lifecycle/status.js +20 -3
- package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
- package/dist/services/document-lifecycle/system-fields.js +112 -74
- package/dist/services/document-lifecycle/tree.d.ts +22 -24
- package/dist/services/document-lifecycle/tree.js +244 -123
- package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
- package/dist/services/document-lifecycle/tree.test.node.js +663 -0
- package/dist/services/document-lifecycle/update.d.ts +4 -4
- package/dist/services/document-lifecycle/update.js +11 -5
- package/dist/services/document-lifecycle.test.node.js +426 -16
- package/dist/services/document-read.d.ts +14 -6
- package/dist/services/document-read.js +47 -9
- package/dist/services/field-upload.test.node.js +70 -0
- package/dist/services/index.d.ts +3 -2
- package/dist/services/index.js +3 -2
- package/dist/services/normalize-numeric-fields.d.ts +23 -0
- package/dist/services/normalize-numeric-fields.js +89 -0
- package/dist/services/normalize-numeric-fields.test.node.d.ts +8 -0
- package/dist/services/normalize-numeric-fields.test.node.js +99 -0
- package/dist/services/populate.d.ts +9 -23
- package/dist/services/populate.js +180 -28
- package/dist/services/populate.test.node.js +59 -0
- package/dist/services/richtext-embed.d.ts +39 -2
- package/dist/services/richtext-embed.js +4 -34
- package/dist/services/richtext-embed.test.node.js +15 -0
- package/dist/services/richtext-populate.d.ts +21 -3
- package/dist/services/richtext-populate.js +160 -19
- package/dist/services/richtext-populate.test.node.js +523 -2
- package/dist/services/validate-search-config.d.ts +1 -1
- package/dist/storage/collection-fingerprint.js +6 -0
- package/dist/storage/collection-fingerprint.test.node.js +21 -0
- package/dist/utils/root-relative-redirect.d.ts +6 -0
- package/dist/utils/root-relative-redirect.js +37 -0
- 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
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
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
|
|
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 —
|
|
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.
|
|
784
|
-
*
|
|
785
|
-
*
|
|
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
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
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
|
|
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 —
|
|
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')`) —
|
|
979
|
-
*
|
|
980
|
-
*
|
|
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
|
-
*
|
|
65
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
241
|
-
audit
|
|
220
|
+
/** Audit-log reads — per-document history, system-wide report. */
|
|
221
|
+
audit: IAuditQueries;
|
|
242
222
|
};
|
|
243
223
|
/**
|
|
244
|
-
*
|
|
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
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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).
|
|
985
|
-
*
|
|
986
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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:
|
|
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 / `
|
|
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
|
package/dist/@types/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/@types/index.js
CHANGED
|
@@ -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';
|