@byline/core 3.20.3 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/@types/collection-types.d.ts +28 -3
- package/dist/@types/field-data-types.d.ts +8 -10
- package/dist/@types/field-data-types.js +1 -1
- package/dist/@types/field-data-types.test.node.d.ts +1 -0
- package/dist/@types/field-data-types.test.node.js +70 -0
- package/dist/@types/index.d.ts +1 -0
- package/dist/@types/index.js +1 -0
- package/dist/@types/relation-types.d.ts +38 -0
- package/dist/@types/relation-types.js +8 -0
- package/dist/@types/site-config.d.ts +1 -1
- package/dist/codegen/fixtures/all-fields.d.ts +342 -0
- package/dist/codegen/fixtures/all-fields.expected.d.ts +120 -0
- package/dist/codegen/fixtures/all-fields.expected.js +1 -0
- package/dist/codegen/fixtures/all-fields.js +94 -0
- package/dist/codegen/index.d.ts +16 -0
- package/dist/codegen/index.js +431 -0
- package/dist/codegen/index.test.node.d.ts +1 -0
- package/dist/codegen/index.test.node.js +230 -0
- package/dist/config/validate-collections.d.ts +3 -0
- package/dist/config/validate-collections.js +25 -0
- package/dist/config/validate-collections.test.node.js +42 -0
- package/dist/core.d.ts +1 -1
- package/dist/query/parse-where.d.ts +1 -1
- package/dist/schemas/zod/builder.js +7 -3
- package/dist/schemas/zod/builder.test.node.d.ts +1 -0
- package/dist/schemas/zod/builder.test.node.js +49 -0
- package/dist/services/collection-bootstrap.d.ts +1 -1
- package/dist/services/discover-counter-groups.d.ts +1 -1
- package/dist/services/document-lifecycle/create.d.ts +2 -2
- package/dist/services/document-lifecycle/create.js +5 -2
- package/dist/services/document-lifecycle/update.d.ts +4 -4
- package/dist/services/document-lifecycle/update.js +9 -4
- package/dist/services/document-lifecycle.test.node.js +66 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/normalize-numeric-fields.d.ts +23 -0
- package/dist/services/normalize-numeric-fields.js +89 -0
- package/dist/services/normalize-numeric-fields.test.node.d.ts +8 -0
- package/dist/services/normalize-numeric-fields.test.node.js +99 -0
- package/dist/services/populate.d.ts +2 -20
- package/dist/services/richtext-populate.d.ts +1 -1
- package/dist/services/validate-search-config.d.ts +1 -1
- package/dist/storage/collection-fingerprint.js +6 -0
- package/dist/storage/collection-fingerprint.test.node.js +21 -0
- package/package.json +7 -2
|
@@ -24,6 +24,14 @@ const minimalCollection = {
|
|
|
24
24
|
],
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
|
+
const numericCollection = {
|
|
28
|
+
...minimalCollection,
|
|
29
|
+
fields: [
|
|
30
|
+
{ name: 'quantity', type: 'integer' },
|
|
31
|
+
{ name: 'score', type: 'float' },
|
|
32
|
+
{ name: 'price', type: 'decimal' },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
27
35
|
/** Build a mock IDbAdapter. Returns the adapter plus individual mock fns. */
|
|
28
36
|
function createMockDb() {
|
|
29
37
|
const createDocumentVersion = vi.fn().mockResolvedValue({
|
|
@@ -235,6 +243,23 @@ describe('Document lifecycle service', () => {
|
|
|
235
243
|
const persistedData = createDocumentVersion.mock.calls[0]?.[0].documentData;
|
|
236
244
|
expect(persistedData.title).toBe('Mutated');
|
|
237
245
|
});
|
|
246
|
+
it('normalizes numeric values before and after beforeCreate', async () => {
|
|
247
|
+
const { db, createDocumentVersion } = createMockDb();
|
|
248
|
+
const beforeCreate = vi.fn(({ data }) => {
|
|
249
|
+
expect(data).toMatchObject({ quantity: 2, score: 1.5, price: '10.00' });
|
|
250
|
+
data.quantity = '3';
|
|
251
|
+
data.price = 12.5;
|
|
252
|
+
});
|
|
253
|
+
const definition = { ...numericCollection, hooks: { beforeCreate } };
|
|
254
|
+
await createDocument(buildCtx(db, definition), {
|
|
255
|
+
data: { quantity: '2', score: '1.5', price: ' 10.00 ' },
|
|
256
|
+
});
|
|
257
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].documentData).toMatchObject({
|
|
258
|
+
quantity: 3,
|
|
259
|
+
score: 1.5,
|
|
260
|
+
price: '12.5',
|
|
261
|
+
});
|
|
262
|
+
});
|
|
238
263
|
it('derives path from useAsPath source field via the slugifier', async () => {
|
|
239
264
|
const { db, createDocumentVersion } = createMockDb();
|
|
240
265
|
const definition = { ...minimalCollection, useAsPath: 'title' };
|
|
@@ -378,6 +403,24 @@ describe('Document lifecycle service', () => {
|
|
|
378
403
|
}));
|
|
379
404
|
expect(createDocumentVersion).toHaveBeenCalledOnce();
|
|
380
405
|
});
|
|
406
|
+
it('normalizes numeric values before and after beforeUpdate', async () => {
|
|
407
|
+
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
408
|
+
getDocumentById.mockResolvedValue({ fields: { quantity: 1, score: 1, price: '1.0' } });
|
|
409
|
+
const beforeUpdate = vi.fn(({ data }) => {
|
|
410
|
+
expect(data.score).toBe(25);
|
|
411
|
+
data.score = '3.5';
|
|
412
|
+
});
|
|
413
|
+
const definition = { ...numericCollection, hooks: { beforeUpdate } };
|
|
414
|
+
await updateDocument(buildCtx(db, definition), {
|
|
415
|
+
documentId: 'doc-1',
|
|
416
|
+
data: { quantity: '2', score: '2.5e1', price: 4.25 },
|
|
417
|
+
});
|
|
418
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].documentData).toEqual({
|
|
419
|
+
quantity: 2,
|
|
420
|
+
score: 3.5,
|
|
421
|
+
price: '4.25',
|
|
422
|
+
});
|
|
423
|
+
});
|
|
381
424
|
it('afterUpdate receives documentId and documentVersionId', async () => {
|
|
382
425
|
const afterUpdate = vi.fn();
|
|
383
426
|
const { db, getDocumentById } = createMockDb();
|
|
@@ -670,6 +713,29 @@ describe('Document lifecycle service', () => {
|
|
|
670
713
|
documentVersionId: 'ver-1',
|
|
671
714
|
}));
|
|
672
715
|
});
|
|
716
|
+
it('normalizes patched and hook-produced numeric values before persistence', async () => {
|
|
717
|
+
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
718
|
+
getDocumentById.mockResolvedValue({
|
|
719
|
+
fields: { quantity: 1, score: 1, price: '1.00' },
|
|
720
|
+
});
|
|
721
|
+
const beforeUpdate = vi.fn(({ data }) => {
|
|
722
|
+
expect(data.quantity).toBe(7);
|
|
723
|
+
data.price = ' 8.500 ';
|
|
724
|
+
});
|
|
725
|
+
const definition = { ...numericCollection, hooks: { beforeUpdate } };
|
|
726
|
+
await updateDocumentWithPatches(buildCtx(db, definition), {
|
|
727
|
+
documentId: 'doc-1',
|
|
728
|
+
patches: [
|
|
729
|
+
{ kind: 'field.set', path: 'quantity', value: '7.0' },
|
|
730
|
+
{ kind: 'field.set', path: 'score', value: '2.75' },
|
|
731
|
+
],
|
|
732
|
+
});
|
|
733
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].documentData).toEqual({
|
|
734
|
+
quantity: 7,
|
|
735
|
+
score: 2.75,
|
|
736
|
+
price: '8.500',
|
|
737
|
+
});
|
|
738
|
+
});
|
|
673
739
|
});
|
|
674
740
|
// -----------------------------------------------------------------------
|
|
675
741
|
// changeDocumentStatus
|
package/dist/services/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './document-read.js';
|
|
|
9
9
|
export * from './document-to-markdown.js';
|
|
10
10
|
export * from './field-upload.js';
|
|
11
11
|
export { type InterfaceI18nConfig, type TranslationDriftWarning, type ValidateTranslationsResult, validateTranslations, } from './i18n-validator.js';
|
|
12
|
+
export { type CanonicalNumericFieldType, type CanonicalNumericValue, isCanonicalNumericValue, normalizeNumericFields, normalizeNumericValue, } from './normalize-numeric-fields.js';
|
|
12
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';
|
|
13
14
|
export { buildRelationSummaryPopulateMap, type RelationTargetResolver, resolveRelationProjection, } from './relation-projection.js';
|
|
14
15
|
export { type EmbedRichTextFieldsOptions, embedRichTextFields, resolveEmbedOnSave, } from './richtext-embed.js';
|
package/dist/services/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export * from './document-read.js';
|
|
|
10
10
|
export * from './document-to-markdown.js';
|
|
11
11
|
export * from './field-upload.js';
|
|
12
12
|
export { validateTranslations, } from './i18n-validator.js';
|
|
13
|
+
export { isCanonicalNumericValue, normalizeNumericFields, normalizeNumericValue, } from './normalize-numeric-fields.js';
|
|
13
14
|
export { createReadContext, populateDocuments, resolveIdentityField, } from './populate.js';
|
|
14
15
|
export { buildRelationSummaryPopulateMap, resolveRelationProjection, } from './relation-projection.js';
|
|
15
16
|
export { embedRichTextFields, resolveEmbedOnSave, } from './richtext-embed.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export type CanonicalNumericFieldType = 'integer' | 'float' | 'decimal';
|
|
10
|
+
export type CanonicalNumericValue = number | string;
|
|
11
|
+
/** Return whether a value already has the storage-facing canonical type and shape. */
|
|
12
|
+
export declare function isCanonicalNumericValue(fieldType: CanonicalNumericFieldType, value: unknown): value is CanonicalNumericValue;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a tolerant numeric write value to its canonical representation.
|
|
15
|
+
* `undefined` means the value was empty and should be removed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizeNumericValue(fieldType: CanonicalNumericFieldType, value: unknown, path: string): CanonicalNumericValue | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Normalize every user-writable numeric leaf in a schema-shaped data tree.
|
|
20
|
+
* Mutates `data` in place. Counter fields are deliberately excluded because
|
|
21
|
+
* their values are supplied by the lifecycle allocator.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeNumericFields(fields: FieldSet, data: Record<string, any>): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { ERR_VALIDATION } from '../lib/errors.js';
|
|
9
|
+
import { walkFieldTree } from './walk-field-tree.js';
|
|
10
|
+
const NUMERIC_LITERAL_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
11
|
+
/** Return whether a value already has the storage-facing canonical type and shape. */
|
|
12
|
+
export function isCanonicalNumericValue(fieldType, value) {
|
|
13
|
+
if (fieldType === 'decimal') {
|
|
14
|
+
return typeof value === 'string' && value.trim() === value && NUMERIC_LITERAL_RE.test(value);
|
|
15
|
+
}
|
|
16
|
+
return (typeof value === 'number' &&
|
|
17
|
+
Number.isFinite(value) &&
|
|
18
|
+
(fieldType !== 'integer' || Number.isInteger(value)));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Convert a tolerant numeric write value to its canonical representation.
|
|
22
|
+
* `undefined` means the value was empty and should be removed.
|
|
23
|
+
*/
|
|
24
|
+
export function normalizeNumericValue(fieldType, value, path) {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed === '')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!NUMERIC_LITERAL_RE.test(trimmed))
|
|
30
|
+
throwNumericValidation(fieldType, value, path);
|
|
31
|
+
if (fieldType === 'decimal')
|
|
32
|
+
return trimmed;
|
|
33
|
+
const numberValue = Number(trimmed);
|
|
34
|
+
if (!Number.isFinite(numberValue))
|
|
35
|
+
throwNumericValidation(fieldType, value, path);
|
|
36
|
+
if (fieldType === 'integer' && !Number.isInteger(numberValue)) {
|
|
37
|
+
throwNumericValidation(fieldType, value, path);
|
|
38
|
+
}
|
|
39
|
+
return numberValue;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === 'number') {
|
|
42
|
+
if (!Number.isFinite(value))
|
|
43
|
+
throwNumericValidation(fieldType, value, path);
|
|
44
|
+
if (fieldType === 'integer' && !Number.isInteger(value)) {
|
|
45
|
+
throwNumericValidation(fieldType, value, path);
|
|
46
|
+
}
|
|
47
|
+
return fieldType === 'decimal' ? String(value) : value;
|
|
48
|
+
}
|
|
49
|
+
throwNumericValidation(fieldType, value, path);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalize every user-writable numeric leaf in a schema-shaped data tree.
|
|
53
|
+
* Mutates `data` in place. Counter fields are deliberately excluded because
|
|
54
|
+
* their values are supplied by the lifecycle allocator.
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeNumericFields(fields, data) {
|
|
57
|
+
for (const leaf of walkFieldTree(fields, data)) {
|
|
58
|
+
if (!isWritableNumericType(leaf.field.type))
|
|
59
|
+
continue;
|
|
60
|
+
if (leaf.field.localized === true && isLocaleMap(leaf.value)) {
|
|
61
|
+
for (const [locale, localeValue] of Object.entries(leaf.value)) {
|
|
62
|
+
normalizeLeafValue(leaf.field.type, leaf.value, locale, localeValue, `${leaf.fieldPath}.${locale}`);
|
|
63
|
+
}
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
normalizeLeafValue(leaf.field.type, leaf.parent, leaf.key, leaf.value, leaf.fieldPath);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function normalizeLeafValue(fieldType, parent, key, value, path) {
|
|
70
|
+
const normalized = normalizeNumericValue(fieldType, value, path);
|
|
71
|
+
if (normalized === undefined) {
|
|
72
|
+
delete parent[key];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
parent[key] = normalized;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isWritableNumericType(type) {
|
|
79
|
+
return type === 'integer' || type === 'float' || type === 'decimal';
|
|
80
|
+
}
|
|
81
|
+
function isLocaleMap(value) {
|
|
82
|
+
return value != null && typeof value === 'object' && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function throwNumericValidation(fieldType, value, path) {
|
|
85
|
+
throw ERR_VALIDATION({
|
|
86
|
+
message: `invalid ${fieldType} value at '${path}'`,
|
|
87
|
+
details: { path, fieldType, value },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -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,99 @@
|
|
|
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 } from 'vitest';
|
|
9
|
+
import { BylineError, ErrorCodes } from '../lib/errors.js';
|
|
10
|
+
import { normalizeNumericFields, normalizeNumericValue } from './normalize-numeric-fields.js';
|
|
11
|
+
const fields = [
|
|
12
|
+
{ name: 'count', type: 'integer' },
|
|
13
|
+
{ name: 'ratio', type: 'float' },
|
|
14
|
+
{ name: 'price', type: 'decimal' },
|
|
15
|
+
{ name: 'sequence', type: 'counter', group: 'test' },
|
|
16
|
+
{
|
|
17
|
+
name: 'settings',
|
|
18
|
+
type: 'group',
|
|
19
|
+
fields: [{ name: 'threshold', type: 'float' }],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'rows',
|
|
23
|
+
type: 'array',
|
|
24
|
+
fields: [{ name: 'quantity', type: 'integer' }],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'content',
|
|
28
|
+
type: 'blocks',
|
|
29
|
+
blocks: [
|
|
30
|
+
{
|
|
31
|
+
blockType: 'amount',
|
|
32
|
+
fields: [{ name: 'value', type: 'decimal' }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
describe('normalizeNumericFields', () => {
|
|
38
|
+
it('normalizes numeric leaves through groups, arrays, and blocks while leaving counters alone', () => {
|
|
39
|
+
const data = {
|
|
40
|
+
count: '12',
|
|
41
|
+
ratio: ' 1.25e2 ',
|
|
42
|
+
price: ' 001.2300 ',
|
|
43
|
+
sequence: 'caller-owned-value',
|
|
44
|
+
settings: { threshold: 2 },
|
|
45
|
+
rows: [{ quantity: '3.0' }, { quantity: 4 }],
|
|
46
|
+
content: [{ _type: 'amount', value: 5.5 }],
|
|
47
|
+
};
|
|
48
|
+
normalizeNumericFields(fields, data);
|
|
49
|
+
expect(data).toEqual({
|
|
50
|
+
count: 12,
|
|
51
|
+
ratio: 125,
|
|
52
|
+
price: '001.2300',
|
|
53
|
+
sequence: 'caller-owned-value',
|
|
54
|
+
settings: { threshold: 2 },
|
|
55
|
+
rows: [{ quantity: 3 }, { quantity: 4 }],
|
|
56
|
+
content: [{ _type: 'amount', value: '5.5' }],
|
|
57
|
+
});
|
|
58
|
+
normalizeNumericFields(fields, data);
|
|
59
|
+
expect(data.price).toBe('001.2300');
|
|
60
|
+
});
|
|
61
|
+
it('removes empty and whitespace-only values', () => {
|
|
62
|
+
const data = { count: '', ratio: ' ', price: '\t' };
|
|
63
|
+
normalizeNumericFields(fields, data);
|
|
64
|
+
expect(data).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
it('normalizes all-locale value maps and reports the locale-qualified path', () => {
|
|
67
|
+
const localizedFields = [
|
|
68
|
+
{ name: 'amount', type: 'decimal', localized: true },
|
|
69
|
+
];
|
|
70
|
+
const data = { amount: { en: ' 1.20 ', fr: 'not-a-number', de: '' } };
|
|
71
|
+
expect(() => normalizeNumericFields(localizedFields, data)).toThrowError(expect.objectContaining({
|
|
72
|
+
code: ErrorCodes.VALIDATION,
|
|
73
|
+
details: expect.objectContaining({ path: 'amount.fr' }),
|
|
74
|
+
}));
|
|
75
|
+
expect(data.amount.en).toBe('1.20');
|
|
76
|
+
});
|
|
77
|
+
it.each([
|
|
78
|
+
['integer', '1.2'],
|
|
79
|
+
['integer', Number.NaN],
|
|
80
|
+
['float', Number.POSITIVE_INFINITY],
|
|
81
|
+
['float', '1e'],
|
|
82
|
+
['decimal', '--1'],
|
|
83
|
+
])('rejects invalid %s input', (fieldType, value) => {
|
|
84
|
+
expect(() => normalizeNumericValue(fieldType, value, 'nested.2.value')).toThrowError(expect.objectContaining({
|
|
85
|
+
code: ErrorCodes.VALIDATION,
|
|
86
|
+
details: expect.objectContaining({ path: 'nested.2.value' }),
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
it('throws a BylineError with ERR_VALIDATION', () => {
|
|
90
|
+
try {
|
|
91
|
+
normalizeNumericValue('integer', {}, 'count');
|
|
92
|
+
expect.fail('expected ERR_VALIDATION');
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
expect(error).toBeInstanceOf(BylineError);
|
|
96
|
+
expect(error).toMatchObject({ code: ErrorCodes.VALIDATION });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -134,10 +134,11 @@ export type { ReadContext } from '../@types/index.js';
|
|
|
134
134
|
/** Build a fresh ReadContext. */
|
|
135
135
|
export declare function createReadContext(overrides?: Partial<ReadContext>): ReadContext;
|
|
136
136
|
export type { PopulateFieldOptions, PopulateFieldSpec, PopulateMap, PopulateSpec, } from '../@types/populate-types.js';
|
|
137
|
+
export type { CycleRelationValue, PopulatedRelationValue, RelationFieldReadValue, RelationReadValue, UnpopulatedRelationValue, UnresolvedRelationValue, } from '../@types/relation-types.js';
|
|
137
138
|
export interface PopulateOptions {
|
|
138
139
|
db: IDbAdapter;
|
|
139
140
|
/** Every collection definition in the app — needed to resolve target fields. */
|
|
140
|
-
collections: CollectionDefinition[];
|
|
141
|
+
collections: readonly CollectionDefinition[];
|
|
141
142
|
/** The source collection id for `documents`. */
|
|
142
143
|
collectionId: string;
|
|
143
144
|
/**
|
|
@@ -204,25 +205,6 @@ export interface PopulateOptions {
|
|
|
204
205
|
*/
|
|
205
206
|
richTextPopulate?: import('../@types/index.js').RichTextPopulateFn;
|
|
206
207
|
}
|
|
207
|
-
/** Marker placed in a relation leaf when the target was already materialised earlier in this request. */
|
|
208
|
-
export interface CycleRelationValue extends RelatedDocumentValue {
|
|
209
|
-
_resolved: true;
|
|
210
|
-
_cycle: true;
|
|
211
|
-
}
|
|
212
|
-
/** Marker placed in a relation leaf when the target was not found (deleted). */
|
|
213
|
-
export interface UnresolvedRelationValue extends RelatedDocumentValue {
|
|
214
|
-
_resolved: false;
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Envelope placed in a relation leaf when populate successfully fetched
|
|
218
|
-
* the target document. The `document` field carries the raw storage-shape
|
|
219
|
-
* doc (`@byline/client` then reshapes it to `ClientDocument` during
|
|
220
|
-
* response shaping).
|
|
221
|
-
*/
|
|
222
|
-
export interface PopulatedRelationValue extends RelatedDocumentValue {
|
|
223
|
-
_resolved: true;
|
|
224
|
-
document: Record<string, any>;
|
|
225
|
-
}
|
|
226
208
|
/**
|
|
227
209
|
* Populate relation leaves in `opts.documents` in place, one DB
|
|
228
210
|
* round-trip per depth level per target collection.
|
|
@@ -102,4 +102,4 @@ export interface RichTextAdapterPresence {
|
|
|
102
102
|
* Called once at `initBylineCore()` time. Fail-fast at boot is the right
|
|
103
103
|
* posture; the alternative is a silent broken renderer at request time.
|
|
104
104
|
*/
|
|
105
|
-
export declare function validateRichTextFieldFlags(collections: CollectionDefinition[], adapters: RichTextAdapterPresence): void;
|
|
105
|
+
export declare function validateRichTextFieldFlags(collections: readonly CollectionDefinition[], adapters: RichTextAdapterPresence): void;
|
|
@@ -25,4 +25,4 @@ export interface SearchProviderPresence {
|
|
|
25
25
|
* actually opts in. Installations that don't use search leave
|
|
26
26
|
* `ServerConfig.search` unset and pass cleanly.
|
|
27
27
|
*/
|
|
28
|
-
export declare function validateSearchConfig(collections: CollectionDefinition[], adapters: SearchProviderPresence): void;
|
|
28
|
+
export declare function validateSearchConfig(collections: readonly CollectionDefinition[], adapters: SearchProviderPresence): void;
|
|
@@ -33,6 +33,12 @@ function canonicalField(field) {
|
|
|
33
33
|
base.targetCollection = field.targetCollection;
|
|
34
34
|
if (field.displayField !== undefined)
|
|
35
35
|
base.displayField = field.displayField;
|
|
36
|
+
if (field.hasMany === true)
|
|
37
|
+
base.hasMany = true;
|
|
38
|
+
if (field.minItems !== undefined)
|
|
39
|
+
base.minItems = field.minItems;
|
|
40
|
+
if (field.maxItems !== undefined)
|
|
41
|
+
base.maxItems = field.maxItems;
|
|
36
42
|
return base;
|
|
37
43
|
case 'datetime':
|
|
38
44
|
if (field.mode !== undefined)
|
|
@@ -145,6 +145,27 @@ describe('fingerprintCollection', () => {
|
|
|
145
145
|
rel.targetCollection = 'tags';
|
|
146
146
|
expect(await fingerprintCollection(b)).not.toBe(a);
|
|
147
147
|
});
|
|
148
|
+
it('changes when a relation becomes hasMany', async () => {
|
|
149
|
+
const a = await fingerprintCollection(baseCollection());
|
|
150
|
+
const b = baseCollection();
|
|
151
|
+
const rel = b.fields.find((f) => f.name === 'category');
|
|
152
|
+
rel.hasMany = true;
|
|
153
|
+
expect(await fingerprintCollection(b)).not.toBe(a);
|
|
154
|
+
});
|
|
155
|
+
it.each([
|
|
156
|
+
'minItems',
|
|
157
|
+
'maxItems',
|
|
158
|
+
])('changes when relation %s changes', async (constraint) => {
|
|
159
|
+
const a = baseCollection();
|
|
160
|
+
const aRel = a.fields.find((f) => f.name === 'category');
|
|
161
|
+
aRel.hasMany = true;
|
|
162
|
+
aRel[constraint] = 1;
|
|
163
|
+
const b = baseCollection();
|
|
164
|
+
const bRel = b.fields.find((f) => f.name === 'category');
|
|
165
|
+
bRel.hasMany = true;
|
|
166
|
+
bRel[constraint] = 2;
|
|
167
|
+
expect(await fingerprintCollection(b)).not.toBe(await fingerprintCollection(a));
|
|
168
|
+
});
|
|
148
169
|
it('changes when a block variant is renamed', async () => {
|
|
149
170
|
const a = await fingerprintCollection(baseCollection());
|
|
150
171
|
const b = baseCollection();
|
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": "3.
|
|
5
|
+
"version": "3.21.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"import": "./dist/index.js",
|
|
32
32
|
"require": "./dist/index.js"
|
|
33
33
|
},
|
|
34
|
+
"./codegen": {
|
|
35
|
+
"types": "./dist/codegen/index.d.ts",
|
|
36
|
+
"import": "./dist/codegen/index.js",
|
|
37
|
+
"require": "./dist/codegen/index.js"
|
|
38
|
+
},
|
|
34
39
|
"./zod-schemas": {
|
|
35
40
|
"types": "./dist/schemas/zod/index.d.ts",
|
|
36
41
|
"import": "./dist/schemas/zod/index.js",
|
|
@@ -76,7 +81,7 @@
|
|
|
76
81
|
"pino": "^10.3.1",
|
|
77
82
|
"sharp": "^0.35.3",
|
|
78
83
|
"zod": "^4.4.3",
|
|
79
|
-
"@byline/auth": "3.
|
|
84
|
+
"@byline/auth": "3.21.0"
|
|
80
85
|
},
|
|
81
86
|
"devDependencies": {
|
|
82
87
|
"@biomejs/biome": "2.5.2",
|