@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,443 @@
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 { CollectionDefinition, ICollectionCommands, IDocumentCommands, TreeDeleteMutationResult, TreeMutationResult } from '@byline/core';
9
+ import type { DBManager } from '../../lib/db-manager.js';
10
+ /**
11
+ * CollectionCommands
12
+ *
13
+ * Mirrors `packages/db-postgres/src/modules/storage/storage-commands.ts`'s
14
+ * `CollectionCommands`. MySQL has no `RETURNING` clause, so every write
15
+ * constructs its return value in JS instead — collection ids are
16
+ * app-generated UUIDv7, so `create` already knows every value it needs
17
+ * without a round trip. `update` re-`SELECT`s because `patch` is partial
18
+ * and the caller expects the merged row back.
19
+ */
20
+ export declare class CollectionCommands implements ICollectionCommands {
21
+ private dbManager;
22
+ constructor(dbManager: DBManager);
23
+ /**
24
+ * The executor for this call — the ambient transaction when a
25
+ * `withTransaction` boundary is open, otherwise the pool. Resolved per
26
+ * access so every `this.db.*` below transparently joins an enclosing
27
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
28
+ */
29
+ private get db();
30
+ create(path: string, config: CollectionDefinition, opts?: {
31
+ version?: number;
32
+ schemaHash?: string;
33
+ }): Promise<{
34
+ id: string;
35
+ path: string;
36
+ singular: string;
37
+ plural: string;
38
+ config: CollectionDefinition;
39
+ version: number;
40
+ schema_hash: string | null;
41
+ created_at: Date;
42
+ updated_at: Date;
43
+ }[]>;
44
+ update(id: string, patch: {
45
+ config?: CollectionDefinition;
46
+ version?: number;
47
+ schemaHash?: string;
48
+ }): Promise<{
49
+ created_at: Date;
50
+ updated_at: Date;
51
+ id: string;
52
+ path: string;
53
+ singular: string;
54
+ plural: string;
55
+ config: unknown;
56
+ version: number;
57
+ schema_hash: string | null;
58
+ }[]>;
59
+ delete(id: string): Promise<import("drizzle-orm/mysql2").MySqlRawQueryResult>;
60
+ }
61
+ /**
62
+ * DocumentCommands
63
+ *
64
+ * Ported from `packages/db-postgres/src/modules/storage/storage-commands.ts`'s
65
+ * `DocumentCommands`. Task 9A landed `createDocumentVersion` and its private
66
+ * helpers; Task 9B (this task) ports the rest of the write surface —
67
+ * `updateDocumentPath`, `setDocumentAvailableLocales`, `setDocumentStatus`,
68
+ * `archivePublishedVersions`, `softDeleteDocument`, `deleteDocumentLocale`,
69
+ * `setOrderKey`, `placeTreeNode`, `removeFromTree`, and
70
+ * `promoteChildrenAndRemoveFromTree` — completing `IDocumentCommands`. The
71
+ * pg source's `reAnchorDocument` / `reAnchorDocuments` / `backfillVersionLocales`
72
+ * are deliberately NOT ported here: `@byline/db-conformance`'s own suites
73
+ * (`document-paths.ts`, `locale-fallback.ts`) document them as "Postgres-only
74
+ * maintenance operations documented as off the core `IDbAdapter` contract" —
75
+ * not something a conforming adapter is required to implement, and neither is
76
+ * a member of `IDocumentCommands`.
77
+ */
78
+ export declare class DocumentCommands implements IDocumentCommands {
79
+ private dbManager;
80
+ private defaultContentLocale;
81
+ constructor(dbManager: DBManager, defaultContentLocale: string);
82
+ /**
83
+ * The executor for this call — the ambient transaction when a
84
+ * `withTransaction` boundary is open, otherwise the pool. Resolved per
85
+ * access so every `this.db.*` below transparently joins an enclosing
86
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
87
+ */
88
+ private get db();
89
+ /**
90
+ * createDocumentVersion
91
+ *
92
+ * Creates a new document or a new version of an existing document. Ported
93
+ * from `packages/db-postgres/src/modules/storage/storage-commands.ts`'s
94
+ * `DocumentCommands.createDocumentVersion` — see spec §2's normative
95
+ * conversion table, applied at each site below:
96
+ *
97
+ * - `.returning()` → construct-in-JS. Every id here is app-generated
98
+ * UUIDv7 (minted before the insert), so the value is already known —
99
+ * no re-`SELECT` is needed anywhere in this method.
100
+ * - `onConflictDoUpdate` (path upsert) → NOT `.onDuplicateKeyUpdate()`
101
+ * (found live: MySQL's `ON DUPLICATE KEY UPDATE` has no per-constraint
102
+ * targeting the way pg's `onConflictDoUpdate({ target })` does, so it
103
+ * would silently absorb a genuine cross-document path conflict instead
104
+ * of erroring). `writeDocumentPath` below does the targeting itself —
105
+ * see its docblock.
106
+ * - `ON CONFLICT (document_version_id, field_path, locale) DO NOTHING`
107
+ * (7 sites, the per-locale carry-forward) → MySQL has no per-row
108
+ * `gen_random_uuid()` to call from an `INSERT … SELECT`, and ids must
109
+ * be app-generated UUIDv7 — never `INSERT IGNORE`, which would swallow
110
+ * unrelated errors, and never a DB-generated id. So each site becomes
111
+ * a typed `SELECT` of the previous version's carry-forward rows,
112
+ * followed by a JS-side UUIDv7-per-row bulk `INSERT … ON DUPLICATE KEY
113
+ * UPDATE id = id` (drizzle's own documented no-op idiom for "do
114
+ * nothing" on MySQL) — see `copyForwardStoreRows` below, which the 7
115
+ * call sites share.
116
+ * - `::uuid` casts → dropped (MySQL ids are plain `CHAR(36)`).
117
+ *
118
+ * @param params - Options for creating the document
119
+ * @returns The created document and the number of field values inserted
120
+ */
121
+ createDocumentVersion(params: {
122
+ documentId?: string;
123
+ collectionId: string;
124
+ collectionVersion: number;
125
+ collectionConfig: CollectionDefinition;
126
+ action: string;
127
+ documentData: any;
128
+ path?: string;
129
+ availableLocales?: string[];
130
+ locale?: string;
131
+ status?: string;
132
+ createdBy?: string;
133
+ previousVersionId?: string;
134
+ orderKey?: string;
135
+ }): Promise<{
136
+ document: {
137
+ id: string;
138
+ document_id: string;
139
+ collection_id: string;
140
+ collection_version: number;
141
+ doc: null;
142
+ event_type: string;
143
+ status: string;
144
+ is_deleted: boolean;
145
+ created_by: string | null;
146
+ change_summary: null;
147
+ created_at: Date;
148
+ updated_at: Date;
149
+ };
150
+ fieldCount: number;
151
+ }>;
152
+ /**
153
+ * writeDocumentPath
154
+ *
155
+ * Upsert the `byline_document_paths` row for a (document, locale) pair. The
156
+ * path row is document-grain and sticky across versions — it lives under the
157
+ * document's `source_locale` (its data anchor), not the mutable global
158
+ * default.
159
+ *
160
+ * Real dialect divergence from pg, found live (a test that should have
161
+ * raised a conflict silently "succeeded" instead): pg's `onConflictDoUpdate`
162
+ * takes an explicit `target` — the (`document_id`, `locale`) unique index —
163
+ * so a collision on the *other* unique index
164
+ * (`idx_document_paths_collection_locale_path`, a genuine path conflict
165
+ * from a different document) is deliberately left untargeted and bubbles up
166
+ * as `23505`. MySQL's `ON DUPLICATE KEY UPDATE` has no per-constraint
167
+ * targeting — it fires for a collision on *any* unique/primary key on the
168
+ * table, so a naive `.onDuplicateKeyUpdate()` would silently rewrite
169
+ * whichever existing row the *other* document's path collided with instead
170
+ * of raising `ER_DUP_ENTRY`. So this does the targeting itself: try the
171
+ * `INSERT` first; if it fails on the *own-document* unique index
172
+ * (`unique_document_paths_document_locale`, the ordinary "this document is
173
+ * resaving its already-anchored path" case — detected via `classifyError`,
174
+ * the same cause-chain-walking classifier the lifecycle layer uses, since
175
+ * mysql2/Drizzle wraps the duplicate-key error rather than throwing it
176
+ * bare), fall through to an explicit `UPDATE` instead. Any other
177
+ * duplicate-key error — in particular the collection-scoped
178
+ * path-uniqueness index — is rethrown unchanged, so it still surfaces as
179
+ * `ER_DUP_ENTRY` for the lifecycle layer's own `classifyError`-based
180
+ * `ERR_PATH_CONFLICT` mapping.
181
+ */
182
+ private writeDocumentPath;
183
+ /**
184
+ * writeDocumentAvailableLocales
185
+ *
186
+ * Replace a document's `byline_document_available_locales` rows wholesale —
187
+ * the editorial advertised-locale set. Document-grain and sticky across
188
+ * versions: `delete`-then-`insert`, deduplicated so a caller-supplied
189
+ * duplicate doesn't collide on the `(document_id, locale)` primary key. An
190
+ * empty array clears the set (advertise nothing).
191
+ */
192
+ private writeDocumentAvailableLocales;
193
+ /**
194
+ * writeVersionLocaleLedger
195
+ *
196
+ * Compute and insert a version's `byline_document_version_locales` rows: a
197
+ * locale is recorded when it covers every localized field path the version's
198
+ * `sourceLocale` has (path-coverage), and a version with no localized content
199
+ * records a single `'all'` sentinel. Reads the version's persisted store rows,
200
+ * so callers must have written them first. `::uuid` casts are dropped
201
+ * (MySQL ids are plain `CHAR(36)`); the WITH/UNION/HAVING NOT EXISTS logic is
202
+ * otherwise unchanged MySQL 8+ syntax — but the WITH clause's *position* is a
203
+ * real dialect difference, found live (a syntax error, not assumed): Postgres
204
+ * accepts `WITH … INSERT INTO … SELECT …`, whereas MySQL requires the WITH
205
+ * clause *after* `INSERT INTO <table>` and immediately before the `SELECT`
206
+ * it modifies (confirmed against a live MySQL 9.7.1 server) — so the two
207
+ * clauses are transposed relative to the pg original. See
208
+ * docs/07-internationalization/index.md.
209
+ */
210
+ private writeVersionLocaleLedger;
211
+ /**
212
+ * copyForwardStoreRows
213
+ *
214
+ * Carry forward one store table's rows for every locale except `'all'` and
215
+ * the version's active write locale, from `prevVersionId` to `newVersionId`.
216
+ * The MySQL counterpart of pg's `INSERT … SELECT gen_random_uuid(), … ON
217
+ * CONFLICT (document_version_id, field_path, locale) DO NOTHING`: MySQL has
218
+ * no per-row id generator usable from an `INSERT … SELECT`, and ids must be
219
+ * app-generated UUIDv7 (never DB-generated, never `INSERT IGNORE` — see the
220
+ * method doc on `createDocumentVersion`) — so this reads the candidate rows
221
+ * with a typed `SELECT`, mints a fresh UUIDv7 per row in JS, and bulk-inserts
222
+ * with `.onDuplicateKeyUpdate({ set: { id: sql\`id\` } })`, drizzle's own
223
+ * documented no-op idiom for "do nothing on conflict" on MySQL. One generic
224
+ * helper shared by all 7 call sites (pg keeps 7 near-identical raw-SQL
225
+ * blocks because it doesn't need this per-row JS step; MySQL does, so
226
+ * factoring it once here avoids seven copies of the same shape).
227
+ *
228
+ * `table` is threaded through as `AnyMySqlTable` and cast internally —
229
+ * the 7 store tables share the same `(id, document_version_id, …, locale,
230
+ * created_at, updated_at)` column shape (`baseStoreColumns` in the schema),
231
+ * but Drizzle's query builder generics don't unify cleanly across a union
232
+ * of distinct table types for `.from()` / `.insert()`, so the cast is
233
+ * confined to this one private helper rather than leaking into the 7 call
234
+ * sites, which stay fully typed.
235
+ */
236
+ private copyForwardStoreRows;
237
+ /**
238
+ * updateDocumentPath
239
+ *
240
+ * Standalone, non-versioned write of a document's URL path. Backs the admin
241
+ * path widget's direct-write Save path: it edits `byline_document_paths`
242
+ * in-place (document-grain, sticky) **without** minting a new document
243
+ * version or touching workflow status. The path's document-grain nature means
244
+ * the change is immediate and applies across every version of the document.
245
+ *
246
+ * Second caller into `writeDocumentPath` (the first is `createDocumentVersion`
247
+ * step 2a) — see that method's docblock for the insert-then-catch-and-
248
+ * conditionally-update targeting it does in place of pg's
249
+ * `onConflictDoUpdate({ target })`.
250
+ *
251
+ * Source-locale enforcement and `ERR_PATH_CONFLICT` mapping live in the
252
+ * lifecycle service that calls this; the command itself only performs the
253
+ * upsert (and surfaces the raw `ER_DUP_ENTRY` for the service to translate
254
+ * via `classifyError`).
255
+ */
256
+ updateDocumentPath(params: {
257
+ documentId: string;
258
+ collectionId: string;
259
+ locale: string;
260
+ path: string;
261
+ }): Promise<void>;
262
+ /**
263
+ * setDocumentAvailableLocales
264
+ *
265
+ * Standalone, non-versioned write of a document's editorial advertised-locale
266
+ * set. Backs the admin available-locales widget's direct-write Save path: it
267
+ * replaces `byline_document_available_locales` wholesale (document-grain)
268
+ * **without** minting a new document version or touching workflow status. The
269
+ * change is immediate and applies across every version of the document; the
270
+ * public advertised set remains the intersection with the resolved version's
271
+ * completeness ledger. See docs/07-internationalization/index.md.
272
+ */
273
+ setDocumentAvailableLocales(params: {
274
+ documentId: string;
275
+ collectionId: string;
276
+ availableLocales: string[];
277
+ }): Promise<void>;
278
+ /**
279
+ * copyAllVersionStoreRows
280
+ *
281
+ * Copy every store row — all seven value-store tables (optionally excluding
282
+ * one locale) plus the locale-agnostic `byline_store_meta` identity rows
283
+ * (always copied wholesale, unfiltered — a block's identity is shared
284
+ * across locales) — from one document version to another, verbatim. New
285
+ * `id`s are minted per row (MySQL has no per-row `gen_random_uuid()`
286
+ * usable from an `INSERT … SELECT`, and ids must be app-generated UUIDv7 —
287
+ * see `copyForwardStoreRows` above); `created_at`/`updated_at` are left off
288
+ * the inserted row so the column default (`CURRENT_TIMESTAMP(6)`) supplies
289
+ * a fresh timestamp, matching pg's explicit `NOW(), NOW()`.
290
+ *
291
+ * The target version is assumed fresh (no existing rows), so — unlike
292
+ * `copyForwardStoreRows`, which may collide with rows `createDocumentVersion`
293
+ * already wrote in the same transaction — this performs a plain `INSERT`
294
+ * with no conflict handling. A collision here would mean the caller reused
295
+ * a non-fresh version id, which should fail loudly rather than being
296
+ * silently absorbed. Used by `deleteDocumentLocale` to snapshot the current
297
+ * version into the new one with the target locale's rows dropped.
298
+ */
299
+ private copyAllVersionStoreRows;
300
+ /** One value-store table's share of `copyAllVersionStoreRows`. */
301
+ private copyVersionStoreRows;
302
+ /**
303
+ * deleteDocumentLocale
304
+ *
305
+ * Remove one content locale's data from a document by writing a **new
306
+ * immutable version** that carries forward every store row except the
307
+ * target locale's (the `'all'` rows and all other locales are kept). The
308
+ * prior version still holds the deleted locale, so the operation is
309
+ * recoverable via version restore, and a previously-published version keeps
310
+ * serving until the new version is published.
311
+ *
312
+ * The new version's status is supplied by the caller (the lifecycle service
313
+ * passes the workflow's default — a fresh draft, matching `copyToLocale`).
314
+ * The derived availability ledger is recomputed from the carried-forward
315
+ * rows, so the deleted locale drops out automatically. The default content
316
+ * locale (the document's anchor) must never be passed here — the lifecycle
317
+ * service enforces that.
318
+ *
319
+ * Defensively returns `null` when the document has no current version (the
320
+ * service validates existence first, so this is a guard).
321
+ */
322
+ deleteDocumentLocale(params: {
323
+ documentId: string;
324
+ locale: string;
325
+ status?: string;
326
+ createdBy?: string;
327
+ }): Promise<{
328
+ newVersionId: string;
329
+ previousVersionId: string;
330
+ } | null>;
331
+ /**
332
+ * setDocumentStatus
333
+ *
334
+ * Mutate the status field on an existing document version row.
335
+ * This is the one case where we UPDATE a version in-place — status is
336
+ * lifecycle metadata, not content.
337
+ */
338
+ setDocumentStatus(params: {
339
+ document_version_id: string;
340
+ status: string;
341
+ }): Promise<void>;
342
+ /**
343
+ * archivePublishedVersions
344
+ *
345
+ * Set ALL versions of a document that currently have `currentStatus`
346
+ * (defaults to 'published') to 'archived'. Optionally exclude a specific
347
+ * version so the caller can protect the version it is about to publish.
348
+ *
349
+ * Returns the number of rows updated. MySQL has no `RETURNING` and drizzle's
350
+ * mysql2 `update()` resolves to a `[ResultSetHeader, FieldPacket[]]` tuple
351
+ * rather than pg's driver result object — `affectedRows` lives on the first
352
+ * element (confirmed live against the test database), not `.rowCount`. See
353
+ * `affectedRowCount()` in `storage-utils.ts`, shared by every guarded write
354
+ * in this adapter (this method, `softDeleteDocument` below, and the admin
355
+ * repositories) for that cast.
356
+ */
357
+ archivePublishedVersions(params: {
358
+ document_id: string;
359
+ currentStatus?: string;
360
+ excludeVersionId?: string;
361
+ }): Promise<number>;
362
+ /**
363
+ * softDeleteDocument
364
+ *
365
+ * Mark ALL versions of a document as deleted by setting `is_deleted = true`.
366
+ * The `current_documents` view filters these out, so the document disappears
367
+ * from listings without physically removing data.
368
+ *
369
+ * Returns the number of version rows marked as deleted.
370
+ */
371
+ softDeleteDocument(params: {
372
+ document_id: string;
373
+ }): Promise<number>;
374
+ /**
375
+ * Write `order_key` on a single `byline_documents` row. Single-column
376
+ * metadata update — no new version row, no `documentVersions` touch.
377
+ * `updated_at` on the document row is bumped so list caches invalidate.
378
+ */
379
+ setOrderKey(params: {
380
+ document_id: string;
381
+ order_key: string;
382
+ }): Promise<void>;
383
+ /**
384
+ * Serialize structural changes per collection and verify the collection
385
+ * exists. `FOR UPDATE` — no `FOR UPDATE OF` qualifier needed since this
386
+ * query selects from a single table (confirmed against a live MySQL 9.7.1
387
+ * server; MySQL 8.0.1+ supports `FOR UPDATE OF tbl_name` for the
388
+ * multi-table case used below in `lockDocumentCollection`).
389
+ */
390
+ private lockTreeCollection;
391
+ /**
392
+ * Resolve a document's collection while locking only the collection row.
393
+ * `FOR UPDATE OF c` — confirmed live against a MySQL 9.7.1 server (MySQL
394
+ * 8.0.1+ supports the per-table lock qualifier; `::uuid` casts dropped).
395
+ */
396
+ private lockDocumentCollection;
397
+ /** Read one ordered sibling group on the already collection-locked transaction. */
398
+ private treeGroup;
399
+ /** Read a node's placement from the already collection-locked transaction. */
400
+ private treePlacement;
401
+ /** Read a raw node-and-descendants set while the collection tree lock is held. */
402
+ private treeSubtreeIds;
403
+ /**
404
+ * placeTreeNode — see {@link IDocumentCommands.placeTreeNode}.
405
+ *
406
+ * Single transaction: same-collection guard → cycle guard → resolve the
407
+ * target sibling group's neighbour keys → mint a fractional key → upsert the
408
+ * edge row. Unversioned; touches only `byline_document_relationships`.
409
+ *
410
+ * The edge upsert uses a plain `.onDuplicateKeyUpdate()` — unlike
411
+ * `writeDocumentPath`, `byline_document_relationships` carries exactly one
412
+ * unique constraint (`uq_document_relationships_child`, on
413
+ * `child_document_id`), so MySQL's lack of per-constraint targeting is not
414
+ * an issue here: any duplicate-key collision on this table can only be that
415
+ * one index, so there is no ambiguity to guard against.
416
+ */
417
+ placeTreeNode(params: {
418
+ collectionId: string;
419
+ documentId: string;
420
+ parentDocumentId: string | null;
421
+ beforeDocumentId?: string | null;
422
+ afterDocumentId?: string | null;
423
+ ifUnplaced?: boolean;
424
+ }): Promise<TreeMutationResult>;
425
+ /**
426
+ * removeFromTree — see {@link IDocumentCommands.removeFromTree}.
427
+ * Single-row delete; no-op when the node is already unplaced.
428
+ */
429
+ removeFromTree(params: {
430
+ collectionId: string;
431
+ documentId: string;
432
+ includeSubtree?: boolean;
433
+ }): Promise<TreeMutationResult>;
434
+ /** Promote direct children to roots and remove the parent edge under one lock. */
435
+ promoteChildrenAndRemoveFromTree(params: {
436
+ collectionId: string;
437
+ documentId: string;
438
+ }): Promise<TreeDeleteMutationResult>;
439
+ }
440
+ export declare function createCommandBuilders(dbManager: DBManager, defaultContentLocale: string): {
441
+ collections: CollectionCommands;
442
+ documents: DocumentCommands;
443
+ };