@byline/core 4.16.0 → 4.17.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/admin-types.d.ts +86 -31
- package/dist/@types/admin-types.js +12 -0
- package/dist/@types/collection-types.d.ts +211 -29
- package/dist/@types/collection-types.js +68 -8
- package/dist/@types/db-types.d.ts +50 -0
- package/dist/@types/form-admin-config.test.node.d.ts +6 -0
- package/dist/@types/form-admin-config.test.node.js +26 -0
- package/dist/@types/singleton-admin-config.test.node.js +66 -0
- package/dist/@types/singleton-definition.test.node.d.ts +8 -0
- package/dist/@types/singleton-definition.test.node.js +75 -0
- package/dist/@types/site-config.d.ts +9 -8
- package/dist/auth/apply-before-read.d.ts +2 -1
- package/dist/auth/apply-before-read.js +13 -3
- package/dist/auth/apply-before-read.test.node.js +44 -0
- package/dist/auth/assert-actor-can-perform.d.ts +4 -3
- package/dist/auth/assert-actor-can-perform.js +9 -7
- package/dist/auth/assert-actor-can-perform.test.node.js +32 -11
- package/dist/auth/filter-readable-collections.d.ts +6 -6
- package/dist/auth/filter-readable-collections.js +8 -8
- package/dist/auth/filter-readable-collections.test.node.js +26 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/register-collection-abilities.d.ts +34 -30
- package/dist/auth/register-collection-abilities.js +81 -77
- package/dist/auth/register-collection-abilities.test.node.js +86 -8
- package/dist/codegen/fixtures/all-fields.expected.d.ts +3 -0
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/index.js +32 -16
- package/dist/codegen/index.test.node.js +35 -2
- package/dist/config/attach-hooks.d.ts +3 -3
- package/dist/config/attach-hooks.js +8 -0
- package/dist/config/attach-hooks.test.node.js +59 -0
- package/dist/config/config-hooks.test.node.js +34 -1
- package/dist/config/config.d.ts +2 -1
- package/dist/config/config.js +9 -1
- package/dist/config/group-collections.d.ts +10 -9
- package/dist/config/group-collections.js +4 -4
- package/dist/config/group-collections.test.node.js +14 -0
- package/dist/config/validate-admin-configs.d.ts +4 -4
- package/dist/config/validate-admin-configs.js +40 -7
- package/dist/config/validate-admin-configs.test.node.js +44 -0
- package/dist/config/validate-collections.js +43 -0
- package/dist/config/validate-collections.test.node.js +75 -0
- package/dist/core.d.ts +2 -3
- package/dist/core.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/lib/errors.d.ts +5 -3
- package/dist/lib/errors.js +1 -0
- package/dist/services/collection-bootstrap.js +12 -0
- package/dist/services/collection-bootstrap.test.node.js +63 -6
- package/dist/services/discover-counter-groups.test.node.js +6 -0
- package/dist/services/document-lifecycle/copy-to-locale.js +9 -17
- package/dist/services/document-lifecycle/create.js +6 -11
- package/dist/services/document-lifecycle/delete-locale.js +1 -1
- package/dist/services/document-lifecycle/delete.js +1 -1
- package/dist/services/document-lifecycle/duplicate.js +7 -18
- package/dist/services/document-lifecycle/persistence.d.ts +29 -0
- package/dist/services/document-lifecycle/persistence.js +42 -0
- package/dist/services/document-lifecycle/restore.js +9 -17
- package/dist/services/document-lifecycle/scheduled-publish.js +5 -5
- package/dist/services/document-lifecycle/status.js +3 -3
- package/dist/services/document-lifecycle/system-fields.js +1 -1
- package/dist/services/document-lifecycle/tree.js +2 -2
- package/dist/services/document-lifecycle/tree.test.node.js +6 -1
- package/dist/services/document-lifecycle/update.js +20 -36
- package/dist/services/document-lifecycle.test.node.js +35 -0
- package/dist/services/field-upload.d.ts +1 -1
- package/dist/services/field-upload.js +20 -6
- package/dist/services/field-upload.test.node.js +125 -9
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/populate.js +1 -1
- package/dist/services/populate.test.node.js +9 -0
- package/dist/services/richtext-populate.js +1 -1
- package/dist/services/singleton-lifecycle/copy-to-locale.d.ts +16 -0
- package/dist/services/singleton-lifecycle/copy-to-locale.js +100 -0
- package/dist/services/singleton-lifecycle/index.d.ts +14 -0
- package/dist/services/singleton-lifecycle/index.js +11 -0
- package/dist/services/singleton-lifecycle/internals.d.ts +49 -0
- package/dist/services/singleton-lifecycle/internals.js +118 -0
- package/dist/services/singleton-lifecycle/restore.d.ts +13 -0
- package/dist/services/singleton-lifecycle/restore.js +93 -0
- package/dist/services/singleton-lifecycle/update.d.ts +16 -0
- package/dist/services/singleton-lifecycle/update.js +107 -0
- package/dist/services/singleton-lifecycle.test.node.d.ts +8 -0
- package/dist/services/singleton-lifecycle.test.node.js +396 -0
- package/dist/storage/collection-fingerprint.js +18 -6
- package/dist/storage/collection-fingerprint.test.node.js +15 -0
- package/package.json +5 -5
- package/dist/config/resolve-item-view-columns.test.node.js +0 -30
- /package/dist/{config/resolve-item-view-columns.test.node.d.ts → @types/singleton-admin-config.test.node.d.ts} +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { BlockFieldData,
|
|
8
|
+
import type { BlockFieldData, MultiCollectionDefinition, SingletonDefinition, WorkflowStatus } from './collection-types.js';
|
|
9
9
|
import type { Block, FieldComponentSlots, RichTextEditorComponent } from './field-types.js';
|
|
10
10
|
/**
|
|
11
11
|
* Props passed to a custom list-view component registered via
|
|
@@ -168,22 +168,23 @@ export interface FieldAdminConfig {
|
|
|
168
168
|
editor?: RichTextEditorComponent;
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
* One labelled group of
|
|
171
|
+
* One labelled group of document resources on the admin dashboard.
|
|
172
172
|
*
|
|
173
173
|
* Declared in display order on `AdminConfig.collectionGroups` and referenced by
|
|
174
|
-
* `name` from
|
|
175
|
-
*
|
|
174
|
+
* `name` from a collection or singleton admin config. The registry keeps its
|
|
175
|
+
* established `collectionGroups` name for compatibility. Array order is the order headings
|
|
176
|
+
* appear on the dashboard; a group with no member resources is not rendered
|
|
176
177
|
* at all, so no heading ever sits above an empty section.
|
|
177
178
|
*/
|
|
178
179
|
export interface CollectionGroupDefinition {
|
|
179
180
|
/**
|
|
180
|
-
* Stable key referenced by `
|
|
181
|
+
* Stable key referenced by an admin resource's `group`. Boot-validated —
|
|
181
182
|
* a reference to an undeclared name throws at startup rather than silently
|
|
182
183
|
* producing an extra heading.
|
|
183
184
|
*/
|
|
184
185
|
name: string;
|
|
185
186
|
/**
|
|
186
|
-
* Heading text rendered above this group's
|
|
187
|
+
* Heading text rendered above this group's document resources.
|
|
187
188
|
*
|
|
188
189
|
* A plain string, deliberately not translated: `CollectionDefinition.labels`
|
|
189
190
|
* are themselves rendered untranslated on the dashboard, so translating group
|
|
@@ -208,6 +209,44 @@ export interface PreviewDocument<F = any> {
|
|
|
208
209
|
status: string;
|
|
209
210
|
fields: F;
|
|
210
211
|
}
|
|
212
|
+
/** Minimal pathless document shape passed to `SingletonAdminConfig.preview.url`. */
|
|
213
|
+
export interface SingletonPreviewDocument<F = any> {
|
|
214
|
+
id: string;
|
|
215
|
+
status: string;
|
|
216
|
+
fields: F;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Admin UI configuration consumed directly by Byline's form renderer.
|
|
220
|
+
*
|
|
221
|
+
* Collection-specific list, dashboard, and preview configuration belongs on
|
|
222
|
+
* {@link CollectionAdminConfig}; this base contains only the reusable form
|
|
223
|
+
* composition contract.
|
|
224
|
+
*/
|
|
225
|
+
export interface FormAdminConfig {
|
|
226
|
+
/** Named tab components. Each set is one tab bar. */
|
|
227
|
+
tabSets?: TabSetDefinition[];
|
|
228
|
+
/** Named horizontal-row layouts. */
|
|
229
|
+
rows?: RowDefinition[];
|
|
230
|
+
/** Named labelled-fieldset clusters. */
|
|
231
|
+
groups?: GroupDefinition[];
|
|
232
|
+
/**
|
|
233
|
+
* Composition: how the primitives above (and any raw schema fields) flow
|
|
234
|
+
* into the form's `main` and `sidebar` regions. When omitted, the
|
|
235
|
+
* renderer synthesises a default that places every schema field in
|
|
236
|
+
* `main` in declaration order.
|
|
237
|
+
*/
|
|
238
|
+
layout?: LayoutDefinition;
|
|
239
|
+
/**
|
|
240
|
+
* Per-field rendering overrides, keyed by schema path: a top-level field
|
|
241
|
+
* name (`title`) or a dotted, index-free path through `group` / `array`
|
|
242
|
+
* structure fields (`files.filesGroup.publicationFile`). Paths address
|
|
243
|
+
* field *declarations*, never item instances (no `[0]` indices), and never
|
|
244
|
+
* traverse a `blocks` field — blocks take their overrides from the
|
|
245
|
+
* blockType-keyed `AdminConfig.blockAdmin` registry instead.
|
|
246
|
+
* Placement is no longer expressed here — see the layout primitives above.
|
|
247
|
+
*/
|
|
248
|
+
fields?: Record<string, FieldAdminConfig>;
|
|
249
|
+
}
|
|
211
250
|
/**
|
|
212
251
|
* Admin UI configuration for a collection.
|
|
213
252
|
* This is the presentation/UI layer — separate from the data schema.
|
|
@@ -215,7 +254,9 @@ export interface PreviewDocument<F = any> {
|
|
|
215
254
|
* Linked to a `CollectionDefinition` by `slug` matching the collection's
|
|
216
255
|
* `path`.
|
|
217
256
|
*/
|
|
218
|
-
export interface CollectionAdminConfig<T = any> {
|
|
257
|
+
export interface CollectionAdminConfig<T = any> extends FormAdminConfig {
|
|
258
|
+
/** Explicit resource discriminator. Stamped by `defineAdmin`. */
|
|
259
|
+
singleton?: false;
|
|
219
260
|
/** Must match the `path` of the corresponding `CollectionDefinition`. */
|
|
220
261
|
slug: string;
|
|
221
262
|
/**
|
|
@@ -281,29 +322,6 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
281
322
|
itemViewSort?: ListDefaultSort<T>;
|
|
282
323
|
/** Default columns to show when no explicit column config is provided. */
|
|
283
324
|
defaultColumns?: string[];
|
|
284
|
-
/** Named tab components. Each set is one tab bar. */
|
|
285
|
-
tabSets?: TabSetDefinition[];
|
|
286
|
-
/** Named horizontal-row layouts. */
|
|
287
|
-
rows?: RowDefinition[];
|
|
288
|
-
/** Named labelled-fieldset clusters. */
|
|
289
|
-
groups?: GroupDefinition[];
|
|
290
|
-
/**
|
|
291
|
-
* Composition: how the primitives above (and any raw schema fields) flow
|
|
292
|
-
* into the form's `main` and `sidebar` regions. When omitted, the
|
|
293
|
-
* renderer synthesises a default that places every schema field in
|
|
294
|
-
* `main` in declaration order.
|
|
295
|
-
*/
|
|
296
|
-
layout?: LayoutDefinition;
|
|
297
|
-
/**
|
|
298
|
-
* Per-field rendering overrides, keyed by schema path: a top-level field
|
|
299
|
-
* name (`title`) or a dotted, index-free path through `group` / `array`
|
|
300
|
-
* structure fields (`files.filesGroup.publicationFile`). Paths address
|
|
301
|
-
* field *declarations*, never item instances (no `[0]` indices), and never
|
|
302
|
-
* traverse a `blocks` field — blocks take their overrides from the
|
|
303
|
-
* blockType-keyed `AdminConfig.blockAdmin` registry instead.
|
|
304
|
-
* Placement is no longer expressed here — see the layout primitives above.
|
|
305
|
-
*/
|
|
306
|
-
fields?: Record<string, FieldAdminConfig>;
|
|
307
325
|
/**
|
|
308
326
|
* Preview URL configuration for the admin's live-preview affordance
|
|
309
327
|
* (`<PreviewLink>` icon on the document edit page header). When omitted,
|
|
@@ -411,6 +429,38 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
411
429
|
*/
|
|
412
430
|
listActions?: Array<(props: ListActionComponentProps) => any>;
|
|
413
431
|
}
|
|
432
|
+
/** Admin UI configuration for a singleton document resource. */
|
|
433
|
+
export interface SingletonAdminConfig<T = any> extends FormAdminConfig {
|
|
434
|
+
/** Explicit resource discriminator. Stamped by `defineSingletonAdmin`. */
|
|
435
|
+
singleton: true;
|
|
436
|
+
/** Must match the `path` of the corresponding `SingletonDefinition`. */
|
|
437
|
+
slug: string;
|
|
438
|
+
/**
|
|
439
|
+
* Dashboard group this resource belongs to. The registry remains named
|
|
440
|
+
* `AdminConfig.collectionGroups` for backwards compatibility.
|
|
441
|
+
*/
|
|
442
|
+
group?: string;
|
|
443
|
+
/**
|
|
444
|
+
* Explicit public preview URL for the singleton's site-wide effect.
|
|
445
|
+
* Unlike collection previews, no internal document path is available or
|
|
446
|
+
* used as a fallback.
|
|
447
|
+
*/
|
|
448
|
+
preview?: {
|
|
449
|
+
url: (doc: SingletonPreviewDocument<T>, ctx: {
|
|
450
|
+
locale?: string;
|
|
451
|
+
}) => string | null;
|
|
452
|
+
};
|
|
453
|
+
/** Collection-list options are invalid for a cardinality-one resource. */
|
|
454
|
+
columns?: never;
|
|
455
|
+
defaultSort?: never;
|
|
456
|
+
defaultColumns?: never;
|
|
457
|
+
itemView?: never;
|
|
458
|
+
itemViewSort?: never;
|
|
459
|
+
listView?: never;
|
|
460
|
+
listActions?: never;
|
|
461
|
+
}
|
|
462
|
+
/** Admin presentation config for either registered document-resource kind. */
|
|
463
|
+
export type AdminResourceConfig<T = any> = CollectionAdminConfig<T> | SingletonAdminConfig<T>;
|
|
414
464
|
/**
|
|
415
465
|
* Props passed to each `CollectionAdminConfig.listActions` component. Kept
|
|
416
466
|
* minimal and framework-agnostic (`@byline/core` is React-free); components
|
|
@@ -424,7 +474,12 @@ export interface ListActionComponentProps {
|
|
|
424
474
|
* Type-safe factory for creating a `CollectionAdminConfig` linked to a schema.
|
|
425
475
|
* Sets `slug` from the schema's `path`.
|
|
426
476
|
*/
|
|
427
|
-
export declare function defineAdmin<T = any>(schema:
|
|
477
|
+
export declare function defineAdmin<T = any>(schema: MultiCollectionDefinition, config: Omit<CollectionAdminConfig<T>, 'slug' | 'singleton'>): CollectionAdminConfig<T>;
|
|
478
|
+
/**
|
|
479
|
+
* Type-safe factory for a `SingletonAdminConfig` linked to a singleton schema.
|
|
480
|
+
* Sets both the schema path and resource discriminator for authors.
|
|
481
|
+
*/
|
|
482
|
+
export declare function defineSingletonAdmin<T = any>(schema: SingletonDefinition, config: Omit<SingletonAdminConfig<T>, 'slug' | 'singleton'>): SingletonAdminConfig<T>;
|
|
428
483
|
/**
|
|
429
484
|
* Admin UI configuration for a block — the block-scoped analogue of
|
|
430
485
|
* `CollectionAdminConfig`, carrying **rendering overrides only**.
|
|
@@ -11,8 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export function defineAdmin(schema, config) {
|
|
13
13
|
return {
|
|
14
|
+
...config,
|
|
15
|
+
singleton: false,
|
|
14
16
|
slug: schema.path,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Type-safe factory for a `SingletonAdminConfig` linked to a singleton schema.
|
|
21
|
+
* Sets both the schema path and resource discriminator for authors.
|
|
22
|
+
*/
|
|
23
|
+
export function defineSingletonAdmin(schema, config) {
|
|
24
|
+
return {
|
|
15
25
|
...config,
|
|
26
|
+
singleton: true,
|
|
27
|
+
slug: schema.path,
|
|
16
28
|
};
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
@@ -483,6 +483,60 @@ export interface AfterUpdateContext {
|
|
|
483
483
|
locale: string;
|
|
484
484
|
};
|
|
485
485
|
}
|
|
486
|
+
/** The public singleton operation represented by a save-hook invocation. */
|
|
487
|
+
export type SingletonSaveOperation = {
|
|
488
|
+
type: 'save';
|
|
489
|
+
} | {
|
|
490
|
+
type: 'restore';
|
|
491
|
+
sourceVersionId: string;
|
|
492
|
+
} | {
|
|
493
|
+
type: 'copyToLocale';
|
|
494
|
+
sourceLocale: string;
|
|
495
|
+
targetLocale: string;
|
|
496
|
+
overwrite: boolean;
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Context passed to a singleton's `beforeSave` hooks.
|
|
500
|
+
*
|
|
501
|
+
* The hook runs inside the singleton transaction after the slot lock and
|
|
502
|
+
* current-value read. It may mutate `data` before the next immutable version
|
|
503
|
+
* is written. `documentId` and `originalData` are `null` only for the first
|
|
504
|
+
* materialising save.
|
|
505
|
+
*
|
|
506
|
+
* Restore carries the complete all-locale field tree with `locale: 'all'`.
|
|
507
|
+
* Copy-to-locale carries the merged target payload and the target locale's
|
|
508
|
+
* prior values; it never exposes the raw source-locale payload as `data`.
|
|
509
|
+
*/
|
|
510
|
+
export interface BeforeSingletonSaveContext {
|
|
511
|
+
data: Record<string, any>;
|
|
512
|
+
originalData: Record<string, any> | null;
|
|
513
|
+
singletonPath: string;
|
|
514
|
+
locale: string;
|
|
515
|
+
requestContext: RequestContext;
|
|
516
|
+
isInitialSave: boolean;
|
|
517
|
+
operation: SingletonSaveOperation;
|
|
518
|
+
documentId: string | null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Context passed to a singleton's `afterSave` hooks after the save transaction
|
|
522
|
+
* commits. A hook failure rejects the public call but cannot roll back the
|
|
523
|
+
* persisted document version or initial slot mapping.
|
|
524
|
+
*
|
|
525
|
+
* Public-cache invalidation is deliberately host-owned rather than automatic.
|
|
526
|
+
* Hosts commonly invalidate preview caches after every save and public caches
|
|
527
|
+
* only when publication changes, using the status hooks for the latter.
|
|
528
|
+
*/
|
|
529
|
+
export interface AfterSingletonSaveContext {
|
|
530
|
+
data: Record<string, any>;
|
|
531
|
+
originalData: Record<string, any> | null;
|
|
532
|
+
singletonPath: string;
|
|
533
|
+
locale: string;
|
|
534
|
+
requestContext: RequestContext;
|
|
535
|
+
isInitialSave: boolean;
|
|
536
|
+
operation: SingletonSaveOperation;
|
|
537
|
+
documentId: string;
|
|
538
|
+
documentVersionId: string;
|
|
539
|
+
}
|
|
486
540
|
/**
|
|
487
541
|
* Context passed to `afterSystemFieldsChange` after a non-versioned document
|
|
488
542
|
* path and/or advertised-locale change commits successfully.
|
|
@@ -829,11 +883,11 @@ export interface AfterReadContext {
|
|
|
829
883
|
* `LEFT JOIN LATERAL` SQL machinery the client's existing `where` parser
|
|
830
884
|
* emits, then ANDs onto whatever the caller passed in `where`.
|
|
831
885
|
*
|
|
832
|
-
* Returning `undefined` (or
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
* predicate without passing an invalid UUID to
|
|
836
|
-
* callers expect empty
|
|
886
|
+
* Returning `undefined` (or `void`) means "no scoping" — typically the
|
|
887
|
+
* superuser / unconditional-read branch. Return `false` when the actor cannot
|
|
888
|
+
* read anything; it normalises to `{ id: { $in: [] } }` and compiles to an
|
|
889
|
+
* always-false SQL predicate without passing an invalid UUID to the database.
|
|
890
|
+
* Do not throw, because callers expect empty results rather than collapsed endpoints.
|
|
837
891
|
*
|
|
838
892
|
* The hook receives:
|
|
839
893
|
* - `requestContext` — the authenticated request, including `actor`. The
|
|
@@ -845,7 +899,7 @@ export interface AfterReadContext {
|
|
|
845
899
|
* same hook function is reused across collections).
|
|
846
900
|
*
|
|
847
901
|
* See `docs/07-auth-and-security/01-authn-authz.md` for the strategic rationale; the Quick
|
|
848
|
-
* Reference there carries
|
|
902
|
+
* Reference there carries seven worked recipes.
|
|
849
903
|
*/
|
|
850
904
|
export interface BeforeReadContext {
|
|
851
905
|
collectionPath: string;
|
|
@@ -854,17 +908,19 @@ export interface BeforeReadContext {
|
|
|
854
908
|
}
|
|
855
909
|
/**
|
|
856
910
|
* A `beforeRead` hook function. Returns a `QueryPredicate` to scope the
|
|
857
|
-
* query, or `void`/`undefined` to apply no
|
|
858
|
-
* needing tenant lookups or role-metadata
|
|
911
|
+
* query, `false` to deny every row, or `void`/`undefined` to apply no
|
|
912
|
+
* scoping. May be async — actors needing tenant lookups or role-metadata
|
|
913
|
+
* fetches commonly are.
|
|
859
914
|
*/
|
|
860
|
-
export type BeforeReadHookFn = (ctx: BeforeReadContext) => QueryPredicate | void | Promise<QueryPredicate | void>;
|
|
915
|
+
export type BeforeReadHookFn = (ctx: BeforeReadContext) => QueryPredicate | false | void | Promise<QueryPredicate | false | void>;
|
|
861
916
|
/**
|
|
862
917
|
* Slot type for `beforeRead`.
|
|
863
918
|
*
|
|
864
919
|
* Distinct from the generic `CollectionHookSlot` because `beforeRead`
|
|
865
920
|
* returns a value (a predicate). When multiple hook functions are
|
|
866
921
|
* configured, their predicates are combined with implicit AND in
|
|
867
|
-
* declaration order;
|
|
922
|
+
* declaration order; `false` remains restrictive in any position, and
|
|
923
|
+
* functions that return `void` are skipped.
|
|
868
924
|
*/
|
|
869
925
|
export type BeforeReadHookSlot = BeforeReadHookFn | BeforeReadHookFn[];
|
|
870
926
|
/**
|
|
@@ -952,6 +1008,31 @@ export interface CollectionHooks {
|
|
|
952
1008
|
*/
|
|
953
1009
|
afterRead?: CollectionHookSlot<AfterReadContext>;
|
|
954
1010
|
}
|
|
1011
|
+
/** A singleton-hook function signature, parameterised by context type. */
|
|
1012
|
+
export type SingletonHookFn<Ctx> = (ctx: Ctx) => void | Promise<void>;
|
|
1013
|
+
/** A singleton hook slot: one function or an ordered array of functions. */
|
|
1014
|
+
export type SingletonHookSlot<Ctx> = SingletonHookFn<Ctx> | SingletonHookFn<Ctx>[];
|
|
1015
|
+
/**
|
|
1016
|
+
* Lifecycle hooks for a singleton document slot.
|
|
1017
|
+
*
|
|
1018
|
+
* The family describes public singleton operations rather than the internal
|
|
1019
|
+
* create/update branch used to persist them. `beforeSave` runs inside the
|
|
1020
|
+
* locked transaction and may mutate `data`; `afterSave` runs only after that
|
|
1021
|
+
* transaction commits. Read, status, and unpublish hooks retain the shared
|
|
1022
|
+
* document contracts.
|
|
1023
|
+
*/
|
|
1024
|
+
export interface SingletonHooks {
|
|
1025
|
+
/** Runs for every singleton save, including initial materialisation. */
|
|
1026
|
+
beforeSave?: SingletonHookSlot<BeforeSingletonSaveContext>;
|
|
1027
|
+
/** Runs after every singleton save commits. See its cache-invalidation JSDoc. */
|
|
1028
|
+
afterSave?: SingletonHookSlot<AfterSingletonSaveContext>;
|
|
1029
|
+
beforeStatusChange?: SingletonHookSlot<StatusChangeContext>;
|
|
1030
|
+
afterStatusChange?: SingletonHookSlot<StatusChangeContext>;
|
|
1031
|
+
beforeUnpublish?: SingletonHookSlot<BeforeUnpublishContext>;
|
|
1032
|
+
afterUnpublish?: SingletonHookSlot<AfterUnpublishContext>;
|
|
1033
|
+
beforeRead?: BeforeReadHookSlot;
|
|
1034
|
+
afterRead?: SingletonHookSlot<AfterReadContext>;
|
|
1035
|
+
}
|
|
955
1036
|
/**
|
|
956
1037
|
* A lazy loader for a collection's hooks — the function form of
|
|
957
1038
|
* `CollectionDefinition.hooks`. Returns the `CollectionHooks` object, or a
|
|
@@ -965,6 +1046,11 @@ export interface CollectionHooks {
|
|
|
965
1046
|
* server-only code through `ServerConfig.hooks.collections` from a server-only
|
|
966
1047
|
* entry instead.
|
|
967
1048
|
*
|
|
1049
|
+
* The loader remains lazy, so its returned family cannot be inspected during
|
|
1050
|
+
* startup. A mismatched family fails on first lifecycle use instead. The
|
|
1051
|
+
* import result is cached, but {@link resolveHooks} repeats family validation
|
|
1052
|
+
* for the current definition on every call, including cache hits.
|
|
1053
|
+
*
|
|
968
1054
|
* @example
|
|
969
1055
|
* // docs.schema.ts — only for isomorphic-safe hooks
|
|
970
1056
|
* export const Docs = defineCollection({
|
|
@@ -982,7 +1068,26 @@ export type CollectionHooksLoader = () => Promise<CollectionHooks | {
|
|
|
982
1068
|
default: CollectionHooks;
|
|
983
1069
|
}>;
|
|
984
1070
|
/**
|
|
985
|
-
*
|
|
1071
|
+
* A lazy loader for a singleton's hooks.
|
|
1072
|
+
*
|
|
1073
|
+
* Loaders remain lazy: their returned family is validated on first lifecycle
|
|
1074
|
+
* use, not at startup, because eagerly importing server hook modules would
|
|
1075
|
+
* defeat the loader boundary. The import result is cached by loader identity,
|
|
1076
|
+
* but family validation still runs against the current definition on every
|
|
1077
|
+
* {@link resolveHooks} call so one reused loader cannot bypass the kind check.
|
|
1078
|
+
*/
|
|
1079
|
+
export type SingletonHooksLoader = () => Promise<SingletonHooks | {
|
|
1080
|
+
default: SingletonHooks;
|
|
1081
|
+
}>;
|
|
1082
|
+
type ResolvedDocumentHooks = CollectionHooks & SingletonHooks;
|
|
1083
|
+
/**
|
|
1084
|
+
* Assert that an inspected hook object belongs to the definition's resource
|
|
1085
|
+
* family. Inline objects are checked during startup attachment; lazily loaded
|
|
1086
|
+
* objects are checked by {@link resolveHooks} on every resolution.
|
|
1087
|
+
*/
|
|
1088
|
+
export declare function assertHooksMatchDefinition(definition: CollectionDefinition, hooks: CollectionHooks | SingletonHooks): void;
|
|
1089
|
+
/**
|
|
1090
|
+
* Resolve a document definition's `hooks` to a concrete, validated hook object.
|
|
986
1091
|
*
|
|
987
1092
|
* - The inline-object form (`hooks: { … }`) is returned as-is.
|
|
988
1093
|
* - The loader form (`hooks: () => import('./docs.hooks.js')`) is invoked
|
|
@@ -991,9 +1096,14 @@ export type CollectionHooksLoader = () => Promise<CollectionHooks | {
|
|
|
991
1096
|
* through here, so a loader's dynamic `import()` runs at most once per
|
|
992
1097
|
* process regardless of how many documents flow through it.
|
|
993
1098
|
*
|
|
1099
|
+
* Inline families are also validated during startup attachment. A loader
|
|
1100
|
+
* cannot be inspected without importing it, so a mismatched loader fails on
|
|
1101
|
+
* first lifecycle use rather than at boot. Validation runs on every call,
|
|
1102
|
+
* including cache hits, against the current definition.
|
|
1103
|
+
*
|
|
994
1104
|
* Returns `undefined` when no hooks are declared.
|
|
995
1105
|
*/
|
|
996
|
-
export declare function resolveHooks(definition: CollectionDefinition): Promise<
|
|
1106
|
+
export declare function resolveHooks(definition: CollectionDefinition): Promise<ResolvedDocumentHooks | undefined>;
|
|
997
1107
|
/**
|
|
998
1108
|
* Type-safe factory for authoring a collection's hooks in a separate
|
|
999
1109
|
* module (the loader form: `hooks: () => import('./docs.hooks.js')`).
|
|
@@ -1014,15 +1124,34 @@ export declare function resolveHooks(definition: CollectionDefinition): Promise<
|
|
|
1014
1124
|
* })
|
|
1015
1125
|
*/
|
|
1016
1126
|
export declare function defineHooks(hooks: CollectionHooks): CollectionHooks;
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
singular: string;
|
|
1020
|
-
plural: string;
|
|
1021
|
-
};
|
|
1127
|
+
/** Fields and lifecycle configuration shared by every document resource kind. */
|
|
1128
|
+
export interface DocumentDefinitionBase {
|
|
1022
1129
|
path: string;
|
|
1023
1130
|
fields: Field[];
|
|
1024
1131
|
/** Sequential workflow configuration. Falls back to DEFAULT_WORKFLOW if omitted. */
|
|
1025
1132
|
workflow?: WorkflowConfig;
|
|
1133
|
+
/**
|
|
1134
|
+
* Optional explicit version pin. When omitted, the startup bootstrap
|
|
1135
|
+
* auto-increments the collection's stored version any time the schema
|
|
1136
|
+
* fingerprint changes. When set, the value is used verbatim as long as it
|
|
1137
|
+
* is >= the currently-stored version; pinning backwards throws at startup.
|
|
1138
|
+
*
|
|
1139
|
+
* The stamped version is written onto every `documentVersions` row so that
|
|
1140
|
+
* a document can later be resolved against the schema shape it was
|
|
1141
|
+
* authored under.
|
|
1142
|
+
*/
|
|
1143
|
+
version?: number;
|
|
1144
|
+
}
|
|
1145
|
+
/** A collection of many documents — Byline's original and default resource kind. */
|
|
1146
|
+
export interface MultiCollectionDefinition extends DocumentDefinitionBase {
|
|
1147
|
+
/** Absent or `false` on a multi-document collection. */
|
|
1148
|
+
singleton?: false;
|
|
1149
|
+
labels: {
|
|
1150
|
+
singular: string;
|
|
1151
|
+
plural: string;
|
|
1152
|
+
};
|
|
1153
|
+
/** A multi-document collection uses `labels`, never the singleton's `label`. */
|
|
1154
|
+
label?: never;
|
|
1026
1155
|
/**
|
|
1027
1156
|
* Lifecycle hooks for server-side document operations.
|
|
1028
1157
|
*
|
|
@@ -1231,30 +1360,82 @@ export interface CollectionDefinition {
|
|
|
1231
1360
|
* docs/04-collections/04-document-trees.md.
|
|
1232
1361
|
*/
|
|
1233
1362
|
tree?: boolean;
|
|
1363
|
+
}
|
|
1364
|
+
/**
|
|
1365
|
+
* A single named document slot for an installation. Cardinality is
|
|
1366
|
+
* zero-or-one: the slot exists as soon as the definition is registered, and
|
|
1367
|
+
* its document is materialised by the first save.
|
|
1368
|
+
*
|
|
1369
|
+
* Collection-only options are absent by construction: there is no list to sort,
|
|
1370
|
+
* search, or paginate, no sibling documents to order or arrange in a tree, and
|
|
1371
|
+
* no public slug (`path` is internal metadata; see the design spec).
|
|
1372
|
+
*/
|
|
1373
|
+
export interface SingletonDefinition extends DocumentDefinitionBase {
|
|
1374
|
+
singleton: true;
|
|
1375
|
+
/** Singular display label. A singleton has no plural form. */
|
|
1376
|
+
label: string;
|
|
1234
1377
|
/**
|
|
1235
|
-
*
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1240
|
-
* The stamped version is written onto every `documentVersions` row so that
|
|
1241
|
-
* a document can later be resolved against the schema shape it was
|
|
1242
|
-
* authored under.
|
|
1378
|
+
* Singleton lifecycle hooks, expressed as save/read/workflow operations.
|
|
1379
|
+
* Inline objects are family-validated at startup. A loader remains lazy and
|
|
1380
|
+
* is therefore family-validated on first lifecycle use and every subsequent
|
|
1381
|
+
* resolution, including cache hits shared with another definition.
|
|
1243
1382
|
*/
|
|
1244
|
-
|
|
1383
|
+
hooks?: SingletonHooks | SingletonHooksLoader;
|
|
1384
|
+
/**
|
|
1385
|
+
* Collection-only members remain explicit `never` properties rather than
|
|
1386
|
+
* being omitted. Without them, `defineSingleton`'s generic parameter can
|
|
1387
|
+
* absorb excess properties and collection-only configuration would pass at
|
|
1388
|
+
* the definition site.
|
|
1389
|
+
*/
|
|
1390
|
+
labels?: never;
|
|
1391
|
+
useAsTitle?: never;
|
|
1392
|
+
useAsPath?: never;
|
|
1393
|
+
orderable?: never;
|
|
1394
|
+
tree?: never;
|
|
1395
|
+
search?: never;
|
|
1396
|
+
listSearch?: never;
|
|
1397
|
+
advertiseLocales?: never;
|
|
1398
|
+
showStats?: never;
|
|
1399
|
+
linksInEditor?: never;
|
|
1400
|
+
buildDocumentPath?: never;
|
|
1245
1401
|
}
|
|
1402
|
+
/** A registered multi-document collection or singleton document slot. */
|
|
1403
|
+
export type CollectionDefinition = MultiCollectionDefinition | SingletonDefinition;
|
|
1246
1404
|
/**
|
|
1247
1405
|
* Type-safe factory for creating a CollectionDefinition.
|
|
1248
1406
|
* Returns the definition as-is but provides type checking.
|
|
1249
1407
|
*/
|
|
1250
|
-
export declare function defineCollection<const C extends
|
|
1408
|
+
export declare function defineCollection<const C extends MultiCollectionDefinition>(definition: C & MultiCollectionDefinition): C;
|
|
1409
|
+
/**
|
|
1410
|
+
* Type-safe factory for a singleton definition. Adds the `singleton: true`
|
|
1411
|
+
* discriminant so authors never write it themselves, and locks in literal
|
|
1412
|
+
* types for `path` and field names so the generated path registries resolve
|
|
1413
|
+
* precisely. Mirrors `defineCollection`.
|
|
1414
|
+
*/
|
|
1415
|
+
export declare function defineSingleton<const S extends Omit<SingletonDefinition, 'singleton'>>(definition: S & Omit<SingletonDefinition, 'singleton'>): S & {
|
|
1416
|
+
singleton: true;
|
|
1417
|
+
};
|
|
1418
|
+
/** Narrows a definition to the singleton variant. */
|
|
1419
|
+
export declare function isSingleton(definition: CollectionDefinition): definition is SingletonDefinition;
|
|
1251
1420
|
export type CollectionFieldData<C extends CollectionDefinition> = FieldSetData<C['fields']>;
|
|
1252
1421
|
export type CollectionFieldDataAllLocales<C extends CollectionDefinition> = FieldSetDataAllLocales<C['fields']>;
|
|
1253
1422
|
/** Broad collection registry used when an application has not supplied its schema types. */
|
|
1254
1423
|
export type CollectionRegistry = Record<string, Record<string, any>>;
|
|
1255
|
-
/** Infer
|
|
1424
|
+
/** Infer the multi-collection path-to-fields registry from a shared definition tuple. */
|
|
1256
1425
|
export type InferCollectionRegistry<TCollections extends readonly CollectionDefinition[]> = {
|
|
1257
|
-
[TCollection in TCollections[number] as TCollection['path']]: CollectionFieldData<TCollection>;
|
|
1426
|
+
[TCollection in TCollections[number] as TCollection extends SingletonDefinition ? never : TCollection['path']]: CollectionFieldData<TCollection>;
|
|
1427
|
+
};
|
|
1428
|
+
/** Infer the all-locale multi-collection registry from a shared definition tuple. */
|
|
1429
|
+
export type InferCollectionRegistryAllLocales<TCollections extends readonly CollectionDefinition[]> = {
|
|
1430
|
+
[TCollection in TCollections[number] as TCollection extends SingletonDefinition ? never : TCollection['path']]: CollectionFieldDataAllLocales<TCollection>;
|
|
1431
|
+
};
|
|
1432
|
+
/** Infer the singleton path-to-fields registry from a shared definition tuple. */
|
|
1433
|
+
export type InferSingletonRegistry<TCollections extends readonly CollectionDefinition[]> = {
|
|
1434
|
+
[TCollection in TCollections[number] as TCollection extends SingletonDefinition ? TCollection['path'] : never]: CollectionFieldData<TCollection>;
|
|
1435
|
+
};
|
|
1436
|
+
/** Infer the all-locale singleton registry from a shared definition tuple. */
|
|
1437
|
+
export type InferSingletonRegistryAllLocales<TCollections extends readonly CollectionDefinition[]> = {
|
|
1438
|
+
[TCollection in TCollections[number] as TCollection extends SingletonDefinition ? TCollection['path'] : never]: CollectionFieldDataAllLocales<TCollection>;
|
|
1258
1439
|
};
|
|
1259
1440
|
/**
|
|
1260
1441
|
* Type-safe factory for creating a Block. Returns the definition as-is,
|
|
@@ -1340,3 +1521,4 @@ export type CollectionDataAllLocales<C extends CollectionDefinition> = Prettify<
|
|
|
1340
1521
|
created_at: Date;
|
|
1341
1522
|
updated_at: Date;
|
|
1342
1523
|
} & CollectionFieldDataAllLocales<C>>;
|
|
1524
|
+
export {};
|
|
@@ -175,8 +175,46 @@ export function normalizeCollectionHook(hook) {
|
|
|
175
175
|
return Array.isArray(hook) ? hook : [hook];
|
|
176
176
|
}
|
|
177
177
|
const resolvedHooksCache = new WeakMap();
|
|
178
|
+
function completeHookNames() {
|
|
179
|
+
return (names) => names;
|
|
180
|
+
}
|
|
181
|
+
const COLLECTION_ONLY_HOOK_NAMES = completeHookNames()([
|
|
182
|
+
'beforeCreate',
|
|
183
|
+
'afterCreate',
|
|
184
|
+
'beforeUpdate',
|
|
185
|
+
'afterUpdate',
|
|
186
|
+
'afterSystemFieldsChange',
|
|
187
|
+
'beforeDelete',
|
|
188
|
+
'afterDelete',
|
|
189
|
+
'afterTreeChange',
|
|
190
|
+
]);
|
|
191
|
+
const SINGLETON_ONLY_HOOK_NAMES = completeHookNames()([
|
|
192
|
+
'beforeSave',
|
|
193
|
+
'afterSave',
|
|
194
|
+
]);
|
|
178
195
|
/**
|
|
179
|
-
*
|
|
196
|
+
* Assert that an inspected hook object belongs to the definition's resource
|
|
197
|
+
* family. Inline objects are checked during startup attachment; lazily loaded
|
|
198
|
+
* objects are checked by {@link resolveHooks} on every resolution.
|
|
199
|
+
*/
|
|
200
|
+
export function assertHooksMatchDefinition(definition, hooks) {
|
|
201
|
+
const record = hooks;
|
|
202
|
+
if (isSingleton(definition)) {
|
|
203
|
+
for (const name of COLLECTION_ONLY_HOOK_NAMES) {
|
|
204
|
+
if (record[name] !== undefined) {
|
|
205
|
+
throw new Error(`Singleton "${definition.path}" cannot use collection hook "${name}".`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
for (const name of SINGLETON_ONLY_HOOK_NAMES) {
|
|
211
|
+
if (record[name] !== undefined) {
|
|
212
|
+
throw new Error(`Collection "${definition.path}" cannot use singleton hook "${name}".`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve a document definition's `hooks` to a concrete, validated hook object.
|
|
180
218
|
*
|
|
181
219
|
* - The inline-object form (`hooks: { … }`) is returned as-is.
|
|
182
220
|
* - The loader form (`hooks: () => import('./docs.hooks.js')`) is invoked
|
|
@@ -185,18 +223,27 @@ const resolvedHooksCache = new WeakMap();
|
|
|
185
223
|
* through here, so a loader's dynamic `import()` runs at most once per
|
|
186
224
|
* process regardless of how many documents flow through it.
|
|
187
225
|
*
|
|
226
|
+
* Inline families are also validated during startup attachment. A loader
|
|
227
|
+
* cannot be inspected without importing it, so a mismatched loader fails on
|
|
228
|
+
* first lifecycle use rather than at boot. Validation runs on every call,
|
|
229
|
+
* including cache hits, against the current definition.
|
|
230
|
+
*
|
|
188
231
|
* Returns `undefined` when no hooks are declared.
|
|
189
232
|
*/
|
|
190
233
|
export async function resolveHooks(definition) {
|
|
191
234
|
const hooks = definition.hooks;
|
|
192
|
-
if (typeof hooks !== 'function')
|
|
235
|
+
if (typeof hooks !== 'function') {
|
|
236
|
+
if (hooks !== undefined)
|
|
237
|
+
assertHooksMatchDefinition(definition, hooks);
|
|
193
238
|
return hooks;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
239
|
+
}
|
|
240
|
+
let resolved = resolvedHooksCache.get(hooks);
|
|
241
|
+
if (resolved === undefined) {
|
|
242
|
+
const loaded = await hooks();
|
|
243
|
+
resolved = ('default' in loaded ? loaded.default : loaded);
|
|
244
|
+
resolvedHooksCache.set(hooks, resolved);
|
|
245
|
+
}
|
|
246
|
+
assertHooksMatchDefinition(definition, resolved);
|
|
200
247
|
return resolved;
|
|
201
248
|
}
|
|
202
249
|
/**
|
|
@@ -228,6 +275,19 @@ export function defineHooks(hooks) {
|
|
|
228
275
|
export function defineCollection(definition) {
|
|
229
276
|
return definition;
|
|
230
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Type-safe factory for a singleton definition. Adds the `singleton: true`
|
|
280
|
+
* discriminant so authors never write it themselves, and locks in literal
|
|
281
|
+
* types for `path` and field names so the generated path registries resolve
|
|
282
|
+
* precisely. Mirrors `defineCollection`.
|
|
283
|
+
*/
|
|
284
|
+
export function defineSingleton(definition) {
|
|
285
|
+
return { ...definition, singleton: true };
|
|
286
|
+
}
|
|
287
|
+
/** Narrows a definition to the singleton variant. */
|
|
288
|
+
export function isSingleton(definition) {
|
|
289
|
+
return definition.singleton === true;
|
|
290
|
+
}
|
|
231
291
|
// ---------------------------------------------------------------------------
|
|
232
292
|
// Block helpers — mirrors of defineCollection / CollectionFieldData / etc.
|
|
233
293
|
// ---------------------------------------------------------------------------
|