@byline/core 4.1.0 → 4.2.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 +74 -2
- package/dist/@types/admin-types.js +29 -0
- package/dist/@types/collection-types.d.ts +29 -7
- package/dist/@types/db-types.d.ts +7 -7
- package/dist/@types/field-data-types.d.ts +1 -0
- package/dist/@types/field-types.d.ts +36 -1
- package/dist/@types/site-config.d.ts +28 -1
- package/dist/@types/storage-types.d.ts +14 -1
- package/dist/codegen/fixtures/all-fields.d.ts +23 -0
- package/dist/codegen/fixtures/all-fields.expected.d.ts +4 -0
- package/dist/codegen/fixtures/all-fields.js +2 -0
- package/dist/codegen/index.js +2 -0
- package/dist/config/config.js +2 -1
- package/dist/config/validate-admin-configs.d.ts +17 -1
- package/dist/config/validate-admin-configs.js +84 -11
- package/dist/config/validate-admin-configs.test.node.js +114 -1
- package/dist/config/validate-collections.js +43 -0
- package/dist/config/validate-collections.test.node.js +60 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/schemas/zod/builder.js +11 -0
- package/dist/services/build-search-document.js +5 -1
- package/dist/services/document-to-markdown.js +10 -0
- package/dist/services/field-upload.d.ts +7 -0
- package/dist/services/field-upload.js +15 -13
- package/dist/services/field-upload.test.node.js +1 -1
- package/dist/services/populate.d.ts +1 -1
- package/dist/services/populate.js +1 -1
- package/dist/storage/collection-fingerprint.js +9 -0
- package/dist/storage/field-store-map.js +1 -0
- package/dist/storage/field-store-map.test.node.js +1 -0
- package/dist/utils/slugify-filename.d.ts +50 -0
- package/dist/utils/slugify-filename.js +36 -0
- package/dist/utils/slugify-filename.test.node.d.ts +8 -0
- package/dist/utils/slugify-filename.test.node.js +50 -0
- package/package.json +2 -2
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { CollectionDefinition, WorkflowStatus } from './collection-types.js';
|
|
9
|
-
import type { FieldComponentSlots, RichTextEditorComponent } from './field-types.js';
|
|
8
|
+
import type { BlockFieldData, CollectionDefinition, WorkflowStatus } from './collection-types.js';
|
|
9
|
+
import type { Block, FieldComponentSlots, RichTextEditorComponent } from './field-types.js';
|
|
10
10
|
/**
|
|
11
11
|
* Props passed to a custom list-view component registered via
|
|
12
12
|
* `CollectionAdminConfig.listView`.
|
|
@@ -229,6 +229,25 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
229
229
|
* the field directly, so the deprecated `picker` alias keeps working.
|
|
230
230
|
*/
|
|
231
231
|
itemView?: ColumnDefinition<T>[];
|
|
232
|
+
/**
|
|
233
|
+
* Sort applied when this collection's documents are listed through the
|
|
234
|
+
* **item view** surfaces (currently the relation picker modal) —
|
|
235
|
+
* independent of the main list view's {@link defaultSort}. A media
|
|
236
|
+
* library, for example, can keep its list view newest-first while the
|
|
237
|
+
* picker orders by title:
|
|
238
|
+
*
|
|
239
|
+
* ```ts
|
|
240
|
+
* itemViewSort: { field: 'title' },
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* Same shape and boot validation as `defaultSort`: `field` must be a
|
|
244
|
+
* top-level schema field or a document column (`createdAt` / `updatedAt` /
|
|
245
|
+
* `path`), `direction` defaults to `'asc'`, and the option is rejected on
|
|
246
|
+
* `orderable: true` collections (manual ordering owns their order
|
|
247
|
+
* everywhere). Falls back to `defaultSort` → `created_at desc` when
|
|
248
|
+
* omitted.
|
|
249
|
+
*/
|
|
250
|
+
itemViewSort?: ListDefaultSort<T>;
|
|
232
251
|
/**
|
|
233
252
|
* @deprecated Renamed to {@link itemView}. Kept as a backwards-compatible
|
|
234
253
|
* alias — `itemView` wins when both are present. Read both via
|
|
@@ -374,3 +393,56 @@ export interface ListActionComponentProps {
|
|
|
374
393
|
* Sets `slug` from the schema's `path`.
|
|
375
394
|
*/
|
|
376
395
|
export declare function defineAdmin<T = any>(schema: CollectionDefinition, config: Omit<CollectionAdminConfig<T>, 'slug'>): CollectionAdminConfig<T>;
|
|
396
|
+
/**
|
|
397
|
+
* Admin UI configuration for a block — the block-scoped analogue of
|
|
398
|
+
* `CollectionAdminConfig`, carrying **rendering overrides only**.
|
|
399
|
+
*
|
|
400
|
+
* Linked to a `Block` by `blockType` matching the block's `blockType`, and
|
|
401
|
+
* registered site-wide on `ClientConfig.blockAdmin`. Because blocks are
|
|
402
|
+
* cross-collection units (the same `defineBlock()` object is typically shared
|
|
403
|
+
* by several collections, and codegen dedupes their contracts structurally),
|
|
404
|
+
* a block's admin config applies wherever the block renders. If one
|
|
405
|
+
* collection genuinely needs different presentation for "the same" block,
|
|
406
|
+
* define a second block with a distinct `blockType`.
|
|
407
|
+
*
|
|
408
|
+
* v1 scope: `fields` keys address the block's **top-level** field names only —
|
|
409
|
+
* the same limitation `CollectionAdminConfig.fields` has for collections.
|
|
410
|
+
* Fields nested inside a group/array within the block inherit site-wide
|
|
411
|
+
* defaults.
|
|
412
|
+
*/
|
|
413
|
+
export interface BlockAdminConfig {
|
|
414
|
+
/** Must match the `blockType` of the corresponding `defineBlock()` definition. */
|
|
415
|
+
blockType: string;
|
|
416
|
+
/**
|
|
417
|
+
* Per-field rendering overrides (`components` slots, richtext `editor`),
|
|
418
|
+
* keyed by the block's top-level field names. Same shape and semantics as
|
|
419
|
+
* `CollectionAdminConfig.fields`.
|
|
420
|
+
*/
|
|
421
|
+
fields?: Record<string, FieldAdminConfig>;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Type-safe factory for creating a `BlockAdminConfig` linked to a block
|
|
425
|
+
* schema. Sets `blockType` from the block's `blockType` and constrains the
|
|
426
|
+
* `fields` keys to the block's own top-level field names.
|
|
427
|
+
*
|
|
428
|
+
* Lives on the admin side of the schema/admin split: block schema files stay
|
|
429
|
+
* React-free and tsx-loadable, while this config may carry React component
|
|
430
|
+
* references (editor overrides, slot components). Keep them in separate
|
|
431
|
+
* files — the admin file imports the schema, never the reverse.
|
|
432
|
+
*
|
|
433
|
+
* @example
|
|
434
|
+
* ```ts
|
|
435
|
+
* // quote-block.admin.ts (admin-side — may import React)
|
|
436
|
+
* import { defineBlockAdmin } from '@byline/core'
|
|
437
|
+
* import { QuoteBlock } from './quote-block.js'
|
|
438
|
+
*
|
|
439
|
+
* export const QuoteBlockAdmin = defineBlockAdmin(QuoteBlock, {
|
|
440
|
+
* fields: {
|
|
441
|
+
* quoteText: { editor: PlainLexicalEditor },
|
|
442
|
+
* },
|
|
443
|
+
* })
|
|
444
|
+
* ```
|
|
445
|
+
*/
|
|
446
|
+
export declare function defineBlockAdmin<B extends Block>(block: B, config: {
|
|
447
|
+
fields?: Partial<Record<Extract<keyof BlockFieldData<B>, string>, FieldAdminConfig>>;
|
|
448
|
+
}): BlockAdminConfig;
|
|
@@ -15,3 +15,32 @@ export function defineAdmin(schema, config) {
|
|
|
15
15
|
...config,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Type-safe factory for creating a `BlockAdminConfig` linked to a block
|
|
20
|
+
* schema. Sets `blockType` from the block's `blockType` and constrains the
|
|
21
|
+
* `fields` keys to the block's own top-level field names.
|
|
22
|
+
*
|
|
23
|
+
* Lives on the admin side of the schema/admin split: block schema files stay
|
|
24
|
+
* React-free and tsx-loadable, while this config may carry React component
|
|
25
|
+
* references (editor overrides, slot components). Keep them in separate
|
|
26
|
+
* files — the admin file imports the schema, never the reverse.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // quote-block.admin.ts (admin-side — may import React)
|
|
31
|
+
* import { defineBlockAdmin } from '@byline/core'
|
|
32
|
+
* import { QuoteBlock } from './quote-block.js'
|
|
33
|
+
*
|
|
34
|
+
* export const QuoteBlockAdmin = defineBlockAdmin(QuoteBlock, {
|
|
35
|
+
* fields: {
|
|
36
|
+
* quoteText: { editor: PlainLexicalEditor },
|
|
37
|
+
* },
|
|
38
|
+
* })
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function defineBlockAdmin(block, config) {
|
|
42
|
+
return {
|
|
43
|
+
blockType: block.blockType,
|
|
44
|
+
fields: config.fields,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -82,6 +82,28 @@ export interface UploadConfig {
|
|
|
82
82
|
* Omit to allow all types.
|
|
83
83
|
*/
|
|
84
84
|
mimeTypes?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* Declarative storage location (key scope) for this field's uploads,
|
|
87
|
+
* replacing the default `<collectionPath>/` scope. Nested segments are
|
|
88
|
+
* allowed — e.g. `'publications/covers'`, `'news/attachments'`. Providers
|
|
89
|
+
* keep their own entropy and filename sanitisation beneath it
|
|
90
|
+
* (`<location>/<filename>-<suffix>.<ext>`), so collision behaviour is unchanged.
|
|
91
|
+
*
|
|
92
|
+
* Precedence: a `beforeStore` hook returning `{ storagePath }` (written
|
|
93
|
+
* verbatim) → `location` → the collection-path default. A `{ filename }`
|
|
94
|
+
* hook override keeps composing with `location`. Use `location` for
|
|
95
|
+
* static scoping; hooks remain the tool for *dynamic* keys (per-document
|
|
96
|
+
* serials, tenant prefixes).
|
|
97
|
+
*
|
|
98
|
+
* Boot-validated: POSIX-style forward slashes; segments of
|
|
99
|
+
* `A–Z a–z 0–9 . _ -`; no leading/trailing/duplicate slashes; no `.` or
|
|
100
|
+
* `..` segments.
|
|
101
|
+
*
|
|
102
|
+
* Note: changing `location` later does not move previously stored objects
|
|
103
|
+
* (same as renaming via hooks) — existing documents keep their recorded
|
|
104
|
+
* `storagePath`.
|
|
105
|
+
*/
|
|
106
|
+
location?: string;
|
|
85
107
|
/** Maximum file size in bytes. Omit for no limit. */
|
|
86
108
|
maxFileSize?: number;
|
|
87
109
|
/**
|
|
@@ -546,7 +568,7 @@ export interface DeleteContext {
|
|
|
546
568
|
}
|
|
547
569
|
/**
|
|
548
570
|
* Context passed to the `afterTreeChange` hook — the structural-change
|
|
549
|
-
* invalidation event for `tree: true` collections (docs/04-collections/
|
|
571
|
+
* invalidation event for `tree: true` collections (docs/04-collections/04-document-trees.md).
|
|
550
572
|
*
|
|
551
573
|
* Tree mutations are document-grain and **unversioned**, so the normal
|
|
552
574
|
* version-write invalidation (`afterCreate` / `afterUpdate` / `afterStatusChange`)
|
|
@@ -594,11 +616,11 @@ export interface TreeChangeContext {
|
|
|
594
616
|
* override is threaded into `storage.upload(...)`, so generated
|
|
595
617
|
* image variants automatically inherit the new prefix. The storage
|
|
596
618
|
* provider still derives the final key (e.g. local storage prefixes
|
|
597
|
-
* `<collection>/<
|
|
619
|
+
* `<collection>/<basename>-<suffix>.<ext>` for collision avoidance).
|
|
598
620
|
* - Take **full control of the storage key** by returning
|
|
599
621
|
* `{ storagePath }` — a fully-qualified, POSIX-style path (no
|
|
600
622
|
* leading slash) that is threaded into `storage.upload(...)` as
|
|
601
|
-
* `targetStoragePath` and written **verbatim**: no
|
|
623
|
+
* `targetStoragePath` and written **verbatim**: no entropy suffix, no
|
|
602
624
|
* collection namespace, no provider rewriting. The hook assumes
|
|
603
625
|
* responsibility for sanitisation and collision avoidance (see
|
|
604
626
|
* `UploadFileOptions.targetStoragePath`). Generated image variants
|
|
@@ -657,7 +679,7 @@ export interface BeforeStoreContext {
|
|
|
657
679
|
* provider still derives the final key.
|
|
658
680
|
* - `{ storagePath }` → take full control of the storage key: written
|
|
659
681
|
* verbatim via `UploadFileOptions.targetStoragePath`
|
|
660
|
-
* (no
|
|
682
|
+
* (no entropy suffix / provider rewriting). May be
|
|
661
683
|
* combined with `filename`; without it, the stored
|
|
662
684
|
* filename defaults to the path's basename.
|
|
663
685
|
* - `{ error }` → reject the upload; surfaces as
|
|
@@ -787,7 +809,7 @@ export declare function resolveUploadHooks(hooks: UploadHooks | UploadHooksLoade
|
|
|
787
809
|
* by populate. A hook that performs its own reads should thread this
|
|
788
810
|
* context back in via `client.collection(...).findById(id, { _readContext:
|
|
789
811
|
* readContext })` so the visited set and read budget are preserved —
|
|
790
|
-
* essential to foreclose the A→B→A loop (see `docs/04-collections/
|
|
812
|
+
* essential to foreclose the A→B→A loop (see `docs/04-collections/03-relationships.md`).
|
|
791
813
|
*/
|
|
792
814
|
export interface AfterReadContext {
|
|
793
815
|
/** The raw reconstructed document. Mutate in place — changes persist. */
|
|
@@ -906,7 +928,7 @@ export interface CollectionHooks {
|
|
|
906
928
|
* (`removeFromTree`), or the promote-children-to-root that accompanies a
|
|
907
929
|
* delete. Tree writes mint no document version, so this is the only
|
|
908
930
|
* invalidation signal for them. Fires once per write with the full affected
|
|
909
|
-
* set ({@link TreeChangeContext}). See docs/04-collections/
|
|
931
|
+
* set ({@link TreeChangeContext}). See docs/04-collections/04-document-trees.md.
|
|
910
932
|
*/
|
|
911
933
|
afterTreeChange?: CollectionHookSlot<TreeChangeContext>;
|
|
912
934
|
/**
|
|
@@ -1200,7 +1222,7 @@ export interface CollectionDefinition {
|
|
|
1200
1222
|
* touch no user fields. Do **not** also declare a `parent` relation field; the
|
|
1201
1223
|
* tree owns structure (a topic that genuinely belongs in two places is a
|
|
1202
1224
|
* cross-link relation field, never a second tree edge). See
|
|
1203
|
-
* docs/04-collections/
|
|
1225
|
+
* docs/04-collections/04-document-trees.md.
|
|
1204
1226
|
*/
|
|
1205
1227
|
tree?: boolean;
|
|
1206
1228
|
/**
|
|
@@ -649,7 +649,7 @@ export interface IDocumentCommands {
|
|
|
649
649
|
* **before** (its right neighbour). Either may be null — `{}` appends as the
|
|
650
650
|
* only/last child, `beforeDocumentId` alone appends after it, `afterDocumentId`
|
|
651
651
|
* alone prepends before it. Both are resolved within the *target* parent
|
|
652
|
-
* group. See docs/04-collections/
|
|
652
|
+
* group. See docs/04-collections/04-document-trees.md.
|
|
653
653
|
*/
|
|
654
654
|
placeTreeNode(params: {
|
|
655
655
|
collectionId: string;
|
|
@@ -664,7 +664,7 @@ export interface IDocumentCommands {
|
|
|
664
664
|
* Remove a document's edge row, returning it to the *unplaced* state (in the
|
|
665
665
|
* collection, but not in the tree). Distinct from document deletion — the
|
|
666
666
|
* document and its content are untouched. No-op when the node is already
|
|
667
|
-
* unplaced. See docs/04-collections/
|
|
667
|
+
* unplaced. See docs/04-collections/04-document-trees.md.
|
|
668
668
|
*/
|
|
669
669
|
removeFromTree(params: {
|
|
670
670
|
collectionId: string;
|
|
@@ -774,7 +774,7 @@ export interface IDocumentQueries {
|
|
|
774
774
|
*
|
|
775
775
|
* Source-locale only: this resolves the single canonical slug, which is the
|
|
776
776
|
* only path row a document has today. When per-locale paths land (see
|
|
777
|
-
* docs/04-collections/
|
|
777
|
+
* docs/04-collections/05-document-paths.md → "Phase — per-locale paths"), the write-side hook
|
|
778
778
|
* contexts that consume this must be enriched to carry the locale each path
|
|
779
779
|
* was derived under (or the full `locale → path` set) — a single canonical
|
|
780
780
|
* `path` is no longer sufficient for per-localised-URL cache/CDN purges.
|
|
@@ -1009,7 +1009,7 @@ export interface IDocumentQueries {
|
|
|
1009
1009
|
* parent, increasing toward the root). Empty for a root or unplaced node.
|
|
1010
1010
|
*
|
|
1011
1011
|
* Backs breadcrumbs and the read-time hierarchical-URL canonicalization
|
|
1012
|
-
* (docs/04-collections/
|
|
1012
|
+
* (docs/04-collections/04-document-trees.md). Depth-bounded as a backstop against pathological
|
|
1013
1013
|
* key state even though the write-path cycle guard prevents true cycles.
|
|
1014
1014
|
*
|
|
1015
1015
|
* **Status-at-edge.** `readMode: 'published'` joins
|
|
@@ -1036,7 +1036,7 @@ export interface IDocumentQueries {
|
|
|
1036
1036
|
* `order_key`. `parentDocumentId: null` returns the collection's root nodes.
|
|
1037
1037
|
* Scoped to `collectionId` so root reads (which have no parent to scope by)
|
|
1038
1038
|
* stay within the collection. One level only — the recursive subtree read is
|
|
1039
|
-
* a separate query. See docs/04-collections/
|
|
1039
|
+
* a separate query. See docs/04-collections/04-document-trees.md.
|
|
1040
1040
|
*/
|
|
1041
1041
|
getTreeChildren(params: {
|
|
1042
1042
|
collectionId: string;
|
|
@@ -1047,7 +1047,7 @@ export interface IDocumentQueries {
|
|
|
1047
1047
|
}>>;
|
|
1048
1048
|
/**
|
|
1049
1049
|
* Resolve a single node's placement state in the tree, distinguishing the
|
|
1050
|
-
* three tri-states (docs/04-collections/
|
|
1050
|
+
* three tri-states (docs/04-collections/04-document-trees.md → "Node placement"):
|
|
1051
1051
|
*
|
|
1052
1052
|
* - **Unplaced** (no edge row) → `{ placed: false, parentDocumentId: null }`
|
|
1053
1053
|
* - **Root** (edge row, null parent) → `{ placed: true, parentDocumentId: null }`
|
|
@@ -1084,7 +1084,7 @@ export interface IDocumentQueries {
|
|
|
1084
1084
|
* `byline_current_published_documents` and drops any node without a current
|
|
1085
1085
|
* published version; because the walk only recurses through *included*
|
|
1086
1086
|
* nodes, an unpublished node's entire subtree is omitted — the spine is
|
|
1087
|
-
* broken and descendants are not promoted (see docs/04-collections/
|
|
1087
|
+
* broken and descendants are not promoted (see docs/04-collections/04-document-trees.md).
|
|
1088
1088
|
* `readMode: 'any'` (the default) includes every current, non-deleted node.
|
|
1089
1089
|
* Optional `filters` enforce `beforeRead` at every edge with the same
|
|
1090
1090
|
* no-promotion semantics. Depth-bounded by `maxDepth` as a backstop.
|
|
@@ -334,6 +334,41 @@ export interface TextAreaField extends LocalizableField {
|
|
|
334
334
|
rules?: ValidationRule[];
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* A dedicated source-code field. Stores a plain string (text store — no
|
|
339
|
+
* migration relative to `text` / `textArea`); the admin renders a
|
|
340
|
+
* lazily-loaded CodeMirror editor with syntax highlighting.
|
|
341
|
+
*/
|
|
342
|
+
export interface CodeField extends LocalizableField {
|
|
343
|
+
type: 'code';
|
|
344
|
+
defaultValue?: DefaultValue<string>;
|
|
345
|
+
/**
|
|
346
|
+
* Default syntax-highlight language for the admin editor (e.g.
|
|
347
|
+
* `'typescript'`, `'json'`, `'markdown'`). Unknown or omitted values render
|
|
348
|
+
* as plain text. Presentation-only — never persisted with the value.
|
|
349
|
+
*/
|
|
350
|
+
language?: string;
|
|
351
|
+
/**
|
|
352
|
+
* Name of a SIBLING field (same group/block level) whose string value
|
|
353
|
+
* drives the editor's highlight language at runtime — typically a `select`
|
|
354
|
+
* of languages next to the code field. Takes precedence over `language`
|
|
355
|
+
* when the sibling has a value. Presentation-only.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```ts
|
|
359
|
+
* fields: [
|
|
360
|
+
* { name: 'language', type: 'select', options: [ ... ] },
|
|
361
|
+
* { name: 'code', type: 'code', languageField: 'language' },
|
|
362
|
+
* ]
|
|
363
|
+
* ```
|
|
364
|
+
*/
|
|
365
|
+
languageField?: string;
|
|
366
|
+
validation?: {
|
|
367
|
+
minLength?: number;
|
|
368
|
+
maxLength?: number;
|
|
369
|
+
rules?: ValidationRule[];
|
|
370
|
+
};
|
|
371
|
+
}
|
|
337
372
|
export interface CheckboxField extends NonlocalizableField {
|
|
338
373
|
type: 'checkbox';
|
|
339
374
|
defaultValue?: DefaultValue<boolean>;
|
|
@@ -539,7 +574,7 @@ export interface ImageField extends NonlocalizableField {
|
|
|
539
574
|
upload?: UploadConfig;
|
|
540
575
|
}
|
|
541
576
|
export type StructureField = GroupField | ArrayField | BlocksField;
|
|
542
|
-
export type ValueField = TextField | TextAreaField | CheckboxField | BooleanField | SelectField | RichTextField | TimeField | DateField | DateTimeField | FloatField | IntegerField | DecimalField | CounterField | JsonField | ObjectField | RelationField | FileField | ImageField;
|
|
577
|
+
export type ValueField = TextField | TextAreaField | CodeField | CheckboxField | BooleanField | SelectField | RichTextField | TimeField | DateField | DateTimeField | FloatField | IntegerField | DecimalField | CounterField | JsonField | ObjectField | RelationField | FileField | ImageField;
|
|
543
578
|
export type Field = StructureField | ValueField;
|
|
544
579
|
export type FieldSet = readonly Field[];
|
|
545
580
|
export type ValueFieldType = ValueField['type'];
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionProvider } from '@byline/auth';
|
|
9
9
|
import type { SlugifierFn } from '../utils/slugify.js';
|
|
10
|
-
import type {
|
|
10
|
+
import type { FilenameSlugifierFn } from '../utils/slugify-filename.js';
|
|
11
|
+
import type { BlockAdminConfig, CollectionAdminConfig } from './admin-types.js';
|
|
11
12
|
import type { CollectionDefinition, CollectionHooks, CollectionHooksLoader, UploadHooks, UploadHooksLoader } from './collection-types.js';
|
|
12
13
|
import type { IDbAdapter } from './db-types.js';
|
|
13
14
|
import type { RichTextEditorComponent, RichTextEmbedFn, RichTextPopulateFn, RichTextToMarkdownFn, RichTextToTextFn } from './field-types.js';
|
|
@@ -147,6 +148,15 @@ export type TranslationBundleShape = Readonly<{
|
|
|
147
148
|
export interface ClientConfig extends BaseConfig {
|
|
148
149
|
/** Admin UI configuration for collections (client-side only). */
|
|
149
150
|
admin?: CollectionAdminConfig[];
|
|
151
|
+
/**
|
|
152
|
+
* Admin UI configuration for blocks, keyed by `blockType` — the block-scoped
|
|
153
|
+
* analogue of `admin`. Because blocks are shared across collections, an
|
|
154
|
+
* entry applies wherever its block renders (any collection, any nesting).
|
|
155
|
+
* Boot-validated against the blocks declared in `collections`.
|
|
156
|
+
*
|
|
157
|
+
* @see BlockAdminConfig
|
|
158
|
+
*/
|
|
159
|
+
blockAdmin?: BlockAdminConfig[];
|
|
150
160
|
/**
|
|
151
161
|
* Installation-wide slugifier — the **client-side** copy, used by the
|
|
152
162
|
* admin path-widget to render the live `path` preview (create-mode
|
|
@@ -274,6 +284,23 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
274
284
|
* `defineClientConfig`. See {@link ClientConfig.slugifier}.
|
|
275
285
|
*/
|
|
276
286
|
slugifier?: SlugifierFn;
|
|
287
|
+
/**
|
|
288
|
+
* Installation-wide upload defaults.
|
|
289
|
+
*
|
|
290
|
+
* `filenameSlugifier` — the filename parallel of {@link slugifier}: applied
|
|
291
|
+
* to every uploaded file's **base name** (extension split off, lowercased,
|
|
292
|
+
* and reattached by the framework) before the `beforeStore` hook chain and
|
|
293
|
+
* storage-key composition. Falls back to the default `slugifyFilename`
|
|
294
|
+
* from `@byline/core` when not set. Server-only — filenames are derived
|
|
295
|
+
* exclusively at write time, so no client copy is needed.
|
|
296
|
+
*
|
|
297
|
+
* The stored key's basename becomes
|
|
298
|
+
* `<slugified-base>-<6-char suffix>.<ext>` (see the storage providers), so
|
|
299
|
+
* this function decides the human-readable half of every stored filename.
|
|
300
|
+
*/
|
|
301
|
+
uploads?: {
|
|
302
|
+
filenameSlugifier?: FilenameSlugifierFn;
|
|
303
|
+
};
|
|
277
304
|
/**
|
|
278
305
|
* Session provider for admin authentication. Optional in Phase 3 —
|
|
279
306
|
* installations without a provider configured simply can't sign in
|
|
@@ -38,10 +38,23 @@ export interface UploadFileOptions {
|
|
|
38
38
|
* Providers may use this to namespace storage paths.
|
|
39
39
|
*/
|
|
40
40
|
collection?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Declarative storage-key scope replacing the `collection` default —
|
|
43
|
+
* threaded from the field's `UploadConfig.location` (e.g.
|
|
44
|
+
* `'news/attachments'`, nested segments allowed). Providers use it exactly
|
|
45
|
+
* where they would have used `collection`, keeping their own entropy and
|
|
46
|
+
* filename sanitisation (unlike `targetStoragePath`, which is written
|
|
47
|
+
* verbatim and bypasses the collision guard). Ignored when
|
|
48
|
+
* `targetStoragePath` is set.
|
|
49
|
+
*
|
|
50
|
+
* Always POSIX-style (forward slashes), no leading/trailing slash —
|
|
51
|
+
* boot-validated on the schema side before it reaches providers.
|
|
52
|
+
*/
|
|
53
|
+
location?: string;
|
|
41
54
|
/**
|
|
42
55
|
* Explicit, fully-qualified storage path / object key the provider must
|
|
43
56
|
* write to verbatim. When set, providers MUST place the file at exactly
|
|
44
|
-
* this path — no
|
|
57
|
+
* this path — no entropy suffix, year/month rewrite, or `pathPrefix` injection.
|
|
45
58
|
*
|
|
46
59
|
* Used by the image-variant pipeline (`generateImageVariants`) to write
|
|
47
60
|
* sibling files alongside an already-stored original (e.g. so the
|
|
@@ -15,6 +15,11 @@ export declare const QuoteBlock: {
|
|
|
15
15
|
readonly label: "Quoted";
|
|
16
16
|
readonly value: "author's";
|
|
17
17
|
}];
|
|
18
|
+
}, {
|
|
19
|
+
readonly name: "snippet";
|
|
20
|
+
readonly type: "code";
|
|
21
|
+
readonly language: "typescript";
|
|
22
|
+
readonly optional: true;
|
|
18
23
|
}];
|
|
19
24
|
};
|
|
20
25
|
export declare const MediaBlock: {
|
|
@@ -67,6 +72,10 @@ export declare const AllFieldsCollection: {
|
|
|
67
72
|
}, {
|
|
68
73
|
readonly name: "textArea";
|
|
69
74
|
readonly type: "textArea";
|
|
75
|
+
}, {
|
|
76
|
+
readonly name: "code";
|
|
77
|
+
readonly type: "code";
|
|
78
|
+
readonly language: "json";
|
|
70
79
|
}, {
|
|
71
80
|
readonly name: "boolean";
|
|
72
81
|
readonly type: "boolean";
|
|
@@ -159,6 +168,11 @@ export declare const AllFieldsCollection: {
|
|
|
159
168
|
readonly label: "Quoted";
|
|
160
169
|
readonly value: "author's";
|
|
161
170
|
}];
|
|
171
|
+
}, {
|
|
172
|
+
readonly name: "snippet";
|
|
173
|
+
readonly type: "code";
|
|
174
|
+
readonly language: "typescript";
|
|
175
|
+
readonly optional: true;
|
|
162
176
|
}];
|
|
163
177
|
}, {
|
|
164
178
|
readonly blockType: "media";
|
|
@@ -223,6 +237,10 @@ export declare const AllFieldsCollections: readonly [{
|
|
|
223
237
|
}, {
|
|
224
238
|
readonly name: "textArea";
|
|
225
239
|
readonly type: "textArea";
|
|
240
|
+
}, {
|
|
241
|
+
readonly name: "code";
|
|
242
|
+
readonly type: "code";
|
|
243
|
+
readonly language: "json";
|
|
226
244
|
}, {
|
|
227
245
|
readonly name: "boolean";
|
|
228
246
|
readonly type: "boolean";
|
|
@@ -315,6 +333,11 @@ export declare const AllFieldsCollections: readonly [{
|
|
|
315
333
|
readonly label: "Quoted";
|
|
316
334
|
readonly value: "author's";
|
|
317
335
|
}];
|
|
336
|
+
}, {
|
|
337
|
+
readonly name: "snippet";
|
|
338
|
+
readonly type: "code";
|
|
339
|
+
readonly language: "typescript";
|
|
340
|
+
readonly optional: true;
|
|
318
341
|
}];
|
|
319
342
|
}, {
|
|
320
343
|
readonly blockType: "media";
|
|
@@ -18,6 +18,7 @@ export type QuoteBlockData = {
|
|
|
18
18
|
_type: "quote'block";
|
|
19
19
|
quotation: string;
|
|
20
20
|
"tone'choice"?: 'calm' | "author's" | undefined;
|
|
21
|
+
snippet?: string | undefined;
|
|
21
22
|
};
|
|
22
23
|
export type QuoteBlockDataAllLocales = {
|
|
23
24
|
_id: string;
|
|
@@ -26,6 +27,7 @@ export type QuoteBlockDataAllLocales = {
|
|
|
26
27
|
[locale: string]: string;
|
|
27
28
|
};
|
|
28
29
|
"tone'choice"?: 'calm' | "author's" | undefined;
|
|
30
|
+
snippet?: string | undefined;
|
|
29
31
|
};
|
|
30
32
|
export type AllFieldsFields = {
|
|
31
33
|
integer: number;
|
|
@@ -38,6 +40,7 @@ export type AllFieldsFields = {
|
|
|
38
40
|
text: string;
|
|
39
41
|
localizedText: string;
|
|
40
42
|
textArea: string;
|
|
43
|
+
code: string;
|
|
41
44
|
boolean: boolean;
|
|
42
45
|
checkbox: boolean;
|
|
43
46
|
select: 'first' | "second's";
|
|
@@ -74,6 +77,7 @@ export type AllFieldsFieldsAllLocales = {
|
|
|
74
77
|
[locale: string]: string;
|
|
75
78
|
};
|
|
76
79
|
textArea: string;
|
|
80
|
+
code: string;
|
|
77
81
|
boolean: boolean;
|
|
78
82
|
checkbox: boolean;
|
|
79
83
|
select: 'first' | "second's";
|
|
@@ -12,6 +12,7 @@ export const QuoteBlock = defineBlock({
|
|
|
12
12
|
{ label: 'Quoted', value: "author's" },
|
|
13
13
|
],
|
|
14
14
|
},
|
|
15
|
+
{ name: 'snippet', type: 'code', language: 'typescript', optional: true },
|
|
15
16
|
],
|
|
16
17
|
});
|
|
17
18
|
export const MediaBlock = defineBlock({
|
|
@@ -35,6 +36,7 @@ export const AllFieldsCollection = defineCollection({
|
|
|
35
36
|
{ name: 'text', type: 'text' },
|
|
36
37
|
{ name: 'localizedText', type: 'text', localized: true },
|
|
37
38
|
{ name: 'textArea', type: 'textArea' },
|
|
39
|
+
{ name: 'code', type: 'code', language: 'json' },
|
|
38
40
|
{ name: 'boolean', type: 'boolean' },
|
|
39
41
|
{ name: 'checkbox', type: 'checkbox' },
|
|
40
42
|
{
|
package/dist/codegen/index.js
CHANGED
|
@@ -198,6 +198,7 @@ function analyze(collections) {
|
|
|
198
198
|
break;
|
|
199
199
|
case 'boolean':
|
|
200
200
|
case 'checkbox':
|
|
201
|
+
case 'code':
|
|
201
202
|
case 'counter':
|
|
202
203
|
case 'date':
|
|
203
204
|
case 'datetime':
|
|
@@ -341,6 +342,7 @@ function emitBody(analysis) {
|
|
|
341
342
|
case 'datetime':
|
|
342
343
|
value = ['Date'];
|
|
343
344
|
break;
|
|
345
|
+
case 'code':
|
|
344
346
|
case 'decimal':
|
|
345
347
|
case 'text':
|
|
346
348
|
case 'textArea':
|
package/dist/config/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { commitHookAttachment, prepareHookAttachment, } from './attach-hooks.js';
|
|
2
2
|
import { resolveRoutes } from './routes.js';
|
|
3
|
-
import { validateAdminConfigs } from './validate-admin-configs.js';
|
|
3
|
+
import { validateAdminConfigs, validateBlockAdminConfigs } from './validate-admin-configs.js';
|
|
4
4
|
import { validateCollections } from './validate-collections.js';
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Global config storage
|
|
@@ -71,6 +71,7 @@ export const resolveItemViewColumns = (config) => config?.itemView ?? config?.pi
|
|
|
71
71
|
export function defineClientConfig(config) {
|
|
72
72
|
validateCollections(config.collections);
|
|
73
73
|
validateAdminConfigs(config.admin, config.collections);
|
|
74
|
+
validateBlockAdminConfigs(config.blockAdmin, config.collections);
|
|
74
75
|
const resolved = { ...config, routes: resolveRoutes(config.routes) };
|
|
75
76
|
setClientConfigInstance(resolved);
|
|
76
77
|
return resolved;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { CollectionAdminConfig, CollectionDefinition } from '../@types/index.js';
|
|
8
|
+
import type { BlockAdminConfig, CollectionAdminConfig, CollectionDefinition } from '../@types/index.js';
|
|
9
9
|
/**
|
|
10
10
|
* Validate every admin config in a configuration.
|
|
11
11
|
*
|
|
@@ -35,3 +35,19 @@ import type { CollectionAdminConfig, CollectionDefinition } from '../@types/inde
|
|
|
35
35
|
* necessarily wired up.
|
|
36
36
|
*/
|
|
37
37
|
export declare function validateAdminConfigs(admins: readonly CollectionAdminConfig[] | undefined, collections: readonly CollectionDefinition[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Validate every block admin config in a configuration.
|
|
40
|
+
*
|
|
41
|
+
* Enforced rules (per `ClientConfig.blockAdmin` entry):
|
|
42
|
+
* 1. Block pairing — `blockType` matches a block declared on at least one
|
|
43
|
+
* `type: 'blocks'` field across the registered collections (blocks have
|
|
44
|
+
* no global registry; the collections walk is the source of truth).
|
|
45
|
+
* 2. Uniqueness — no two entries share a `blockType`.
|
|
46
|
+
* 3. `fields` map sanity — every key matches a top-level field name of the
|
|
47
|
+
* block (when the same `blockType` appears in several collections, the
|
|
48
|
+
* union of their top-level field names is accepted).
|
|
49
|
+
*
|
|
50
|
+
* Throws a plain `Error` for the same reason `validateAdminConfigs` does —
|
|
51
|
+
* this runs at startup, before the logger is necessarily wired up.
|
|
52
|
+
*/
|
|
53
|
+
export declare function validateBlockAdminConfigs(blockAdmins: readonly BlockAdminConfig[] | undefined, collections: readonly CollectionDefinition[]): void;
|