@byline/core 4.6.1 → 4.7.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/store-types.d.ts +6 -42
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.js +4 -0
- package/dist/storage/multi-relation.test.node.d.ts +8 -0
- package/dist/storage/multi-relation.test.node.js +92 -0
- package/dist/storage/storage-flatten-reconstruct.test.node.d.ts +8 -0
- package/dist/storage/storage-flatten-reconstruct.test.node.js +408 -0
- package/dist/storage/storage-flatten.d.ts +18 -0
- package/dist/storage/storage-flatten.js +319 -0
- package/dist/storage/storage-restore.d.ts +28 -0
- package/dist/storage/storage-restore.js +370 -0
- package/dist/storage/storage-row-types.d.ts +115 -0
- package/dist/storage/storage-row-types.js +8 -0
- package/dist/storage/storage-utils.d.ts +21 -0
- package/dist/storage/storage-utils.js +60 -0
- package/dist/storage/store-manifest.d.ts +50 -0
- package/dist/storage/store-manifest.js +215 -0
- package/dist/storage/store-manifest.test.node.d.ts +8 -0
- package/dist/storage/store-manifest.test.node.js +128 -0
- package/package.json +3 -2
|
@@ -0,0 +1,370 @@
|
|
|
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 { RESERVED_FIELD_NAMES } from '../config/validate-collections.js';
|
|
9
|
+
import { ERR_DATABASE } from '../lib/errors.js';
|
|
10
|
+
import { getLogger } from '../lib/logger.js';
|
|
11
|
+
/**
|
|
12
|
+
* Main entrypoint for restoring a document's field data from flattened form
|
|
13
|
+
* back into the original nested structure.
|
|
14
|
+
*
|
|
15
|
+
* Always returns `{ data, warnings }`. Warnings carry per-row reasons (e.g.
|
|
16
|
+
* "Invalid block index 'undefined'") that are typically the result of a
|
|
17
|
+
* collection schema change leaving orphan rows behind. Callers decide what
|
|
18
|
+
* to do with them — strict reads reject any non-empty `warnings`, lenient
|
|
19
|
+
* reads (the admin edit path) surface them to the UI.
|
|
20
|
+
*
|
|
21
|
+
* @param fields - The field definitions for the collection.
|
|
22
|
+
* @param flattenedData - The flattened field data to restore.
|
|
23
|
+
*/
|
|
24
|
+
export const restoreFieldSetData = (fields, flattenedData, resolveLocale) => {
|
|
25
|
+
const result = {};
|
|
26
|
+
const warnings = [];
|
|
27
|
+
for (const item of flattenedData) {
|
|
28
|
+
// biome-ignore lint/style/noNonNullAssertion: TODO: put in a proper check here?
|
|
29
|
+
const fieldName = item.field_path[0];
|
|
30
|
+
// Reserved system attributes (e.g. `path`) live on `documentVersions`,
|
|
31
|
+
// not in the schema field tree. Silently skip orphan rows that may
|
|
32
|
+
// remain in store_* tables from earlier schemas where these names
|
|
33
|
+
// were declared as user fields.
|
|
34
|
+
if (RESERVED_FIELD_NAMES.has(fieldName)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const field = fields.find((f) => f.name === fieldName);
|
|
38
|
+
if (!field) {
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
result[fieldName] = restoreFieldData(field, result[fieldName], item, 1, warnings, resolveLocale);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Array/blocks/group/hasMany-relation fixup — a required list-shaped field
|
|
45
|
+
// with no stored rows reconstructs as an empty array rather than absent.
|
|
46
|
+
for (const field of fields) {
|
|
47
|
+
if ((field.type === 'array' ||
|
|
48
|
+
field.type === 'blocks' ||
|
|
49
|
+
(field.type === 'relation' && field.hasMany)) &&
|
|
50
|
+
!field.optional) {
|
|
51
|
+
result[field.name] = result[field.name] || [];
|
|
52
|
+
}
|
|
53
|
+
else if (field.type === 'group' && !field.optional) {
|
|
54
|
+
result[field.name] = result[field.name] || {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { data: result, warnings };
|
|
58
|
+
};
|
|
59
|
+
const restoreFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
60
|
+
if (field.type === 'group') {
|
|
61
|
+
return restoreGroupFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
62
|
+
}
|
|
63
|
+
else if (field.type === 'array') {
|
|
64
|
+
return restoreArrayFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
65
|
+
}
|
|
66
|
+
else if (field.type === 'blocks') {
|
|
67
|
+
return restoreBlocksFieldData(field, target, data, pathIndex, warnings, resolveLocale);
|
|
68
|
+
}
|
|
69
|
+
else if (field.type === 'relation' && field.hasMany) {
|
|
70
|
+
return restoreMultiRelationFieldData(target, data, pathIndex, warnings);
|
|
71
|
+
}
|
|
72
|
+
if (field.localized) {
|
|
73
|
+
if (data.locale === 'all') {
|
|
74
|
+
warnings.push(`Received non-localized data for localized field at path ${data.field_path.join('.')}`);
|
|
75
|
+
}
|
|
76
|
+
else if (resolveLocale) {
|
|
77
|
+
// When resolving a specific locale, only accept the matching locale row
|
|
78
|
+
// and set the value directly instead of wrapping in { locale: value }.
|
|
79
|
+
if (data.locale === resolveLocale) {
|
|
80
|
+
target = extractValueFieldData(data);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
target = target || {};
|
|
85
|
+
target[data.locale] = extractValueFieldData(data);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (data.locale !== 'all') {
|
|
90
|
+
warnings.push(`Received localized data for non-localized field at path ${data.field_path.join('.')}`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
target = extractValueFieldData(data);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return target;
|
|
97
|
+
};
|
|
98
|
+
const restoreGroupFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
99
|
+
if (pathIndex >= data.field_path.length) {
|
|
100
|
+
warnings.push(`Path ended unexpectedly while restoring group: ${data.field_path.join('.')}`);
|
|
101
|
+
return target;
|
|
102
|
+
}
|
|
103
|
+
// biome-ignore lint/style/noNonNullAssertion: pathIndex is assumed to be a non-negative integer
|
|
104
|
+
const fieldName = data.field_path[pathIndex];
|
|
105
|
+
const subField = field.fields.find((f) => f.name === fieldName);
|
|
106
|
+
if (subField == null) {
|
|
107
|
+
// Sub-field was removed from the schema; silently skip orphaned rows.
|
|
108
|
+
return target;
|
|
109
|
+
}
|
|
110
|
+
target = target || {};
|
|
111
|
+
target[fieldName] = restoreFieldData(subField, target[fieldName], data, pathIndex + 1, warnings, resolveLocale);
|
|
112
|
+
return target;
|
|
113
|
+
};
|
|
114
|
+
const restoreArrayFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
115
|
+
if (pathIndex >= data.field_path.length) {
|
|
116
|
+
warnings.push(`Path ended unexpectedly while restoring array: ${data.field_path.join('.')}`);
|
|
117
|
+
return target;
|
|
118
|
+
}
|
|
119
|
+
// biome-ignore lint/style/noNonNullAssertion: pathIndex is assumed to be a non-negative integer
|
|
120
|
+
const arrayIndex = Number.parseInt(data.field_path[pathIndex], 10);
|
|
121
|
+
if (Number.isNaN(arrayIndex) || arrayIndex < 0) {
|
|
122
|
+
warnings.push(`Invalid array index '${data.field_path[pathIndex]}' in path ${data.field_path.join('.')}`);
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
target = target || [];
|
|
126
|
+
target[arrayIndex] = target[arrayIndex] || {};
|
|
127
|
+
if (pathIndex + 1 === data.field_path.length) {
|
|
128
|
+
if (data.field_type === 'meta') {
|
|
129
|
+
target[arrayIndex]._id = data.item_id;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
warnings.push(`Expected meta field for array item but got ${data.field_type} at path ${data.field_path.join('.')}`);
|
|
133
|
+
}
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
// biome-ignore lint/style/noNonNullAssertion: pathIndex is assumed to be a non-negative integer
|
|
137
|
+
const fieldName = data.field_path[pathIndex + 1];
|
|
138
|
+
const subField = field.fields.find((f) => f.name === fieldName);
|
|
139
|
+
if (subField == null) {
|
|
140
|
+
// Sub-field was removed from the schema; silently skip orphaned rows.
|
|
141
|
+
return target;
|
|
142
|
+
}
|
|
143
|
+
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 2, warnings, resolveLocale);
|
|
144
|
+
return target;
|
|
145
|
+
};
|
|
146
|
+
const restoreBlocksFieldData = (field, target, data, pathIndex, warnings, resolveLocale) => {
|
|
147
|
+
const arrayIndex = Number.parseInt(data.field_path[pathIndex] ?? '', 10);
|
|
148
|
+
if (Number.isNaN(arrayIndex) || arrayIndex < 0) {
|
|
149
|
+
warnings.push(`Invalid block index '${data.field_path[pathIndex]}' in path ${data.field_path.join('.')}`);
|
|
150
|
+
return target;
|
|
151
|
+
}
|
|
152
|
+
const blockType = data.field_path[pathIndex + 1];
|
|
153
|
+
if (typeof blockType !== 'string') {
|
|
154
|
+
warnings.push(`Invalid block type '${data.field_path[pathIndex + 1]}' in path ${data.field_path.join('.')}`);
|
|
155
|
+
return target;
|
|
156
|
+
}
|
|
157
|
+
const block = field.blocks.find((f) => f.blockType === blockType);
|
|
158
|
+
if (block == null) {
|
|
159
|
+
// Block type was removed from the schema; silently skip orphaned rows.
|
|
160
|
+
return target;
|
|
161
|
+
}
|
|
162
|
+
target = target || [];
|
|
163
|
+
target[arrayIndex] = target[arrayIndex] || {};
|
|
164
|
+
if (pathIndex + 2 === data.field_path.length) {
|
|
165
|
+
if (data.field_type === 'meta') {
|
|
166
|
+
target[arrayIndex]._id = data.item_id;
|
|
167
|
+
target[arrayIndex]._type = blockType;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
warnings.push(`Expected meta field for block item but got ${data.field_type} at path ${data.field_path.join('.')}`);
|
|
171
|
+
}
|
|
172
|
+
return target;
|
|
173
|
+
}
|
|
174
|
+
// biome-ignore lint/style/noNonNullAssertion: pathIndex is assumed to be a non-negative integer
|
|
175
|
+
const fieldName = data.field_path[pathIndex + 2];
|
|
176
|
+
const subField = block.fields.find((f) => f.name === fieldName);
|
|
177
|
+
if (subField == null) {
|
|
178
|
+
// Sub-field was removed from the schema; silently skip orphaned rows.
|
|
179
|
+
return target;
|
|
180
|
+
}
|
|
181
|
+
target[arrayIndex][fieldName] = restoreFieldData(subField, target[arrayIndex][fieldName], data, pathIndex + 3, warnings, resolveLocale);
|
|
182
|
+
return target;
|
|
183
|
+
};
|
|
184
|
+
const restoreMultiRelationFieldData = (target, data, pathIndex, warnings) => {
|
|
185
|
+
// Path shape: `[...prefix, <field>, <index>]`. At `pathIndex` we have the
|
|
186
|
+
// ordered position; the row itself is a single relation value.
|
|
187
|
+
const arrayIndex = Number.parseInt(data.field_path[pathIndex] ?? '', 10);
|
|
188
|
+
if (Number.isNaN(arrayIndex) || arrayIndex < 0) {
|
|
189
|
+
warnings.push(`Invalid relation index '${data.field_path[pathIndex]}' in path ${data.field_path.join('.')}`);
|
|
190
|
+
return target;
|
|
191
|
+
}
|
|
192
|
+
if (data.field_type !== 'relation') {
|
|
193
|
+
warnings.push(`Expected relation row for hasMany relation but got ${data.field_type} at path ${data.field_path.join('.')}`);
|
|
194
|
+
return target;
|
|
195
|
+
}
|
|
196
|
+
target = target || [];
|
|
197
|
+
target[arrayIndex] = extractValueFieldData(data);
|
|
198
|
+
return target;
|
|
199
|
+
};
|
|
200
|
+
const extractValueFieldData = (data) => {
|
|
201
|
+
switch (data.field_type) {
|
|
202
|
+
case 'text':
|
|
203
|
+
return data.value;
|
|
204
|
+
case 'boolean':
|
|
205
|
+
return data.value;
|
|
206
|
+
case 'json':
|
|
207
|
+
return data.value;
|
|
208
|
+
case 'numeric': {
|
|
209
|
+
if (data.number_type === 'float') {
|
|
210
|
+
return data.value_float;
|
|
211
|
+
}
|
|
212
|
+
else if (data.number_type === 'integer') {
|
|
213
|
+
return data.value_integer;
|
|
214
|
+
}
|
|
215
|
+
else if (data.number_type === 'decimal') {
|
|
216
|
+
return data.value_decimal;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
throw ERR_DATABASE({
|
|
220
|
+
message: `unsupported number type: ${data.number_type}`,
|
|
221
|
+
details: { numberType: data.number_type },
|
|
222
|
+
}).log(getLogger());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
case 'datetime': {
|
|
226
|
+
if (data.date_type === 'time') {
|
|
227
|
+
return data.value_time;
|
|
228
|
+
}
|
|
229
|
+
else if (data.date_type === 'date') {
|
|
230
|
+
return data.value_date;
|
|
231
|
+
}
|
|
232
|
+
else if (data.date_type === 'datetime') {
|
|
233
|
+
return data.value_timestamp_tz;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
throw ERR_DATABASE({
|
|
237
|
+
message: `unsupported date type: ${data.date_type}`,
|
|
238
|
+
details: { dateType: data.date_type },
|
|
239
|
+
}).log(getLogger());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
case 'file':
|
|
243
|
+
return {
|
|
244
|
+
fileId: data.file_id,
|
|
245
|
+
filename: data.filename,
|
|
246
|
+
originalFilename: data.original_filename,
|
|
247
|
+
mimeType: data.mime_type,
|
|
248
|
+
fileSize: data.file_size,
|
|
249
|
+
storageProvider: data.storage_provider,
|
|
250
|
+
storagePath: data.storage_path,
|
|
251
|
+
storageUrl: data.storage_url,
|
|
252
|
+
fileHash: data.file_hash,
|
|
253
|
+
imageWidth: data.image_width,
|
|
254
|
+
imageHeight: data.image_height,
|
|
255
|
+
imageFormat: data.image_format,
|
|
256
|
+
processingStatus: data.processing_status,
|
|
257
|
+
thumbnailGenerated: data.thumbnail_generated,
|
|
258
|
+
variants: data.variants?.map((variant) => ({
|
|
259
|
+
name: variant.name,
|
|
260
|
+
storagePath: variant.storage_path,
|
|
261
|
+
storageUrl: variant.storage_url,
|
|
262
|
+
width: variant.width,
|
|
263
|
+
height: variant.height,
|
|
264
|
+
format: variant.format,
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
case 'relation':
|
|
268
|
+
return {
|
|
269
|
+
targetDocumentId: data.target_document_id,
|
|
270
|
+
targetCollectionId: data.target_collection_id,
|
|
271
|
+
relationshipType: data.relationship_type,
|
|
272
|
+
cascadeDelete: data.cascade_delete,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
// ------------------------------------------------------------------------------
|
|
277
|
+
// Unified row extraction: convert a UNION-ALL `UnifiedFieldValue` row (the shape
|
|
278
|
+
// the adapter reads back from the seven store tables) into a `FlattenedFieldValue`
|
|
279
|
+
// ready to feed into `restoreFieldSetData`. Paired with restore because it's the
|
|
280
|
+
// read-path adapter between raw SQL results and the reconstruct pipeline.
|
|
281
|
+
// ------------------------------------------------------------------------------
|
|
282
|
+
export const extractFlattenedFieldValue = (unifiedValue) => {
|
|
283
|
+
const baseValue = {
|
|
284
|
+
locale: unifiedValue.locale,
|
|
285
|
+
field_path: unifiedValue.field_path.split('.'),
|
|
286
|
+
};
|
|
287
|
+
switch (unifiedValue.field_type) {
|
|
288
|
+
case 'text':
|
|
289
|
+
return {
|
|
290
|
+
...baseValue,
|
|
291
|
+
field_type: 'text',
|
|
292
|
+
value: unifiedValue.text_value,
|
|
293
|
+
};
|
|
294
|
+
case 'numeric':
|
|
295
|
+
return {
|
|
296
|
+
...baseValue,
|
|
297
|
+
field_type: 'numeric',
|
|
298
|
+
number_type: unifiedValue.number_type,
|
|
299
|
+
value_float: orUndefined(unifiedValue.value_float),
|
|
300
|
+
value_integer: orUndefined(unifiedValue.value_integer),
|
|
301
|
+
value_decimal: orUndefined(unifiedValue.value_decimal),
|
|
302
|
+
};
|
|
303
|
+
case 'boolean':
|
|
304
|
+
return {
|
|
305
|
+
...baseValue,
|
|
306
|
+
field_type: 'boolean',
|
|
307
|
+
value: unifiedValue.boolean_value,
|
|
308
|
+
};
|
|
309
|
+
case 'datetime':
|
|
310
|
+
return {
|
|
311
|
+
...baseValue,
|
|
312
|
+
field_type: 'datetime',
|
|
313
|
+
date_type: unifiedValue.date_type,
|
|
314
|
+
value_date: orUndefined(unifiedValue.value_date),
|
|
315
|
+
value_time: orUndefined(unifiedValue.value_time),
|
|
316
|
+
value_timestamp_tz: orUndefined(unifiedValue.value_timestamp_tz),
|
|
317
|
+
};
|
|
318
|
+
case 'file':
|
|
319
|
+
return {
|
|
320
|
+
...baseValue,
|
|
321
|
+
field_type: 'file',
|
|
322
|
+
file_id: unifiedValue.file_id,
|
|
323
|
+
filename: unifiedValue.filename,
|
|
324
|
+
original_filename: unifiedValue.original_filename,
|
|
325
|
+
mime_type: unifiedValue.mime_type,
|
|
326
|
+
file_size: Number(unifiedValue.file_size),
|
|
327
|
+
storage_provider: unifiedValue.storage_provider,
|
|
328
|
+
storage_path: unifiedValue.storage_path,
|
|
329
|
+
storage_url: orUndefined(unifiedValue.storage_url),
|
|
330
|
+
file_hash: orUndefined(unifiedValue.file_hash),
|
|
331
|
+
image_width: orUndefined(unifiedValue.image_width),
|
|
332
|
+
image_height: orUndefined(unifiedValue.image_height),
|
|
333
|
+
image_format: orUndefined(unifiedValue.image_format),
|
|
334
|
+
processing_status: orUndefined(unifiedValue.processing_status),
|
|
335
|
+
thumbnail_generated: orUndefined(unifiedValue.thumbnail_generated),
|
|
336
|
+
variants: orUndefined(unifiedValue.variants),
|
|
337
|
+
};
|
|
338
|
+
case 'relation':
|
|
339
|
+
return {
|
|
340
|
+
...baseValue,
|
|
341
|
+
field_type: 'relation',
|
|
342
|
+
target_document_id: unifiedValue.target_document_id,
|
|
343
|
+
target_collection_id: unifiedValue.target_collection_id,
|
|
344
|
+
relationship_type: orUndefined(unifiedValue.relationship_type),
|
|
345
|
+
cascade_delete: orUndefined(unifiedValue.cascade_delete),
|
|
346
|
+
};
|
|
347
|
+
case 'richText':
|
|
348
|
+
case 'json':
|
|
349
|
+
return {
|
|
350
|
+
...baseValue,
|
|
351
|
+
field_type: 'json',
|
|
352
|
+
value: unifiedValue.json_value,
|
|
353
|
+
};
|
|
354
|
+
case 'meta':
|
|
355
|
+
return {
|
|
356
|
+
...baseValue,
|
|
357
|
+
field_type: 'meta',
|
|
358
|
+
type: unifiedValue.meta_type,
|
|
359
|
+
item_id: unifiedValue.meta_item_id,
|
|
360
|
+
};
|
|
361
|
+
default:
|
|
362
|
+
throw ERR_DATABASE({
|
|
363
|
+
message: `unexpected field type: ${unifiedValue.field_type}`,
|
|
364
|
+
details: { fieldType: unifiedValue.field_type },
|
|
365
|
+
}).log(getLogger());
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
const orUndefined = (value) => {
|
|
369
|
+
return value === null ? undefined : value;
|
|
370
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
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 { FileStoreVariant } from '../@types/index.js';
|
|
9
|
+
interface BaseFlattenedFieldData {
|
|
10
|
+
locale: string;
|
|
11
|
+
field_path: string[];
|
|
12
|
+
}
|
|
13
|
+
interface FlattenedTextFieldValue extends BaseFlattenedFieldData {
|
|
14
|
+
field_type: 'text';
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
interface FlattenedNumericFieldValue extends BaseFlattenedFieldData {
|
|
18
|
+
field_type: 'numeric';
|
|
19
|
+
number_type: 'integer' | 'float' | 'decimal';
|
|
20
|
+
value_float?: number;
|
|
21
|
+
value_integer?: number;
|
|
22
|
+
value_decimal?: string;
|
|
23
|
+
}
|
|
24
|
+
interface FlattenedBooleanFieldValue extends BaseFlattenedFieldData {
|
|
25
|
+
field_type: 'boolean';
|
|
26
|
+
value: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface FlattenedDateTimeFieldValue extends BaseFlattenedFieldData {
|
|
29
|
+
field_type: 'datetime';
|
|
30
|
+
date_type: 'datetime' | 'date' | 'time';
|
|
31
|
+
value_time?: string;
|
|
32
|
+
value_date?: Date | string;
|
|
33
|
+
value_timestamp_tz?: Date | string;
|
|
34
|
+
}
|
|
35
|
+
interface FlattenedFileFieldValue extends BaseFlattenedFieldData {
|
|
36
|
+
field_type: 'file';
|
|
37
|
+
file_id: string;
|
|
38
|
+
filename: string;
|
|
39
|
+
original_filename: string;
|
|
40
|
+
mime_type: string;
|
|
41
|
+
file_size: number;
|
|
42
|
+
storage_provider: string;
|
|
43
|
+
storage_path: string;
|
|
44
|
+
storage_url?: string;
|
|
45
|
+
file_hash?: string;
|
|
46
|
+
image_width?: number;
|
|
47
|
+
image_height?: number;
|
|
48
|
+
image_format?: string;
|
|
49
|
+
processing_status?: string;
|
|
50
|
+
thumbnail_generated?: boolean;
|
|
51
|
+
variants?: FileStoreVariant[];
|
|
52
|
+
}
|
|
53
|
+
interface FlattenedRelationFieldValue extends BaseFlattenedFieldData {
|
|
54
|
+
field_type: 'relation';
|
|
55
|
+
target_document_id: string;
|
|
56
|
+
target_collection_id: string;
|
|
57
|
+
relationship_type?: string;
|
|
58
|
+
cascade_delete?: boolean;
|
|
59
|
+
}
|
|
60
|
+
interface FlattenedJsonFieldValue extends BaseFlattenedFieldData {
|
|
61
|
+
field_type: 'json';
|
|
62
|
+
value: unknown;
|
|
63
|
+
}
|
|
64
|
+
interface FlattenedMetaFieldValue extends BaseFlattenedFieldData {
|
|
65
|
+
field_type: 'meta';
|
|
66
|
+
type: 'array_item' | 'group';
|
|
67
|
+
item_id: string;
|
|
68
|
+
meta?: unknown;
|
|
69
|
+
}
|
|
70
|
+
export type FlattenedFieldValue = FlattenedTextFieldValue | FlattenedNumericFieldValue | FlattenedBooleanFieldValue | FlattenedDateTimeFieldValue | FlattenedFileFieldValue | FlattenedRelationFieldValue | FlattenedJsonFieldValue | FlattenedMetaFieldValue;
|
|
71
|
+
export interface UnifiedFieldValue {
|
|
72
|
+
id: string;
|
|
73
|
+
document_version_id: string;
|
|
74
|
+
collection_id: string;
|
|
75
|
+
field_type: string;
|
|
76
|
+
field_path: string;
|
|
77
|
+
field_name: string;
|
|
78
|
+
locale: string;
|
|
79
|
+
parent_path: string | null;
|
|
80
|
+
text_value: string | null;
|
|
81
|
+
boolean_value: boolean | null;
|
|
82
|
+
json_value: any | null;
|
|
83
|
+
date_type: string | null;
|
|
84
|
+
value_date: Date | null;
|
|
85
|
+
value_time: string | null;
|
|
86
|
+
value_timestamp_tz: Date | null;
|
|
87
|
+
file_id: string | null;
|
|
88
|
+
filename: string | null;
|
|
89
|
+
original_filename: string | null;
|
|
90
|
+
mime_type: string | null;
|
|
91
|
+
file_size: number | string | null;
|
|
92
|
+
storage_provider: string | null;
|
|
93
|
+
storage_path: string | null;
|
|
94
|
+
storage_url: string | null;
|
|
95
|
+
file_hash: string | null;
|
|
96
|
+
image_width: number | null;
|
|
97
|
+
image_height: number | null;
|
|
98
|
+
image_format: string | null;
|
|
99
|
+
processing_status: string | null;
|
|
100
|
+
thumbnail_generated: boolean | null;
|
|
101
|
+
variants: FileStoreVariant[] | null;
|
|
102
|
+
target_document_id: string | null;
|
|
103
|
+
target_collection_id: string | null;
|
|
104
|
+
relationship_type: string | null;
|
|
105
|
+
cascade_delete: boolean | null;
|
|
106
|
+
json_schema: string | null;
|
|
107
|
+
object_keys: string[] | null;
|
|
108
|
+
number_type: string | null;
|
|
109
|
+
value_integer: number | null;
|
|
110
|
+
value_decimal: string | null;
|
|
111
|
+
value_float: number | null;
|
|
112
|
+
meta_type: string | null;
|
|
113
|
+
meta_item_id: string | null;
|
|
114
|
+
}
|
|
115
|
+
export {};
|
|
@@ -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,21 @@
|
|
|
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 StoreType } from './field-store-map.js';
|
|
9
|
+
import type { FieldSet } from '../@types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Given a CollectionDefinition and a list of field names, determine which
|
|
12
|
+
* StoreTypes are needed to satisfy the query. This enables selective field
|
|
13
|
+
* loading — instead of a 7-table UNION ALL, we query only the relevant stores.
|
|
14
|
+
*
|
|
15
|
+
* Field names that don't match a collection field (e.g. 'status', 'updated_at')
|
|
16
|
+
* are silently ignored — they come from the document version row, not EAV stores.
|
|
17
|
+
*
|
|
18
|
+
* Structure fields (array, blocks) recursively include all their children's
|
|
19
|
+
* store types plus 'meta' for _id/_type tracking.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveStoreTypes(fields: FieldSet, fieldNames: string[]): Set<StoreType>;
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { fieldTypeToStoreType } from './field-store-map.js';
|
|
9
|
+
// ------------------------------------------------------------------------------
|
|
10
|
+
// Field name → store type resolution
|
|
11
|
+
// ------------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Given a CollectionDefinition and a list of field names, determine which
|
|
14
|
+
* StoreTypes are needed to satisfy the query. This enables selective field
|
|
15
|
+
* loading — instead of a 7-table UNION ALL, we query only the relevant stores.
|
|
16
|
+
*
|
|
17
|
+
* Field names that don't match a collection field (e.g. 'status', 'updated_at')
|
|
18
|
+
* are silently ignored — they come from the document version row, not EAV stores.
|
|
19
|
+
*
|
|
20
|
+
* Structure fields (array, blocks) recursively include all their children's
|
|
21
|
+
* store types plus 'meta' for _id/_type tracking.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveStoreTypes(fields, fieldNames) {
|
|
24
|
+
const stores = new Set();
|
|
25
|
+
for (const name of fieldNames) {
|
|
26
|
+
const field = fields.find((f) => f.name === name);
|
|
27
|
+
if (!field)
|
|
28
|
+
continue;
|
|
29
|
+
collectStoreTypes(field, stores);
|
|
30
|
+
}
|
|
31
|
+
return stores;
|
|
32
|
+
}
|
|
33
|
+
function collectStoreTypes(field, stores) {
|
|
34
|
+
const mapped = fieldTypeToStoreType[field.type];
|
|
35
|
+
if (mapped === 'meta') {
|
|
36
|
+
// Structure field — recurse into children and include meta for _id/_type
|
|
37
|
+
if (field.type === 'array') {
|
|
38
|
+
for (const child of field.fields) {
|
|
39
|
+
collectStoreTypes(child, stores);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (field.type === 'blocks') {
|
|
43
|
+
for (const block of field.blocks) {
|
|
44
|
+
for (const child of block.fields) {
|
|
45
|
+
collectStoreTypes(child, stores);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Meta rows are fetched separately (not via UNION ALL), so no store type to add
|
|
50
|
+
}
|
|
51
|
+
else if (mapped) {
|
|
52
|
+
stores.add(mapped);
|
|
53
|
+
}
|
|
54
|
+
// undefined (group) or unrecognized — recurse if group
|
|
55
|
+
if (field.type === 'group') {
|
|
56
|
+
for (const child of field.fields) {
|
|
57
|
+
collectStoreTypes(child, stores);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
* The dialect-independent column manifest for the EAV store tables.
|
|
9
|
+
*
|
|
10
|
+
* Every adapter's UNION ALL over its `store_*` tables projects into the same
|
|
11
|
+
* unified row shape (see `UnifiedFieldValue`). Instead of each adapter
|
|
12
|
+
* hand-maintaining its own list of columns, adapters generate their SELECT
|
|
13
|
+
* lists from this single manifest — adding a column or a new store table is
|
|
14
|
+
* a one-line change here rather than N hand-synchronized SQL fragments.
|
|
15
|
+
*
|
|
16
|
+
* This module is pure data: no SQL, no drizzle-orm, no pg. Each adapter owns
|
|
17
|
+
* its own SQL generation (e.g. `@byline/db-postgres`'s `storeSelectList()` +
|
|
18
|
+
* `pgNullCast()`) consuming this manifest.
|
|
19
|
+
*/
|
|
20
|
+
import type { StoreType } from './field-store-map.js';
|
|
21
|
+
/**
|
|
22
|
+
* Each entry describes one column in the unified UNION ALL output.
|
|
23
|
+
*
|
|
24
|
+
* - `name`: The output column alias (matches UnifiedFieldValue / FlattenedFieldValue).
|
|
25
|
+
* - `nullCast`: The abstract SQL type name used when a store table does NOT
|
|
26
|
+
* provide this column (e.g. `'boolean'`, `'uuid'`, `'text'`).
|
|
27
|
+
* Only the first SELECT in a UNION ALL strictly needs casts,
|
|
28
|
+
* but including them everywhere is harmless and makes each
|
|
29
|
+
* fragment self-describing. Each adapter maps this abstract
|
|
30
|
+
* name to its own dialect cast — the Postgres adapter's
|
|
31
|
+
* `pgNullCast()` renders it as `NULL::<cast>`.
|
|
32
|
+
* - `sources`: A map from store type key → the source column name that
|
|
33
|
+
* produces this column's value from that table. If a store
|
|
34
|
+
* type is absent, the column is emitted as a typed NULL.
|
|
35
|
+
*/
|
|
36
|
+
export interface ColumnDef {
|
|
37
|
+
name: string;
|
|
38
|
+
nullCast: string;
|
|
39
|
+
sources?: Partial<Record<StoreType, string>>;
|
|
40
|
+
}
|
|
41
|
+
/** Store table names, keyed by StoreType. Shared naming convention across adapters. */
|
|
42
|
+
export declare const storeTableNames: Record<StoreType, string>;
|
|
43
|
+
/**
|
|
44
|
+
* Canonical column order for the unified UNION ALL output.
|
|
45
|
+
*
|
|
46
|
+
* The first 7 columns are shared across all store tables (base columns).
|
|
47
|
+
* The remaining columns are type-specific — each one declares which store
|
|
48
|
+
* table(s) provide it and what source column to read.
|
|
49
|
+
*/
|
|
50
|
+
export declare const storeColumnManifest: ColumnDef[];
|