@byline/db-postgres 1.3.1 → 1.6.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.
@@ -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
- return restoreFieldSetData(definition.fields, flattenedData, resolveLocale);
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 enrichedDocument = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
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: enrichedDocument,
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) => any;
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
- if (warnings.length > 0) {
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 throw when the only row present is a reserved-name orphan', () => {
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
- assert.doesNotThrow(() => {
323
- restoreFieldSetData(DocsCollectionConfig.fields, [orphanPathRow]);
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.3.1",
5
+ "version": "1.6.0",
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.3.0",
56
- "@byline/core": "1.4.0",
57
- "@byline/admin": "1.3.1"
55
+ "@byline/auth": "1.6.0",
56
+ "@byline/core": "1.6.0",
57
+ "@byline/admin": "1.6.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@biomejs/biome": "2.4.14",