@byline/db-postgres 0.9.3

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.
Files changed (99) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +18 -0
  3. package/dist/database/schema/auth.d.ts +857 -0
  4. package/dist/database/schema/auth.d.ts.map +1 -0
  5. package/dist/database/schema/auth.js +176 -0
  6. package/dist/database/schema/auth.js.map +1 -0
  7. package/dist/database/schema/index.d.ts +2955 -0
  8. package/dist/database/schema/index.d.ts.map +1 -0
  9. package/dist/database/schema/index.js +500 -0
  10. package/dist/database/schema/index.js.map +1 -0
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +30 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lib/test-helper.d.ts +17 -0
  16. package/dist/lib/test-helper.d.ts.map +1 -0
  17. package/dist/lib/test-helper.js +47 -0
  18. package/dist/lib/test-helper.js.map +1 -0
  19. package/dist/modules/admin/admin-permissions-repository.d.ts +17 -0
  20. package/dist/modules/admin/admin-permissions-repository.d.ts.map +1 -0
  21. package/dist/modules/admin/admin-permissions-repository.js +76 -0
  22. package/dist/modules/admin/admin-permissions-repository.js.map +1 -0
  23. package/dist/modules/admin/admin-roles-repository.d.ts +12 -0
  24. package/dist/modules/admin/admin-roles-repository.d.ts.map +1 -0
  25. package/dist/modules/admin/admin-roles-repository.js +168 -0
  26. package/dist/modules/admin/admin-roles-repository.js.map +1 -0
  27. package/dist/modules/admin/admin-store.d.ts +20 -0
  28. package/dist/modules/admin/admin-store.d.ts.map +1 -0
  29. package/dist/modules/admin/admin-store.js +28 -0
  30. package/dist/modules/admin/admin-store.js.map +1 -0
  31. package/dist/modules/admin/admin-users-repository.d.ts +12 -0
  32. package/dist/modules/admin/admin-users-repository.d.ts.map +1 -0
  33. package/dist/modules/admin/admin-users-repository.js +208 -0
  34. package/dist/modules/admin/admin-users-repository.js.map +1 -0
  35. package/dist/modules/admin/index.d.ts +27 -0
  36. package/dist/modules/admin/index.d.ts.map +1 -0
  37. package/dist/modules/admin/index.js +27 -0
  38. package/dist/modules/admin/index.js.map +1 -0
  39. package/dist/modules/admin/refresh-tokens-repository.d.ts +16 -0
  40. package/dist/modules/admin/refresh-tokens-repository.d.ts.map +1 -0
  41. package/dist/modules/admin/refresh-tokens-repository.js +132 -0
  42. package/dist/modules/admin/refresh-tokens-repository.js.map +1 -0
  43. package/dist/modules/admin/tests/auth-integration.test.d.ts +9 -0
  44. package/dist/modules/admin/tests/auth-integration.test.d.ts.map +1 -0
  45. package/dist/modules/admin/tests/auth-integration.test.js +392 -0
  46. package/dist/modules/admin/tests/auth-integration.test.js.map +1 -0
  47. package/dist/modules/admin/tests/session-provider.test.d.ts +9 -0
  48. package/dist/modules/admin/tests/session-provider.test.d.ts.map +1 -0
  49. package/dist/modules/admin/tests/session-provider.test.js +370 -0
  50. package/dist/modules/admin/tests/session-provider.test.js.map +1 -0
  51. package/dist/modules/storage/@types.d.ts +116 -0
  52. package/dist/modules/storage/@types.d.ts.map +1 -0
  53. package/dist/modules/storage/@types.js +9 -0
  54. package/dist/modules/storage/@types.js.map +1 -0
  55. package/dist/modules/storage/storage-commands.d.ts +136 -0
  56. package/dist/modules/storage/storage-commands.d.ts.map +1 -0
  57. package/dist/modules/storage/storage-commands.js +272 -0
  58. package/dist/modules/storage/storage-commands.js.map +1 -0
  59. package/dist/modules/storage/storage-flatten.d.ts +19 -0
  60. package/dist/modules/storage/storage-flatten.d.ts.map +1 -0
  61. package/dist/modules/storage/storage-flatten.js +261 -0
  62. package/dist/modules/storage/storage-flatten.js.map +1 -0
  63. package/dist/modules/storage/storage-insert.d.ts +22 -0
  64. package/dist/modules/storage/storage-insert.d.ts.map +1 -0
  65. package/dist/modules/storage/storage-insert.js +115 -0
  66. package/dist/modules/storage/storage-insert.js.map +1 -0
  67. package/dist/modules/storage/storage-queries.d.ts +377 -0
  68. package/dist/modules/storage/storage-queries.d.ts.map +1 -0
  69. package/dist/modules/storage/storage-queries.js +976 -0
  70. package/dist/modules/storage/storage-queries.js.map +1 -0
  71. package/dist/modules/storage/storage-restore.d.ts +19 -0
  72. package/dist/modules/storage/storage-restore.d.ts.map +1 -0
  73. package/dist/modules/storage/storage-restore.js +350 -0
  74. package/dist/modules/storage/storage-restore.js.map +1 -0
  75. package/dist/modules/storage/storage-store-manifest.d.ts +71 -0
  76. package/dist/modules/storage/storage-store-manifest.d.ts.map +1 -0
  77. package/dist/modules/storage/storage-store-manifest.js +294 -0
  78. package/dist/modules/storage/storage-store-manifest.js.map +1 -0
  79. package/dist/modules/storage/storage-utils.d.ts +23 -0
  80. package/dist/modules/storage/storage-utils.d.ts.map +1 -0
  81. package/dist/modules/storage/storage-utils.js +72 -0
  82. package/dist/modules/storage/storage-utils.js.map +1 -0
  83. package/dist/modules/storage/tests/storage-field-types.test.d.ts +9 -0
  84. package/dist/modules/storage/tests/storage-field-types.test.d.ts.map +1 -0
  85. package/dist/modules/storage/tests/storage-field-types.test.js +146 -0
  86. package/dist/modules/storage/tests/storage-field-types.test.js.map +1 -0
  87. package/dist/modules/storage/tests/storage-flatten-reconstruct.test.d.ts +9 -0
  88. package/dist/modules/storage/tests/storage-flatten-reconstruct.test.d.ts.map +1 -0
  89. package/dist/modules/storage/tests/storage-flatten-reconstruct.test.js +327 -0
  90. package/dist/modules/storage/tests/storage-flatten-reconstruct.test.js.map +1 -0
  91. package/dist/modules/storage/tests/storage-store-manifest.test.d.ts +9 -0
  92. package/dist/modules/storage/tests/storage-store-manifest.test.d.ts.map +1 -0
  93. package/dist/modules/storage/tests/storage-store-manifest.test.js +141 -0
  94. package/dist/modules/storage/tests/storage-store-manifest.test.js.map +1 -0
  95. package/dist/modules/storage/tests/storage-versioning.test.d.ts +9 -0
  96. package/dist/modules/storage/tests/storage-versioning.test.d.ts.map +1 -0
  97. package/dist/modules/storage/tests/storage-versioning.test.js +336 -0
  98. package/dist/modules/storage/tests/storage-versioning.test.js.map +1 -0
  99. package/package.json +81 -0
@@ -0,0 +1,976 @@
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
+ // TODO: getLogger() is used here as a global escape hatch because pgAdapter()
9
+ // constructs query/command classes before initBylineCore() wires up the Pino
10
+ // logger. A future refactor could inject the logger at construction time by
11
+ // either deferring adapter construction or accepting a lazy logger parameter.
12
+ import { ERR_DATABASE, ERR_NOT_FOUND, getLogger } from '@byline/core';
13
+ import { and, eq, inArray, sql } from 'drizzle-orm';
14
+ import { collections, currentDocumentsView, currentPublishedDocumentsView, documentVersions, metaStore, } from '../../database/schema/index.js';
15
+ import { extractFlattenedFieldValue, restoreFieldSetData } from './storage-restore.js';
16
+ import { allStoreTypes, storeSelectList, storeTableNames, } from './storage-store-manifest.js';
17
+ import { resolveStoreTypes } from './storage-utils.js';
18
+ /**
19
+ * CollectionQueries
20
+ */
21
+ export class CollectionQueries {
22
+ db;
23
+ constructor(db) {
24
+ this.db = db;
25
+ }
26
+ async getAllCollections() {
27
+ return await this.db.select().from(collections);
28
+ }
29
+ async getCollectionByPath(path) {
30
+ return this.db.query.collections.findFirst({ where: eq(collections.path, path) });
31
+ }
32
+ async getCollectionById(id) {
33
+ return this.db.query.collections.findFirst({ where: eq(collections.id, id) });
34
+ }
35
+ }
36
+ /**
37
+ * DocumentQueries
38
+ */
39
+ export class DocumentQueries {
40
+ db;
41
+ collections;
42
+ collectionPathCache = new Map();
43
+ constructor(db, collections) {
44
+ this.db = db;
45
+ this.collections = collections;
46
+ }
47
+ /**
48
+ * Resolve a collection UUID to its CollectionDefinition by looking up the
49
+ * collection's path in the DB and matching it against the injected array.
50
+ */
51
+ async getDefinitionForCollection(collectionId) {
52
+ let path = this.collectionPathCache.get(collectionId);
53
+ if (!path) {
54
+ const row = await this.db.query.collections.findFirst({
55
+ where: eq(collections.id, collectionId),
56
+ });
57
+ if (!row) {
58
+ throw ERR_NOT_FOUND({
59
+ message: `collection not found in database: ${collectionId}`,
60
+ details: { collectionId },
61
+ }).log(getLogger());
62
+ }
63
+ path = row.path;
64
+ this.collectionPathCache.set(collectionId, path);
65
+ }
66
+ const definition = this.collections.find((c) => c.path === path);
67
+ if (!definition) {
68
+ throw ERR_NOT_FOUND({
69
+ message: `no CollectionDefinition found for path: ${path}`,
70
+ details: { collectionPath: path },
71
+ }).log(getLogger());
72
+ }
73
+ return definition;
74
+ }
75
+ /**
76
+ * Pick the Drizzle view reference to read from based on `readMode`.
77
+ *
78
+ * - `'any'` (default) → `current_documents` — the latest version of
79
+ * each logical document, regardless of status.
80
+ * - `'published'` → `current_published_documents` — the latest
81
+ * version whose status is `'published'`, falling back past newer
82
+ * drafts so public readers keep seeing previously-published
83
+ * content while editors work on an unpublished draft.
84
+ *
85
+ * Both views share the same row shape, so the returned reference is
86
+ * drop-in substitutable at every select/where site.
87
+ */
88
+ pickCurrentView(readMode) {
89
+ return readMode === 'published' ? currentPublishedDocumentsView : currentDocumentsView;
90
+ }
91
+ /**
92
+ * Reconstruct document fields from unified row values using schema-aware
93
+ * restoration. Meta rows (from store_meta) are converted to
94
+ * FlattenedFieldValue entries so that restoreFieldSetData can inject
95
+ * _id and _type for blocks and array items inline.
96
+ */
97
+ reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows) {
98
+ const flattenedData = unifiedFieldValues.map((row) => extractFlattenedFieldValue(row));
99
+ if (metaRows) {
100
+ for (const meta of metaRows) {
101
+ flattenedData.push({
102
+ locale: 'all',
103
+ field_path: meta.path.split('.'),
104
+ field_type: 'meta',
105
+ type: meta.type,
106
+ item_id: meta.item_id,
107
+ });
108
+ }
109
+ }
110
+ const resolveLocale = locale !== 'all' ? locale : undefined;
111
+ return restoreFieldSetData(definition.fields, flattenedData, resolveLocale);
112
+ }
113
+ /**
114
+ * getCurrentVersionMetadata — narrow metadata fetch for the current version.
115
+ *
116
+ * Hits `current_documents` only; no field reconstruction, no meta fetch.
117
+ * Used by lifecycle operations (status changes, delete checks) that need
118
+ * `document_version_id` / `status` / `path` but not the document body.
119
+ */
120
+ async getCurrentVersionMetadata({ collection_id, document_id, }) {
121
+ const [row] = await this.db
122
+ .select({
123
+ document_version_id: currentDocumentsView.id,
124
+ document_id: currentDocumentsView.document_id,
125
+ collection_id: currentDocumentsView.collection_id,
126
+ path: currentDocumentsView.path,
127
+ status: currentDocumentsView.status,
128
+ created_at: currentDocumentsView.created_at,
129
+ updated_at: currentDocumentsView.updated_at,
130
+ })
131
+ .from(currentDocumentsView)
132
+ .where(and(eq(currentDocumentsView.collection_id, collection_id), eq(currentDocumentsView.document_id, document_id)))
133
+ .limit(1);
134
+ if (!row)
135
+ return null;
136
+ return {
137
+ document_version_id: row.document_version_id,
138
+ document_id: row.document_id,
139
+ collection_id: row.collection_id ?? '',
140
+ path: row.path,
141
+ status: row.status ?? 'draft',
142
+ created_at: row.created_at ?? new Date(),
143
+ updated_at: row.updated_at ?? new Date(),
144
+ };
145
+ }
146
+ /**
147
+ * getDocumentById — gets the current version of a document by its logical document ID.
148
+ */
149
+ async getDocumentById({ collection_id, document_id, locale = 'en', reconstruct = true, readMode, filters, }) {
150
+ const view = this.pickCurrentView(readMode);
151
+ // 1. Get current version (or current published version, per readMode)
152
+ const baseConditions = [
153
+ eq(view.collection_id, collection_id),
154
+ eq(view.document_id, document_id),
155
+ ];
156
+ if (filters?.length) {
157
+ const outerScope = {
158
+ docVersionId: sql `${view.id}`,
159
+ status: sql `${view.status}`,
160
+ path: sql `${view.path}`,
161
+ };
162
+ for (const f of filters) {
163
+ baseConditions.push(this.buildFilterExists(f, locale, outerScope, readMode, 0));
164
+ }
165
+ }
166
+ const [document] = await this.db
167
+ .select()
168
+ .from(view)
169
+ .where(and(...baseConditions));
170
+ if (document == null) {
171
+ return null;
172
+ }
173
+ // 2. Get all field values for this document
174
+ const unifiedFieldValues = await this.getAllFieldValues(document.id, locale);
175
+ // 3. If reconstruct is true, reconstruct the fields and attach meta
176
+ if (reconstruct === true) {
177
+ const definition = await this.getDefinitionForCollection(collection_id);
178
+ const metaRows = await this.db
179
+ .select({
180
+ type: metaStore.type,
181
+ path: metaStore.path,
182
+ item_id: metaStore.item_id,
183
+ meta: metaStore.meta,
184
+ })
185
+ .from(metaStore)
186
+ .where(eq(metaStore.document_version_id, document.id));
187
+ const fields = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
188
+ return {
189
+ document_version_id: document.id,
190
+ document_id: document.document_id,
191
+ path: document.path,
192
+ status: document.status,
193
+ created_at: document.created_at,
194
+ updated_at: document.updated_at,
195
+ fields,
196
+ };
197
+ }
198
+ // Non-reconstructed: return raw flattened values
199
+ const fieldValues = this.convertUnionRowToFlattenedStores(unifiedFieldValues);
200
+ return {
201
+ document_version_id: document.id,
202
+ document_id: document.document_id,
203
+ path: document.path,
204
+ status: document.status,
205
+ created_at: document.created_at,
206
+ updated_at: document.updated_at,
207
+ fields: fieldValues,
208
+ };
209
+ }
210
+ async getDocumentByPath({ collection_id, path, locale = 'en', reconstruct = true, readMode, filters, }) {
211
+ const view = this.pickCurrentView(readMode);
212
+ // 1. Get current version (or current published version, per readMode)
213
+ const baseConditions = [eq(view.collection_id, collection_id), eq(view.path, path)];
214
+ if (filters?.length) {
215
+ const outerScope = {
216
+ docVersionId: sql `${view.id}`,
217
+ status: sql `${view.status}`,
218
+ path: sql `${view.path}`,
219
+ };
220
+ for (const f of filters) {
221
+ baseConditions.push(this.buildFilterExists(f, locale, outerScope, readMode, 0));
222
+ }
223
+ }
224
+ const [document] = await this.db
225
+ .select()
226
+ .from(view)
227
+ .where(and(...baseConditions));
228
+ if (document == null) {
229
+ return null;
230
+ }
231
+ // 2. Get all field values for this document
232
+ const unifiedFieldValues = await this.getAllFieldValues(document.id, locale);
233
+ // 3. If reconstruct is true, reconstruct the fields and attach meta
234
+ if (reconstruct === true) {
235
+ const definition = await this.getDefinitionForCollection(collection_id);
236
+ const metaRows = await this.db
237
+ .select({
238
+ type: metaStore.type,
239
+ path: metaStore.path,
240
+ item_id: metaStore.item_id,
241
+ meta: metaStore.meta,
242
+ })
243
+ .from(metaStore)
244
+ .where(eq(metaStore.document_version_id, document.id));
245
+ const fields = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
246
+ return {
247
+ document_version_id: document.id,
248
+ document_id: document.document_id,
249
+ path: document.path,
250
+ status: document.status,
251
+ created_at: document.created_at,
252
+ updated_at: document.updated_at,
253
+ fields,
254
+ };
255
+ }
256
+ // Non-reconstructed: return raw flattened values
257
+ const fieldValues = this.convertUnionRowToFlattenedStores(unifiedFieldValues);
258
+ return {
259
+ document_version_id: document.id,
260
+ document_id: document.document_id,
261
+ path: document.path,
262
+ status: document.status,
263
+ created_at: document.created_at,
264
+ updated_at: document.updated_at,
265
+ fields: fieldValues,
266
+ };
267
+ }
268
+ /**
269
+ * getDocumentByVersion — fetches a specific version and reconstructs its fields.
270
+ */
271
+ async getDocumentByVersion({ document_version_id, locale = 'all', }) {
272
+ const document = await this.db.query.documentVersions.findFirst({
273
+ where: eq(documentVersions.id, document_version_id),
274
+ });
275
+ if (document == null) {
276
+ throw ERR_NOT_FOUND({
277
+ message: `no current version found for document ${document_version_id}`,
278
+ details: { documentVersionId: document_version_id },
279
+ }).log(getLogger());
280
+ }
281
+ const unifiedFieldValues = await this.getAllFieldValues(document.id, locale);
282
+ const definition = await this.getDefinitionForCollection(document.collection_id);
283
+ const metaRows = await this.db
284
+ .select({
285
+ type: metaStore.type,
286
+ path: metaStore.path,
287
+ item_id: metaStore.item_id,
288
+ meta: metaStore.meta,
289
+ })
290
+ .from(metaStore)
291
+ .where(eq(metaStore.document_version_id, document.id));
292
+ const enrichedDocument = this.reconstructFromUnifiedRows(unifiedFieldValues, definition, locale, metaRows);
293
+ const documentWithFields = {
294
+ document_version_id: document.id,
295
+ document_id: document.document_id,
296
+ path: document.path,
297
+ status: document.status,
298
+ created_at: document.created_at,
299
+ updated_at: document.updated_at,
300
+ fields: enrichedDocument,
301
+ };
302
+ return documentWithFields;
303
+ }
304
+ /**
305
+ * getDocumentsByVersionIds — fetches and reconstructs multiple documents by
306
+ * version ID. Used for batch loading a known set of versions (e.g.
307
+ * migration scripts, tests).
308
+ */
309
+ async getDocumentsByVersionIds({ document_version_ids, locale = 'all', }) {
310
+ if (document_version_ids.length === 0)
311
+ return [];
312
+ const docs = await this.db
313
+ .select()
314
+ .from(documentVersions)
315
+ .where(inArray(documentVersions.id, document_version_ids));
316
+ return this.reconstructDocuments({ documents: docs, locale });
317
+ }
318
+ /**
319
+ * getDocumentsByDocumentIds — batch-fetch current versions for a list of
320
+ * logical document IDs, with optional selective field loading.
321
+ *
322
+ * Resolves each document_id to its current version via the
323
+ * `current_documents` view (soft-deleted documents are excluded by the
324
+ * view definition), then delegates to `reconstructDocuments` for the
325
+ * shared field + meta reconstruction path.
326
+ *
327
+ * Primary consumer is the client API's relationship populate pass —
328
+ * `store_relation` rows carry `target_document_id` (not version ID), so
329
+ * populate collects those IDs and resolves them here in one round trip.
330
+ */
331
+ async getDocumentsByDocumentIds({ collection_id, document_ids, locale = 'all', fields, readMode, filters, }) {
332
+ if (document_ids.length === 0)
333
+ return [];
334
+ const view = this.pickCurrentView(readMode);
335
+ // The locale used to compile filter EXISTS subqueries should resolve
336
+ // values from a real locale, even when the surrounding read uses the
337
+ // sentinel `'all'` (populate batches that span every locale do this).
338
+ // Falling back to `'en'` here matches the default used by the
339
+ // single-doc lookup methods.
340
+ const filterLocale = locale === 'all' ? 'en' : locale;
341
+ const baseConditions = [
342
+ eq(view.collection_id, collection_id),
343
+ inArray(view.document_id, document_ids),
344
+ ];
345
+ if (filters?.length) {
346
+ const outerScope = {
347
+ docVersionId: sql `${view.id}`,
348
+ status: sql `${view.status}`,
349
+ path: sql `${view.path}`,
350
+ };
351
+ for (const f of filters) {
352
+ baseConditions.push(this.buildFilterExists(f, filterLocale, outerScope, readMode, 0));
353
+ }
354
+ }
355
+ const docs = await this.db
356
+ .select()
357
+ .from(view)
358
+ .where(and(...baseConditions));
359
+ return this.reconstructDocuments({ documents: docs, locale, fields });
360
+ }
361
+ /**
362
+ * getDocumentHistory — paginated version history for a document,
363
+ * including soft-deleted versions.
364
+ */
365
+ async getDocumentHistory({ collection_id, document_id, locale = 'all', page = 1, page_size = 20, order = 'updated_at', desc = true, }) {
366
+ const collection = await this.db.query.collections.findFirst({
367
+ where: eq(collections.id, collection_id),
368
+ });
369
+ if (collection == null || collection.config == null) {
370
+ throw ERR_NOT_FOUND({
371
+ message: `collection not found or missing config: ${collection_id}`,
372
+ details: { collectionId: collection_id },
373
+ }).log(getLogger());
374
+ }
375
+ const totalResult = await this.db
376
+ .select({
377
+ count: sql `count(*)`,
378
+ })
379
+ .from(documentVersions)
380
+ .where(and(eq(documentVersions.collection_id, collection_id), eq(documentVersions.document_id, document_id)));
381
+ const total = Number(totalResult[0]?.count) || 0;
382
+ const total_pages = Math.ceil(total / page_size);
383
+ const offset = (page - 1) * page_size;
384
+ const orderColumn = order === 'path' ? documentVersions.path : documentVersions.created_at;
385
+ const orderFunc = desc === true ? sql `DESC` : sql `ASC`;
386
+ const result = await this.db
387
+ .select()
388
+ .from(documentVersions)
389
+ .where(and(eq(documentVersions.collection_id, collection_id), eq(documentVersions.document_id, document_id)))
390
+ .orderBy(sql `${orderColumn} ${orderFunc}`)
391
+ .limit(page_size)
392
+ .offset(offset);
393
+ const history = await this.reconstructDocuments({ documents: result, locale });
394
+ return {
395
+ documents: history,
396
+ meta: { total, page, page_size, total_pages, order, desc },
397
+ };
398
+ }
399
+ /**
400
+ * getPublishedVersion
401
+ *
402
+ * Find the latest version of a document that has a specific status
403
+ * (defaults to 'published'). Queries `document_versions` directly so it
404
+ * can find a published version even when a newer draft exists.
405
+ *
406
+ * Returns minimal version metadata (not reconstructed content), or null
407
+ * if no version with the requested status exists.
408
+ */
409
+ async getPublishedVersion({ collection_id, document_id, status = 'published', }) {
410
+ const [row] = await this.db
411
+ .select({
412
+ document_version_id: documentVersions.id,
413
+ document_id: documentVersions.document_id,
414
+ status: documentVersions.status,
415
+ created_at: documentVersions.created_at,
416
+ updated_at: documentVersions.updated_at,
417
+ })
418
+ .from(documentVersions)
419
+ .where(and(eq(documentVersions.collection_id, collection_id), eq(documentVersions.document_id, document_id), eq(documentVersions.status, status), eq(documentVersions.is_deleted, false)))
420
+ .orderBy(sql `${documentVersions.id} DESC`)
421
+ .limit(1);
422
+ if (!row)
423
+ return null;
424
+ return {
425
+ document_version_id: row.document_version_id,
426
+ document_id: row.document_id,
427
+ status: row.status ?? 'draft',
428
+ created_at: row.created_at ?? new Date(),
429
+ updated_at: row.updated_at ?? new Date(),
430
+ };
431
+ }
432
+ /**
433
+ * getPublishedDocumentIds
434
+ *
435
+ * Given a list of document IDs, return the subset that have at least one
436
+ * version with the requested status (defaults to 'published'). Uses a
437
+ * single batch query instead of per-document lookups.
438
+ */
439
+ async getPublishedDocumentIds({ collection_id, document_ids, status = 'published', }) {
440
+ if (document_ids.length === 0)
441
+ return new Set();
442
+ const rows = await this.db
443
+ .select({ document_id: documentVersions.document_id })
444
+ .from(documentVersions)
445
+ .where(and(inArray(documentVersions.document_id, document_ids), eq(documentVersions.collection_id, collection_id), eq(documentVersions.status, status), eq(documentVersions.is_deleted, false)))
446
+ .groupBy(documentVersions.document_id);
447
+ return new Set(rows.map((r) => r.document_id));
448
+ }
449
+ /**
450
+ * getDocumentCountsByStatus
451
+ *
452
+ * Returns a count of current documents grouped by workflow status for a
453
+ * given collection. Uses the `current_documents` view so each logical
454
+ * document is counted once (at its latest/current version).
455
+ */
456
+ async getDocumentCountsByStatus({ collection_id, filters, }) {
457
+ const conditions = [eq(currentDocumentsView.collection_id, collection_id)];
458
+ if (filters?.length) {
459
+ const outerScope = {
460
+ docVersionId: sql `${currentDocumentsView.id}`,
461
+ status: sql `${currentDocumentsView.status}`,
462
+ path: sql `${currentDocumentsView.path}`,
463
+ };
464
+ for (const f of filters) {
465
+ conditions.push(this.buildFilterExists(f, 'en', outerScope, undefined, 0));
466
+ }
467
+ }
468
+ const rows = await this.db
469
+ .select({
470
+ status: currentDocumentsView.status,
471
+ count: sql `count(*)::int`,
472
+ })
473
+ .from(currentDocumentsView)
474
+ .where(and(...conditions))
475
+ .groupBy(currentDocumentsView.status);
476
+ return rows.map((r) => ({
477
+ status: r.status ?? 'unknown',
478
+ count: r.count,
479
+ }));
480
+ }
481
+ /**
482
+ * reconstructDocuments — retrieve field values and reconstruct multiple documents.
483
+ * Supports selective field loading via the `fields` parameter.
484
+ */
485
+ async reconstructDocuments({ documents, locale = 'all', fields: requestedFields, }) {
486
+ if (documents.length === 0)
487
+ return [];
488
+ const versionIds = documents.map((v) => v.id);
489
+ // Resolve definition once for the batch (safe — early return above guarantees length > 0)
490
+ const firstDoc = documents[0];
491
+ const definition = await this.getDefinitionForCollection(firstDoc.collection_id);
492
+ // When specific fields are requested, resolve which store tables we need
493
+ // and query only those — skipping irrelevant tables entirely.
494
+ const storeTypes = requestedFields?.length
495
+ ? resolveStoreTypes(definition.fields, requestedFields)
496
+ : undefined;
497
+ // Get field values for all versions in one query
498
+ const allFieldValues = await this.getAllFieldValuesForMultipleVersions(versionIds, locale, storeTypes);
499
+ // Group field values by document version
500
+ const fieldValuesByVersion = new Map();
501
+ for (const fieldValue of allFieldValues) {
502
+ if (!fieldValuesByVersion.has(fieldValue.document_version_id)) {
503
+ fieldValuesByVersion.set(fieldValue.document_version_id, []);
504
+ }
505
+ fieldValuesByVersion.get(fieldValue.document_version_id)?.push(fieldValue);
506
+ }
507
+ // Fetch meta rows for all versions in one query
508
+ const allMetaRows = await this.db
509
+ .select({
510
+ document_version_id: metaStore.document_version_id,
511
+ type: metaStore.type,
512
+ path: metaStore.path,
513
+ item_id: metaStore.item_id,
514
+ meta: metaStore.meta,
515
+ })
516
+ .from(metaStore)
517
+ .where(inArray(metaStore.document_version_id, versionIds));
518
+ const metaByVersion = new Map();
519
+ for (const row of allMetaRows) {
520
+ const list = metaByVersion.get(row.document_version_id) ?? [];
521
+ list.push({
522
+ type: row.type,
523
+ path: row.path,
524
+ item_id: row.item_id,
525
+ meta: row.meta,
526
+ });
527
+ if (!metaByVersion.has(row.document_version_id)) {
528
+ metaByVersion.set(row.document_version_id, list);
529
+ }
530
+ }
531
+ // Reconstruct each document with document data at root level
532
+ const result = [];
533
+ for (const doc of documents) {
534
+ const versionFieldValues = fieldValuesByVersion.get(doc.id) || [];
535
+ const docMetaRows = (metaByVersion.get(doc.id) ?? []);
536
+ const fields = this.reconstructFromUnifiedRows(versionFieldValues, definition, locale, docMetaRows);
537
+ // When specific fields were requested, trim the reconstructed object
538
+ // to only those fields. Store-level filtering avoids querying unused
539
+ // tables, but fields sharing a store (e.g. price + views in numeric)
540
+ // still appear — this final pass removes them.
541
+ const trimmedFields = requestedFields?.length
542
+ ? Object.fromEntries(Object.entries(fields).filter(([k]) => requestedFields.includes(k)))
543
+ : fields;
544
+ const documentWithFields = {
545
+ document_version_id: doc.id,
546
+ document_id: doc.document_id,
547
+ path: doc.path,
548
+ status: doc.status,
549
+ created_at: doc.created_at,
550
+ updated_at: doc.updated_at,
551
+ fields: trimmedFields,
552
+ };
553
+ result.push(documentWithFields);
554
+ }
555
+ return result;
556
+ }
557
+ /**
558
+ * Gets all field values for a single document version.
559
+ * Delegates to the multi-version dynamic UNION ALL builder.
560
+ */
561
+ async getAllFieldValues(documentVersionId, locale = 'all') {
562
+ return this.getAllFieldValuesForMultipleVersions([documentVersionId], locale);
563
+ }
564
+ /**
565
+ * Gets field values for multiple versions in a single query.
566
+ *
567
+ * When `storeTypes` is provided, only those store tables are included in
568
+ * the UNION ALL — this is the selective field loading optimisation for
569
+ * list views that only need a subset of fields.
570
+ */
571
+ async getAllFieldValuesForMultipleVersions(documentVersionIds, locale = 'all', storeTypes) {
572
+ if (documentVersionIds.length === 0)
573
+ return [];
574
+ const localeCondition = locale === 'all' ? sql `` : sql `AND (locale = ${locale} OR locale = 'all')`;
575
+ const documentCondition = sql `document_version_id = ANY(ARRAY[${sql.join(documentVersionIds.map((id) => sql `${id}::uuid`), sql `, `)}])`;
576
+ const typesToQuery = storeTypes ?? new Set(allStoreTypes);
577
+ // Build UNION ALL from only the required store tables.
578
+ const fragments = [];
579
+ for (const st of allStoreTypes) {
580
+ if (!typesToQuery.has(st))
581
+ continue;
582
+ fragments.push(sql `SELECT ${storeSelectList(st)} FROM ${sql.raw(storeTableNames[st])} WHERE ${documentCondition} ${localeCondition}`);
583
+ }
584
+ if (fragments.length === 0)
585
+ return [];
586
+ // Join with UNION ALL
587
+ let unionQuery = fragments[0];
588
+ for (let i = 1; i < fragments.length; i++) {
589
+ unionQuery = sql `${unionQuery} UNION ALL ${fragments[i]}`;
590
+ }
591
+ const query = sql `${unionQuery} ORDER BY document_version_id, field_path, locale`;
592
+ const { rows } = await this.db.execute(query);
593
+ return rows;
594
+ }
595
+ /**
596
+ * findDocuments — field-level filtered, sorted, paginated query.
597
+ *
598
+ * Each `FieldFilter` becomes an EXISTS subquery against the appropriate EAV
599
+ * store table. A `RelationFilter` becomes a nested EXISTS that joins
600
+ * `store_relation` to the target collection's current-documents view
601
+ * (selected by `readMode` so draft leaks can't happen through filter
602
+ * predicates) and recurses into its own `nested` filters. A `FieldSort`
603
+ * becomes a LEFT JOIN LATERAL to pull the sort value into the outer query.
604
+ * Document-level conditions (status, path) are applied directly on the
605
+ * current_documents view.
606
+ */
607
+ async findDocuments({ collection_id, filters = [], status, pathFilter, query, sort, orderBy = 'created_at', orderDirection = 'desc', locale = 'en', page = 1, pageSize = 20, fields: requestedFields, readMode, }) {
608
+ const offset = (page - 1) * pageSize;
609
+ const sourceTable = readMode === 'published'
610
+ ? sql.raw('byline_current_published_documents')
611
+ : sql.raw('byline_current_documents');
612
+ // -- Build WHERE conditions -----------------------------------------------
613
+ const conditions = [sql `d.collection_id = ${collection_id}`];
614
+ if (status) {
615
+ conditions.push(sql `d.status = ${status}`);
616
+ }
617
+ if (pathFilter) {
618
+ conditions.push(this.buildDocumentLevelCondition('d.path', pathFilter.operator, pathFilter.value));
619
+ }
620
+ // Text search across configured search fields via EXISTS on store_text.
621
+ if (query) {
622
+ const definition = await this.getDefinitionForCollection(collection_id);
623
+ const searchFields = definition.search?.fields ?? ['title'];
624
+ const searchConditions = searchFields.map((fieldName) => sql `(field_name = ${fieldName} AND value ILIKE ${`%${query}%`})`);
625
+ conditions.push(sql `EXISTS (
626
+ SELECT 1 FROM byline_store_text
627
+ WHERE document_version_id = d.id
628
+ AND (locale = ${locale} OR locale = 'all')
629
+ AND (${sql.join(searchConditions, sql ` OR `)})
630
+ )`);
631
+ }
632
+ // Field-level / relation-level EXISTS subqueries. Each relation hop
633
+ // introduces its own alias scope (`r${depth}`, `td${depth}`) so nested
634
+ // EXISTS clauses don't shadow their outer relation's aliases.
635
+ for (const filter of filters) {
636
+ conditions.push(this.buildFilterExists(filter, locale, { docVersionId: sql `d.id`, status: sql `d.status`, path: sql `d.path` }, readMode, 0));
637
+ }
638
+ const whereClause = sql.join(conditions, sql ` AND `);
639
+ // -- Build ORDER BY -------------------------------------------------------
640
+ let orderClause;
641
+ let sortJoin = sql ``;
642
+ if (sort) {
643
+ // Field-level sort via LEFT JOIN LATERAL
644
+ const storeTable = storeTableNames[sort.storeType];
645
+ if (storeTable) {
646
+ sortJoin = sql `LEFT JOIN LATERAL (
647
+ SELECT ${sql.raw(sort.valueColumn)} AS _sort_value
648
+ FROM ${sql.raw(storeTable)}
649
+ WHERE document_version_id = d.id
650
+ AND field_name = ${sort.fieldName}
651
+ AND (locale = ${locale} OR locale = 'all')
652
+ LIMIT 1
653
+ ) _sort ON true`;
654
+ orderClause =
655
+ sort.direction === 'desc'
656
+ ? sql `_sort._sort_value DESC NULLS LAST`
657
+ : sql `_sort._sort_value ASC NULLS LAST`;
658
+ }
659
+ else {
660
+ // Unrecognised store type — fall back to document-level sort
661
+ orderClause = this.buildDocumentOrderClause(orderBy, orderDirection);
662
+ }
663
+ }
664
+ else {
665
+ orderClause = this.buildDocumentOrderClause(orderBy, orderDirection);
666
+ }
667
+ // -- Count query ----------------------------------------------------------
668
+ const countQuery = sql `
669
+ SELECT count(*)::int AS total
670
+ FROM ${sourceTable} d
671
+ ${sortJoin}
672
+ WHERE ${whereClause}
673
+ `;
674
+ const countResult = await this.db.execute(countQuery);
675
+ const total = countResult.rows[0]?.total ?? 0;
676
+ if (total === 0) {
677
+ return { documents: [], total: 0 };
678
+ }
679
+ // -- Main query -----------------------------------------------------------
680
+ const mainQuery = sql `
681
+ SELECT d.*
682
+ FROM ${sourceTable} d
683
+ ${sortJoin}
684
+ WHERE ${whereClause}
685
+ ORDER BY ${orderClause}
686
+ LIMIT ${pageSize}
687
+ OFFSET ${offset}
688
+ `;
689
+ const { rows } = await this.db.execute(mainQuery);
690
+ const currentDocuments = rows.map((row) => ({
691
+ id: row.id,
692
+ document_id: row.document_id,
693
+ collection_id: row.collection_id,
694
+ collection_version: row.collection_version,
695
+ path: row.path,
696
+ event_type: row.event_type,
697
+ status: row.status,
698
+ is_deleted: row.is_deleted,
699
+ created_at: new Date(row.created_at),
700
+ updated_at: new Date(row.updated_at),
701
+ created_by: row.created_by,
702
+ change_summary: row.change_summary,
703
+ }));
704
+ const documents = await this.reconstructDocuments({
705
+ documents: currentDocuments,
706
+ locale,
707
+ fields: requestedFields,
708
+ });
709
+ return { documents, total };
710
+ }
711
+ /**
712
+ * Build an EXISTS subquery for a single DocumentFilter. Dispatches on
713
+ * `kind` — field filters emit a direct EXISTS against the field's EAV
714
+ * store; relation filters emit a nested EXISTS that joins through
715
+ * `store_relation` to the target collection's current-documents view
716
+ * and recurses against the target's own stores; combinator filters
717
+ * emit a parenthesised AND/OR group; document-column filters emit a
718
+ * direct comparison on the outer scope's status/path column.
719
+ *
720
+ * `outerScope` carries SQL references to the enclosing scope's
721
+ * `document_version_id`, `status`, and `path` — `d.id`/`d.status`/
722
+ * `d.path` at the top level, the equivalent column references on the
723
+ * Drizzle view for single-doc lookups, and `td${n}.…` inside relation
724
+ * hops. `depth` is the current relation-nesting level; each relation
725
+ * hop bumps it so aliases stay unique across nested EXISTS scopes
726
+ * (Postgres would otherwise resolve `td.id` to the innermost `td`,
727
+ * silently producing the wrong rows).
728
+ */
729
+ buildFilterExists(filter, locale, outerScope, readMode, depth) {
730
+ switch (filter.kind) {
731
+ case 'field':
732
+ return this.buildFieldExists(filter, locale, outerScope.docVersionId);
733
+ case 'relation':
734
+ return this.buildRelationExists(filter, locale, outerScope, readMode, depth);
735
+ case 'and':
736
+ case 'or':
737
+ return this.buildCombinatorGroup(filter, locale, outerScope, readMode, depth);
738
+ case 'docColumn':
739
+ return this.buildDocColumnFilter(filter, outerScope);
740
+ }
741
+ }
742
+ /**
743
+ * Build a parenthesised AND/OR group from a CombinatorFilter. Each child
744
+ * compiles through `buildFilterExists` recursively, so combinators nest
745
+ * freely and inherit the outer scope.
746
+ *
747
+ * An empty `children` array would emit `()` and produce a syntax error,
748
+ * so callers (the parser) skip empty groups; this method assumes at
749
+ * least one child by construction.
750
+ */
751
+ buildCombinatorGroup(filter, locale, outerScope, readMode, depth) {
752
+ const childSql = filter.children.map((child) => this.buildFilterExists(child, locale, outerScope, readMode, depth));
753
+ const joiner = filter.kind === 'or' ? sql ` OR ` : sql ` AND `;
754
+ return sql `(${sql.join(childSql, joiner)})`;
755
+ }
756
+ /**
757
+ * Compile a `DocumentColumnFilter` against the outer scope's status or
758
+ * path column. Plain comparison — no EXISTS — because the column lives
759
+ * directly on the outer relation (`document_versions` row), not in the
760
+ * EAV stores.
761
+ */
762
+ buildDocColumnFilter(filter, outerScope) {
763
+ const column = filter.column === 'status' ? outerScope.status : outerScope.path;
764
+ return this.buildFilterCondition(column, filter.operator, filter.value);
765
+ }
766
+ /**
767
+ * Build an EXISTS subquery for a single field-level filter.
768
+ */
769
+ buildFieldExists(filter, locale, outerDocVersionId) {
770
+ const storeTable = storeTableNames[filter.storeType];
771
+ if (!storeTable) {
772
+ throw ERR_DATABASE({
773
+ message: `unknown store type: ${filter.storeType}`,
774
+ details: { storeType: filter.storeType },
775
+ }).log(getLogger());
776
+ }
777
+ const valueCol = sql.raw(filter.valueColumn);
778
+ const condition = this.buildFilterCondition(valueCol, filter.operator, filter.value);
779
+ return sql `EXISTS (
780
+ SELECT 1 FROM ${sql.raw(storeTable)}
781
+ WHERE document_version_id = ${outerDocVersionId}
782
+ AND field_name = ${filter.fieldName}
783
+ AND (locale = ${locale} OR locale = 'all')
784
+ AND ${condition}
785
+ )`;
786
+ }
787
+ /**
788
+ * Build a nested EXISTS subquery for a cross-collection relation filter.
789
+ *
790
+ * Joins `store_relation` to the target collection's current-documents
791
+ * view (`current_published_documents` under `readMode: 'published'`,
792
+ * `current_documents` otherwise — so a draft target doesn't leak when
793
+ * the outer read is in published mode), then recurses each nested
794
+ * filter against the target version's own `td.id`.
795
+ *
796
+ * With no nested filters this reduces to "source has any relation row
797
+ * at all on this field pointing at a target that resolves in the
798
+ * selected view" — useful as a base case but more typically the
799
+ * nested list carries a predicate.
800
+ */
801
+ buildRelationExists(filter, locale, outerScope, readMode, depth) {
802
+ const targetView = readMode === 'published'
803
+ ? sql.raw('byline_current_published_documents')
804
+ : sql.raw('byline_current_documents');
805
+ // Use depth-scoped aliases so nested relations don't shadow their
806
+ // outer scope. e.g. outer relation gets `r0`/`td0`; a relation filter
807
+ // nested inside that gets `r1`/`td1`.
808
+ const rAlias = sql.raw(`r${depth}`);
809
+ const tdAlias = sql.raw(`td${depth}`);
810
+ const innerScope = {
811
+ docVersionId: sql.raw(`td${depth}.id`),
812
+ status: sql.raw(`td${depth}.status`),
813
+ path: sql.raw(`td${depth}.path`),
814
+ };
815
+ const nestedConditions = filter.nested.map((nested) => this.buildFilterExists(nested, locale, innerScope, readMode, depth + 1));
816
+ const nestedAnd = nestedConditions.length > 0 ? sql ` AND ${sql.join(nestedConditions, sql ` AND `)}` : sql ``;
817
+ return sql `EXISTS (
818
+ SELECT 1 FROM byline_store_relation ${rAlias}
819
+ JOIN ${targetView} ${tdAlias}
820
+ ON ${tdAlias}.document_id = ${rAlias}.target_document_id
821
+ AND ${tdAlias}.collection_id = ${rAlias}.target_collection_id
822
+ WHERE ${rAlias}.document_version_id = ${outerScope.docVersionId}
823
+ AND ${rAlias}.field_name = ${filter.fieldName}
824
+ AND ${rAlias}.target_collection_id = ${filter.targetCollectionId}
825
+ AND (${rAlias}.locale = ${locale} OR ${rAlias}.locale = 'all')${nestedAnd}
826
+ )`;
827
+ }
828
+ /**
829
+ * Build a comparison condition for a filter operator.
830
+ */
831
+ buildFilterCondition(column, operator, value) {
832
+ switch (operator) {
833
+ case '$eq':
834
+ return value === null ? sql `${column} IS NULL` : sql `${column} = ${value}`;
835
+ case '$ne':
836
+ return value === null ? sql `${column} IS NOT NULL` : sql `${column} != ${value}`;
837
+ case '$gt':
838
+ return sql `${column} > ${value}`;
839
+ case '$gte':
840
+ return sql `${column} >= ${value}`;
841
+ case '$lt':
842
+ return sql `${column} < ${value}`;
843
+ case '$lte':
844
+ return sql `${column} <= ${value}`;
845
+ case '$contains':
846
+ return sql `${column} ILIKE ${`%${String(value)}%`}`;
847
+ case '$in': {
848
+ const arr = value;
849
+ return sql `${column} = ANY(${arr})`;
850
+ }
851
+ case '$nin': {
852
+ const arr = value;
853
+ return sql `${column} != ALL(${arr})`;
854
+ }
855
+ default:
856
+ throw ERR_DATABASE({
857
+ message: `unsupported filter operator: ${operator}`,
858
+ details: { operator },
859
+ }).log(getLogger());
860
+ }
861
+ }
862
+ /**
863
+ * Build a condition for a document-level column (status, path).
864
+ */
865
+ buildDocumentLevelCondition(column, operator, value) {
866
+ const col = sql.raw(column);
867
+ return this.buildFilterCondition(col, operator, value);
868
+ }
869
+ /**
870
+ * Build an ORDER BY clause for a document-level column.
871
+ */
872
+ buildDocumentOrderClause(orderBy, direction) {
873
+ const columnMap = {
874
+ created_at: 'd.created_at',
875
+ updated_at: 'd.updated_at',
876
+ path: 'd.path',
877
+ };
878
+ const col = columnMap[orderBy] ?? 'd.created_at';
879
+ return direction === 'desc' ? sql `${sql.raw(col)} DESC` : sql `${sql.raw(col)} ASC`;
880
+ }
881
+ /**
882
+ * Converts a union field row - back into an array of FlattenedStore
883
+ * that the reconstruction utilities expect
884
+ */
885
+ convertUnionRowToFlattenedStores(unionRowValues) {
886
+ return unionRowValues.map((row) => {
887
+ const baseValue = {
888
+ field_path: row.field_path,
889
+ field_name: row.field_name,
890
+ locale: row.locale,
891
+ parent_path: row.parent_path ?? undefined,
892
+ };
893
+ switch (row.field_type) {
894
+ case 'text':
895
+ return {
896
+ ...baseValue,
897
+ field_type: 'text',
898
+ value: row.text_value,
899
+ };
900
+ case 'richText':
901
+ return {
902
+ ...baseValue,
903
+ field_type: 'richText',
904
+ value: row.json_value,
905
+ };
906
+ case 'numeric':
907
+ return {
908
+ ...baseValue,
909
+ field_type: row.number_type,
910
+ number_type: row.number_type,
911
+ value_integer: row.value_integer,
912
+ value_decimal: row.value_decimal,
913
+ value_float: row.value_float,
914
+ };
915
+ case 'boolean':
916
+ return {
917
+ ...baseValue,
918
+ field_type: 'boolean',
919
+ value: row.boolean_value,
920
+ };
921
+ case 'time':
922
+ case 'date':
923
+ case 'datetime':
924
+ return {
925
+ ...baseValue,
926
+ field_type: row.date_type,
927
+ date_type: row.date_type,
928
+ value_time: row.value_time,
929
+ value_date: row.value_date,
930
+ value_timestamp_tz: row.value_timestamp_tz,
931
+ };
932
+ case 'image':
933
+ case 'file':
934
+ return {
935
+ ...baseValue,
936
+ field_type: row.field_type,
937
+ file_id: row.file_id,
938
+ filename: row.filename,
939
+ original_filename: row.original_filename,
940
+ mime_type: row.mime_type,
941
+ file_size: row.file_size,
942
+ storage_provider: row.storage_provider,
943
+ storage_path: row.storage_path,
944
+ storage_url: row.storage_url,
945
+ file_hash: row.file_hash,
946
+ image_width: row.image_width,
947
+ image_height: row.image_height,
948
+ image_format: row.image_format,
949
+ processing_status: row.processing_status,
950
+ thumbnail_generated: row.thumbnail_generated,
951
+ };
952
+ case 'relation':
953
+ return {
954
+ ...baseValue,
955
+ field_type: 'relation',
956
+ target_document_id: row.target_document_id,
957
+ target_collection_id: row.target_collection_id,
958
+ relationship_type: row.relationship_type,
959
+ cascade_delete: row.cascade_delete,
960
+ };
961
+ default:
962
+ throw ERR_DATABASE({
963
+ message: `unknown field type: ${row.field_type}`,
964
+ details: { fieldType: row.field_type },
965
+ }).log(getLogger());
966
+ }
967
+ });
968
+ }
969
+ }
970
+ export function createQueryBuilders(db, collections) {
971
+ return {
972
+ collections: new CollectionQueries(db),
973
+ documents: new DocumentQueries(db, collections),
974
+ };
975
+ }
976
+ //# sourceMappingURL=storage-queries.js.map