@byline/core 6.8.0 → 6.9.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 +6 -6
- package/dist/@types/admin-types.js +3 -3
- package/dist/@types/form-admin-config.test.node.js +1 -1
- package/dist/@types/singleton-admin-config.test.node.js +8 -5
- package/dist/config/config.d.ts +2 -2
- package/dist/config/config.js +6 -6
- package/dist/config/group-collections.js +1 -1
- package/dist/config/group-collections.test.node.js +6 -6
- package/dist/config/validate-admin-configs.d.ts +1 -1
- package/dist/config/validate-admin-configs.js +10 -10
- package/dist/config/validate-admin-configs.test.node.js +22 -22
- package/dist/paths/path-dialects.test.node.js +2 -2
- package/dist/paths/paths.test.node.js +1 -1
- package/dist/services/document-lifecycle/copy-to-locale.js +2 -1
- package/dist/services/document-lifecycle/delete-locale.js +27 -0
- package/dist/services/document-lifecycle/internals.d.ts +3 -2
- package/dist/services/document-lifecycle/internals.js +3 -3
- package/dist/services/document-lifecycle/merge-write-locale.d.ts +12 -0
- package/dist/services/document-lifecycle/merge-write-locale.js +53 -0
- package/dist/services/document-lifecycle/persistence.d.ts +4 -1
- package/dist/services/document-lifecycle/persistence.js +23 -1
- package/dist/services/document-lifecycle/update.js +2 -2
- package/dist/services/document-lifecycle.test.node.js +4 -2
- package/dist/services/singleton-lifecycle/copy-to-locale.js +1 -1
- package/dist/services/singleton-lifecycle/update.js +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/storage-flatten.js +8 -2
- package/dist/storage/storage-restore.d.ts +15 -2
- package/dist/storage/storage-restore.js +66 -15
- package/dist/storage/unavailable-blocks.test.node.d.ts +8 -0
- package/dist/storage/unavailable-blocks.test.node.js +132 -0
- package/dist/validation/document-fields.js +7 -4
- package/package.json +2 -2
|
@@ -251,14 +251,14 @@ export interface FormAdminConfig {
|
|
|
251
251
|
* Admin UI configuration for a collection.
|
|
252
252
|
* This is the presentation/UI layer — separate from the data schema.
|
|
253
253
|
*
|
|
254
|
-
* Linked to a `CollectionDefinition` by `
|
|
254
|
+
* Linked to a `CollectionDefinition` by `path` matching the collection's
|
|
255
255
|
* `path`.
|
|
256
256
|
*/
|
|
257
257
|
export interface CollectionAdminConfig<T = any> extends FormAdminConfig {
|
|
258
258
|
/** Explicit resource discriminator. Stamped by `defineAdmin`. */
|
|
259
259
|
singleton?: false;
|
|
260
260
|
/** Must match the `path` of the corresponding `CollectionDefinition`. */
|
|
261
|
-
|
|
261
|
+
path: string;
|
|
262
262
|
/**
|
|
263
263
|
* Dashboard group this collection belongs to. Must name an entry in
|
|
264
264
|
* `AdminConfig.collectionGroups`; an unknown name throws at startup.
|
|
@@ -447,7 +447,7 @@ export interface SingletonAdminConfig<T = any> extends FormAdminConfig {
|
|
|
447
447
|
/** Explicit resource discriminator. Stamped by `defineSingletonAdmin`. */
|
|
448
448
|
singleton: true;
|
|
449
449
|
/** Must match the `path` of the corresponding `SingletonDefinition`. */
|
|
450
|
-
|
|
450
|
+
path: string;
|
|
451
451
|
/**
|
|
452
452
|
* Dashboard group this resource belongs to. The registry remains named
|
|
453
453
|
* `AdminConfig.collectionGroups` for backwards compatibility.
|
|
@@ -487,14 +487,14 @@ export interface ListActionComponentProps {
|
|
|
487
487
|
}
|
|
488
488
|
/**
|
|
489
489
|
* Type-safe factory for creating a `CollectionAdminConfig` linked to a schema.
|
|
490
|
-
* Sets `
|
|
490
|
+
* Sets `path` from the schema's `path`.
|
|
491
491
|
*/
|
|
492
|
-
export declare function defineAdmin<T = any>(schema: MultiCollectionDefinition, config: Omit<CollectionAdminConfig<T>, '
|
|
492
|
+
export declare function defineAdmin<T = any>(schema: MultiCollectionDefinition, config: Omit<CollectionAdminConfig<T>, 'path' | 'singleton'>): CollectionAdminConfig<T>;
|
|
493
493
|
/**
|
|
494
494
|
* Type-safe factory for a `SingletonAdminConfig` linked to a singleton schema.
|
|
495
495
|
* Sets both the schema path and resource discriminator for authors.
|
|
496
496
|
*/
|
|
497
|
-
export declare function defineSingletonAdmin<T = any>(schema: SingletonDefinition, config: Omit<SingletonAdminConfig<T>, '
|
|
497
|
+
export declare function defineSingletonAdmin<T = any>(schema: SingletonDefinition, config: Omit<SingletonAdminConfig<T>, 'path' | 'singleton'>): SingletonAdminConfig<T>;
|
|
498
498
|
/**
|
|
499
499
|
* Admin UI configuration for a block — the block-scoped analogue of
|
|
500
500
|
* `CollectionAdminConfig`, carrying **rendering overrides only**.
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* Type-safe factory for creating a `CollectionAdminConfig` linked to a schema.
|
|
10
|
-
* Sets `
|
|
10
|
+
* Sets `path` from the schema's `path`.
|
|
11
11
|
*/
|
|
12
12
|
export function defineAdmin(schema, config) {
|
|
13
13
|
return {
|
|
14
14
|
...config,
|
|
15
15
|
singleton: false,
|
|
16
|
-
|
|
16
|
+
path: schema.path,
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -24,7 +24,7 @@ export function defineSingletonAdmin(schema, config) {
|
|
|
24
24
|
return {
|
|
25
25
|
...config,
|
|
26
26
|
singleton: true,
|
|
27
|
-
|
|
27
|
+
path: schema.path,
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
@@ -24,19 +24,22 @@ describe('singleton admin config', () => {
|
|
|
24
24
|
const singletonAdmin = defineSingletonAdmin(settings, {});
|
|
25
25
|
expect(collectionAdmin.singleton).toBe(false);
|
|
26
26
|
expect(singletonAdmin.singleton).toBe(true);
|
|
27
|
-
expect(
|
|
27
|
+
expect(collectionAdmin.path).toBe(pages.path);
|
|
28
|
+
expect(singletonAdmin.path).toBe(settings.path);
|
|
29
|
+
expect(collectionAdmin).not.toHaveProperty('slug');
|
|
30
|
+
expect(singletonAdmin).not.toHaveProperty('slug');
|
|
28
31
|
});
|
|
29
32
|
it('does not let untyped config input override factory-owned identity', () => {
|
|
30
33
|
const collectionAdmin = defineAdmin(pages, {
|
|
31
34
|
singleton: true,
|
|
32
|
-
|
|
35
|
+
path: 'wrong-collection',
|
|
33
36
|
});
|
|
34
37
|
const singletonAdmin = defineSingletonAdmin(settings, {
|
|
35
38
|
singleton: false,
|
|
36
|
-
|
|
39
|
+
path: 'wrong-singleton',
|
|
37
40
|
});
|
|
38
|
-
expect(collectionAdmin).toMatchObject({ singleton: false,
|
|
39
|
-
expect(singletonAdmin).toMatchObject({ singleton: true,
|
|
41
|
+
expect(collectionAdmin).toMatchObject({ singleton: false, path: pages.path });
|
|
42
|
+
expect(singletonAdmin).toMatchObject({ singleton: true, path: settings.path });
|
|
40
43
|
});
|
|
41
44
|
it('shares the reusable form composition contract', () => {
|
|
42
45
|
const singletonAdmin = defineSingletonAdmin(settings, {
|
package/dist/config/config.d.ts
CHANGED
|
@@ -11,8 +11,8 @@ import type { AdminConfig, CollectionAdminConfig, CollectionDefinition, Resolved
|
|
|
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
|
-
export declare const getCollectionAdminConfig: (
|
|
15
|
-
export declare const getSingletonAdminConfig: (
|
|
14
|
+
export declare const getCollectionAdminConfig: (path: string) => CollectionAdminConfig | null;
|
|
15
|
+
export declare const getSingletonAdminConfig: (path: string) => SingletonAdminConfig | null;
|
|
16
16
|
export declare function defineAdminConfig(config: AdminConfig): ResolvedAdminConfig;
|
|
17
17
|
export declare function defineServerConfig<TAdminStore = unknown>(config: ServerConfig<TAdminStore>): ResolvedServerConfig<TAdminStore>;
|
|
18
18
|
/** Resolve and validate server config without replacing the registered singleton. */
|
package/dist/config/config.js
CHANGED
|
@@ -52,18 +52,18 @@ export const getCollectionDefinition = (path) => {
|
|
|
52
52
|
return null;
|
|
53
53
|
return config.collections.find((collection) => collection.path === path) ?? null;
|
|
54
54
|
};
|
|
55
|
-
const getAdminResourceConfig = (
|
|
55
|
+
const getAdminResourceConfig = (path) => {
|
|
56
56
|
const adminConfig = getAdminConfigInstance();
|
|
57
57
|
if (adminConfig == null)
|
|
58
58
|
return null;
|
|
59
|
-
return adminConfig.admin?.find((admin) => admin.
|
|
59
|
+
return adminConfig.admin?.find((admin) => admin.path === path) ?? null;
|
|
60
60
|
};
|
|
61
|
-
export const getCollectionAdminConfig = (
|
|
62
|
-
const admin = getAdminResourceConfig(
|
|
61
|
+
export const getCollectionAdminConfig = (path) => {
|
|
62
|
+
const admin = getAdminResourceConfig(path);
|
|
63
63
|
return admin?.singleton === true ? null : admin;
|
|
64
64
|
};
|
|
65
|
-
export const getSingletonAdminConfig = (
|
|
66
|
-
const admin = getAdminResourceConfig(
|
|
65
|
+
export const getSingletonAdminConfig = (path) => {
|
|
66
|
+
const admin = getAdminResourceConfig(path);
|
|
67
67
|
return admin?.singleton === true ? admin : null;
|
|
68
68
|
};
|
|
69
69
|
export function defineAdminConfig(config) {
|
|
@@ -33,7 +33,7 @@ export function groupCollectionsForAdmin(collections, admin, collectionGroups) {
|
|
|
33
33
|
const groupByCollectionPath = new Map();
|
|
34
34
|
for (const entry of admin ?? []) {
|
|
35
35
|
if (entry.group != null)
|
|
36
|
-
groupByCollectionPath.set(entry.
|
|
36
|
+
groupByCollectionPath.set(entry.path, entry.group);
|
|
37
37
|
}
|
|
38
38
|
const membersByGroup = new Map();
|
|
39
39
|
for (const group of collectionGroups ?? []) {
|
|
@@ -29,9 +29,9 @@ const groups = [
|
|
|
29
29
|
{ name: 'taxonomy', label: 'Taxonomies' },
|
|
30
30
|
];
|
|
31
31
|
const admin = [
|
|
32
|
-
{
|
|
33
|
-
{
|
|
34
|
-
{
|
|
32
|
+
{ path: 'images', group: 'media' },
|
|
33
|
+
{ path: 'authors', group: 'authorities' },
|
|
34
|
+
{ path: 'categories', group: 'taxonomy' },
|
|
35
35
|
];
|
|
36
36
|
describe('groupCollectionsForAdmin', () => {
|
|
37
37
|
it('returns one ungrouped bucket when no registry is declared', () => {
|
|
@@ -67,13 +67,13 @@ describe('groupCollectionsForAdmin', () => {
|
|
|
67
67
|
});
|
|
68
68
|
it('preserves collection declaration order within a bucket', () => {
|
|
69
69
|
const more = define('videos');
|
|
70
|
-
const result = groupCollectionsForAdmin([more, images], [...admin, {
|
|
70
|
+
const result = groupCollectionsForAdmin([more, images], [...admin, { path: 'videos', group: 'media' }], groups);
|
|
71
71
|
expect(result[0]?.collections).toEqual([more, images]);
|
|
72
72
|
});
|
|
73
73
|
it('groups singleton and multi-collection resources together in declaration order', () => {
|
|
74
74
|
const resourceAdmin = [
|
|
75
75
|
...admin,
|
|
76
|
-
{ singleton: true,
|
|
76
|
+
{ singleton: true, path: settings.path, group: 'media' },
|
|
77
77
|
];
|
|
78
78
|
const result = groupCollectionsForAdmin([images, settings], resourceAdmin, groups);
|
|
79
79
|
expect(result[0]?.collections).toEqual([images, settings]);
|
|
@@ -85,7 +85,7 @@ describe('groupCollectionsForAdmin', () => {
|
|
|
85
85
|
it('treats an undeclared group name as ungrouped rather than throwing', () => {
|
|
86
86
|
// Boot validation rejects this configuration, but the function stays total
|
|
87
87
|
// so a renderer can never crash on a stale or hand-built config object.
|
|
88
|
-
const result = groupCollectionsForAdmin([pages], [{
|
|
88
|
+
const result = groupCollectionsForAdmin([pages], [{ path: 'pages', group: 'ghost' }], groups);
|
|
89
89
|
expect(result).toEqual([{ name: null, label: null, collections: [pages] }]);
|
|
90
90
|
});
|
|
91
91
|
it('ignores admin configs whose collection is not registered', () => {
|
|
@@ -25,7 +25,7 @@ export declare function validateCollectionGroups(collectionGroups: readonly Coll
|
|
|
25
25
|
* Validate every admin config in a configuration.
|
|
26
26
|
*
|
|
27
27
|
* Enforced rules (per admin config):
|
|
28
|
-
* 1.
|
|
28
|
+
* 1. Path pairing — `admin.path` matches exactly one `collection.path`.
|
|
29
29
|
* 2. Placement — when `layout` is present, every top-level schema field
|
|
30
30
|
* name appears exactly once across the union of all `tabSet.tabs[].fields`,
|
|
31
31
|
* all `row.fields`, all `group.fields`, `layout.main`, and `layout.sidebar`.
|
|
@@ -91,14 +91,14 @@ export function validateCollectionGroups(collectionGroups, admins) {
|
|
|
91
91
|
const known = declared.size === 0
|
|
92
92
|
? '`collectionGroups` was not declared, or is empty'
|
|
93
93
|
: `declared groups: ${[...declared].map((name) => `"${name}"`).join(', ')}`;
|
|
94
|
-
throw new Error(`Admin
|
|
94
|
+
throw new Error(`Admin config for path "${admin.path}": \`group: '${admin.group}'\` does not name a declared collection group (${known}). Add it to \`AdminConfig.collectionGroups\`, or remove the \`group\` property.`);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
98
|
* Validate every admin config in a configuration.
|
|
99
99
|
*
|
|
100
100
|
* Enforced rules (per admin config):
|
|
101
|
-
* 1.
|
|
101
|
+
* 1. Path pairing — `admin.path` matches exactly one `collection.path`.
|
|
102
102
|
* 2. Placement — when `layout` is present, every top-level schema field
|
|
103
103
|
* name appears exactly once across the union of all `tabSet.tabs[].fields`,
|
|
104
104
|
* all `row.fields`, all `group.fields`, `layout.main`, and `layout.sidebar`.
|
|
@@ -210,24 +210,24 @@ export function validateBlockAdminConfigs(blockAdmins, collections) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
function validateOne(admin, collectionsByPath) {
|
|
213
|
-
// Rule 1 —
|
|
214
|
-
const collection = collectionsByPath.get(admin.
|
|
213
|
+
// Rule 1 — path pairing.
|
|
214
|
+
const collection = collectionsByPath.get(admin.path);
|
|
215
215
|
if (collection == null) {
|
|
216
216
|
if (admin.singleton === true) {
|
|
217
|
-
throw new Error(`Singleton admin config "${admin.
|
|
217
|
+
throw new Error(`Singleton admin config "${admin.path}" has no matching singleton definition (no singleton with \`path: '${admin.path}'\` was registered).`);
|
|
218
218
|
}
|
|
219
|
-
throw new Error(`Admin config "${admin.
|
|
219
|
+
throw new Error(`Admin config "${admin.path}" has no matching collection (no collection with \`path: '${admin.path}'\` was registered).`);
|
|
220
220
|
}
|
|
221
221
|
if (admin.singleton === true && !isSingleton(collection)) {
|
|
222
|
-
throw new Error(`Singleton admin config "${admin.
|
|
222
|
+
throw new Error(`Singleton admin config "${admin.path}" targets a collection definition. Register it with \`defineAdmin\`, or point it at a singleton definition.`);
|
|
223
223
|
}
|
|
224
224
|
if (admin.singleton !== true && isSingleton(collection)) {
|
|
225
|
-
throw new Error(`Collection admin config "${admin.
|
|
225
|
+
throw new Error(`Collection admin config "${admin.path}" targets a singleton definition. Register it with \`defineSingletonAdmin\`, or point it at a multi-collection definition.`);
|
|
226
226
|
}
|
|
227
|
-
const
|
|
227
|
+
const path = admin.path;
|
|
228
228
|
const fail = (msg) => {
|
|
229
229
|
const resource = admin.singleton === true ? 'Singleton' : 'Collection';
|
|
230
|
-
throw new Error(`${resource} "${
|
|
230
|
+
throw new Error(`${resource} "${path}": ${msg}`);
|
|
231
231
|
};
|
|
232
232
|
if (admin.singleton === true) {
|
|
233
233
|
for (const key of COLLECTION_ONLY_ADMIN_KEYS) {
|
|
@@ -21,7 +21,7 @@ const collection = {
|
|
|
21
21
|
],
|
|
22
22
|
};
|
|
23
23
|
const baseAdmin = {
|
|
24
|
-
|
|
24
|
+
path: 'news',
|
|
25
25
|
tabSets: [
|
|
26
26
|
{
|
|
27
27
|
name: 'main',
|
|
@@ -80,20 +80,20 @@ describe('validateAdminConfigs', () => {
|
|
|
80
80
|
expect(() => validateAdminConfigs([admin], [collection, singleton])).toThrow(/lockPath.*not allowed on a singleton admin config/);
|
|
81
81
|
});
|
|
82
82
|
it('rejects a singleton admin config targeting a multi-collection', () => {
|
|
83
|
-
const admin = { ...defineSingletonAdmin(singleton, {}),
|
|
83
|
+
const admin = { ...defineSingletonAdmin(singleton, {}), path: collection.path };
|
|
84
84
|
expect(() => validateAdminConfigs([admin], [collection, singleton])).toThrow(/targets a collection definition/);
|
|
85
85
|
});
|
|
86
86
|
it('rejects a singleton admin config with no matching definition', () => {
|
|
87
|
-
const admin = { ...defineSingletonAdmin(singleton, {}),
|
|
87
|
+
const admin = { ...defineSingletonAdmin(singleton, {}), path: 'missing-settings' };
|
|
88
88
|
expect(() => validateAdminConfigs([admin], [collection, singleton])).toThrow(/no matching singleton definition/);
|
|
89
89
|
});
|
|
90
90
|
it('rejects a multi-collection admin config targeting a singleton', () => {
|
|
91
|
-
const admin = {
|
|
91
|
+
const admin = { path: singleton.path };
|
|
92
92
|
expect(() => validateAdminConfigs([admin], [collection, singleton])).toThrow(/targets a singleton definition/);
|
|
93
93
|
});
|
|
94
|
-
// Rule 1 —
|
|
95
|
-
it('rejects an admin config whose
|
|
96
|
-
const orphan = {
|
|
94
|
+
// Rule 1 — path pairing.
|
|
95
|
+
it('rejects an admin config whose path has no matching collection', () => {
|
|
96
|
+
const orphan = { path: 'nonexistent', layout: { main: [] } };
|
|
97
97
|
expect(() => validateAdminConfigs([orphan], [collection])).toThrow(/no matching collection/);
|
|
98
98
|
});
|
|
99
99
|
// Rule 2 — placement bookkeeping.
|
|
@@ -177,7 +177,7 @@ describe('validateAdminConfigs', () => {
|
|
|
177
177
|
// Rule 5 — nesting.
|
|
178
178
|
it('rejects a row whose fields include a non-field primitive', () => {
|
|
179
179
|
const admin = {
|
|
180
|
-
|
|
180
|
+
path: 'news',
|
|
181
181
|
tabSets: [
|
|
182
182
|
{
|
|
183
183
|
name: 'main',
|
|
@@ -198,7 +198,7 @@ describe('validateAdminConfigs', () => {
|
|
|
198
198
|
});
|
|
199
199
|
it('rejects a tab set referenced from inside a tab', () => {
|
|
200
200
|
const admin = {
|
|
201
|
-
|
|
201
|
+
path: 'news',
|
|
202
202
|
tabSets: [
|
|
203
203
|
{
|
|
204
204
|
name: 'inner',
|
|
@@ -226,7 +226,7 @@ describe('validateAdminConfigs', () => {
|
|
|
226
226
|
});
|
|
227
227
|
it('rejects a group nested inside another group', () => {
|
|
228
228
|
const admin = {
|
|
229
|
-
|
|
229
|
+
path: 'news',
|
|
230
230
|
tabSets: [
|
|
231
231
|
{
|
|
232
232
|
name: 'main',
|
|
@@ -290,7 +290,7 @@ describe('validateAdminConfigs', () => {
|
|
|
290
290
|
],
|
|
291
291
|
};
|
|
292
292
|
const nestedAdmin = (fields) => ({
|
|
293
|
-
|
|
293
|
+
path: 'library',
|
|
294
294
|
fields: fields,
|
|
295
295
|
});
|
|
296
296
|
it('accepts a dotted fields key through array and group declarations', () => {
|
|
@@ -386,14 +386,14 @@ describe('validateAdminConfigs', () => {
|
|
|
386
386
|
// Layout omitted — bookkeeping skipped, primitive declarations still checked.
|
|
387
387
|
it('skips placement bookkeeping when layout is omitted', () => {
|
|
388
388
|
const admin = {
|
|
389
|
-
|
|
389
|
+
path: 'news',
|
|
390
390
|
// No layout — renderer will synthesise a default.
|
|
391
391
|
};
|
|
392
392
|
expect(() => validateAdminConfigs([admin], [collection])).not.toThrow();
|
|
393
393
|
});
|
|
394
394
|
it('still rejects a primitive collision when layout is omitted', () => {
|
|
395
395
|
const admin = {
|
|
396
|
-
|
|
396
|
+
path: 'news',
|
|
397
397
|
rows: [
|
|
398
398
|
{ name: 'titles', fields: ['text1', 'text2'] },
|
|
399
399
|
// 'title' is also a schema field — collision still caught.
|
|
@@ -518,34 +518,34 @@ describe('validateAdminConfigs — collection groups', () => {
|
|
|
518
518
|
{ name: 'taxonomy', label: 'Taxonomies' },
|
|
519
519
|
];
|
|
520
520
|
it('accepts a valid registry and a valid reference', () => {
|
|
521
|
-
expect(() => validateAdminConfigs([{
|
|
521
|
+
expect(() => validateAdminConfigs([{ path: 'news', group: 'media' }], [collection], groups)).not.toThrow();
|
|
522
522
|
});
|
|
523
523
|
it('accepts a collection with no group when a registry is declared', () => {
|
|
524
|
-
expect(() => validateAdminConfigs([{
|
|
524
|
+
expect(() => validateAdminConfigs([{ path: 'news' }], [collection], groups)).not.toThrow();
|
|
525
525
|
});
|
|
526
526
|
it('is a no-op when no registry and no group references are present', () => {
|
|
527
|
-
expect(() => validateAdminConfigs([{
|
|
527
|
+
expect(() => validateAdminConfigs([{ path: 'news' }], [collection])).not.toThrow();
|
|
528
528
|
});
|
|
529
529
|
it('rejects a duplicate group name', () => {
|
|
530
|
-
expect(() => validateAdminConfigs([{
|
|
530
|
+
expect(() => validateAdminConfigs([{ path: 'news' }], [collection], [
|
|
531
531
|
{ name: 'media', label: 'Media' },
|
|
532
532
|
{ name: 'media', label: 'Media Library' },
|
|
533
533
|
])).toThrow(/declared more than once/);
|
|
534
534
|
});
|
|
535
535
|
it('rejects a blank group name', () => {
|
|
536
|
-
expect(() => validateAdminConfigs([{
|
|
536
|
+
expect(() => validateAdminConfigs([{ path: 'news' }], [collection], [{ name: ' ', label: 'Media' }])).toThrow(/blank `name`/);
|
|
537
537
|
});
|
|
538
538
|
it('rejects a blank group label', () => {
|
|
539
|
-
expect(() => validateAdminConfigs([{
|
|
539
|
+
expect(() => validateAdminConfigs([{ path: 'news' }], [collection], [{ name: 'media', label: '' }])).toThrow(/blank `label`/);
|
|
540
540
|
});
|
|
541
541
|
it('rejects a group reference that names no declared group', () => {
|
|
542
|
-
expect(() => validateAdminConfigs([{
|
|
542
|
+
expect(() => validateAdminConfigs([{ path: 'news', group: 'medai' }], [collection], groups)).toThrow(/does not name a declared collection group/);
|
|
543
543
|
});
|
|
544
544
|
it('names the declared groups in the unresolved-reference error', () => {
|
|
545
|
-
expect(() => validateAdminConfigs([{
|
|
545
|
+
expect(() => validateAdminConfigs([{ path: 'news', group: 'medai' }], [collection], groups)).toThrow(/"media", "taxonomy"/);
|
|
546
546
|
});
|
|
547
547
|
it('rejects a group reference when no registry was declared at all', () => {
|
|
548
|
-
expect(() => validateAdminConfigs([{
|
|
548
|
+
expect(() => validateAdminConfigs([{ path: 'news', group: 'media' }], [collection])).toThrow(/was not declared/);
|
|
549
549
|
});
|
|
550
550
|
// Registry sanity must not be skipped by the `admins` early return.
|
|
551
551
|
it('validates the registry even when there are no admin configs', () => {
|
|
@@ -139,7 +139,7 @@ describe('path dialect — upload hook registry keys', () => {
|
|
|
139
139
|
// one registration applies wherever the block renders.
|
|
140
140
|
// ---------------------------------------------------------------------------
|
|
141
141
|
describe('path dialect — admin fields{} override keys', () => {
|
|
142
|
-
const admin = (key) => ({
|
|
142
|
+
const admin = (key) => ({ path: 'pages', fields: { [key]: {} } });
|
|
143
143
|
const validate = (key) => () => validateAdminConfigs([admin(key)], [fixture()]);
|
|
144
144
|
it('accepts a top-level field name', () => {
|
|
145
145
|
expect(validate('title')).not.toThrow();
|
|
@@ -262,6 +262,6 @@ describe('path dialects — cross-dialect relationships', () => {
|
|
|
262
262
|
collections: [fixture()],
|
|
263
263
|
hooks: { uploads: { [registryKey]: async () => ({}) } },
|
|
264
264
|
})).not.toThrow();
|
|
265
|
-
expect(() => validateAdminConfigs([{
|
|
265
|
+
expect(() => validateAdminConfigs([{ path: 'pages', fields: { [adminKey]: {} } }], [fixture()])).not.toThrow();
|
|
266
266
|
});
|
|
267
267
|
});
|
|
@@ -309,7 +309,7 @@ describe('resolveDeclarationPath — agreement with the admin fields{} validator
|
|
|
309
309
|
};
|
|
310
310
|
const validatorAccepts = (key) => {
|
|
311
311
|
try {
|
|
312
|
-
validateAdminConfigs([{
|
|
312
|
+
validateAdminConfigs([{ path: 'pages', fields: { [key]: {} } }], [collection]);
|
|
313
313
|
return true;
|
|
314
314
|
}
|
|
315
315
|
catch {
|
|
@@ -104,6 +104,7 @@ export async function copyToLocale(ctx, params) {
|
|
|
104
104
|
const committed = await commitGuardedDocumentMutation(ctx, { ...params, previousVersionId, locale: params.targetLocale }, async () => {
|
|
105
105
|
if ((targetRecord._availableVersionLocales?.includes(params.targetLocale) ||
|
|
106
106
|
targetRecord._localeAgnostic === true) &&
|
|
107
|
+
!targetRecord.unavailableBlocks?.length &&
|
|
107
108
|
sameDocumentData(merged.data, baseline))
|
|
108
109
|
return { value: previousVersionId, changed: false };
|
|
109
110
|
const writeResult = await persistExistingDocumentVersion(ctx, {
|
|
@@ -113,7 +114,7 @@ export async function copyToLocale(ctx, params) {
|
|
|
113
114
|
status: getDefaultStatus(definition),
|
|
114
115
|
locale: params.targetLocale,
|
|
115
116
|
previousVersionId,
|
|
116
|
-
});
|
|
117
|
+
}, targetRecord);
|
|
117
118
|
return { value: extractVersionId(writeResult.document), changed: true };
|
|
118
119
|
});
|
|
119
120
|
const documentVersionId = committed.value;
|
|
@@ -9,6 +9,8 @@ import { resolveHooks } from '../../@types/index.js';
|
|
|
9
9
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
10
10
|
import { ERR_NOT_FOUND, ERR_VALIDATION } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
|
+
import { flattenFieldSetData } from '../../storage/storage-flatten.js';
|
|
13
|
+
import { restoreFieldSetData } from '../../storage/storage-restore.js';
|
|
12
14
|
import { getDefaultStatus } from '../../workflow/workflow.js';
|
|
13
15
|
import { runCommittedDocumentHook } from './committed-hook.js';
|
|
14
16
|
import { actorId, invokeHook } from './internals.js';
|
|
@@ -84,6 +86,31 @@ export async function deleteLocale(ctx, params) {
|
|
|
84
86
|
ctx,
|
|
85
87
|
documentId: params.documentId,
|
|
86
88
|
write: async () => {
|
|
89
|
+
// The ordinary delete-locale primitive copies raw stored rows.
|
|
90
|
+
// Reconstruct affected versions so unavailable blocks are omitted too.
|
|
91
|
+
if (targetRecord.unavailableBlocks?.length) {
|
|
92
|
+
const previous = await db.queries.documents.getDocumentByVersion({
|
|
93
|
+
document_version_id: previousVersionId,
|
|
94
|
+
document_id: params.documentId,
|
|
95
|
+
collection_id: collectionId,
|
|
96
|
+
locale: 'all',
|
|
97
|
+
});
|
|
98
|
+
const rows = flattenFieldSetData(definition.fields, previous.fields, 'all').filter((row) => row.locale !== params.locale);
|
|
99
|
+
const { data } = restoreFieldSetData(definition.fields, rows);
|
|
100
|
+
const saved = await db.commands.documents.createDocumentVersion({
|
|
101
|
+
documentId: params.documentId,
|
|
102
|
+
collectionId,
|
|
103
|
+
collectionVersion: ctx.collectionVersion,
|
|
104
|
+
collectionConfig: definition,
|
|
105
|
+
action: 'delete_locale',
|
|
106
|
+
documentData: data,
|
|
107
|
+
locale: 'all',
|
|
108
|
+
previousVersionId,
|
|
109
|
+
status: getDefaultStatus(definition),
|
|
110
|
+
createdBy: actorId(ctx),
|
|
111
|
+
});
|
|
112
|
+
return { newVersionId: saved.document.id, previousVersionId };
|
|
113
|
+
}
|
|
87
114
|
const deleted = await db.commands.documents.deleteDocumentLocale({
|
|
88
115
|
documentId: params.documentId,
|
|
89
116
|
locale: params.locale,
|
|
@@ -112,8 +112,9 @@ export declare function resolvePathForUpdate(args: {
|
|
|
112
112
|
*/
|
|
113
113
|
export declare function derivePath(definition: CollectionDefinition, data: Record<string, any>, defaultLocale: string, slugifier: SlugifierFn): string;
|
|
114
114
|
/**
|
|
115
|
-
* Strip the synthetic `_id`
|
|
116
|
-
* array-item node in a reconstructed document tree.
|
|
115
|
+
* Strip the synthetic `_id` identity from every block and
|
|
116
|
+
* array-item node in a reconstructed document tree. Preserve `_type`, which
|
|
117
|
+
* selects the schema used to flatten each surviving block.
|
|
117
118
|
*
|
|
118
119
|
* Reconstructed `locale: 'all'` trees carry stable `_id` values for
|
|
119
120
|
* blocks and array items (see CLAUDE.md → "Block/array items carry a
|
|
@@ -232,8 +232,9 @@ export function derivePath(definition, data, defaultLocale, slugifier) {
|
|
|
232
232
|
return crypto.randomUUID();
|
|
233
233
|
}
|
|
234
234
|
/**
|
|
235
|
-
* Strip the synthetic `_id`
|
|
236
|
-
* array-item node in a reconstructed document tree.
|
|
235
|
+
* Strip the synthetic `_id` identity from every block and
|
|
236
|
+
* array-item node in a reconstructed document tree. Preserve `_type`, which
|
|
237
|
+
* selects the schema used to flatten each surviving block.
|
|
237
238
|
*
|
|
238
239
|
* Reconstructed `locale: 'all'` trees carry stable `_id` values for
|
|
239
240
|
* blocks and array items (see CLAUDE.md → "Block/array items carry a
|
|
@@ -254,7 +255,6 @@ export function stripMetaIdsInPlace(value) {
|
|
|
254
255
|
if (value != null && typeof value === 'object' && !(value instanceof Date)) {
|
|
255
256
|
const obj = value;
|
|
256
257
|
delete obj._id;
|
|
257
|
-
delete obj._type;
|
|
258
258
|
for (const key of Object.keys(obj)) {
|
|
259
259
|
stripMetaIdsInPlace(obj[key]);
|
|
260
260
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { FieldSet } from '../../@types/index.js';
|
|
9
|
+
/** Preserve other locales by item identity after unavailable blocks were compacted.
|
|
10
|
+
* Incoming structure is authoritative; never carry old positional store rows forward.
|
|
11
|
+
*/
|
|
12
|
+
export declare function mergeWriteLocale(fields: FieldSet, previous: Record<string, any>, incoming: Record<string, any>, locale: string): Record<string, any>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/** Preserve other locales by item identity after unavailable blocks were compacted.
|
|
9
|
+
* Incoming structure is authoritative; never carry old positional store rows forward.
|
|
10
|
+
*/
|
|
11
|
+
export function mergeWriteLocale(fields, previous, incoming, locale) {
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const field of fields) {
|
|
14
|
+
const value = incoming[field.name];
|
|
15
|
+
const old = previous?.[field.name];
|
|
16
|
+
if (field.type === 'group') {
|
|
17
|
+
if (value != null)
|
|
18
|
+
result[field.name] = mergeWriteLocale(field.fields, old ?? {}, value, locale);
|
|
19
|
+
}
|
|
20
|
+
else if (field.type === 'array' || field.type === 'blocks') {
|
|
21
|
+
if (!Array.isArray(value))
|
|
22
|
+
continue;
|
|
23
|
+
result[field.name] = value.map((item, index) => {
|
|
24
|
+
// Hand-built payloads may omit identity metadata. Match their compacted
|
|
25
|
+
// position, as ordinary locale writes do; an explicit ID stays authoritative.
|
|
26
|
+
const prior = Array.isArray(old)
|
|
27
|
+
? item._id != null
|
|
28
|
+
? old.find((candidate) => candidate._id === item._id)
|
|
29
|
+
: old[index]
|
|
30
|
+
: undefined;
|
|
31
|
+
const childFields = field.type === 'array'
|
|
32
|
+
? field.fields
|
|
33
|
+
: field.blocks.find((block) => block.blockType === item._type)?.fields;
|
|
34
|
+
return {
|
|
35
|
+
...(item._id != null ? { _id: item._id } : {}),
|
|
36
|
+
...(field.type === 'blocks' ? { _type: item._type } : {}),
|
|
37
|
+
...mergeWriteLocale(childFields ?? [], prior ?? {}, item, locale),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (field.localized) {
|
|
42
|
+
const localized = { ...old };
|
|
43
|
+
delete localized[locale];
|
|
44
|
+
if (value !== undefined)
|
|
45
|
+
localized[locale] = value;
|
|
46
|
+
if (Object.keys(localized).length > 0)
|
|
47
|
+
result[field.name] = localized;
|
|
48
|
+
}
|
|
49
|
+
else if (value !== undefined)
|
|
50
|
+
result[field.name] = value;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import type { IDocumentCommands } from '../../@types/index.js';
|
|
9
|
+
import type { UnavailableBlock } from '../../storage/storage-restore.js';
|
|
9
10
|
import type { DocumentLifecycleContext } from './context.js';
|
|
10
11
|
type AdapterVersionWrite = Parameters<IDocumentCommands['createDocumentVersion']>[0];
|
|
11
12
|
type LifecycleOwnedWriteKeys = 'collectionId' | 'collectionVersion' | 'collectionConfig' | 'createdBy';
|
|
@@ -25,5 +26,7 @@ export declare function persistInitialDocumentVersion(ctx: DocumentLifecycleCont
|
|
|
25
26
|
* publication schedule atomically. All operation-specific preparation and
|
|
26
27
|
* hook sequencing remains in the caller.
|
|
27
28
|
*/
|
|
28
|
-
export declare function persistExistingDocumentVersion(ctx: DocumentLifecycleContext, write: ExistingDocumentVersionWrite
|
|
29
|
+
export declare function persistExistingDocumentVersion(ctx: DocumentLifecycleContext, write: ExistingDocumentVersionWrite, previousRead?: {
|
|
30
|
+
unavailableBlocks?: readonly UnavailableBlock[];
|
|
31
|
+
} | null): ReturnType<IDocumentCommands['createDocumentVersion']>;
|
|
29
32
|
export {};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { assertDocumentFields } from '../../validation/document-fields.js';
|
|
9
9
|
import { requireAuditCapability } from './audit.js';
|
|
10
10
|
import { actorId } from './internals.js';
|
|
11
|
+
import { mergeWriteLocale } from './merge-write-locale.js';
|
|
11
12
|
import { commitContentVersionWithScheduleSuspension } from './publish-schedule-consistency.js';
|
|
12
13
|
import { normalizeDocumentVersionParentError } from './stale-document.js';
|
|
13
14
|
import { appendTreeRoot } from './tree.js';
|
|
@@ -73,8 +74,29 @@ export async function persistInitialDocumentVersion(ctx, write) {
|
|
|
73
74
|
* publication schedule atomically. All operation-specific preparation and
|
|
74
75
|
* hook sequencing remains in the caller.
|
|
75
76
|
*/
|
|
76
|
-
export function persistExistingDocumentVersion(ctx, write) {
|
|
77
|
+
export async function persistExistingDocumentVersion(ctx, write, previousRead) {
|
|
77
78
|
assertWritableContent(ctx, write);
|
|
79
|
+
// Single-locale storage writes copy other locales' positional rows forward.
|
|
80
|
+
// After compaction those rows can resurrect unavailable blocks or attach a
|
|
81
|
+
// translation to the wrong survivor. Rebuild all locales by item identity.
|
|
82
|
+
if (previousRead?.unavailableBlocks?.length &&
|
|
83
|
+
write.previousVersionId &&
|
|
84
|
+
write.locale &&
|
|
85
|
+
write.locale !== 'all') {
|
|
86
|
+
const previous = await ctx.db.queries.documents.getDocumentByVersion({
|
|
87
|
+
document_version_id: write.previousVersionId,
|
|
88
|
+
document_id: write.documentId,
|
|
89
|
+
collection_id: ctx.collectionId,
|
|
90
|
+
locale: 'all',
|
|
91
|
+
});
|
|
92
|
+
if (previous?.unavailableBlocks?.length) {
|
|
93
|
+
write = {
|
|
94
|
+
...write,
|
|
95
|
+
documentData: mergeWriteLocale(ctx.definition.fields, previous.fields, write.documentData, write.locale),
|
|
96
|
+
locale: 'all',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
78
100
|
return commitContentVersionWithScheduleSuspension({
|
|
79
101
|
ctx,
|
|
80
102
|
documentId: write.documentId,
|
|
@@ -102,7 +102,7 @@ async function replaceFields(ctx, params, policy) {
|
|
|
102
102
|
status: policy === 'preserve' ? preservedStatus(ctx, locked.status) : defaultStatus,
|
|
103
103
|
locale: requestLocale,
|
|
104
104
|
previousVersionId,
|
|
105
|
-
});
|
|
105
|
+
}, latest);
|
|
106
106
|
if (policy === 'preserve' && locked.status === 'published') {
|
|
107
107
|
await db.commands.documents.archivePublishedVersions({
|
|
108
108
|
document_id: params.documentId,
|
|
@@ -212,7 +212,7 @@ export async function updateDocumentWithPatches(ctx, params) {
|
|
|
212
212
|
status: defaultStatus,
|
|
213
213
|
locale: requestLocale,
|
|
214
214
|
previousVersionId,
|
|
215
|
-
});
|
|
215
|
+
}, latest);
|
|
216
216
|
await selfHealTreePlacement(ctx, params.documentId);
|
|
217
217
|
return { value: { result, systemFields }, changed: true };
|
|
218
218
|
}).catch((err) => rethrowPathConflict(db, err, pathForCommand ?? '', sourceLocale, 'update'));
|
|
@@ -2857,7 +2857,7 @@ describe('Document lifecycle service', () => {
|
|
|
2857
2857
|
expect(call.path).toBe('hello-world-copy');
|
|
2858
2858
|
expect(result.newPath).toBe('hello-world-copy');
|
|
2859
2859
|
});
|
|
2860
|
-
it('strips _id
|
|
2860
|
+
it('strips _id identities but preserves block types when duplicating', async () => {
|
|
2861
2861
|
const mocks = createMockDb();
|
|
2862
2862
|
setupSource(mocks, {
|
|
2863
2863
|
sourceFields: {
|
|
@@ -2881,7 +2881,9 @@ describe('Document lifecycle service', () => {
|
|
|
2881
2881
|
const call = mocks.createDocumentVersion.mock.calls[0]?.[0];
|
|
2882
2882
|
const sections = call.documentData.sections;
|
|
2883
2883
|
expect(sections[0]._id).toBeUndefined();
|
|
2884
|
-
expect(sections[0]._type).
|
|
2884
|
+
expect(sections[0]._type).toBe('section');
|
|
2885
|
+
expect(sections[0].blocks[0]._type).toBe('photoBlock');
|
|
2886
|
+
expect(sections[0].blocks[1]._type).toBe('textBlock');
|
|
2885
2887
|
expect(sections[0].blocks[0]._id).toBeUndefined();
|
|
2886
2888
|
expect(sections[0].blocks[1]._id).toBeUndefined();
|
|
2887
2889
|
// Content survives — only meta is stripped.
|
|
@@ -98,7 +98,7 @@ export async function copySingletonToLocale(ctx, params) {
|
|
|
98
98
|
status: getDefaultStatus(definition),
|
|
99
99
|
locale: params.targetLocale,
|
|
100
100
|
previousVersionId: slot.currentVersion?.document_version_id,
|
|
101
|
-
});
|
|
101
|
+
}, existingTarget ?? source);
|
|
102
102
|
},
|
|
103
103
|
};
|
|
104
104
|
},
|
|
@@ -139,7 +139,7 @@ export async function updateSingleton(ctx, params) {
|
|
|
139
139
|
action: 'update',
|
|
140
140
|
documentId: mappedDocumentId,
|
|
141
141
|
previousVersionId,
|
|
142
|
-
});
|
|
142
|
+
}, observed.current);
|
|
143
143
|
const documentId = extractDocumentId(result.document);
|
|
144
144
|
const documentVersionId = extractVersionId(result.document);
|
|
145
145
|
if (!documentId || !documentVersionId)
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { assertDocumentVersionParent, type DocumentVersionParentAssertion, type
|
|
|
10
10
|
export { ALL_STORE_TYPES, type FieldStoreKind, type FieldStoreMapping, fieldTypeToStore, fieldTypeToStoreType, type StoreType, } from './field-store-map.js';
|
|
11
11
|
export { deriveVersionLocaleLedger, resolveLocaleVisibility, resolveReadLocale, type VersionLocaleLedger, } from './locale-resolution.js';
|
|
12
12
|
export { flattenFieldSetData } from './storage-flatten.js';
|
|
13
|
-
export { extractFlattenedFieldValue, type RestoreResult, restoreFieldSetData, } from './storage-restore.js';
|
|
13
|
+
export { extractFlattenedFieldValue, type RestoreResult, restoreFieldSetData, type UnavailableBlock, } from './storage-restore.js';
|
|
14
14
|
export { resolveStoreTypes } from './storage-utils.js';
|
|
15
15
|
export { type ColumnDef, storeColumnManifest, storeTableNames } from './store-manifest.js';
|
|
16
16
|
export type { FlattenedFieldValue, UnifiedFieldValue } from './storage-row-types.js';
|
|
@@ -101,8 +101,14 @@ function* flattenFieldDataGen(field, data, locale, field_path) {
|
|
|
101
101
|
else if (field.type === 'blocks') {
|
|
102
102
|
const items = data;
|
|
103
103
|
for (let i = 0; i < items.length; i++) {
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
const item = items[i];
|
|
105
|
+
if (item == null || typeof item !== 'object' || Array.isArray(item)) {
|
|
106
|
+
throw ERR_DATABASE({
|
|
107
|
+
message: `${field_path.join('.')}[${i}]: Block item is missing or must be an object`,
|
|
108
|
+
details: { fieldPath: field_path, index: i },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const { _id, _type, ...item_data } = item;
|
|
106
112
|
const item_path = [...field_path, `${i}`, _type];
|
|
107
113
|
const block = field.blocks.find((f) => f.blockType === _type);
|
|
108
114
|
if (!block) {
|
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { FieldSet } from '../@types/index.js';
|
|
9
9
|
import type { FlattenedFieldValue, UnifiedFieldValue } from './storage-row-types.js';
|
|
10
|
+
export interface UnavailableBlock {
|
|
11
|
+
/** Instance path in the compacted document, e.g. sections[0].body. */
|
|
12
|
+
fieldPath: string;
|
|
13
|
+
/** Position in the stored blocks array before compaction. */
|
|
14
|
+
storedIndex: number;
|
|
15
|
+
blockType: string;
|
|
16
|
+
id?: string;
|
|
17
|
+
}
|
|
10
18
|
export interface RestoreResult {
|
|
11
19
|
data: any;
|
|
12
20
|
warnings: string[];
|
|
21
|
+
unavailableBlocks: UnavailableBlock[];
|
|
13
22
|
}
|
|
14
23
|
/**
|
|
15
24
|
* Main entrypoint for restoring a document's field data from flattened form
|
|
16
25
|
* back into the original nested structure.
|
|
17
26
|
*
|
|
18
|
-
* Always returns `{ data, warnings }`. Warnings carry per-row reasons (e.g.
|
|
27
|
+
* Always returns `{ data, warnings, unavailableBlocks }`. Warnings carry per-row reasons (e.g.
|
|
19
28
|
* "Invalid block index 'undefined'") that are typically the result of a
|
|
20
29
|
* collection schema change leaving orphan rows behind. Callers decide what
|
|
21
30
|
* to do with them — strict reads reject any non-empty `warnings`, lenient
|
|
@@ -24,5 +33,9 @@ export interface RestoreResult {
|
|
|
24
33
|
* @param fields - The field definitions for the collection.
|
|
25
34
|
* @param flattenedData - The flattened field data to restore.
|
|
26
35
|
*/
|
|
27
|
-
export declare const restoreFieldSetData: (fields: FieldSet, flattenedData: FlattenedFieldValue[], resolveLocale?: string
|
|
36
|
+
export declare const restoreFieldSetData: (fields: FieldSet, flattenedData: FlattenedFieldValue[], resolveLocale?: string, identity?: {
|
|
37
|
+
collectionPath: string;
|
|
38
|
+
documentId: string;
|
|
39
|
+
versionId: string;
|
|
40
|
+
}) => RestoreResult;
|
|
28
41
|
export declare const extractFlattenedFieldValue: (unifiedValue: UnifiedFieldValue) => FlattenedFieldValue;
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
import { RESERVED_FIELD_NAMES } from '../config/validate-collections.js';
|
|
9
9
|
import { ERR_DATABASE } from '../lib/errors.js';
|
|
10
10
|
import { getLogger } from '../lib/logger.js';
|
|
11
|
+
// Shared across duplicate server module graphs, just like the logger registry.
|
|
12
|
+
const loggedVersionsKey = Symbol.for('byline.unavailable-block-versions');
|
|
13
|
+
const processState = globalThis;
|
|
14
|
+
processState[loggedVersionsKey] ??= new Set();
|
|
15
|
+
const loggedVersions = processState[loggedVersionsKey];
|
|
11
16
|
/**
|
|
12
17
|
* Main entrypoint for restoring a document's field data from flattened form
|
|
13
18
|
* back into the original nested structure.
|
|
14
19
|
*
|
|
15
|
-
* Always returns `{ data, warnings }`. Warnings carry per-row reasons (e.g.
|
|
20
|
+
* Always returns `{ data, warnings, unavailableBlocks }`. Warnings carry per-row reasons (e.g.
|
|
16
21
|
* "Invalid block index 'undefined'") that are typically the result of a
|
|
17
22
|
* collection schema change leaving orphan rows behind. Callers decide what
|
|
18
23
|
* to do with them — strict reads reject any non-empty `warnings`, lenient
|
|
@@ -21,9 +26,10 @@ import { getLogger } from '../lib/logger.js';
|
|
|
21
26
|
* @param fields - The field definitions for the collection.
|
|
22
27
|
* @param flattenedData - The flattened field data to restore.
|
|
23
28
|
*/
|
|
24
|
-
export const restoreFieldSetData = (fields, flattenedData, resolveLocale) => {
|
|
29
|
+
export const restoreFieldSetData = (fields, flattenedData, resolveLocale, identity) => {
|
|
25
30
|
const result = {};
|
|
26
31
|
const warnings = [];
|
|
32
|
+
const unavailableByArray = new Map();
|
|
27
33
|
for (const item of flattenedData) {
|
|
28
34
|
// biome-ignore lint/style/noNonNullAssertion: TODO: put in a proper check here?
|
|
29
35
|
const fieldName = item.field_path[0];
|
|
@@ -38,7 +44,7 @@ export const restoreFieldSetData = (fields, flattenedData, resolveLocale) => {
|
|
|
38
44
|
if (!field) {
|
|
39
45
|
}
|
|
40
46
|
else {
|
|
41
|
-
result[fieldName] = restoreFieldData(field, result[fieldName], item, 1, warnings, resolveLocale);
|
|
47
|
+
result[fieldName] = restoreFieldData(field, result[fieldName], item, 1, warnings, unavailableByArray, resolveLocale);
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
// Array/blocks/group/hasMany-relation fixup — a required list-shaped field
|
|
@@ -54,17 +60,52 @@ export const restoreFieldSetData = (fields, flattenedData, resolveLocale) => {
|
|
|
54
60
|
result[field.name] = result[field.name] || {};
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
|
-
|
|
63
|
+
const unavailableBlocks = [];
|
|
64
|
+
// Finish only after all rows (including identity metadata) have been read.
|
|
65
|
+
// Walk parents before children so nested report paths use compacted indices.
|
|
66
|
+
const finish = (fields, value, prefix = '') => {
|
|
67
|
+
for (const field of fields) {
|
|
68
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
69
|
+
const child = value?.[field.name];
|
|
70
|
+
if (field.type === 'group' && child)
|
|
71
|
+
finish(field.fields, child, path);
|
|
72
|
+
if ((field.type === 'blocks' || field.type === 'array') && Array.isArray(child)) {
|
|
73
|
+
for (const issue of [...(unavailableByArray.get(child)?.values() ?? [])].sort((a, b) => a.storedIndex - b.storedIndex))
|
|
74
|
+
unavailableBlocks.push({ fieldPath: path, ...issue });
|
|
75
|
+
const compacted = child.filter((item) => item != null);
|
|
76
|
+
value[field.name] = compacted;
|
|
77
|
+
compacted.forEach((item, index) => {
|
|
78
|
+
const itemFields = field.type === 'array'
|
|
79
|
+
? field.fields
|
|
80
|
+
: field.blocks.find((block) => block.blockType === item._type)?.fields;
|
|
81
|
+
if (itemFields)
|
|
82
|
+
finish(itemFields, item, `${path}[${index}]`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
finish(fields, result);
|
|
88
|
+
if (identity && unavailableBlocks.length > 0) {
|
|
89
|
+
const key = JSON.stringify([identity.collectionPath, identity.documentId, identity.versionId]);
|
|
90
|
+
if (!loggedVersions.has(key)) {
|
|
91
|
+
loggedVersions.add(key);
|
|
92
|
+
getLogger().warn({
|
|
93
|
+
...identity,
|
|
94
|
+
blockTypes: [...new Set(unavailableBlocks.map((issue) => issue.blockType))],
|
|
95
|
+
}, 'Document contains unavailable blocks; saving content will remove them');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { data: result, warnings, unavailableBlocks };
|
|
58
99
|
};
|
|
59
|
-
const restoreFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
100
|
+
const restoreFieldData = (field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale) => {
|
|
60
101
|
if (field.type === 'group') {
|
|
61
|
-
return restoreGroupFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
102
|
+
return restoreGroupFieldData(field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale);
|
|
62
103
|
}
|
|
63
104
|
else if (field.type === 'array') {
|
|
64
|
-
return restoreArrayFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
105
|
+
return restoreArrayFieldData(field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale);
|
|
65
106
|
}
|
|
66
107
|
else if (field.type === 'blocks') {
|
|
67
|
-
return restoreBlocksFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
108
|
+
return restoreBlocksFieldData(field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale);
|
|
68
109
|
}
|
|
69
110
|
else if (field.type === 'relation' && field.hasMany) {
|
|
70
111
|
return restoreMultiRelationFieldData(target, data, pathIndex, warnings);
|
|
@@ -95,7 +136,7 @@ const restoreFieldData = (field, target, data, pathIndex, warnings, resolveLocal
|
|
|
95
136
|
}
|
|
96
137
|
return target;
|
|
97
138
|
};
|
|
98
|
-
const restoreGroupFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
139
|
+
const restoreGroupFieldData = (field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale) => {
|
|
99
140
|
if (pathIndex >= data.field_path.length) {
|
|
100
141
|
warnings.push(`Path ended unexpectedly while restoring group: ${data.field_path.join('.')}`);
|
|
101
142
|
return target;
|
|
@@ -108,10 +149,10 @@ const restoreGroupFieldData = (field, target, data, pathIndex, warnings, resolve
|
|
|
108
149
|
return target;
|
|
109
150
|
}
|
|
110
151
|
target = target || {};
|
|
111
|
-
target[fieldName] = restoreFieldData(subField, target[fieldName], data, pathIndex + 1, warnings, resolveLocale);
|
|
152
|
+
target[fieldName] = restoreFieldData(subField, target[fieldName], data, pathIndex + 1, warnings, unavailableByArray, resolveLocale);
|
|
112
153
|
return target;
|
|
113
154
|
};
|
|
114
|
-
const restoreArrayFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
155
|
+
const restoreArrayFieldData = (field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale) => {
|
|
115
156
|
if (pathIndex >= data.field_path.length) {
|
|
116
157
|
warnings.push(`Path ended unexpectedly while restoring array: ${data.field_path.join('.')}`);
|
|
117
158
|
return target;
|
|
@@ -140,10 +181,10 @@ const restoreArrayFieldData = (field, target, data, pathIndex, warnings, resolve
|
|
|
140
181
|
// Sub-field was removed from the schema; silently skip orphaned rows.
|
|
141
182
|
return target;
|
|
142
183
|
}
|
|
143
|
-
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 2, warnings, resolveLocale);
|
|
184
|
+
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 2, warnings, unavailableByArray, resolveLocale);
|
|
144
185
|
return target;
|
|
145
186
|
};
|
|
146
|
-
const restoreBlocksFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
187
|
+
const restoreBlocksFieldData = (field, target, data, pathIndex, warnings, unavailableByArray, resolveLocale) => {
|
|
147
188
|
const arrayIndex = Number.parseInt(data.field_path[pathIndex] ?? '', 10);
|
|
148
189
|
if (Number.isNaN(arrayIndex) || arrayIndex < 0) {
|
|
149
190
|
warnings.push(`Invalid block index '${data.field_path[pathIndex]}' in path ${data.field_path.join('.')}`);
|
|
@@ -156,7 +197,17 @@ const restoreBlocksFieldData = (field, target, data, pathIndex, warnings, resolv
|
|
|
156
197
|
}
|
|
157
198
|
const block = field.blocks.find((f) => f.blockType === blockType);
|
|
158
199
|
if (block == null) {
|
|
159
|
-
|
|
200
|
+
target = target || [];
|
|
201
|
+
let issues = unavailableByArray.get(target);
|
|
202
|
+
if (!issues) {
|
|
203
|
+
issues = new Map();
|
|
204
|
+
unavailableByArray.set(target, issues);
|
|
205
|
+
}
|
|
206
|
+
const issue = issues.get(arrayIndex) ?? { storedIndex: arrayIndex, blockType };
|
|
207
|
+
if (data.field_type === 'meta' && pathIndex + 2 === data.field_path.length) {
|
|
208
|
+
issue.id = data.item_id;
|
|
209
|
+
}
|
|
210
|
+
issues.set(arrayIndex, issue);
|
|
160
211
|
return target;
|
|
161
212
|
}
|
|
162
213
|
target = target || [];
|
|
@@ -178,7 +229,7 @@ const restoreBlocksFieldData = (field, target, data, pathIndex, warnings, resolv
|
|
|
178
229
|
// Sub-field was removed from the schema; silently skip orphaned rows.
|
|
179
230
|
return target;
|
|
180
231
|
}
|
|
181
|
-
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 3, warnings, resolveLocale);
|
|
232
|
+
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 3, warnings, unavailableByArray, resolveLocale);
|
|
182
233
|
return target;
|
|
183
234
|
};
|
|
184
235
|
const restoreMultiRelationFieldData = (target, data, pathIndex, warnings) => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
9
|
+
import { getLogger } from '../lib/logger.js';
|
|
10
|
+
import { applyPatches } from '../patches/apply-patches.js';
|
|
11
|
+
import { validateDocumentFields } from '../validation/document-fields.js';
|
|
12
|
+
import { flattenFieldSetData } from './storage-flatten.js';
|
|
13
|
+
import { restoreFieldSetData } from './storage-restore.js';
|
|
14
|
+
const logger = vi.hoisted(() => ({ warn: vi.fn() }));
|
|
15
|
+
vi.mock('../lib/logger.js', () => ({ getLogger: () => logger }));
|
|
16
|
+
const live = { blockType: 'live', fields: [{ name: 'text', type: 'text' }] };
|
|
17
|
+
const retired = { ...live, blockType: 'retired' };
|
|
18
|
+
const blocks = (optional = false) => ({
|
|
19
|
+
name: 'content',
|
|
20
|
+
type: 'blocks',
|
|
21
|
+
optional,
|
|
22
|
+
blocks: [live, retired],
|
|
23
|
+
});
|
|
24
|
+
const item = (_type, i) => ({ _id: `item-${i}`, _type, text: `${_type}-${i}` });
|
|
25
|
+
describe('unavailable blocks', () => {
|
|
26
|
+
for (const optional of [false, true]) {
|
|
27
|
+
for (const types of [
|
|
28
|
+
['retired', 'live'],
|
|
29
|
+
['live', 'retired', 'live'],
|
|
30
|
+
['live', 'retired'],
|
|
31
|
+
['retired'],
|
|
32
|
+
['retired', 'retired', 'live'],
|
|
33
|
+
]) {
|
|
34
|
+
it(`compacts ${types.join(',')} (optional=${optional}) with an independent report`, () => {
|
|
35
|
+
const before = [blocks(optional)];
|
|
36
|
+
const after = [{ ...blocks(optional), blocks: [live] }];
|
|
37
|
+
const original = { content: types.map(item) };
|
|
38
|
+
const rows = flattenFieldSetData(before, original, 'en');
|
|
39
|
+
const restored = restoreFieldSetData(after, rows, 'en');
|
|
40
|
+
expect(restored.warnings).toEqual([]);
|
|
41
|
+
expect(restored.unavailableBlocks).toEqual(types.flatMap((type, storedIndex) => type === 'retired'
|
|
42
|
+
? [{ fieldPath: 'content', storedIndex, blockType: type, id: `item-${storedIndex}` }]
|
|
43
|
+
: []));
|
|
44
|
+
expect(restored.data.content).toEqual(original.content.filter((item) => item._type === 'live'));
|
|
45
|
+
for (const data of [restored.data, JSON.parse(JSON.stringify(restored.data))]) {
|
|
46
|
+
expect(validateDocumentFields(after, data)).toEqual([]);
|
|
47
|
+
const saved = restoreFieldSetData(after, flattenFieldSetData(after, data, 'en'), 'en');
|
|
48
|
+
expect(saved.unavailableBlocks).toEqual([]);
|
|
49
|
+
expect(saved.data.content ?? []).toEqual(restored.data.content);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
it('reports groups, arrays and nested blocks using compacted parent indices', () => {
|
|
55
|
+
const before = [
|
|
56
|
+
{ name: 'group', type: 'group', fields: [blocks()] },
|
|
57
|
+
{ name: 'sections', type: 'array', fields: [blocks()] },
|
|
58
|
+
{
|
|
59
|
+
name: 'outer',
|
|
60
|
+
type: 'blocks',
|
|
61
|
+
blocks: [retired, { blockType: 'container', fields: [blocks()] }],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
const after = [
|
|
65
|
+
{ name: 'group', type: 'group', fields: [{ ...blocks(), blocks: [live] }] },
|
|
66
|
+
{ name: 'sections', type: 'array', fields: [{ ...blocks(), blocks: [live] }] },
|
|
67
|
+
{
|
|
68
|
+
name: 'outer',
|
|
69
|
+
type: 'blocks',
|
|
70
|
+
blocks: [{ blockType: 'container', fields: [{ ...blocks(), blocks: [live] }] }],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
const content = [item('retired', 0), item('live', 1)];
|
|
74
|
+
const rows = flattenFieldSetData(before, {
|
|
75
|
+
group: { content },
|
|
76
|
+
sections: [{ content }],
|
|
77
|
+
outer: [item('retired', 0), { _type: 'container', content }],
|
|
78
|
+
}, 'en');
|
|
79
|
+
const result = restoreFieldSetData(after, rows.reverse(), 'en');
|
|
80
|
+
expect(result.unavailableBlocks.map((issue) => issue.fieldPath)).toEqual([
|
|
81
|
+
'group.content',
|
|
82
|
+
'sections[0].content',
|
|
83
|
+
'outer',
|
|
84
|
+
'outer[0].content',
|
|
85
|
+
]);
|
|
86
|
+
expect(result.warnings).toEqual([]);
|
|
87
|
+
expect(result.data.outer[0].content).toEqual([item('live', 1)]);
|
|
88
|
+
});
|
|
89
|
+
it('applies a positional patch to a surviving block after the missing card', () => {
|
|
90
|
+
const before = [blocks()];
|
|
91
|
+
const after = [{ ...blocks(), blocks: [live] }];
|
|
92
|
+
const source = { content: ['retired', 'live', 'live'].map(item) };
|
|
93
|
+
const { data } = restoreFieldSetData(after, flattenFieldSetData(before, source, 'en'));
|
|
94
|
+
const definition = {
|
|
95
|
+
path: 'pages',
|
|
96
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
97
|
+
fields: after,
|
|
98
|
+
};
|
|
99
|
+
const patched = applyPatches(definition, data, [
|
|
100
|
+
{ kind: 'field.set', path: 'content[1].text', value: 'edited' },
|
|
101
|
+
]);
|
|
102
|
+
expect(patched.errors).toEqual([]);
|
|
103
|
+
const saved = restoreFieldSetData(after, flattenFieldSetData(after, patched.doc, 'en'));
|
|
104
|
+
expect(saved.data.content.map((item) => [item._id, item.text])).toEqual([
|
|
105
|
+
['item-1', 'live-1'],
|
|
106
|
+
['item-2', 'edited'],
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
it.each([new Array(1), [null], [undefined], [42], [[]]].map((content) => ({ content })))('rejects malformed items without a destructure crash: $content', ({ content }) => {
|
|
110
|
+
expect(validateDocumentFields([blocks()], { content })).not.toEqual([]);
|
|
111
|
+
expect(() => flattenFieldSetData([blocks()], { content }, 'en')).toThrow(/Block item is missing or must be an object/);
|
|
112
|
+
});
|
|
113
|
+
it('logs once per document version without adding a strict-read warning', () => {
|
|
114
|
+
const spy = vi.spyOn(getLogger(), 'warn').mockImplementation(() => { });
|
|
115
|
+
try {
|
|
116
|
+
const rows = flattenFieldSetData([blocks()], { content: [item('retired', 0)] }, 'en');
|
|
117
|
+
const identity = { collectionPath: 'pages', documentId: crypto.randomUUID(), versionId: 'v1' };
|
|
118
|
+
for (let i = 0; i < 2; i++)
|
|
119
|
+
expect(restoreFieldSetData([{ ...blocks(), blocks: [live] }], rows, 'en', identity).warnings).toEqual([]);
|
|
120
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(spy).toHaveBeenCalledWith({ ...identity, blockTypes: ['retired'] }, expect.any(String));
|
|
122
|
+
restoreFieldSetData([{ ...blocks(), blocks: [live] }], rows, 'en', {
|
|
123
|
+
...identity,
|
|
124
|
+
versionId: 'v2',
|
|
125
|
+
});
|
|
126
|
+
expect(spy).toHaveBeenCalledTimes(2);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
spy.mockRestore();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -119,13 +119,16 @@ export function validateDocumentFields(fields, data, options = {}) {
|
|
|
119
119
|
if (field.validation?.maxLength != null && value.length > field.validation.maxLength)
|
|
120
120
|
add(path, `${label} allows at most ${field.validation.maxLength} items`);
|
|
121
121
|
}
|
|
122
|
-
value.
|
|
122
|
+
for (let index = 0; index < value.length; index++) {
|
|
123
|
+
const item = value[index];
|
|
123
124
|
const itemPath = record(item) && typeof item._id === 'string' && /^[\w-]+$/.test(item._id)
|
|
124
125
|
? `${path}[id=${item._id}]`
|
|
125
126
|
: `${path}[${index}]`;
|
|
126
127
|
if (!record(item)) {
|
|
127
|
-
add(itemPath,
|
|
128
|
-
|
|
128
|
+
add(itemPath, field.type === 'blocks'
|
|
129
|
+
? 'Block item is missing or must be an object'
|
|
130
|
+
: 'Item must be an object');
|
|
131
|
+
continue;
|
|
129
132
|
}
|
|
130
133
|
if (field.type === 'array')
|
|
131
134
|
visit(field.fields, item, itemPath);
|
|
@@ -136,7 +139,7 @@ export function validateDocumentFields(fields, data, options = {}) {
|
|
|
136
139
|
else
|
|
137
140
|
visit(block.fields, item, itemPath);
|
|
138
141
|
}
|
|
139
|
-
}
|
|
142
|
+
}
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
else {
|
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": "6.
|
|
5
|
+
"version": "6.9.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.22.2"
|
|
8
8
|
},
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"dist"
|
|
83
83
|
],
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"@byline/auth": "6.
|
|
85
|
+
"@byline/auth": "6.9.0",
|
|
86
86
|
"dotenv": "^18.0.8",
|
|
87
87
|
"pino": "^10.4.0",
|
|
88
88
|
"sharp": "^0.35.5",
|