@byline/core 4.6.2 → 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.
@@ -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[];