@byline/db-postgres 1.4.0 → 1.6.1
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/modules/storage/storage-queries.d.ts +15 -1
- package/dist/modules/storage/storage-queries.js +28 -8
- package/dist/modules/storage/storage-restore.d.ts +11 -1
- package/dist/modules/storage/storage-restore.js +7 -11
- package/dist/modules/storage/tests/storage-flatten-reconstruct.test.js +8 -7
- package/package.json +4 -4
|
@@ -81,6 +81,13 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
81
81
|
* restoration. Meta rows (from store_meta) are converted to
|
|
82
82
|
* FlattenedFieldValue entries so that restoreFieldSetData can inject
|
|
83
83
|
* _id and _type for blocks and array items inline.
|
|
84
|
+
*
|
|
85
|
+
* Returns `{ fields, warnings }`. When `lenient` is false (default), any
|
|
86
|
+
* non-empty `warnings` are promoted to a thrown `BylineError` — preserving
|
|
87
|
+
* the original strict behaviour. When `lenient` is true, the caller
|
|
88
|
+
* receives the partial reconstruction and the warnings list and decides
|
|
89
|
+
* how to surface them (the admin edit path uses this to render a
|
|
90
|
+
* "best-effort load" banner against an out-of-date document).
|
|
84
91
|
*/
|
|
85
92
|
private reconstructFromUnifiedRows;
|
|
86
93
|
/**
|
|
@@ -104,15 +111,22 @@ export declare class DocumentQueries implements IDocumentQueries {
|
|
|
104
111
|
} | null>;
|
|
105
112
|
/**
|
|
106
113
|
* getDocumentById — gets the current version of a document by its logical document ID.
|
|
114
|
+
*
|
|
115
|
+
* When `lenient` is true, schema-mismatch warnings emitted during
|
|
116
|
+
* reconstruction are surfaced on the returned object as `restoreWarnings`
|
|
117
|
+
* rather than thrown. This is the admin edit path's "best-effort load"
|
|
118
|
+
* mode for documents written under a previous collection schema.
|
|
107
119
|
*/
|
|
108
|
-
getDocumentById({ collection_id, document_id, locale, reconstruct, readMode, filters, }: {
|
|
120
|
+
getDocumentById({ collection_id, document_id, locale, reconstruct, readMode, filters, lenient, }: {
|
|
109
121
|
collection_id: string;
|
|
110
122
|
document_id: string;
|
|
111
123
|
locale?: string;
|
|
112
124
|
reconstruct?: boolean;
|
|
113
125
|
readMode?: ReadMode;
|
|
114
126
|
filters?: DocumentFilter[];
|
|
127
|
+
lenient?: boolean;
|
|
115
128
|
}): Promise<{
|
|
129
|
+
restoreWarnings?: string[] | undefined;
|
|
116
130
|
document_version_id: string;
|
|
117
131
|
document_id: string;
|
|
118
132
|
path: string;
|
|
@@ -93,8 +93,15 @@ export class DocumentQueries {
|
|
|
93
93
|
* restoration. Meta rows (from store_meta) are converted to
|
|
94
94
|
* FlattenedFieldValue entries so that restoreFieldSetData can inject
|
|
95
95
|
* _id and _type for blocks and array items inline.
|
|
96
|
+
*
|
|
97
|
+
* Returns `{ fields, warnings }`. When `lenient` is false (default), any
|
|
98
|
+
* non-empty `warnings` are promoted to a thrown `BylineError` — preserving
|
|
99
|
+
* the original strict behaviour. When `lenient` is true, the caller
|
|
100
|
+
* receives the partial reconstruction and the warnings list and decides
|
|
101
|
+
* how to surface them (the admin edit path uses this to render a
|
|
102
|
+
* "best-effort load" banner against an out-of-date document).
|
|
96
103
|
*/
|
|
97
|
-
reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows) {
|
|
104
|
+
reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient = false) {
|
|
98
105
|
const flattenedData = unifiedFieldValues.map((row) => extractFlattenedFieldValue(row));
|
|
99
106
|
if (metaRows) {
|
|
100
107
|
for (const meta of metaRows) {
|
|
@@ -108,7 +115,14 @@ export class DocumentQueries {
|
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
const resolveLocale = locale !== 'all' ? locale : undefined;
|
|
111
|
-
|
|
118
|
+
const { data, warnings } = restoreFieldSetData(definition.fields, flattenedData, resolveLocale);
|
|
119
|
+
if (!lenient && warnings.length > 0) {
|
|
120
|
+
throw ERR_DATABASE({
|
|
121
|
+
message: `document reconstruction failed with ${warnings.length} warnings`,
|
|
122
|
+
details: { warnings },
|
|
123
|
+
}).log(getLogger());
|
|
124
|
+
}
|
|
125
|
+
return { fields: data, warnings };
|
|
112
126
|
}
|
|
113
127
|
/**
|
|
114
128
|
* getCurrentVersionMetadata — narrow metadata fetch for the current version.
|
|
@@ -145,8 +159,13 @@ export class DocumentQueries {
|
|
|
145
159
|
}
|
|
146
160
|
/**
|
|
147
161
|
* getDocumentById — gets the current version of a document by its logical document ID.
|
|
162
|
+
*
|
|
163
|
+
* When `lenient` is true, schema-mismatch warnings emitted during
|
|
164
|
+
* reconstruction are surfaced on the returned object as `restoreWarnings`
|
|
165
|
+
* rather than thrown. This is the admin edit path's "best-effort load"
|
|
166
|
+
* mode for documents written under a previous collection schema.
|
|
148
167
|
*/
|
|
149
|
-
async getDocumentById({ collection_id, document_id, locale = 'en', reconstruct = true, readMode, filters, }) {
|
|
168
|
+
async getDocumentById({ collection_id, document_id, locale = 'en', reconstruct = true, readMode, filters, lenient = false, }) {
|
|
150
169
|
const view = this.pickCurrentView(readMode);
|
|
151
170
|
// 1. Get current version (or current published version, per readMode)
|
|
152
171
|
const baseConditions = [
|
|
@@ -184,7 +203,7 @@ export class DocumentQueries {
|
|
|
184
203
|
})
|
|
185
204
|
.from(metaStore)
|
|
186
205
|
.where(eq(metaStore.document_version_id, document.id));
|
|
187
|
-
const fields = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
|
|
206
|
+
const { fields, warnings } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows, lenient);
|
|
188
207
|
return {
|
|
189
208
|
document_version_id: document.id,
|
|
190
209
|
document_id: document.document_id,
|
|
@@ -193,6 +212,7 @@ export class DocumentQueries {
|
|
|
193
212
|
created_at: document.created_at,
|
|
194
213
|
updated_at: document.updated_at,
|
|
195
214
|
fields,
|
|
215
|
+
...(lenient && warnings.length > 0 ? { restoreWarnings: warnings } : {}),
|
|
196
216
|
};
|
|
197
217
|
}
|
|
198
218
|
// Non-reconstructed: return raw flattened values
|
|
@@ -242,7 +262,7 @@ export class DocumentQueries {
|
|
|
242
262
|
})
|
|
243
263
|
.from(metaStore)
|
|
244
264
|
.where(eq(metaStore.document_version_id, document.id));
|
|
245
|
-
const fields = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
|
|
265
|
+
const { fields } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
|
|
246
266
|
return {
|
|
247
267
|
document_version_id: document.id,
|
|
248
268
|
document_id: document.document_id,
|
|
@@ -289,7 +309,7 @@ export class DocumentQueries {
|
|
|
289
309
|
})
|
|
290
310
|
.from(metaStore)
|
|
291
311
|
.where(eq(metaStore.document_version_id, document.id));
|
|
292
|
-
const
|
|
312
|
+
const { fields } = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
|
|
293
313
|
const documentWithFields = {
|
|
294
314
|
document_version_id: document.id,
|
|
295
315
|
document_id: document.document_id,
|
|
@@ -297,7 +317,7 @@ export class DocumentQueries {
|
|
|
297
317
|
status: document.status,
|
|
298
318
|
created_at: document.created_at,
|
|
299
319
|
updated_at: document.updated_at,
|
|
300
|
-
fields
|
|
320
|
+
fields,
|
|
301
321
|
};
|
|
302
322
|
return documentWithFields;
|
|
303
323
|
}
|
|
@@ -533,7 +553,7 @@ export class DocumentQueries {
|
|
|
533
553
|
for (const doc of documents) {
|
|
534
554
|
const versionFieldValues = fieldValuesByVersion.get(doc.id) || [];
|
|
535
555
|
const docMetaRows = (metaByVersion.get(doc.id) ?? []);
|
|
536
|
-
const fields = this.reconstructFromUnifiedRows(versionFieldValues, definition, locale, docMetaRows);
|
|
556
|
+
const { fields } = this.reconstructFromUnifiedRows(versionFieldValues, definition, locale, docMetaRows);
|
|
537
557
|
// When specific fields were requested, trim the reconstructed object
|
|
538
558
|
// to only those fields. Store-level filtering avoids querying unused
|
|
539
559
|
// tables, but fields sharing a store (e.g. price + views in numeric)
|
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { FieldSet } from '@byline/core';
|
|
9
9
|
import type { FlattenedFieldValue, UnifiedFieldValue } from './@types.js';
|
|
10
|
+
export interface RestoreResult {
|
|
11
|
+
data: any;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
10
14
|
/**
|
|
11
15
|
* Main entrypoint for restoring a document's field data from flattened form
|
|
12
16
|
* back into the original nested structure.
|
|
13
17
|
*
|
|
18
|
+
* Always returns `{ data, warnings }`. Warnings carry per-row reasons (e.g.
|
|
19
|
+
* "Invalid block index 'undefined'") that are typically the result of a
|
|
20
|
+
* collection schema change leaving orphan rows behind. Callers decide what
|
|
21
|
+
* to do with them — strict reads reject any non-empty `warnings`, lenient
|
|
22
|
+
* reads (the admin edit path) surface them to the UI.
|
|
23
|
+
*
|
|
14
24
|
* @param fields - The field definitions for the collection.
|
|
15
25
|
* @param flattenedData - The flattened field data to restore.
|
|
16
26
|
*/
|
|
17
|
-
export declare const restoreFieldSetData: (fields: FieldSet, flattenedData: FlattenedFieldValue[], resolveLocale?: string) =>
|
|
27
|
+
export declare const restoreFieldSetData: (fields: FieldSet, flattenedData: FlattenedFieldValue[], resolveLocale?: string) => RestoreResult;
|
|
18
28
|
export declare const extractFlattenedFieldValue: (unifiedValue: UnifiedFieldValue) => FlattenedFieldValue;
|
|
@@ -6,14 +6,16 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { ERR_DATABASE, getLogger, RESERVED_FIELD_NAMES } from '@byline/core';
|
|
9
|
-
// ------------------------------------------------------------------------------
|
|
10
|
-
// Restoration logic: take flattened field data and restore it to the original
|
|
11
|
-
// nested structure.
|
|
12
|
-
// ------------------------------------------------------------------------------
|
|
13
9
|
/**
|
|
14
10
|
* Main entrypoint for restoring a document's field data from flattened form
|
|
15
11
|
* back into the original nested structure.
|
|
16
12
|
*
|
|
13
|
+
* Always returns `{ data, warnings }`. Warnings carry per-row reasons (e.g.
|
|
14
|
+
* "Invalid block index 'undefined'") that are typically the result of a
|
|
15
|
+
* collection schema change leaving orphan rows behind. Callers decide what
|
|
16
|
+
* to do with them — strict reads reject any non-empty `warnings`, lenient
|
|
17
|
+
* reads (the admin edit path) surface them to the UI.
|
|
18
|
+
*
|
|
17
19
|
* @param fields - The field definitions for the collection.
|
|
18
20
|
* @param flattenedData - The flattened field data to restore.
|
|
19
21
|
*/
|
|
@@ -46,13 +48,7 @@ export const restoreFieldSetData = (fields, flattenedData, resolveLocale) => {
|
|
|
46
48
|
result[field.name] = result[field.name] || {};
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
|
-
|
|
50
|
-
throw ERR_DATABASE({
|
|
51
|
-
message: `document reconstruction failed with ${warnings.length} warnings`,
|
|
52
|
-
details: { warnings },
|
|
53
|
-
}).log(getLogger());
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
51
|
+
return { data: result, warnings };
|
|
56
52
|
};
|
|
57
53
|
const restoreFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
58
54
|
if (field.type === 'group') {
|
|
@@ -229,15 +229,16 @@ describe('01 Document Flattening and Reconstruction', () => {
|
|
|
229
229
|
const flattened = flattenFieldSetData(DocsCollectionConfig.fields, sampleDocument, 'all');
|
|
230
230
|
assert(flattened, 'Flattened document should not be null or undefined');
|
|
231
231
|
assert(flattened.length > 0, 'Flattened document should contain field values');
|
|
232
|
-
const restored = restoreFieldSetData(DocsCollectionConfig.fields, flattened);
|
|
232
|
+
const { data: restored, warnings } = restoreFieldSetData(DocsCollectionConfig.fields, flattened);
|
|
233
233
|
assert(restored, 'Restored document should not be null or undefined');
|
|
234
|
+
assert.deepStrictEqual(warnings, [], 'Round-trip restore should produce no warnings');
|
|
234
235
|
const restoredJson = JSON.stringify(restored, null, 2);
|
|
235
236
|
const expectedJson = JSON.stringify(expectedRestored, null, 2);
|
|
236
237
|
assert.deepStrictEqual(JSON.parse(restoredJson), JSON.parse(expectedJson), 'Restored document should match the expected flat block shape');
|
|
237
238
|
});
|
|
238
239
|
it('should resolve localized fields when a specific locale is requested', () => {
|
|
239
240
|
const flattened = flattenFieldSetData(DocsCollectionConfig.fields, sampleDocument, 'all');
|
|
240
|
-
const restored = restoreFieldSetData(DocsCollectionConfig.fields, flattened, 'en');
|
|
241
|
+
const { data: restored } = restoreFieldSetData(DocsCollectionConfig.fields, flattened, 'en');
|
|
241
242
|
assert.strictEqual(restored.title, 'My First Document');
|
|
242
243
|
assert.strictEqual(restored.summary, 'This is a sample document for testing purposes.');
|
|
243
244
|
});
|
|
@@ -308,19 +309,19 @@ describe('reserved field-name tolerance on restore', () => {
|
|
|
308
309
|
field_type: 'text',
|
|
309
310
|
value: 'Hello',
|
|
310
311
|
};
|
|
311
|
-
const restored = restoreFieldSetData(DocsCollectionConfig.fields, [orphanPathRow, titleRow], 'en');
|
|
312
|
+
const { data: restored, warnings } = restoreFieldSetData(DocsCollectionConfig.fields, [orphanPathRow, titleRow], 'en');
|
|
312
313
|
assert.strictEqual(restored.path, undefined, 'reserved-name row must not land on the reconstructed document');
|
|
313
314
|
assert.strictEqual(restored.title, 'Hello', 'non-reserved rows must still be restored');
|
|
315
|
+
assert.deepStrictEqual(warnings, [], 'reserved-name orphan must not surface as a restore warning');
|
|
314
316
|
});
|
|
315
|
-
it('does not
|
|
317
|
+
it('does not raise warnings when the only row present is a reserved-name orphan', () => {
|
|
316
318
|
const orphanPathRow = {
|
|
317
319
|
locale: 'all',
|
|
318
320
|
field_path: ['path'],
|
|
319
321
|
field_type: 'text',
|
|
320
322
|
value: 'whatever',
|
|
321
323
|
};
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}, 'a reserved-name orphan must not be treated as an unknown field');
|
|
324
|
+
const { warnings } = restoreFieldSetData(DocsCollectionConfig.fields, [orphanPathRow]);
|
|
325
|
+
assert.deepStrictEqual(warnings, [], 'a reserved-name orphan must not be treated as an unknown field');
|
|
325
326
|
});
|
|
326
327
|
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/db-postgres",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.6.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"pg": "^8.20.0",
|
|
53
53
|
"uuid": "^14.0.0",
|
|
54
54
|
"zod": "^4.4.2",
|
|
55
|
-
"@byline/auth": "1.
|
|
56
|
-
"@byline/core": "1.
|
|
57
|
-
"@byline/admin": "1.
|
|
55
|
+
"@byline/auth": "1.6.1",
|
|
56
|
+
"@byline/core": "1.6.1",
|
|
57
|
+
"@byline/admin": "1.6.1"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@biomejs/biome": "2.4.14",
|