@byline/db-mysql 4.8.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.
Files changed (75) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +283 -0
  3. package/dist/database/schema/auth.d.ts +951 -0
  4. package/dist/database/schema/auth.js +226 -0
  5. package/dist/database/schema/common.d.ts +168 -0
  6. package/dist/database/schema/common.js +130 -0
  7. package/dist/database/schema/index.d.ts +3419 -0
  8. package/dist/database/schema/index.js +895 -0
  9. package/dist/database/schema/schema-pins.test.node.d.ts +52 -0
  10. package/dist/database/schema/schema-pins.test.node.js +398 -0
  11. package/dist/index.d.ts +79 -0
  12. package/dist/index.js +151 -0
  13. package/dist/lib/boot-check.d.ts +22 -0
  14. package/dist/lib/boot-check.js +42 -0
  15. package/dist/lib/boot-check.test.node.d.ts +8 -0
  16. package/dist/lib/boot-check.test.node.js +46 -0
  17. package/dist/lib/db-manager.d.ts +54 -0
  18. package/dist/lib/db-manager.js +49 -0
  19. package/dist/lib/test-db.d.ts +37 -0
  20. package/dist/lib/test-db.js +111 -0
  21. package/dist/lib/test-helper.d.ts +33 -0
  22. package/dist/lib/test-helper.js +68 -0
  23. package/dist/modules/admin/admin-permissions-repository.d.ts +35 -0
  24. package/dist/modules/admin/admin-permissions-repository.js +101 -0
  25. package/dist/modules/admin/admin-preferences-repository.d.ts +62 -0
  26. package/dist/modules/admin/admin-preferences-repository.js +156 -0
  27. package/dist/modules/admin/admin-roles-repository.d.ts +11 -0
  28. package/dist/modules/admin/admin-roles-repository.js +209 -0
  29. package/dist/modules/admin/admin-store.d.ts +20 -0
  30. package/dist/modules/admin/admin-store.js +30 -0
  31. package/dist/modules/admin/admin-users-repository.d.ts +11 -0
  32. package/dist/modules/admin/admin-users-repository.js +303 -0
  33. package/dist/modules/admin/index.d.ts +27 -0
  34. package/dist/modules/admin/index.js +27 -0
  35. package/dist/modules/admin/refresh-tokens-repository.d.ts +34 -0
  36. package/dist/modules/admin/refresh-tokens-repository.js +160 -0
  37. package/dist/modules/audit/audit-commands.d.ts +29 -0
  38. package/dist/modules/audit/audit-commands.js +40 -0
  39. package/dist/modules/audit/audit-queries.d.ts +41 -0
  40. package/dist/modules/audit/audit-queries.js +169 -0
  41. package/dist/modules/counters/counters-commands.d.ts +54 -0
  42. package/dist/modules/counters/counters-commands.js +121 -0
  43. package/dist/modules/counters/tests/counters-concurrency.test.d.ts +8 -0
  44. package/dist/modules/counters/tests/counters-concurrency.test.js +111 -0
  45. package/dist/modules/storage/classify-error.d.ts +34 -0
  46. package/dist/modules/storage/classify-error.js +50 -0
  47. package/dist/modules/storage/classify-error.test.node.d.ts +8 -0
  48. package/dist/modules/storage/classify-error.test.node.js +89 -0
  49. package/dist/modules/storage/normalize-row.d.ts +55 -0
  50. package/dist/modules/storage/normalize-row.js +135 -0
  51. package/dist/modules/storage/normalize-row.test.node.d.ts +8 -0
  52. package/dist/modules/storage/normalize-row.test.node.js +89 -0
  53. package/dist/modules/storage/storage-commands.d.ts +443 -0
  54. package/dist/modules/storage/storage-commands.js +1263 -0
  55. package/dist/modules/storage/storage-insert.d.ts +21 -0
  56. package/dist/modules/storage/storage-insert.js +152 -0
  57. package/dist/modules/storage/storage-queries.d.ts +805 -0
  58. package/dist/modules/storage/storage-queries.js +1815 -0
  59. package/dist/modules/storage/storage-store-manifest.d.ts +77 -0
  60. package/dist/modules/storage/storage-store-manifest.js +168 -0
  61. package/dist/modules/storage/storage-utils.d.ts +49 -0
  62. package/dist/modules/storage/storage-utils.js +76 -0
  63. package/dist/modules/storage/tests/dialect-pins.integration.test.d.ts +8 -0
  64. package/dist/modules/storage/tests/dialect-pins.integration.test.js +266 -0
  65. package/dist/modules/storage/tests/storage-commands.test.d.ts +8 -0
  66. package/dist/modules/storage/tests/storage-commands.test.js +324 -0
  67. package/dist/modules/storage/tests/storage-document-paths.test.d.ts +8 -0
  68. package/dist/modules/storage/tests/storage-document-paths.test.js +214 -0
  69. package/dist/modules/storage/tests/storage-document-tree.test.d.ts +8 -0
  70. package/dist/modules/storage/tests/storage-document-tree.test.js +361 -0
  71. package/dist/modules/storage/tests/storage-queries.test.d.ts +8 -0
  72. package/dist/modules/storage/tests/storage-queries.test.js +685 -0
  73. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.d.ts +8 -0
  74. package/dist/modules/storage/tests/storage-status-and-lifecycle.test.js +268 -0
  75. package/package.json +91 -0
@@ -0,0 +1,805 @@
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 MySQL storage read path. Ported from
9
+ * `packages/db-postgres/src/modules/storage/storage-queries.ts`.
10
+ *
11
+ * Task 10A landed the UNION ALL reconstruction path (`getDocumentById`,
12
+ * `getDocumentByVersion`, `getDocumentHistory`) and a `findDocuments` scoped
13
+ * to what the `versioning` + `field-types` conformance suites needed.
14
+ *
15
+ * Task 10B (this port) completes the surface: the `DocumentFilter[]`
16
+ * predicate compiler (`$and`/`$or` combinators, relation hops,
17
+ * document-column filters), `findDocuments`' `pathFilter`/`query` (LIKE
18
+ * search)/`sort` (`LEFT JOIN LATERAL` field sort), `getDocumentByPath`,
19
+ * `getDocumentsByVersionIds`, `getDocumentsByDocumentIds`,
20
+ * `getPublishedVersion`, `getPublishedDocumentIds`,
21
+ * `getDocumentCountsByStatus`, order-key / tree reads, `getCurrentPath`, and
22
+ * `getDocumentSystemFieldsForUpdate`. `DocumentQueries` now declares
23
+ * `implements IDocumentQueries`.
24
+ */
25
+ import type { CollectionDefinition, DocumentFilter, FieldFilterOperator, FieldSort, FlattenedStore, ICollectionQueries, IDocumentQueries, MissingLocalePolicy, ReadMode } from '@byline/core';
26
+ import type { MySql2Database } from 'drizzle-orm/mysql2';
27
+ import type * as schema from '../../database/schema/index.js';
28
+ import type { DBManager } from '../../lib/db-manager.js';
29
+ type DatabaseConnection = MySql2Database<typeof schema>;
30
+ /**
31
+ * CollectionQueries
32
+ *
33
+ * Identical to pg's — a plain query-builder read against `byline_collections`,
34
+ * with no dialect-specific SQL anywhere.
35
+ */
36
+ export declare class CollectionQueries implements ICollectionQueries {
37
+ private db;
38
+ constructor(db: DatabaseConnection);
39
+ getAllCollections(): Promise<{
40
+ created_at: Date;
41
+ updated_at: Date;
42
+ id: string;
43
+ path: string;
44
+ singular: string;
45
+ plural: string;
46
+ config: unknown;
47
+ version: number;
48
+ schema_hash: string | null;
49
+ }[]>;
50
+ getCollectionByPath(path: string): Promise<{
51
+ config: unknown;
52
+ created_at: Date;
53
+ id: string;
54
+ path: string;
55
+ plural: string;
56
+ schema_hash: string | null;
57
+ singular: string;
58
+ updated_at: Date;
59
+ version: number;
60
+ } | undefined>;
61
+ getCollectionById(id: string): Promise<{
62
+ config: unknown;
63
+ created_at: Date;
64
+ id: string;
65
+ path: string;
66
+ plural: string;
67
+ schema_hash: string | null;
68
+ singular: string;
69
+ updated_at: Date;
70
+ version: number;
71
+ } | undefined>;
72
+ }
73
+ /**
74
+ * DocumentQueries
75
+ */
76
+ export declare class DocumentQueries implements IDocumentQueries {
77
+ private db;
78
+ private transactionDb;
79
+ private collections;
80
+ private defaultContentLocale;
81
+ private collectionPathCache;
82
+ constructor(db: DatabaseConnection, collections: readonly CollectionDefinition[], defaultContentLocale: string, transactionDb: DBManager);
83
+ /**
84
+ * Lock the logical document row before reading its document-grain system
85
+ * fields. Every audited system-field writer takes this same parent-row lock,
86
+ * so path and advertised-locale snapshots serialize without locking a
87
+ * variable set of child rows.
88
+ *
89
+ * Runs on `this.transactionDb.get()` — the ambient `withTransaction`
90
+ * executor when one is open, else the pool — so `FOR UPDATE` (unchanged
91
+ * syntax from pg) actually takes its lock inside the caller's transaction.
92
+ * A lock taken on a bare pool connection would release at the end of its
93
+ * own implicit transaction and serialise nothing; see the §H ruling in the
94
+ * Task 10B report for why `transactionDb` is a required constructor
95
+ * parameter rather than a silently-defaulted one.
96
+ */
97
+ getDocumentSystemFieldsForUpdate({ collection_id, document_id, }: {
98
+ collection_id: string;
99
+ document_id: string;
100
+ }): Promise<{
101
+ source_locale: string;
102
+ path: string | null;
103
+ availableLocales: string[];
104
+ } | null>;
105
+ /**
106
+ * Resolve a collection UUID to its CollectionDefinition by looking up the
107
+ * collection's path in the DB and matching it against the injected array.
108
+ */
109
+ private getDefinitionForCollection;
110
+ /**
111
+ * Pick the Drizzle view reference to read from based on `readMode`.
112
+ *
113
+ * - `'any'` (default) → `current_documents` — the latest version of
114
+ * each logical document, regardless of status.
115
+ * - `'published'` → `current_published_documents` — the latest
116
+ * version whose status is `'published'`, falling back past newer
117
+ * drafts so public readers keep seeing previously-published
118
+ * content while editors work on an unpublished draft.
119
+ *
120
+ * Both views share the same row shape, so the returned reference is
121
+ * drop-in substitutable at every select/where site.
122
+ */
123
+ private pickCurrentView;
124
+ /**
125
+ * Build the locale priority chain for fallback resolution:
126
+ * `[requested, floor]`, deduplicated when both are the same. The floor is
127
+ * the document's own `source_locale` anchor when known (so a re-anchored
128
+ * document, or any document read after the global default is switched, falls
129
+ * back to the locale it was actually authored in) — otherwise the configured
130
+ * global default, which is correct for not-yet-anchored rows and for
131
+ * row-less lookups (findByPath). See docs/07-internationalization/index.md.
132
+ */
133
+ private buildLocaleChain;
134
+ /**
135
+ * Build the `onMissingLocale: 'omit'` availability gate — an EXISTS against
136
+ * the version-locale ledger (`byline_document_version_locales`) that keeps
137
+ * only documents available in the requested locale. The `'all'` sentinel row
138
+ * covers locale-agnostic documents (no localized content). Returns `null`
139
+ * when the gate does not apply — a non-`'omit'` policy (`'empty'` /
140
+ * `'fallback'` / unset), or the admin sentinel `'all'` read — so callers can
141
+ * conditionally push it into a WHERE. No dialect-specific SQL — ports
142
+ * unchanged from pg.
143
+ */
144
+ private localeAvailabilityExists;
145
+ /**
146
+ * Batch-fetch the version-locale availability sets from the
147
+ * `byline_document_version_locales` ledger. For each version returns the
148
+ * concrete locales its content is complete in (`availableLocales`, in
149
+ * configured content-locale order), or `localeAgnostic: true` when the
150
+ * version carries only the `'all'` sentinel. Drives the
151
+ * `_availableVersionLocales` read metadata. One indexed query per call.
152
+ * Plain query-builder code — ports unchanged from pg.
153
+ */
154
+ private getAvailableLocalesByVersion;
155
+ /**
156
+ * Batch-fetch the editorial advertised-locale sets from
157
+ * `byline_document_available_locales` (document-grain). For each logical
158
+ * document returns the set of locales the editor has elected to advertise,
159
+ * in configured content-locale order. Surfaced on reads as
160
+ * `availableLocales`. One indexed query per call. Plain query-builder
161
+ * code — ports unchanged from pg.
162
+ */
163
+ private getAdvertisedLocalesByDocument;
164
+ /**
165
+ * Emit a SQL fragment that resolves the path string for a document via
166
+ * the locale priority chain. Used as a projected column expression
167
+ * inside `SELECT` lists.
168
+ *
169
+ * Postgres:
170
+ * ```sql
171
+ * (SELECT path FROM byline_document_paths
172
+ * WHERE document_id = <docIdSql> AND locale = ANY(<chain>)
173
+ * ORDER BY array_position(<chain>, locale) LIMIT 1)
174
+ * ```
175
+ * MySQL has no `ANY(ARRAY[...])` or `array_position` — the locale-chain
176
+ * conversion (design spec §2): `IN (…)` for the membership test, and
177
+ * `ORDER BY FIELD(locale, …)` for the priority order. `FIELD()` returns
178
+ * the 1-based position of the first argument within the remaining
179
+ * argument list (0 if absent); since the `WHERE … IN (chain)` clause
180
+ * already restricts every candidate row to a locale that's a member of
181
+ * the chain, `FIELD()` can never actually return 0 here — every row that
182
+ * reaches the `ORDER BY` has a genuine position, and ascending order
183
+ * therefore picks the earliest (highest-priority, i.e. most-requested)
184
+ * chain entry first, exactly matching `array_position`'s semantics.
185
+ * Verified live against MySQL 9.7.1 (see the Task 10A report) and pinned
186
+ * by `storage-queries.test.ts`'s locale-chain-ordering test.
187
+ */
188
+ private pathProjection;
189
+ /**
190
+ * Emit a SQL fragment that resolves a `(collection_id, path)` tuple to a
191
+ * `document_id` via the locale priority chain. Used inside `WHERE` clauses
192
+ * for findByPath-style lookups. Same `IN (…)` / `FIELD()` locale-chain
193
+ * conversion as `pathProjection` — see that method's docblock. Returns
194
+ * NULL when no row matches in any locale, which makes the outer `=`
195
+ * predicate fail cleanly (no document found).
196
+ */
197
+ private resolveDocumentIdByPath;
198
+ /**
199
+ * Project list for `current_documents` / `current_published_documents`
200
+ * reads, with `path` resolved through the locale priority chain. Used
201
+ * everywhere a read previously did `.select()` (which auto-pulls every
202
+ * view column) — `path` is no longer projected by the views, so call
203
+ * sites must list the projection explicitly.
204
+ */
205
+ private viewProjection;
206
+ /**
207
+ * Project list for direct `byline_document_versions` reads (history,
208
+ * version-by-id lookups). Mirrors `viewProjection` but against the
209
+ * underlying table — `path` is sourced from `byline_document_paths` via
210
+ * the locale priority chain, since it no longer lives on the version row.
211
+ */
212
+ private documentVersionsProjection;
213
+ /**
214
+ * Resolve the single effective content locale a version should be restored
215
+ * in, walking the fallback chain (`[requested, default]`) and returning the
216
+ * first locale the version is *available* in. Pure JS/data-structure code
217
+ * — no SQL — ports unchanged from pg. See pg's docblock for the full
218
+ * phase-1 availability rule.
219
+ */
220
+ private resolveEffectiveLocale;
221
+ /**
222
+ * Reconstruct document fields from unified row values using schema-aware
223
+ * restoration. Meta rows (from store_meta) are converted to
224
+ * FlattenedFieldValue entries so that restoreFieldSetData can inject
225
+ * _id and _type for blocks and array items inline. Pure JS/core-delegate
226
+ * code — ports unchanged from pg.
227
+ */
228
+ private reconstructFromUnifiedRows;
229
+ /**
230
+ * getCurrentVersionMetadata — narrow metadata fetch for the current version.
231
+ *
232
+ * Hits `current_documents` only; no field reconstruction, no meta fetch,
233
+ * no path subquery. Used by lifecycle operations (status changes,
234
+ * restore, delete checks) that only need `document_version_id` /
235
+ * `status` / timestamps before mutating.
236
+ */
237
+ getCurrentVersionMetadata({ collection_id, document_id, }: {
238
+ collection_id: string;
239
+ document_id: string;
240
+ }): Promise<{
241
+ document_version_id: string;
242
+ document_id: string;
243
+ collection_id: string;
244
+ status: string;
245
+ created_at: Date;
246
+ updated_at: Date;
247
+ } | null>;
248
+ /**
249
+ * getCurrentPath — resolve a document's canonical (source-locale) path.
250
+ *
251
+ * Reuses `pathProjection` against `current_documents`, passing
252
+ * `requestedLocale: undefined` so the projection's fallback floor — the
253
+ * document's own `source_locale` (COALESCE-guarded to the default content
254
+ * locale for not-yet-anchored rows) — supplies the canonical path. Used by
255
+ * the lifecycle to populate `path` on the status-change / unpublish hook
256
+ * contexts. Returns `null` when no path row (or document) exists.
257
+ */
258
+ getCurrentPath({ collection_id, document_id, }: {
259
+ collection_id: string;
260
+ document_id: string;
261
+ }): Promise<string | null>;
262
+ /**
263
+ * getDocumentById — gets the current version of a document by its logical document ID.
264
+ *
265
+ * When `lenient` is true, schema-mismatch warnings emitted during
266
+ * reconstruction are surfaced on the returned object as `restoreWarnings`
267
+ * rather than thrown. This is the admin edit path's "best-effort load"
268
+ * mode for documents written under a previous collection schema.
269
+ */
270
+ getDocumentById({ collection_id, document_id, locale, reconstruct, readMode, filters, lenient, onMissingLocale, }: {
271
+ collection_id: string;
272
+ document_id: string;
273
+ locale?: string;
274
+ reconstruct?: boolean;
275
+ readMode?: ReadMode;
276
+ filters?: DocumentFilter[];
277
+ lenient?: boolean;
278
+ onMissingLocale?: MissingLocalePolicy;
279
+ }): Promise<{
280
+ document_version_id: string;
281
+ document_id: string;
282
+ path: string;
283
+ source_locale: string;
284
+ status: string | null;
285
+ event_type: string;
286
+ created_at: Date;
287
+ updated_at: Date;
288
+ created_by: string | null;
289
+ fields: any;
290
+ availableLocales: string[];
291
+ _availableVersionLocales: string[];
292
+ _localeAgnostic: boolean;
293
+ restoreWarnings?: string[] | undefined;
294
+ } | {
295
+ document_version_id: string;
296
+ document_id: string;
297
+ path: string;
298
+ source_locale: string;
299
+ status: string | null;
300
+ event_type: string;
301
+ created_at: Date;
302
+ updated_at: Date;
303
+ created_by: string | null;
304
+ fields: FlattenedStore[];
305
+ } | null>;
306
+ /**
307
+ * getDocumentByPath — resolves `(collection_id, path)` through the locale
308
+ * priority chain to a document_id, then reads and reconstructs its
309
+ * current version. See `resolveDocumentIdByPath` for the locale-chain
310
+ * SQL.
311
+ */
312
+ getDocumentByPath({ collection_id, path, locale, reconstruct, readMode, filters, onMissingLocale, }: {
313
+ collection_id: string;
314
+ path: string;
315
+ locale?: string;
316
+ reconstruct: boolean;
317
+ readMode?: ReadMode;
318
+ filters?: DocumentFilter[];
319
+ onMissingLocale?: MissingLocalePolicy;
320
+ }): Promise<{
321
+ document_version_id: string;
322
+ document_id: string;
323
+ path: string;
324
+ source_locale: string;
325
+ status: string | null;
326
+ event_type: string;
327
+ created_at: Date;
328
+ updated_at: Date;
329
+ created_by: string | null;
330
+ fields: any;
331
+ availableLocales: string[];
332
+ _availableVersionLocales: string[];
333
+ _localeAgnostic: boolean;
334
+ } | {
335
+ availableLocales?: undefined;
336
+ _availableVersionLocales?: undefined;
337
+ _localeAgnostic?: undefined;
338
+ document_version_id: string;
339
+ document_id: string;
340
+ path: string;
341
+ source_locale: string;
342
+ status: string | null;
343
+ event_type: string;
344
+ created_at: Date;
345
+ updated_at: Date;
346
+ created_by: string | null;
347
+ fields: FlattenedStore[];
348
+ } | null>;
349
+ /**
350
+ * getDocumentByVersion — fetches a specific version and reconstructs its fields.
351
+ */
352
+ getDocumentByVersion({ document_version_id, locale, collection_id, filters, }: {
353
+ document_version_id: string;
354
+ locale?: string;
355
+ collection_id?: string;
356
+ filters?: DocumentFilter[];
357
+ }): Promise<any | null>;
358
+ /**
359
+ * getDocumentsByVersionIds — fetches and reconstructs multiple documents by
360
+ * version ID. Used for batch loading a known set of versions (e.g.
361
+ * migration scripts, tests).
362
+ */
363
+ getDocumentsByVersionIds({ document_version_ids, locale, }: {
364
+ document_version_ids: string[];
365
+ locale?: string;
366
+ }): Promise<any[]>;
367
+ /**
368
+ * getDocumentsByDocumentIds — batch-fetch current versions for a list of
369
+ * logical document IDs, with optional selective field loading.
370
+ *
371
+ * Resolves each document_id to its current version via the
372
+ * `current_documents` view (soft-deleted documents are excluded by the
373
+ * view definition), then delegates to `reconstructDocuments` for the
374
+ * shared field + meta reconstruction path.
375
+ *
376
+ * Primary consumer is the client API's relationship populate pass —
377
+ * `store_relation` rows carry `target_document_id` (not version ID), so
378
+ * populate collects those IDs and resolves them here in one round trip.
379
+ */
380
+ getDocumentsByDocumentIds({ collection_id, document_ids, locale, fields, readMode, filters, }: {
381
+ collection_id: string;
382
+ document_ids: string[];
383
+ locale?: string;
384
+ fields?: string[];
385
+ readMode?: ReadMode;
386
+ filters?: DocumentFilter[];
387
+ }): Promise<any[]>;
388
+ /**
389
+ * getDocumentHistory — paginated version history for a document,
390
+ * including soft-deleted versions.
391
+ */
392
+ getDocumentHistory({ collection_id, document_id, locale, page, page_size, order, desc: descending, filters, }: {
393
+ collection_id: string;
394
+ document_id: string;
395
+ locale?: string;
396
+ page?: number;
397
+ page_size?: number;
398
+ order?: string;
399
+ desc?: boolean;
400
+ query?: string;
401
+ filters?: DocumentFilter[];
402
+ }): Promise<{
403
+ documents: any[];
404
+ meta: {
405
+ total: number;
406
+ page: number;
407
+ page_size: number;
408
+ total_pages: number;
409
+ order: string;
410
+ desc: boolean;
411
+ };
412
+ }>;
413
+ /**
414
+ * getPublishedVersion
415
+ *
416
+ * Find the latest version of a document that has a specific status
417
+ * (defaults to 'published'). Queries `document_versions` directly so it
418
+ * can find a published version even when a newer draft exists.
419
+ *
420
+ * Returns minimal version metadata (not reconstructed content), or null
421
+ * if no version with the requested status exists.
422
+ */
423
+ getPublishedVersion({ collection_id, document_id, status, }: {
424
+ collection_id: string;
425
+ document_id: string;
426
+ status?: string;
427
+ }): Promise<{
428
+ document_version_id: string;
429
+ document_id: string;
430
+ status: string;
431
+ created_at: Date;
432
+ updated_at: Date;
433
+ } | null>;
434
+ /**
435
+ * getPublishedDocumentIds
436
+ *
437
+ * Given a list of document IDs, return the subset that have at least one
438
+ * version with the requested status (defaults to 'published'). Uses a
439
+ * single batch query instead of per-document lookups.
440
+ */
441
+ getPublishedDocumentIds({ collection_id, document_ids, status, }: {
442
+ collection_id: string;
443
+ document_ids: string[];
444
+ status?: string;
445
+ }): Promise<Set<string>>;
446
+ /**
447
+ * getLastOrderKey
448
+ *
449
+ * Largest `order_key` currently in use for the given collection. Used
450
+ * at create-time on `orderable: true` collections to append the new
451
+ * row at the end. Returns `null` when no keyed rows exist yet.
452
+ */
453
+ getLastOrderKey({ collection_id }: {
454
+ collection_id: string;
455
+ }): Promise<string | null>;
456
+ /**
457
+ * getNeighborOrderKeys
458
+ *
459
+ * Resolve the `order_key` values bracketing a target gap in one query.
460
+ * `before_document_id` is the doc the moved row should land *after*;
461
+ * `after_document_id` is the doc it should land *before*. Either or
462
+ * both may be null (append / prepend / empty collection).
463
+ *
464
+ * Resolves both keys in a single round trip to keep the read consistent
465
+ * with the next-key computation that follows in the caller.
466
+ */
467
+ getNeighborOrderKeys({ collection_id, before_document_id, after_document_id, }: {
468
+ collection_id: string;
469
+ before_document_id: string | null;
470
+ after_document_id: string | null;
471
+ }): Promise<{
472
+ left: string | null;
473
+ right: string | null;
474
+ }>;
475
+ /**
476
+ * getCanonicalDocumentOrder
477
+ *
478
+ * Returns every document in the collection in its canonical list-view
479
+ * order: `order_key ASC NULLS LAST, created_at DESC`. Used by the reorder
480
+ * server fn for backfill and recovery from key corruption.
481
+ *
482
+ * Divergence from pg: MySQL has no `NULLS LAST`. MySQL's own `ASC`
483
+ * default sorts NULL *first* (the opposite of pg's `NULLS LAST`), so the
484
+ * `(col IS NULL) ASC, col ASC` emulation idiom — the same one
485
+ * `buildDocumentOrderClause` uses — is required here too.
486
+ */
487
+ getCanonicalDocumentOrder({ collection_id, }: {
488
+ collection_id: string;
489
+ }): Promise<Array<{
490
+ id: string;
491
+ order_key: string | null;
492
+ }>>;
493
+ /**
494
+ * getTreeAncestors — see {@link IDocumentQueries.getTreeAncestors}.
495
+ *
496
+ * Recursive CTE walking `parent_document_id` upward from the given node.
497
+ * Returns ancestors root-first (`ORDER BY depth DESC`), each with a 1-based
498
+ * depth (1 = immediate parent). Empty for a root or unplaced node.
499
+ *
500
+ * Status-at-edge: in `published` mode each hop must resolve in
501
+ * `byline_current_published_documents`, so the walk stops at the first
502
+ * unpublished ancestor rather than skipping it (a truncated spine the splat
503
+ * handler turns into a 404). `any` mode walks the raw edges unchanged.
504
+ *
505
+ * `WITH RECURSIVE` syntax is unchanged from pg (the 8.0.14 engine floor
506
+ * guarantees support); the `::uuid` cast on `document_id` is dropped —
507
+ * MySQL has no cast syntax for a UUID-shaped `CHAR(36)` column, and the
508
+ * driver already binds the plain string correctly.
509
+ */
510
+ getTreeAncestors({ document_id, maxDepth, readMode, locale, filters, }: {
511
+ document_id: string;
512
+ maxDepth?: number;
513
+ readMode?: ReadMode;
514
+ locale?: string;
515
+ filters?: DocumentFilter[];
516
+ }): Promise<Array<{
517
+ document_id: string;
518
+ depth: number;
519
+ }>>;
520
+ /**
521
+ * getTreeChildren — see {@link IDocumentQueries.getTreeChildren}.
522
+ *
523
+ * Immediate children of a node ordered by the per-parent `order_key`.
524
+ * `parentDocumentId: null` returns the collection's root nodes; the join to
525
+ * `byline_documents` scopes roots to the collection (they have no parent to
526
+ * scope by). Plain query-builder code — ports unchanged from pg.
527
+ */
528
+ getTreeChildren({ collectionId, parentDocumentId, }: {
529
+ collectionId: string;
530
+ parentDocumentId: string | null;
531
+ }): Promise<Array<{
532
+ document_id: string;
533
+ order_key: string;
534
+ }>>;
535
+ /**
536
+ * getTreeParent — see {@link IDocumentQueries.getTreeParent}.
537
+ *
538
+ * Single indexed lookup on the edge table by `child_document_id` (unique).
539
+ * No row → *unplaced*; a row with a null parent → *root*; a row with a parent
540
+ * → *child*. Distinguishes the unplaced/root states that `getTreeAncestors`
541
+ * (which returns `[]` for both) conflates. `::uuid` cast dropped, same as
542
+ * `getTreeAncestors`.
543
+ */
544
+ getTreeParent({ document_id, readMode, locale, filters, }: {
545
+ document_id: string;
546
+ readMode?: ReadMode;
547
+ locale?: string;
548
+ filters?: DocumentFilter[];
549
+ }): Promise<{
550
+ placed: boolean;
551
+ parentDocumentId: string | null;
552
+ parentRedacted?: true;
553
+ }>;
554
+ /**
555
+ * getTreeSubtree — see {@link IDocumentQueries.getTreeSubtree}.
556
+ *
557
+ * Recursive CTE descending from the requested root (or the collection's
558
+ * roots when `rootDocumentId` is null). Each row carries a `/`-joined path
559
+ * of ancestor `order_key`s; ordering by that path yields a pre-order
560
+ * depth-first walk (a parent's path is a prefix of its children's, and `/`
561
+ * (0x2F) sorts below every key character in an ascii_bin comparison).
562
+ * Status-at-edge: every node — anchor included — must exist in the chosen
563
+ * current-documents view, so an unpublished node and its whole subtree
564
+ * drop out in `published` mode.
565
+ *
566
+ * Three divergences from pg, all found (or, for the third, reproduced)
567
+ * live — not assumed from docs:
568
+ *
569
+ * - `s.path || '/' || r.order_key` (pg's `||` string concat) is MySQL
570
+ * boolean OR unless `PIPES_AS_CONCAT` is in `sql_mode` — confirmed
571
+ * against this database's `@@sql_mode` (it isn't set), and confirmed
572
+ * `SELECT 'a' || 'b'` returns `0`, not `'ab'`. `CONCAT(s.path, '/',
573
+ * r.order_key)` is the portable form.
574
+ * - `ORDER BY path COLLATE "C"` drops the explicit collation: `order_key`
575
+ * (and therefore the CONCAT'd `path` column, once the width fix below
576
+ * is in place) is already `ascii_bin` — byte-comparable — end to end
577
+ * (`varcharByteSorted`, `database/schema/common.ts`), confirmed live
578
+ * that `CONCAT()` over two `ascii_bin` operands plus a `'/'` literal
579
+ * stays `ascii_bin` rather than falling back to the connection's
580
+ * `utf8mb4_0900_ai_ci` default (which would reorder mixed-case keys
581
+ * incorrectly, the same class of bug `varcharByteSorted` exists to
582
+ * prevent on the base columns).
583
+ * - pg's `r.order_key::text AS path` cast does **two** jobs, not one —
584
+ * an earlier version of this docblock dropped the cast on collation
585
+ * grounds alone and missed the second job. In Postgres, `::text`
586
+ * also makes the anchor column **unbounded** (`text` has no length
587
+ * cap), which an accumulating concatenation needs. MySQL infers a
588
+ * recursive CTE's column types from the *non-recursive* (anchor) leg
589
+ * only — a bare `r.order_key` reference in the anchor makes `path`
590
+ * inherit `order_key`'s declared `varchar(128)` width
591
+ * (`varcharByteSorted`, `database/schema/index.ts:299`), so every
592
+ * recursive iteration's `CONCAT` is silently constrained to 128
593
+ * bytes regardless of how the SELECT list is written. Reproduced
594
+ * live against this server (`STRICT_TRANS_TABLES` is in `sql_mode`,
595
+ * so it's a hard `ER_DATA_TOO_LONG`, not silent truncation):
596
+ * `WITH RECURSIVE t AS (SELECT CAST('ab' AS CHAR(128) CHARACTER SET
597
+ * ascii) AS p, 1 AS n UNION ALL SELECT CONCAT(p,'/','abc…xyz'), n+1
598
+ * FROM t WHERE n<6) …` → `ERROR 1406 (22001): Data too long for
599
+ * column 'p' at row 1`. The threshold — `Σ len(order_key) + depth − 1
600
+ * > 128` — is far more reachable than the `cte_max_recursion_depth`
601
+ * ceiling this method's own docblock elsewhere flags: roughly 11–40
602
+ * tree levels for typical fractional-index keys, lower once keys
603
+ * have grown through repeated same-position reordering. Fix: widen
604
+ * the anchor's `path` column explicitly —
605
+ * `CAST(r.order_key AS CHAR(4096) CHARACTER SET ascii) COLLATE
606
+ * ascii_bin` — doing both jobs the pg cast did: width (4096 bytes,
607
+ * generous headroom over the reachable-depth threshold above) *and*
608
+ * collation (`CHARACTER SET ascii` alone resolves to
609
+ * `ascii_general_ci`, not `ascii_bin` — confirmed live that a bare
610
+ * `CAST(… AS CHAR(4096))` with no `CHARACTER SET`/`COLLATE` at all
611
+ * reverts to the *connection's* default collation, not the source
612
+ * column's, so both clauses are required, not just one).
613
+ */
614
+ getTreeSubtree({ collectionId, rootDocumentId, maxDepth, readMode, locale, filters, }: {
615
+ collectionId: string;
616
+ rootDocumentId?: string | null;
617
+ maxDepth?: number;
618
+ readMode?: ReadMode;
619
+ locale?: string;
620
+ filters?: DocumentFilter[];
621
+ }): Promise<Array<{
622
+ document_id: string;
623
+ parent_document_id: string | null;
624
+ depth: number;
625
+ order_key: string;
626
+ }>>;
627
+ /**
628
+ * getDocumentCountsByStatus
629
+ *
630
+ * Returns a count of current documents grouped by workflow status for a
631
+ * given collection. Uses the `current_documents` view so each logical
632
+ * document is counted once (at its latest/current version).
633
+ */
634
+ getDocumentCountsByStatus({ collection_id, filters, }: {
635
+ collection_id: string;
636
+ filters?: DocumentFilter[];
637
+ }): Promise<Array<{
638
+ status: string;
639
+ count: number;
640
+ }>>;
641
+ /**
642
+ * reconstructDocuments — retrieve field values and reconstruct multiple documents.
643
+ * Supports selective field loading via the `fields` parameter. Pure
644
+ * JS/core-delegate orchestration — ports unchanged from pg (the
645
+ * dialect-specific work is one level down, in
646
+ * `getAllFieldValuesForMultipleVersions`).
647
+ */
648
+ private reconstructDocuments;
649
+ /**
650
+ * Gets all field values for a single document version.
651
+ * Delegates to the multi-version dynamic UNION ALL builder.
652
+ */
653
+ private getAllFieldValues;
654
+ /**
655
+ * Gets field values for multiple versions in a single query — the UNION
656
+ * ALL that is the whole point of this task.
657
+ *
658
+ * When `storeTypes` is provided, only those store tables are included in
659
+ * the UNION ALL — this is the selective field loading optimisation for
660
+ * list views that only need a subset of fields.
661
+ *
662
+ * Locale-chain conversion (design spec §2, the store-row locale
663
+ * condition): pg's `locale = ANY(ARRAY[...]) ` → MySQL `locale IN (...)`.
664
+ * No `ORDER BY`/`FIELD()` needed here — this is a membership filter, not a
665
+ * priority pick (unlike `pathProjection`, every matching locale row is
666
+ * kept; `resolveEffectiveLocale`/`restoreFieldSetData` pick the effective
667
+ * one in JS afterwards).
668
+ *
669
+ * `db.execute()` on the mysql2 driver returns a `[rows, fields]` tuple
670
+ * (unlike pg's `{ rows }` result object) — see `storage-commands.ts` for
671
+ * the established pattern this mirrors.
672
+ */
673
+ private getAllFieldValuesForMultipleVersions;
674
+ /**
675
+ * findDocuments — field-level filtered, sorted, paginated query.
676
+ *
677
+ * Each `FieldFilter` becomes an EXISTS subquery against the appropriate EAV
678
+ * store table. A `RelationFilter` becomes a nested EXISTS that joins
679
+ * `store_relation` to the target collection's current-documents view
680
+ * (selected by `readMode` so draft leaks can't happen through filter
681
+ * predicates) and recurses into its own `nested` filters. A `FieldSort`
682
+ * becomes a LEFT JOIN LATERAL to pull the sort value into the outer query
683
+ * — unchanged syntax from pg (the 8.0.14 engine floor guarantees LATERAL
684
+ * support), confirmed live against this container. Document-level
685
+ * conditions (status, path) are applied directly on the current_documents
686
+ * view.
687
+ */
688
+ findDocuments({ collection_id, filters, status, pathFilter, query, sort, orderBy, orderDirection, locale, page, pageSize, fields: requestedFields, readMode, onMissingLocale, }: {
689
+ collection_id: string;
690
+ filters?: DocumentFilter[];
691
+ status?: string;
692
+ pathFilter?: {
693
+ operator: FieldFilterOperator;
694
+ value: string;
695
+ };
696
+ query?: string;
697
+ sort?: FieldSort;
698
+ orderBy?: string;
699
+ orderDirection?: 'asc' | 'desc';
700
+ locale?: string;
701
+ page?: number;
702
+ pageSize?: number;
703
+ fields?: string[];
704
+ readMode?: ReadMode;
705
+ onMissingLocale?: MissingLocalePolicy;
706
+ }): Promise<{
707
+ documents: any[];
708
+ total: number;
709
+ }>;
710
+ /**
711
+ * Compile status and `beforeRead` visibility for one tree edge endpoint.
712
+ * Ports unchanged from pg — the `EXISTS` shape has no dialect-specific
713
+ * SQL of its own (its nested `buildFilterExists` calls carry the
714
+ * dialect-specific bits).
715
+ */
716
+ private buildTreeVisibility;
717
+ /**
718
+ * Build an EXISTS subquery for a single DocumentFilter. Dispatches on
719
+ * `kind` — field filters emit a direct EXISTS against the field's EAV
720
+ * store; relation filters emit a nested EXISTS that joins through
721
+ * `store_relation` to the target collection's current-documents view
722
+ * and recurses against the target's own stores; combinator filters
723
+ * emit a parenthesised AND/OR group; document-column filters emit a
724
+ * direct comparison on the outer scope's status/path column. Ports
725
+ * unchanged from pg — the dispatch itself has no dialect-specific SQL.
726
+ */
727
+ private buildFilterExists;
728
+ /**
729
+ * Build a parenthesised AND/OR group from a CombinatorFilter. Each child
730
+ * compiles through `buildFilterExists` recursively, so combinators nest
731
+ * freely and inherit the outer scope. Ports unchanged from pg.
732
+ */
733
+ private buildCombinatorGroup;
734
+ /**
735
+ * Compile a `DocumentColumnFilter` against the outer scope's `status`,
736
+ * `path`, or `id` column. Plain comparison — no EXISTS — because the
737
+ * column lives directly on the outer relation (current-documents view),
738
+ * not in the EAV stores. Ports unchanged from pg.
739
+ */
740
+ private buildDocColumnFilter;
741
+ /**
742
+ * Build an EXISTS subquery for a single field-level filter. Ports
743
+ * unchanged from pg (aside from `buildFilterCondition`'s `ILIKE` → `LIKE`
744
+ * one level down).
745
+ */
746
+ private buildFieldExists;
747
+ /**
748
+ * Build a nested EXISTS subquery for a cross-collection relation filter.
749
+ *
750
+ * Joins `store_relation` to the target collection's current-documents
751
+ * view (`current_published_documents` under `readMode: 'published'`,
752
+ * `current_documents` otherwise — so a draft target doesn't leak when
753
+ * the outer read is in published mode), then recurses each nested
754
+ * filter against the target version's own `td.id`.
755
+ *
756
+ * `hasMany` relations store one row per item at an indexed field name
757
+ * (`<field>.0`, `<field>.1`, …), so the field match switches to a prefix
758
+ * match; single relations match the exact name.
759
+ *
760
+ * The `quantifier` selects the set semantics over the relation's
761
+ * (resolving) targets — see pg's docblock for the full 'some'/'none'/
762
+ * 'every' semantics; this method ports the SQL shape unchanged.
763
+ */
764
+ private buildRelationExists;
765
+ /**
766
+ * Build a comparison condition for a filter operator.
767
+ *
768
+ * `$contains` divergence (design spec §2, elected — not "fixed"): pg's
769
+ * `ILIKE` has no MySQL equivalent, so this compiles to plain `LIKE`. The
770
+ * store `value` columns keep the schema's default `utf8mb4_0900_ai_ci`
771
+ * collation, which is case- *and* accent-insensitive — a strictly wider
772
+ * match than pg's case-insensitive-only `ILIKE`. `byline_document_paths
773
+ * .path` is the one column this operator also reaches (via
774
+ * `buildDocColumnFilter`'s `path` branch) that is NOT `ai_ci` — it's
775
+ * `utf8mb4_bin` (`varcharCaseSensitive`, `database/schema/common.ts`), so
776
+ * a `$contains` against `path` stays byte-exact and matches pg exactly.
777
+ * Both behaviours fall out of the column's own collation; nothing here
778
+ * special-cases `path` vs a store value.
779
+ */
780
+ private buildFilterCondition;
781
+ /**
782
+ * Build an ORDER BY clause for a document-level column. `path` is
783
+ * intentionally not sortable here — see pg's docblock.
784
+ *
785
+ * Divergence from pg (found by reasoning, confirmed live against this
786
+ * container): MySQL has no `NULLS LAST`. For `DESC`, MySQL already sorts
787
+ * NULL last by default, so `d.order_key DESC` needs no help. For `ASC`,
788
+ * MySQL's default sorts NULL *first* — the opposite of pg's `NULLS LAST`
789
+ * — so the emulation idiom `ORDER BY (col IS NULL), col ASC` is required
790
+ * to match pg's behaviour (`(col IS NULL)` evaluates 0/1; ascending puts
791
+ * real values, which evaluate to 0, before NULLs, which evaluate to 1).
792
+ */
793
+ private buildDocumentOrderClause;
794
+ /**
795
+ * Converts a union field row back into an array of FlattenedStore that
796
+ * the reconstruction utilities expect. Pure JS mapping — ports unchanged
797
+ * from pg.
798
+ */
799
+ private convertUnionRowToFlattenedStores;
800
+ }
801
+ export declare function createQueryBuilders(db: DatabaseConnection, collections: readonly CollectionDefinition[], defaultContentLocale: string, transactionDb: DBManager): {
802
+ collections: CollectionQueries;
803
+ documents: DocumentQueries;
804
+ };
805
+ export {};