@byline/core 4.11.2 → 4.13.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 +8 -14
- package/dist/@types/collection-types.d.ts +1 -58
- package/dist/@types/collection-types.js +0 -45
- package/dist/@types/db-types.d.ts +0 -6
- package/dist/@types/field-types.d.ts +2 -2
- package/dist/@types/site-config.d.ts +19 -20
- package/dist/@types/store-types.d.ts +0 -6
- package/dist/auth/apply-before-read.test.node.js +0 -30
- package/dist/auth/read-context-scope.js +0 -5
- package/dist/config/attach-hooks.test.node.js +1 -2
- package/dist/config/config-hooks.test.node.js +5 -6
- package/dist/config/config.d.ts +4 -15
- package/dist/config/config.js +21 -32
- package/dist/config/order-by-content-locale.test.node.js +2 -4
- package/dist/config/routes.test.node.js +9 -12
- package/dist/config/validate-admin-configs.d.ts +1 -1
- package/dist/config/validate-admin-configs.js +1 -1
- package/dist/core.js +4 -4
- package/dist/core.test.node.js +2 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/i18n-validator.d.ts +2 -2
- package/dist/services/i18n-validator.js +4 -4
- package/dist/services/i18n-validator.test.node.js +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/relation-projection.d.ts +11 -0
- package/dist/services/relation-projection.js +1 -13
- package/package.json +8 -8
|
@@ -155,7 +155,7 @@ export interface FieldAdminConfig {
|
|
|
155
155
|
* Per-field rich-text editor component override. When set on a
|
|
156
156
|
* `type: 'richText'` field's admin entry, the framework renders this
|
|
157
157
|
* component instead of the globally registered
|
|
158
|
-
* `
|
|
158
|
+
* `AdminConfig.fields.richText.editor`. Use to opt one specific field
|
|
159
159
|
* into an alternate editor (e.g. an AI-enabled wrapper around the
|
|
160
160
|
* default Lexical field) without changing the site-wide registration.
|
|
161
161
|
*
|
|
@@ -225,8 +225,6 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
225
225
|
* formatters are shared with the list view. Omit to fall back to a
|
|
226
226
|
* single-line render of `useAsTitle` + `path`.
|
|
227
227
|
*
|
|
228
|
-
* Resolve it through `resolveItemViewColumns(config)` rather than reading
|
|
229
|
-
* the field directly, so the deprecated `picker` alias keeps working.
|
|
230
228
|
*/
|
|
231
229
|
itemView?: ColumnDefinition<T>[];
|
|
232
230
|
/**
|
|
@@ -248,12 +246,6 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
248
246
|
* omitted.
|
|
249
247
|
*/
|
|
250
248
|
itemViewSort?: ListDefaultSort<T>;
|
|
251
|
-
/**
|
|
252
|
-
* @deprecated Renamed to {@link itemView}. Kept as a backwards-compatible
|
|
253
|
-
* alias — `itemView` wins when both are present. Read both via
|
|
254
|
-
* `resolveItemViewColumns(config)`. Will be removed in a future major.
|
|
255
|
-
*/
|
|
256
|
-
picker?: ColumnDefinition<T>[];
|
|
257
249
|
/** Default columns to show when no explicit column config is provided. */
|
|
258
250
|
defaultColumns?: string[];
|
|
259
251
|
/** Named tab components. Each set is one tab bar. */
|
|
@@ -275,7 +267,7 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
275
267
|
* structure fields (`files.filesGroup.publicationFile`). Paths address
|
|
276
268
|
* field *declarations*, never item instances (no `[0]` indices), and never
|
|
277
269
|
* traverse a `blocks` field — blocks take their overrides from the
|
|
278
|
-
* blockType-keyed `
|
|
270
|
+
* blockType-keyed `AdminConfig.blockAdmin` registry instead.
|
|
279
271
|
* Placement is no longer expressed here — see the layout primitives above.
|
|
280
272
|
*/
|
|
281
273
|
fields?: Record<string, FieldAdminConfig>;
|
|
@@ -329,9 +321,11 @@ export interface CollectionAdminConfig<T = any> {
|
|
|
329
321
|
* }
|
|
330
322
|
* ```
|
|
331
323
|
*
|
|
332
|
-
* Returned URLs may be relative (`/news/foo`) for same-origin hosts
|
|
333
|
-
*
|
|
334
|
-
*
|
|
324
|
+
* Returned URLs may be relative (`/news/foo`) for same-origin hosts or
|
|
325
|
+
* absolute (`https://example.com/news/foo`) for navigation to another
|
|
326
|
+
* origin. An absolute URL does not transfer Byline's host-only admin and
|
|
327
|
+
* preview cookies; cross-origin draft preview therefore requires a separate
|
|
328
|
+
* authentication and preview-state handoff.
|
|
335
329
|
*
|
|
336
330
|
* Future consideration — a per-collection `preview.populate` hint
|
|
337
331
|
* (`PopulateSpec`) was prototyped and removed. The edit-view loader
|
|
@@ -403,7 +397,7 @@ export declare function defineAdmin<T = any>(schema: CollectionDefinition, confi
|
|
|
403
397
|
* `CollectionAdminConfig`, carrying **rendering overrides only**.
|
|
404
398
|
*
|
|
405
399
|
* Linked to a `Block` by `blockType` matching the block's `blockType`, and
|
|
406
|
-
* registered site-wide on `
|
|
400
|
+
* registered site-wide on `AdminConfig.blockAdmin`. Because blocks are
|
|
407
401
|
* cross-collection units (the same `defineBlock()` object is typically shared
|
|
408
402
|
* by several collections, and codegen dedupes their contracts structurally),
|
|
409
403
|
* a block's admin config applies wherever the block renders. If one
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { RequestContext } from '@byline/auth';
|
|
9
9
|
import type { ReadContext } from './db-types.js';
|
|
10
10
|
import type { FieldSetData, FieldSetDataAllLocales, StoredFileValue } from './field-data-types.js';
|
|
11
|
-
import type { Block,
|
|
11
|
+
import type { Block, Field, FileField, ImageField } from './field-types.js';
|
|
12
12
|
import type { QueryPredicate } from './query-predicate.js';
|
|
13
13
|
import type { SearchFieldDecl } from './search-types.js';
|
|
14
14
|
import type { IStorageProvider } from './storage-types.js';
|
|
@@ -1340,60 +1340,3 @@ export type CollectionDataAllLocales<C extends CollectionDefinition> = Prettify<
|
|
|
1340
1340
|
created_at: Date;
|
|
1341
1341
|
updated_at: Date;
|
|
1342
1342
|
} & CollectionFieldDataAllLocales<C>>;
|
|
1343
|
-
/**
|
|
1344
|
-
* A field definition with all function-valued properties stripped.
|
|
1345
|
-
*
|
|
1346
|
-
* Safe for JSON serialization — use for API responses, SSR loader return
|
|
1347
|
-
* values, RSC props, mobile clients, and CLI introspection. The following
|
|
1348
|
-
* are omitted:
|
|
1349
|
-
* - `validate` — client UI concern, cannot cross a network boundary
|
|
1350
|
-
* - `hooks` — field-level hooks are always functions
|
|
1351
|
-
* - `defaultValue` — only literal (non-function) defaults are preserved
|
|
1352
|
-
*
|
|
1353
|
-
* Nested `fields` (composite / array / blocks) are recursively serialized.
|
|
1354
|
-
*/
|
|
1355
|
-
export type SerializableField = Omit<Field, 'validate' | 'hooks' | 'defaultValue' | 'fields' | 'blocks'> & {
|
|
1356
|
-
/** Only literal defaults are serializable; function defaults are dropped. */
|
|
1357
|
-
defaultValue?: Exclude<DefaultValue, (...args: any[]) => any>;
|
|
1358
|
-
/** Recursively serializable child fields (group / array). */
|
|
1359
|
-
fields?: SerializableField[];
|
|
1360
|
-
/** Recursively serializable blocks (blocks field). */
|
|
1361
|
-
blocks?: SerializableBlock[];
|
|
1362
|
-
};
|
|
1363
|
-
/**
|
|
1364
|
-
* A block definition with all function-valued properties stripped.
|
|
1365
|
-
*/
|
|
1366
|
-
export type SerializableBlock = Omit<Block, 'validate' | 'hooks' | 'fields'> & {
|
|
1367
|
-
fields: SerializableField[];
|
|
1368
|
-
};
|
|
1369
|
-
/**
|
|
1370
|
-
* A collection definition with all function-valued properties stripped.
|
|
1371
|
-
*
|
|
1372
|
-
* Safe for JSON serialization — use for API schema endpoints, SSR loaders,
|
|
1373
|
-
* RSC components, mobile clients, and any context where the full definition
|
|
1374
|
-
* (with live functions) cannot be transmitted.
|
|
1375
|
-
*
|
|
1376
|
-
* - Collection-level `hooks` are entirely omitted (all entries are functions).
|
|
1377
|
-
* - Field `validate`, `hooks`, and function `defaultValue` are stripped.
|
|
1378
|
-
*
|
|
1379
|
-
* On the receiving end, resolve the full `CollectionDefinition` from the
|
|
1380
|
-
* local config store via `getCollectionDefinition(path)` to regain access
|
|
1381
|
-
* to validators, hooks, and computed defaults.
|
|
1382
|
-
*/
|
|
1383
|
-
export type SerializableCollectionDefinition = Omit<CollectionDefinition, 'hooks' | 'fields'> & {
|
|
1384
|
-
fields: SerializableField[];
|
|
1385
|
-
};
|
|
1386
|
-
/**
|
|
1387
|
-
* Strips all function-valued properties from a `CollectionDefinition`,
|
|
1388
|
-
* producing a version safe for JSON serialization.
|
|
1389
|
-
*
|
|
1390
|
-
* @example
|
|
1391
|
-
* ```ts
|
|
1392
|
-
* // In an API route:
|
|
1393
|
-
* return Response.json(toSerializableCollection(collectionDef))
|
|
1394
|
-
*
|
|
1395
|
-
* // In an SSR loader:
|
|
1396
|
-
* return { schema: toSerializableCollection(collectionDef), document: data }
|
|
1397
|
-
* ```
|
|
1398
|
-
*/
|
|
1399
|
-
export declare function toSerializableCollection(def: CollectionDefinition): SerializableCollectionDefinition;
|
|
@@ -277,48 +277,3 @@ export function defineBlock(definition) {
|
|
|
277
277
|
export function defineField(definition) {
|
|
278
278
|
return definition;
|
|
279
279
|
}
|
|
280
|
-
/**
|
|
281
|
-
* Strips all function-valued properties from a `CollectionDefinition`,
|
|
282
|
-
* producing a version safe for JSON serialization.
|
|
283
|
-
*
|
|
284
|
-
* @example
|
|
285
|
-
* ```ts
|
|
286
|
-
* // In an API route:
|
|
287
|
-
* return Response.json(toSerializableCollection(collectionDef))
|
|
288
|
-
*
|
|
289
|
-
* // In an SSR loader:
|
|
290
|
-
* return { schema: toSerializableCollection(collectionDef), document: data }
|
|
291
|
-
* ```
|
|
292
|
-
*/
|
|
293
|
-
export function toSerializableCollection(def) {
|
|
294
|
-
function serializeField(field) {
|
|
295
|
-
// biome-ignore lint/suspicious/noExplicitAny: intentional structural spread
|
|
296
|
-
const { validate: _v, hooks: _h, defaultValue, fields, blocks, ...rest } = field;
|
|
297
|
-
const serialized = { ...rest };
|
|
298
|
-
// Keep defaultValue only when it is a literal (not a factory function)
|
|
299
|
-
if (defaultValue !== undefined && typeof defaultValue !== 'function') {
|
|
300
|
-
serialized.defaultValue = defaultValue;
|
|
301
|
-
}
|
|
302
|
-
// Recurse into nested child fields (group / array)
|
|
303
|
-
if (Array.isArray(fields)) {
|
|
304
|
-
serialized.fields = fields.map(serializeField);
|
|
305
|
-
}
|
|
306
|
-
// Recurse into blocks (blocks field)
|
|
307
|
-
if (Array.isArray(blocks)) {
|
|
308
|
-
serialized.blocks = blocks.map(serializeBlock);
|
|
309
|
-
}
|
|
310
|
-
return serialized;
|
|
311
|
-
}
|
|
312
|
-
function serializeBlock(block) {
|
|
313
|
-
const { validate: _v, hooks: _h, fields, ...rest } = block;
|
|
314
|
-
return {
|
|
315
|
-
...rest,
|
|
316
|
-
fields: fields.map(serializeField),
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
const { hooks: _hooks, fields, ...rest } = def;
|
|
320
|
-
return {
|
|
321
|
-
...rest,
|
|
322
|
-
fields: fields.map(serializeField),
|
|
323
|
-
};
|
|
324
|
-
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { RequestContext } from '@byline/auth';
|
|
2
2
|
import type { CollectionDefinition } from '@byline/core';
|
|
3
3
|
import type { DbErrorClassification } from '../lib/errors.js';
|
|
4
|
-
import type { QueryPredicate } from './query-predicate.js';
|
|
5
4
|
/**
|
|
6
5
|
* Read mode for document queries.
|
|
7
6
|
*
|
|
@@ -61,11 +60,6 @@ export interface ReadContext {
|
|
|
61
60
|
* (the cycle-stub path).
|
|
62
61
|
*/
|
|
63
62
|
visited: Set<string>;
|
|
64
|
-
/**
|
|
65
|
-
* @deprecated Ignored. `beforeRead` authorization state is module-private
|
|
66
|
-
* and authority-bound; this optional slot remains only for source compatibility.
|
|
67
|
-
*/
|
|
68
|
-
beforeReadCache?: Map<string, QueryPredicate | null>;
|
|
69
63
|
/** Monotonic count of document materialisations; compared against `maxReads`. */
|
|
70
64
|
readCount: number;
|
|
71
65
|
/** Hard ceiling on materialisations per request. Default 500. */
|
|
@@ -715,7 +715,7 @@ export interface FieldComponentSlots {
|
|
|
715
715
|
*
|
|
716
716
|
* Editor packages (e.g. `@byline/richtext-lexical`) expose a component
|
|
717
717
|
* matching this shape, and the installation registers it as the
|
|
718
|
-
* site-wide default via `
|
|
718
|
+
* site-wide default via `AdminConfig.fields.richText.editor`.
|
|
719
719
|
*/
|
|
720
720
|
export interface RichTextEditorProps {
|
|
721
721
|
/** The field definition (label / helpText / optional / hooks / validate). */
|
|
@@ -733,7 +733,7 @@ export interface RichTextEditorProps {
|
|
|
733
733
|
}
|
|
734
734
|
/**
|
|
735
735
|
* Component shape an editor package must export to be plugged in via
|
|
736
|
-
* `
|
|
736
|
+
* `AdminConfig.fields.richText.editor`. Built on `SlotComponent` so the
|
|
737
737
|
* type stays React-agnostic at the `@byline/core` boundary.
|
|
738
738
|
*/
|
|
739
739
|
export type RichTextEditorComponent = SlotComponent<RichTextEditorProps>;
|
|
@@ -33,14 +33,12 @@ export interface RoutesConfigInput {
|
|
|
33
33
|
signIn?: string;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
* Common configuration shared by the server and client.
|
|
37
|
-
*
|
|
38
|
-
* database adapters, no storage providers.
|
|
36
|
+
* Common live configuration shared by the server and admin client. This is an
|
|
37
|
+
* in-process runtime contract, not a JSON or transport representation.
|
|
39
38
|
*/
|
|
40
39
|
export interface BaseConfig {
|
|
41
|
-
serverURL: string;
|
|
42
40
|
i18n: {
|
|
43
|
-
|
|
41
|
+
admin: {
|
|
44
42
|
defaultLocale: string;
|
|
45
43
|
locales: string[];
|
|
46
44
|
/**
|
|
@@ -80,7 +78,7 @@ export interface BaseConfig {
|
|
|
80
78
|
locales: string[];
|
|
81
79
|
/**
|
|
82
80
|
* Optional display names for the content locales a document can be
|
|
83
|
-
* published in. Mirrors `
|
|
81
|
+
* published in. Mirrors `admin.localeDefinitions`, but for the
|
|
84
82
|
* *content* dimension rather than the admin chrome.
|
|
85
83
|
*
|
|
86
84
|
* Byline itself does not render these — the content-locale set has
|
|
@@ -109,8 +107,8 @@ export interface BaseConfig {
|
|
|
109
107
|
*
|
|
110
108
|
* Optional at the type level so `BaseConfig` stays loose for tests
|
|
111
109
|
* and seed scripts; required at runtime via `validateTranslations`
|
|
112
|
-
* whenever `
|
|
113
|
-
* the design.
|
|
110
|
+
* whenever `admin.locales` is non-empty. See
|
|
111
|
+
* `docs/08-internationalization/index.md` for the design.
|
|
114
112
|
*
|
|
115
113
|
* The shape is declared inline (rather than imported from
|
|
116
114
|
* `@byline/i18n`) so `@byline/core` stays a leaf-ish package. The
|
|
@@ -140,12 +138,12 @@ export type TranslationBundleShape = Readonly<{
|
|
|
140
138
|
}>;
|
|
141
139
|
}>;
|
|
142
140
|
/**
|
|
143
|
-
*
|
|
141
|
+
* Admin configuration. Extends BaseConfig with admin UI presentation
|
|
144
142
|
* config (React components, formatters, column definitions, etc.).
|
|
145
143
|
*
|
|
146
|
-
* Used by `
|
|
144
|
+
* Used by `defineAdminConfig()` and consumed by admin UI routes.
|
|
147
145
|
*/
|
|
148
|
-
export interface
|
|
146
|
+
export interface AdminConfig extends BaseConfig {
|
|
149
147
|
/** Admin UI configuration for collections (client-side only). */
|
|
150
148
|
admin?: CollectionAdminConfig[];
|
|
151
149
|
/**
|
|
@@ -169,8 +167,9 @@ export interface ClientConfig extends BaseConfig {
|
|
|
169
167
|
* back to the default `slugify` from `@byline/core` when not set — so
|
|
170
168
|
* installations that keep the default slugifier need not set this at all.
|
|
171
169
|
*
|
|
172
|
-
* Lives on `
|
|
173
|
-
*
|
|
170
|
+
* Lives on `AdminConfig` because only the admin path widget consumes this
|
|
171
|
+
* client-side copy. `ServerConfig` declares the authoritative server copy
|
|
172
|
+
* separately.
|
|
174
173
|
*
|
|
175
174
|
* @see ServerConfig.slugifier
|
|
176
175
|
*/
|
|
@@ -184,7 +183,7 @@ export interface ClientConfig extends BaseConfig {
|
|
|
184
183
|
* ```ts
|
|
185
184
|
* import { RichTextField } from '@byline/richtext-lexical'
|
|
186
185
|
*
|
|
187
|
-
*
|
|
186
|
+
* defineAdminConfig({
|
|
188
187
|
* // ...
|
|
189
188
|
* fields: {
|
|
190
189
|
* richText: { editor: RichTextField },
|
|
@@ -203,8 +202,8 @@ export interface ClientConfig extends BaseConfig {
|
|
|
203
202
|
};
|
|
204
203
|
};
|
|
205
204
|
}
|
|
206
|
-
/**
|
|
207
|
-
export type
|
|
205
|
+
/** Admin config returned after boundary validation and canonicalization. */
|
|
206
|
+
export type ResolvedAdminConfig = Omit<AdminConfig, 'routes'> & {
|
|
208
207
|
routes: RoutesConfig;
|
|
209
208
|
};
|
|
210
209
|
/**
|
|
@@ -230,7 +229,7 @@ export interface ServerHooksConfig {
|
|
|
230
229
|
}
|
|
231
230
|
/**
|
|
232
231
|
* Server-side configuration. Extends BaseConfig with database and storage
|
|
233
|
-
* adapters. Deliberately does NOT extend
|
|
232
|
+
* adapters. Deliberately does NOT extend AdminConfig — the server has no
|
|
234
233
|
* knowledge of React components or admin UI presentation logic.
|
|
235
234
|
*
|
|
236
235
|
* Generic over `TAdminStore` so installations can thread an adapter-built
|
|
@@ -280,8 +279,8 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
280
279
|
*
|
|
281
280
|
* This is the **server-side** copy (the authoritative one — its output is
|
|
282
281
|
* what gets persisted). For the admin path-widget's live preview to match,
|
|
283
|
-
* register the *same* function on `
|
|
284
|
-
* `
|
|
282
|
+
* register the *same* function on `AdminConfig.slugifier` via
|
|
283
|
+
* `defineAdminConfig`. See {@link AdminConfig.slugifier}.
|
|
285
284
|
*/
|
|
286
285
|
slugifier?: SlugifierFn;
|
|
287
286
|
/**
|
|
@@ -353,7 +352,7 @@ export interface ServerConfig<TAdminStore = unknown> extends BaseConfig {
|
|
|
353
352
|
adminStore?: TAdminStore;
|
|
354
353
|
/**
|
|
355
354
|
* Site-wide field-level server adapter slots. Mirrors
|
|
356
|
-
* `
|
|
355
|
+
* `AdminConfig.fields` for the server side — each entry plugs an
|
|
357
356
|
* adapter package into a framework-managed read or write phase.
|
|
358
357
|
*
|
|
359
358
|
* @example
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { UnifiedFieldValue } from '../storage/storage-row-types.js';
|
|
9
8
|
export interface BaseStore {
|
|
10
9
|
field_path: string;
|
|
11
10
|
field_name: string;
|
|
@@ -90,8 +89,3 @@ export declare function isRelationStore(fieldValue: FlattenedStore): fieldValue
|
|
|
90
89
|
export declare function isJsonStore(fieldValue: FlattenedStore): fieldValue is JsonStore;
|
|
91
90
|
export declare function isNumericStore(fieldValue: FlattenedStore): fieldValue is NumericStore;
|
|
92
91
|
export declare function isDateTimeStore(fieldValue: FlattenedStore): fieldValue is DateTimeStore;
|
|
93
|
-
/**
|
|
94
|
-
* @deprecated Use {@link UnifiedFieldValue} from '@byline/core' — same shape;
|
|
95
|
-
* this alias remains for source compatibility.
|
|
96
|
-
*/
|
|
97
|
-
export type UnionRowValue = UnifiedFieldValue;
|
|
@@ -102,36 +102,6 @@ describe('applyBeforeRead', () => {
|
|
|
102
102
|
expect(second).toBeNull();
|
|
103
103
|
expect(callCount).toBe(1);
|
|
104
104
|
});
|
|
105
|
-
it('ignores a caller-preseeded public cache entry', async () => {
|
|
106
|
-
const hook = vi.fn(() => ({ title: 'allowed' }));
|
|
107
|
-
const readContext = createReadContext({
|
|
108
|
-
beforeReadCache: new Map([['posts:any', null]]),
|
|
109
|
-
});
|
|
110
|
-
const result = await applyBeforeRead({
|
|
111
|
-
definition: baseCollection(hook),
|
|
112
|
-
requestContext: createRequestContext(),
|
|
113
|
-
readContext,
|
|
114
|
-
});
|
|
115
|
-
expect(hook).toHaveBeenCalledOnce();
|
|
116
|
-
expect(result).toEqual({ title: 'allowed' });
|
|
117
|
-
});
|
|
118
|
-
it('ignores a caller-preseeded public cache entry on the compiled-filter path', async () => {
|
|
119
|
-
const hook = vi.fn(() => ({ title: 'allowed' }));
|
|
120
|
-
const readContext = createReadContext({
|
|
121
|
-
beforeReadCache: new Map([['posts:any', null]]),
|
|
122
|
-
});
|
|
123
|
-
const filters = await compileBeforeReadFilters({
|
|
124
|
-
definition: baseCollection(hook),
|
|
125
|
-
requestContext: createRequestContext(),
|
|
126
|
-
readContext,
|
|
127
|
-
securityDomain: {},
|
|
128
|
-
parseContext: { collections: [], resolveCollectionId: vi.fn() },
|
|
129
|
-
});
|
|
130
|
-
expect(hook).toHaveBeenCalledOnce();
|
|
131
|
-
expect(filters).toEqual([
|
|
132
|
-
expect.objectContaining({ kind: 'field', fieldName: 'title', value: 'allowed' }),
|
|
133
|
-
]);
|
|
134
|
-
});
|
|
135
105
|
it('rejects reuse under a different actor authority', async () => {
|
|
136
106
|
const hook = vi.fn(({ requestContext }) => ({
|
|
137
107
|
title: requestContext.actor?.id,
|
|
@@ -22,11 +22,6 @@ export function createHookReadContext(parent, entry) {
|
|
|
22
22
|
const scoped = {};
|
|
23
23
|
Object.defineProperties(scoped, {
|
|
24
24
|
visited: { enumerable: true, get: () => root.visited, set: (value) => (root.visited = value) },
|
|
25
|
-
beforeReadCache: {
|
|
26
|
-
enumerable: true,
|
|
27
|
-
get: () => root.beforeReadCache,
|
|
28
|
-
set: (value) => (root.beforeReadCache = value),
|
|
29
|
-
},
|
|
30
25
|
readCount: {
|
|
31
26
|
enumerable: true,
|
|
32
27
|
get: () => root.readCount,
|
|
@@ -137,12 +137,11 @@ describe('server hook attachment', () => {
|
|
|
137
137
|
hooks: { collections: { documents: stable } },
|
|
138
138
|
}));
|
|
139
139
|
const invalid = {
|
|
140
|
-
serverURL: 'https://example.test',
|
|
141
140
|
collections: [definition],
|
|
142
141
|
db: {},
|
|
143
142
|
hooks: { collections: { documents: replacement } },
|
|
144
143
|
i18n: {
|
|
145
|
-
|
|
144
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
146
145
|
content: { defaultLocale: 'en', locales: [] },
|
|
147
146
|
},
|
|
148
147
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { defineAdminConfig, defineServerConfig, getCollectionDefinition } from './config.js';
|
|
3
3
|
const SERVER_CONFIG = Symbol.for('__byline_server_config__');
|
|
4
4
|
const CLIENT_CONFIG = Symbol.for('__byline_client_config__');
|
|
5
5
|
const previousServer = globalThis[SERVER_CONFIG];
|
|
@@ -23,16 +23,16 @@ function definition(path) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
const i18n = {
|
|
26
|
-
|
|
26
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
27
27
|
content: { defaultLocale: 'en', locales: [] },
|
|
28
28
|
};
|
|
29
29
|
describe('collection definition config preference', () => {
|
|
30
|
-
it('uses client definitions when only
|
|
30
|
+
it('uses client definitions when only admin config is registered', () => {
|
|
31
31
|
const globals = globalThis;
|
|
32
32
|
delete globals[SERVER_CONFIG];
|
|
33
33
|
delete globals[CLIENT_CONFIG];
|
|
34
34
|
const client = definition('client-only');
|
|
35
|
-
|
|
35
|
+
defineAdminConfig({ collections: [client], i18n });
|
|
36
36
|
expect(getCollectionDefinition('client-only')).toBe(client);
|
|
37
37
|
});
|
|
38
38
|
it('prefers hook-attached server definitions when both configs are registered', () => {
|
|
@@ -42,9 +42,8 @@ describe('collection definition config preference', () => {
|
|
|
42
42
|
const client = definition('docs');
|
|
43
43
|
const server = definition('docs');
|
|
44
44
|
const hooks = {};
|
|
45
|
-
|
|
45
|
+
defineAdminConfig({ collections: [client], i18n });
|
|
46
46
|
defineServerConfig({
|
|
47
|
-
serverURL: 'https://example.test',
|
|
48
47
|
collections: [server],
|
|
49
48
|
db: {},
|
|
50
49
|
hooks: { collections: { docs: hooks } },
|
package/dist/config/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AdminConfig, CollectionAdminConfig, CollectionDefinition, ResolvedAdminConfig, ResolvedServerConfig, ServerConfig } from '../@types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Resolve a collection definition by `path`. Returns `null` either when
|
|
4
4
|
* no config has been registered (e.g. unit tests, isolated tooling) or
|
|
@@ -7,29 +7,18 @@ import type { ClientConfig, CollectionAdminConfig, CollectionDefinition, ColumnD
|
|
|
7
7
|
* without try/catch.
|
|
8
8
|
*
|
|
9
9
|
* If a caller genuinely *requires* a registered config to proceed, it
|
|
10
|
-
* should reach for `
|
|
10
|
+
* should reach for `getAdminConfig()` / `getServerConfig()` — those
|
|
11
11
|
* still throw the loud "Byline has not been configured" error.
|
|
12
12
|
*/
|
|
13
13
|
export declare const getCollectionDefinition: (path: string) => CollectionDefinition | null;
|
|
14
14
|
export declare const getCollectionAdminConfig: (slug: string) => CollectionAdminConfig | null;
|
|
15
|
-
|
|
16
|
-
* Resolve a collection's item-row/tile columns — the per-collection projection
|
|
17
|
-
* + presentation contract used by the relation picker, relation/`hasMany`
|
|
18
|
-
* tiles, and (planned) search-result rows.
|
|
19
|
-
*
|
|
20
|
-
* Prefers the canonical {@link CollectionAdminConfig.itemView}, falling back to
|
|
21
|
-
* the deprecated `picker` alias. Always read item-view columns through this
|
|
22
|
-
* helper rather than touching `config.picker` directly, so the alias keeps
|
|
23
|
-
* working until it is removed.
|
|
24
|
-
*/
|
|
25
|
-
export declare const resolveItemViewColumns: (config: CollectionAdminConfig | null | undefined) => ColumnDefinition[] | undefined;
|
|
26
|
-
export declare function defineClientConfig(config: ClientConfig): ResolvedClientConfig;
|
|
15
|
+
export declare function defineAdminConfig(config: AdminConfig): ResolvedAdminConfig;
|
|
27
16
|
export declare function defineServerConfig<TAdminStore = unknown>(config: ServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
|
|
28
17
|
/** Resolve and validate server config without replacing the registered singleton. */
|
|
29
18
|
export declare function resolveServerConfig<TAdminStore = unknown>(config: ServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
|
|
30
19
|
/** Internal commit step used after initialization has completed successfully. */
|
|
31
20
|
export declare function registerServerConfig<TAdminStore = unknown>(config: ResolvedServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
|
|
32
|
-
export declare function
|
|
21
|
+
export declare function getAdminConfig(): ResolvedAdminConfig;
|
|
33
22
|
export declare function getServerConfig(): ResolvedServerConfig;
|
|
34
23
|
/**
|
|
35
24
|
* Order a set of locale codes by their position in the configured content
|
package/dist/config/config.js
CHANGED
|
@@ -10,7 +10,7 @@ import { validateCollections } from './validate-collections.js';
|
|
|
10
10
|
// through different module graphs) shares the same state.
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
const BYLINE_SERVER_CONFIG = Symbol.for('__byline_server_config__');
|
|
13
|
-
const
|
|
13
|
+
const BYLINE_ADMIN_CONFIG = Symbol.for('__byline_admin_config__');
|
|
14
14
|
const BYLINE_CORE = Symbol.for('__byline_core__');
|
|
15
15
|
const preparedHookAttachments = new WeakMap();
|
|
16
16
|
function getServerConfigInstance() {
|
|
@@ -20,12 +20,12 @@ function setServerConfigInstance(config) {
|
|
|
20
20
|
;
|
|
21
21
|
globalThis[BYLINE_SERVER_CONFIG] = config;
|
|
22
22
|
}
|
|
23
|
-
function
|
|
24
|
-
return globalThis[
|
|
23
|
+
function getAdminConfigInstance() {
|
|
24
|
+
return globalThis[BYLINE_ADMIN_CONFIG] ?? null;
|
|
25
25
|
}
|
|
26
|
-
function
|
|
26
|
+
function setAdminConfigInstance(config) {
|
|
27
27
|
;
|
|
28
|
-
globalThis[
|
|
28
|
+
globalThis[BYLINE_ADMIN_CONFIG] = config;
|
|
29
29
|
}
|
|
30
30
|
function getBylineCoreInstance() {
|
|
31
31
|
return globalThis[BYLINE_CORE] ?? null;
|
|
@@ -42,38 +42,27 @@ function setBylineCoreInstance(core) {
|
|
|
42
42
|
* without try/catch.
|
|
43
43
|
*
|
|
44
44
|
* If a caller genuinely *requires* a registered config to proceed, it
|
|
45
|
-
* should reach for `
|
|
45
|
+
* should reach for `getAdminConfig()` / `getServerConfig()` — those
|
|
46
46
|
* still throw the loud "Byline has not been configured" error.
|
|
47
47
|
*/
|
|
48
48
|
export const getCollectionDefinition = (path) => {
|
|
49
|
-
const config = getServerConfigInstance() ??
|
|
49
|
+
const config = getServerConfigInstance() ?? getAdminConfigInstance();
|
|
50
50
|
if (config == null)
|
|
51
51
|
return null;
|
|
52
52
|
return config.collections.find((collection) => collection.path === path) ?? null;
|
|
53
53
|
};
|
|
54
54
|
export const getCollectionAdminConfig = (slug) => {
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
55
|
+
const adminConfig = getAdminConfigInstance();
|
|
56
|
+
if (adminConfig == null)
|
|
57
57
|
return null;
|
|
58
|
-
return
|
|
58
|
+
return adminConfig.admin?.find((admin) => admin.slug === slug) ?? null;
|
|
59
59
|
};
|
|
60
|
-
|
|
61
|
-
* Resolve a collection's item-row/tile columns — the per-collection projection
|
|
62
|
-
* + presentation contract used by the relation picker, relation/`hasMany`
|
|
63
|
-
* tiles, and (planned) search-result rows.
|
|
64
|
-
*
|
|
65
|
-
* Prefers the canonical {@link CollectionAdminConfig.itemView}, falling back to
|
|
66
|
-
* the deprecated `picker` alias. Always read item-view columns through this
|
|
67
|
-
* helper rather than touching `config.picker` directly, so the alias keeps
|
|
68
|
-
* working until it is removed.
|
|
69
|
-
*/
|
|
70
|
-
export const resolveItemViewColumns = (config) => config?.itemView ?? config?.picker;
|
|
71
|
-
export function defineClientConfig(config) {
|
|
60
|
+
export function defineAdminConfig(config) {
|
|
72
61
|
validateCollections(config.collections);
|
|
73
62
|
validateAdminConfigs(config.admin, config.collections);
|
|
74
63
|
validateBlockAdminConfigs(config.blockAdmin, config.collections);
|
|
75
64
|
const resolved = { ...config, routes: resolveRoutes(config.routes) };
|
|
76
|
-
|
|
65
|
+
setAdminConfigInstance(resolved);
|
|
77
66
|
return resolved;
|
|
78
67
|
}
|
|
79
68
|
export function defineServerConfig(config) {
|
|
@@ -93,18 +82,18 @@ export function registerServerConfig(config) {
|
|
|
93
82
|
setServerConfigInstance(config);
|
|
94
83
|
return config;
|
|
95
84
|
}
|
|
96
|
-
export function
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
85
|
+
export function getAdminConfig() {
|
|
86
|
+
const adminConfig = getAdminConfigInstance();
|
|
87
|
+
if (adminConfig != null) {
|
|
88
|
+
return adminConfig;
|
|
100
89
|
}
|
|
101
|
-
// During SSR the
|
|
102
|
-
//
|
|
103
|
-
//
|
|
90
|
+
// During SSR the admin entry has not run yet, but the server config carries
|
|
91
|
+
// the shared i18n, route, collection, and slugifier values. Return a
|
|
92
|
+
// compatible object with empty admin presentation so route loaders and
|
|
93
|
+
// components work in both contexts without exposing the browser-only graph.
|
|
104
94
|
const serverConfig = getServerConfigInstance();
|
|
105
95
|
if (serverConfig != null) {
|
|
106
96
|
return {
|
|
107
|
-
serverURL: serverConfig.serverURL,
|
|
108
97
|
i18n: serverConfig.i18n,
|
|
109
98
|
routes: serverConfig.routes,
|
|
110
99
|
collections: serverConfig.collections,
|
|
@@ -115,7 +104,7 @@ export function getClientConfig() {
|
|
|
115
104
|
slugifier: serverConfig.slugifier,
|
|
116
105
|
};
|
|
117
106
|
}
|
|
118
|
-
throw new Error('Byline has not been configured yet. Please call
|
|
107
|
+
throw new Error('Byline has not been configured yet. Please call defineAdminConfig in byline.config.ts first.');
|
|
119
108
|
}
|
|
120
109
|
export function getServerConfig() {
|
|
121
110
|
if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
|
|
@@ -19,9 +19,8 @@ describe('orderByContentLocale', () => {
|
|
|
19
19
|
// Only `collections` is validated by `defineServerConfig`; the rest of
|
|
20
20
|
// the shape is irrelevant to this helper, so a minimal cast is enough.
|
|
21
21
|
defineServerConfig({
|
|
22
|
-
serverURL: 'http://test.local',
|
|
23
22
|
i18n: {
|
|
24
|
-
|
|
23
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
25
24
|
content: { defaultLocale: 'en', locales: ['en', 'fr', 'es', 'de'] },
|
|
26
25
|
},
|
|
27
26
|
collections: [],
|
|
@@ -60,11 +59,10 @@ describe('orderByContentLocale', () => {
|
|
|
60
59
|
describe('orderByContentLocale — boundary robustness', () => {
|
|
61
60
|
beforeAll(() => {
|
|
62
61
|
defineServerConfig({
|
|
63
|
-
serverURL: 'http://test.local',
|
|
64
62
|
i18n: {
|
|
65
63
|
// Interface set (`en`, `de`) deliberately overlaps content only on `en`
|
|
66
64
|
// — `de` is interface-only here, NOT a content locale.
|
|
67
|
-
|
|
65
|
+
admin: { defaultLocale: 'en', locales: ['en', 'de'] },
|
|
68
66
|
content: {
|
|
69
67
|
defaultLocale: 'en',
|
|
70
68
|
locales: ['en', 'fr', 'es'],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { normalizeRootRelativeRedirect } from '../utils/root-relative-redirect.js';
|
|
3
|
-
import {
|
|
3
|
+
import { defineAdminConfig, getAdminConfig } from './config.js';
|
|
4
4
|
import { resolveRoutes } from './routes.js';
|
|
5
5
|
describe('resolveRoutes', () => {
|
|
6
6
|
it('returns canonical defaults', () => {
|
|
@@ -95,13 +95,12 @@ describe('normalizeRootRelativeRedirect', () => {
|
|
|
95
95
|
describe('route configuration boundary', () => {
|
|
96
96
|
it('stores and exposes resolved routes independently of partial input', () => {
|
|
97
97
|
const input = { admin: 'internal/cms/' };
|
|
98
|
-
const registered =
|
|
99
|
-
serverURL: 'https://example.test',
|
|
98
|
+
const registered = defineAdminConfig({
|
|
100
99
|
routes: input,
|
|
101
100
|
collections: [],
|
|
102
101
|
admin: [],
|
|
103
102
|
i18n: {
|
|
104
|
-
|
|
103
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
105
104
|
content: { defaultLocale: 'en', locales: [] },
|
|
106
105
|
},
|
|
107
106
|
});
|
|
@@ -111,17 +110,16 @@ describe('route configuration boundary', () => {
|
|
|
111
110
|
api: '/api',
|
|
112
111
|
signIn: '/sign-in',
|
|
113
112
|
});
|
|
114
|
-
expect(
|
|
113
|
+
expect(getAdminConfig().routes).toBe(registered.routes);
|
|
115
114
|
expect(Object.isFrozen(registered.routes)).toBe(true);
|
|
116
115
|
});
|
|
117
116
|
it('prevents post-registration route mutation', () => {
|
|
118
|
-
const { routes } =
|
|
119
|
-
serverURL: 'https://example.test',
|
|
117
|
+
const { routes } = defineAdminConfig({
|
|
120
118
|
routes: { admin: '/internal/cms' },
|
|
121
119
|
collections: [],
|
|
122
120
|
admin: [],
|
|
123
121
|
i18n: {
|
|
124
|
-
|
|
122
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
125
123
|
content: { defaultLocale: 'en', locales: [] },
|
|
126
124
|
},
|
|
127
125
|
});
|
|
@@ -129,16 +127,15 @@ describe('route configuration boundary', () => {
|
|
|
129
127
|
;
|
|
130
128
|
routes.admin = '/changed';
|
|
131
129
|
}).toThrow(TypeError);
|
|
132
|
-
expect(
|
|
130
|
+
expect(getAdminConfig().routes.admin).toBe('/internal/cms');
|
|
133
131
|
});
|
|
134
132
|
it('rejects unsafe routes during registration', () => {
|
|
135
|
-
expect(() =>
|
|
136
|
-
serverURL: 'https://example.test',
|
|
133
|
+
expect(() => defineAdminConfig({
|
|
137
134
|
routes: { admin: '/cms?next=/account' },
|
|
138
135
|
collections: [],
|
|
139
136
|
admin: [],
|
|
140
137
|
i18n: {
|
|
141
|
-
|
|
138
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
142
139
|
content: { defaultLocale: 'en', locales: [] },
|
|
143
140
|
},
|
|
144
141
|
})).toThrow(/routes\.admin/);
|
|
@@ -39,7 +39,7 @@ export declare function validateAdminConfigs(admins: readonly CollectionAdminCon
|
|
|
39
39
|
/**
|
|
40
40
|
* Validate every block admin config in a configuration.
|
|
41
41
|
*
|
|
42
|
-
* Enforced rules (per `
|
|
42
|
+
* Enforced rules (per `AdminConfig.blockAdmin` entry):
|
|
43
43
|
* 1. Block pairing — `blockType` matches a block declared on at least one
|
|
44
44
|
* `type: 'blocks'` field across the registered collections (blocks have
|
|
45
45
|
* no global registry; the collections walk is the source of truth).
|
|
@@ -81,7 +81,7 @@ export function validateAdminConfigs(admins, collections) {
|
|
|
81
81
|
/**
|
|
82
82
|
* Validate every block admin config in a configuration.
|
|
83
83
|
*
|
|
84
|
-
* Enforced rules (per `
|
|
84
|
+
* Enforced rules (per `AdminConfig.blockAdmin` entry):
|
|
85
85
|
* 1. Block pairing — `blockType` matches a block declared on at least one
|
|
86
86
|
* `type: 'blocks'` field across the registered collections (blocks have
|
|
87
87
|
* no global registry; the collections walk is the source of truth).
|
package/dist/core.js
CHANGED
|
@@ -59,16 +59,16 @@ export const initBylineCore = async (config, pinoLogger) => {
|
|
|
59
59
|
// lock, mutate, and append audit rows in one transaction.
|
|
60
60
|
validateTreeAuditCapability(composed.collections, composed.db);
|
|
61
61
|
// Validate the admin i18n translation registry against the configured
|
|
62
|
-
//
|
|
62
|
+
// admin locale set. Throws on structural errors (missing bundle for
|
|
63
63
|
// a declared locale, defaultLocale outside the permitted set, …); soft
|
|
64
64
|
// warnings (key-set drift between locales) are logged so contributors
|
|
65
65
|
// see translation gaps without it blocking boot. The bundle lives at
|
|
66
|
-
// `i18n.translations` (one axis, not nested under `
|
|
66
|
+
// `i18n.translations` (one axis, not nested under `admin`) — the
|
|
67
67
|
// validator takes the assembled shape so locale config and bundle
|
|
68
68
|
// travel together.
|
|
69
69
|
const i18nValidation = validateTranslations({
|
|
70
|
-
defaultLocale: resolvedConfig.i18n.
|
|
71
|
-
locales: resolvedConfig.i18n.
|
|
70
|
+
defaultLocale: resolvedConfig.i18n.admin.defaultLocale,
|
|
71
|
+
locales: resolvedConfig.i18n.admin.locales,
|
|
72
72
|
translations: resolvedConfig.i18n.translations,
|
|
73
73
|
});
|
|
74
74
|
for (const warning of i18nValidation.warnings) {
|
package/dist/core.test.node.js
CHANGED
|
@@ -3,12 +3,11 @@ import { defineServerConfig, getServerConfig } from './config/config.js';
|
|
|
3
3
|
import { initBylineCore } from './core.js';
|
|
4
4
|
function serverConfig(admin) {
|
|
5
5
|
return {
|
|
6
|
-
serverURL: 'https://example.test',
|
|
7
6
|
routes: { admin },
|
|
8
7
|
collections: [],
|
|
9
8
|
db: {},
|
|
10
9
|
i18n: {
|
|
11
|
-
|
|
10
|
+
admin: { defaultLocale: 'en', locales: [] },
|
|
12
11
|
content: { defaultLocale: 'en', locales: [] },
|
|
13
12
|
},
|
|
14
13
|
};
|
|
@@ -18,7 +17,7 @@ describe('initBylineCore configuration registration', () => {
|
|
|
18
17
|
const valid = defineServerConfig(serverConfig('/stable/admin'));
|
|
19
18
|
const invalid = serverConfig('/replacement/admin');
|
|
20
19
|
invalid.i18n = {
|
|
21
|
-
|
|
20
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
22
21
|
content: { defaultLocale: 'en', locales: [] },
|
|
23
22
|
};
|
|
24
23
|
await expect(initBylineCore(invalid, {})).rejects.toThrow(/translations bundle/i);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './@types/index.js';
|
|
2
2
|
export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
|
|
3
|
-
export {
|
|
3
|
+
export { defineAdminConfig, defineServerConfig, getAdminConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
|
|
4
4
|
export { resolveRoutes } from './config/routes.js';
|
|
5
5
|
export { validateAdminConfigs, validateBlockAdminConfigs, } from './config/validate-admin-configs.js';
|
|
6
6
|
export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
export * from './@types/index.js';
|
|
18
18
|
export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
|
|
19
|
-
export {
|
|
19
|
+
export { defineAdminConfig, defineServerConfig, getAdminConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, } from './config/config.js';
|
|
20
20
|
export { resolveRoutes } from './config/routes.js';
|
|
21
21
|
export { validateAdminConfigs, validateBlockAdminConfigs, } from './config/validate-admin-configs.js';
|
|
22
22
|
export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* normal during community-contributor flow.
|
|
26
26
|
*/
|
|
27
27
|
import type { TranslationBundleShape } from '../@types/site-config.js';
|
|
28
|
-
export interface
|
|
28
|
+
export interface AdminI18nConfig {
|
|
29
29
|
defaultLocale: string;
|
|
30
30
|
locales: string[];
|
|
31
31
|
translations?: TranslationBundleShape;
|
|
@@ -45,4 +45,4 @@ export interface ValidateTranslationsResult {
|
|
|
45
45
|
* locale set. Throws on any structural error; returns soft warnings for
|
|
46
46
|
* key-set drift between locales.
|
|
47
47
|
*/
|
|
48
|
-
export declare function validateTranslations(i18n:
|
|
48
|
+
export declare function validateTranslations(i18n: AdminI18nConfig): ValidateTranslationsResult;
|
|
@@ -15,7 +15,7 @@ export function validateTranslations(i18n) {
|
|
|
15
15
|
const errors = [];
|
|
16
16
|
// (1) Default must be in the permitted set.
|
|
17
17
|
if (locales.length > 0 && !locales.includes(defaultLocale)) {
|
|
18
|
-
errors.push(`defaultLocale '${defaultLocale}' is not in i18n.
|
|
18
|
+
errors.push(`defaultLocale '${defaultLocale}' is not in i18n.admin.locales [${locales.join(', ')}]. ` +
|
|
19
19
|
`Add it to locales, or change defaultLocale to one of the existing entries.`);
|
|
20
20
|
}
|
|
21
21
|
// No locales declared → skip the remaining checks. Hosts that don't
|
|
@@ -26,8 +26,8 @@ export function validateTranslations(i18n) {
|
|
|
26
26
|
}
|
|
27
27
|
// (2) Non-empty locale set requires a translations bundle.
|
|
28
28
|
if (translations == null) {
|
|
29
|
-
errors.push(`i18n.
|
|
30
|
-
`Pass one to
|
|
29
|
+
errors.push(`i18n.admin.locales declares [${locales.join(', ')}] but no translations bundle is registered. ` +
|
|
30
|
+
`Pass one to defineAdminConfig via i18n.translations — see @byline/i18n's adminTranslations() ` +
|
|
31
31
|
`and mergeTranslations() helpers.`);
|
|
32
32
|
if (errors.length > 0) {
|
|
33
33
|
throw new Error(formatErrors(errors));
|
|
@@ -38,7 +38,7 @@ export function validateTranslations(i18n) {
|
|
|
38
38
|
for (const locale of locales) {
|
|
39
39
|
const localeBundle = translations[locale];
|
|
40
40
|
if (localeBundle == null || namespaceKeyCount(localeBundle) === 0) {
|
|
41
|
-
errors.push(`i18n.
|
|
41
|
+
errors.push(`i18n.admin.locales includes '${locale}' but no translations are registered for it. ` +
|
|
42
42
|
`Either drop '${locale}' from locales or wire a community bundle (e.g. @byline/i18n-${locale}).`);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -46,7 +46,7 @@ describe('validateTranslations — structural errors', () => {
|
|
|
46
46
|
en: { 'byline-admin': { a: 'A' } },
|
|
47
47
|
fr: { 'byline-admin': { a: 'A' } },
|
|
48
48
|
},
|
|
49
|
-
})).toThrow(/defaultLocale 'de' is not in i18n\.
|
|
49
|
+
})).toThrow(/defaultLocale 'de' is not in i18n\.admin\.locales/);
|
|
50
50
|
});
|
|
51
51
|
it('throws when locales is non-empty but no translations bundle is registered', () => {
|
|
52
52
|
expect(() => validateTranslations({
|
package/dist/services/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export * from './document-lifecycle/index.js';
|
|
|
8
8
|
export * from './document-read.js';
|
|
9
9
|
export * from './document-to-markdown.js';
|
|
10
10
|
export * from './field-upload.js';
|
|
11
|
-
export { type
|
|
11
|
+
export { type AdminI18nConfig, type TranslationDriftWarning, type ValidateTranslationsResult, validateTranslations, } from './i18n-validator.js';
|
|
12
12
|
export { type CanonicalNumericFieldType, type CanonicalNumericValue, isCanonicalNumericValue, normalizeNumericFields, normalizeNumericValue, } from './normalize-numeric-fields.js';
|
|
13
13
|
export { type CycleRelationValue, createReadContext, type PopulatedRelationValue, type PopulateFieldOptions, type PopulateFieldSpec, type PopulateMap, type PopulateOptions, type PopulateSpec, populateDocuments, type ReadContext, resolveIdentityField, type UnresolvedRelationValue, } from './populate.js';
|
|
14
14
|
export { buildRelationSummaryPopulateMap, type RelationTargetResolver, resolveRelationProjection, } from './relation-projection.js';
|
|
@@ -5,6 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Relation projection helpers.
|
|
10
|
+
*
|
|
11
|
+
* Translate a source collection's relation fields into a `PopulateMap`
|
|
12
|
+
* projection that fetches just enough data from each target to render
|
|
13
|
+
* a relation-summary tile on the admin edit view (picker columns,
|
|
14
|
+
* `useAsTitle`, plus an optional source-side `displayField` override).
|
|
15
|
+
*
|
|
16
|
+
* Consumed by the admin webapp's edit-route server fn so relation tiles
|
|
17
|
+
* arrive pre-hydrated on first paint — no client-side fetch per relation.
|
|
18
|
+
*/
|
|
8
19
|
import type { CollectionAdminConfig, CollectionDefinition, FieldSet, RelationField } from '../@types/index.js';
|
|
9
20
|
import type { PopulateMap } from './populate.js';
|
|
10
21
|
/**
|
|
@@ -5,18 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
/**
|
|
9
|
-
* Relation projection helpers.
|
|
10
|
-
*
|
|
11
|
-
* Translate a source collection's relation fields into a `PopulateMap`
|
|
12
|
-
* projection that fetches just enough data from each target to render
|
|
13
|
-
* a relation-summary tile on the admin edit view (picker columns,
|
|
14
|
-
* `useAsTitle`, plus an optional source-side `displayField` override).
|
|
15
|
-
*
|
|
16
|
-
* Consumed by the admin webapp's edit-route server fn so relation tiles
|
|
17
|
-
* arrive pre-hydrated on first paint — no client-side fetch per relation.
|
|
18
|
-
*/
|
|
19
|
-
import { resolveItemViewColumns } from '../config/config.js';
|
|
20
8
|
/**
|
|
21
9
|
* Union of the target-collection field names needed to render a
|
|
22
10
|
* relation-summary tile for the given source relation field.
|
|
@@ -38,7 +26,7 @@ export function resolveRelationProjection(sourceField, targetDef, targetAdmin) {
|
|
|
38
26
|
const firstText = targetDef?.fields.find((f) => f.type === 'text')?.name;
|
|
39
27
|
if (firstText)
|
|
40
28
|
out.add(firstText);
|
|
41
|
-
const itemViewColumns =
|
|
29
|
+
const itemViewColumns = targetAdmin?.itemView;
|
|
42
30
|
if (itemViewColumns) {
|
|
43
31
|
for (const col of itemViewColumns) {
|
|
44
32
|
const name = String(col.fieldName);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.13.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -80,20 +80,20 @@
|
|
|
80
80
|
"dotenv": "^17.4.2",
|
|
81
81
|
"pino": "^10.3.1",
|
|
82
82
|
"sharp": "^0.35.3",
|
|
83
|
-
"uuid": "^14.0.
|
|
83
|
+
"uuid": "^14.0.2",
|
|
84
84
|
"zod": "^4.4.3",
|
|
85
|
-
"@byline/auth": "4.
|
|
85
|
+
"@byline/auth": "4.13.0"
|
|
86
86
|
},
|
|
87
87
|
"devDependencies": {
|
|
88
|
-
"@biomejs/biome": "2.5.
|
|
89
|
-
"@types/node": "^26.
|
|
88
|
+
"@biomejs/biome": "2.5.9",
|
|
89
|
+
"@types/node": "^26.2.0",
|
|
90
90
|
"chokidar": "^5.0.0",
|
|
91
91
|
"chokidar-cli": "^3.0.0",
|
|
92
92
|
"npm-run-all": "^4.1.5",
|
|
93
|
-
"tsc-alias": "^1.9.
|
|
94
|
-
"tsx": "^4.23.
|
|
93
|
+
"tsc-alias": "^1.9.2",
|
|
94
|
+
"tsx": "^4.23.12",
|
|
95
95
|
"typescript": "^7.0.2",
|
|
96
|
-
"vitest": "^4.1.
|
|
96
|
+
"vitest": "^4.1.11"
|
|
97
97
|
},
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"access": "public",
|