@byline/db-postgres 3.15.1 → 3.16.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.
@@ -20,7 +20,7 @@
20
20
  * to 1; bumped by write paths when needed).
21
21
  * - snake_case column names matching the rest of the Byline schema.
22
22
  *
23
- * See docs/AUTHN-AUTHZ.md for the full data model and present-state
23
+ * See docs/06-auth-and-security/01-authn-authz.md for the full data model and present-state
24
24
  * reference.
25
25
  */
26
26
  import { relations } from 'drizzle-orm';
@@ -21,7 +21,7 @@ import { createdAt, timestamps } from './common.js';
21
21
  *
22
22
  * Captured here (rather than only in a hand-written migration) so future
23
23
  * regenerations from this schema reproduce the COLLATE clause cleanly.
24
- * See migration `0003_order_key_byte_collation.sql` and `docs/COLLECTIONS.md` (Orderable collections).
24
+ * See migration `0003_order_key_byte_collation.sql` and `docs/04-collections/index.md` (Orderable collections).
25
25
  */
26
26
  const varcharByteSorted = customType({
27
27
  dataType(config) {
@@ -61,7 +61,7 @@ export const documents = pgTable('byline_documents', {
61
61
  //
62
62
  // Uses `varcharByteSorted` (COLLATE "C") so DB ordering matches JS string
63
63
  // comparison — the fractional-index algorithm requires this. See
64
- // `varcharByteSorted` above and docs/COLLECTIONS.md (Orderable collections).
64
+ // `varcharByteSorted` above and docs/04-collections/index.md (Orderable collections).
65
65
  order_key: varcharByteSorted('order_key', { length: 128 }),
66
66
  // The content locale this document was first authored in — its per-document
67
67
  // data anchor. Set once at creation (= the global default content locale at
@@ -119,7 +119,7 @@ export const documentVersions = pgTable('byline_document_versions', {
119
119
  // installation's default content locale; per-locale UI is a future phase
120
120
  // that adds rows for additional locales without reshaping the schema.
121
121
  // History is intentionally not preserved here — path rows are updated in
122
- // place. See `docs/DOCUMENT-PATHS.md` § "Path uniqueness".
122
+ // place. See `docs/04-collections/04-document-paths.md` § "Path uniqueness".
123
123
  export const documentPaths = pgTable('byline_document_paths', {
124
124
  document_id: uuid('document_id')
125
125
  .notNull()
@@ -148,7 +148,7 @@ export const documentPaths = pgTable('byline_document_paths', {
148
148
  // carries forward across edits and survives restore. Surfaced on reads as
149
149
  // `availableLocales`; the public advertised set is the intersection with the
150
150
  // ledger's `_availableVersionLocales`. Replaced wholesale on write (the lifecycle
151
- // deletes then re-inserts the set), never appended. See docs/I18N.md.
151
+ // deletes then re-inserts the set), never appended. See docs/07-internationalization/index.md.
152
152
  export const documentAvailableLocales = pgTable('byline_document_available_locales', {
153
153
  document_id: uuid('document_id')
154
154
  .notNull()
@@ -172,14 +172,14 @@ export const documentAvailableLocales = pgTable('byline_document_available_local
172
172
  // identically in any locale). Computed status-blind at write time and frozen
173
173
  // on the immutable version, so restore / point-in-time reads stay consistent.
174
174
  // Drives `localeFallback: 'strict'` reads via an indexed EXISTS gate without
175
- // scanning the store_* tables. See docs/I18N.md.
175
+ // scanning the store_* tables. See docs/07-internationalization/index.md.
176
176
  export const documentVersionLocales = pgTable('byline_document_version_locales', {
177
177
  document_version_id: uuid('document_version_id')
178
178
  .notNull()
179
179
  .references(() => documentVersions.id, { onDelete: 'cascade' }),
180
180
  locale: varchar('locale', { length: 10 }).notNull(),
181
181
  }, (table) => [primaryKey({ columns: [table.document_version_id, table.locale] })]);
182
- // Document Tree — single-parent ordered adjacency. See docs/DOCUMENT-TREE.md.
182
+ // Document Tree — single-parent ordered adjacency. See docs/04-collections/03-document-trees.md.
183
183
  //
184
184
  // A document-grain, unversioned hierarchy primitive for `tree: true`
185
185
  // collections (self-referential, single collection). Rows reference the logical
@@ -217,7 +217,7 @@ export const documentRelationships = pgTable('byline_document_relationships', {
217
217
  // `path` is intentionally NOT projected here. Path resolution is locale-
218
218
  // aware and lives in the storage adapter's read functions, which join
219
219
  // `byline_document_paths` with the requested locale + default-locale
220
- // fallback. See docs/DOCUMENT-PATHS.md.
220
+ // fallback. See docs/04-collections/04-document-paths.md.
221
221
  export const currentDocumentsView = pgView('byline_current_documents').as((qb) => {
222
222
  const sq = qb.$with('sq').as(qb
223
223
  .select({
@@ -260,7 +260,7 @@ export const currentDocumentsView = pgView('byline_current_documents').as((qb) =
260
260
  // read paths (`buildLocaleChain` / `pathProjection` / field-fallback)
261
261
  // re-base onto the per-document source rather than the mutable global
262
262
  // default — a primary-key join, already present for `order_key`.
263
- // See docs/I18N.md.
263
+ // See docs/07-internationalization/index.md.
264
264
  source_locale: documents.source_locale,
265
265
  })
266
266
  .from(sq)
@@ -556,7 +556,7 @@ export const documentRelationshipsRelations = relations(documentRelationships, (
556
556
  }),
557
557
  }));
558
558
  // Document-tree edges on the logical document. The tree read path itself is a
559
- // recursive CTE (see docs/DOCUMENT-TREE.md), not the Drizzle query builder —
559
+ // recursive CTE (see docs/04-collections/03-document-trees.md), not the Drizzle query builder —
560
560
  // these relations exist for completeness / ad-hoc joins.
561
561
  export const documentTreeRelations = relations(documents, ({ many }) => ({
562
562
  // The membership edge where this document is the child — its placement in the
@@ -660,7 +660,7 @@ export const jsonStoreRelations = relations(jsonStore, ({ one }) => ({
660
660
  // fit without a second migration. Append-only and deliberately **FK-free**: an
661
661
  // audit row is an immutable historical fact that must outlive the document,
662
662
  // collection, or actor it references — a `document.deleted` row cannot be
663
- // allowed to cascade-delete itself. See docs/AUDIT.md — Workstream 2.
663
+ // allowed to cascade-delete itself. See docs/06-auth-and-security/02-auditability.md — Workstream 2.
664
664
  export const auditLog = pgTable('byline_audit_log', {
665
665
  id: uuid('id').primaryKey(), // UUIDv7 — time-ordered, so id ordering ≈ time ordering
666
666
  document_id: uuid('document_id'), // NULL for admin-realm (non-document) events; no FK
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface PgAdapter extends IDbAdapter {
31
31
  * existed, so `localeFallback: 'strict'` reads can see pre-existing
32
32
  * documents. Idempotent; uses the configured default content locale. Kept
33
33
  * off the core `IDbAdapter` contract (no service depends on it) — see
34
- * docs/I18N.md.
34
+ * docs/07-internationalization/index.md.
35
35
  */
36
36
  backfillVersionLocales(): Promise<{
37
37
  rowsInserted: number;
@@ -42,7 +42,7 @@ export interface PgAdapter extends IDbAdapter {
42
42
  * default content locale (the anchor they were implicitly authored against).
43
43
  * Idempotent; run automatically at boot by `initBylineCore` (also exposed on
44
44
  * the core `IDbAdapter` contract as an optional method) — see
45
- * docs/I18N.md.
45
+ * docs/07-internationalization/index.md.
46
46
  */
47
47
  backfillSourceLocales(): Promise<{
48
48
  rowsUpdated: number;
@@ -53,7 +53,7 @@ export interface PgAdapter extends IDbAdapter {
53
53
  * unless the document is complete in the target. Writes a new immutable
54
54
  * version. `dryRun` reports the would-be outcome without writing. Off the
55
55
  * core `IDbAdapter` contract (maintenance/admin operation) — see
56
- * docs/I18N.md.
56
+ * docs/07-internationalization/index.md.
57
57
  */
58
58
  reAnchorDocument(params: {
59
59
  documentId: string;
@@ -83,7 +83,7 @@ export declare const pgAdapter: ({ connectionString, collections, defaultContent
83
83
  * for documents whose `source_locale` is not yet backfilled. Per-document
84
84
  * reads and writes otherwise re-base onto each document's own `source_locale`
85
85
  * (carried on the current-documents views), so changing this value does not
86
- * re-interpret existing data. See docs/I18N.md.
86
+ * re-interpret existing data. See docs/07-internationalization/index.md.
87
87
  */
88
88
  defaultContentLocale: string;
89
89
  /**
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
22
22
  connectionTimeoutMillis,
23
23
  });
24
24
  const db = drizzle(pool, { schema });
25
- // Request-scoped transaction propagation (docs/TRANSACTIONS.md). The command
25
+ // Request-scoped transaction propagation (docs/03-architecture/03-transactions.md). The command
26
26
  // builders run on the DBManager — each `this.db` access resolves to the
27
27
  // ambient transaction when a `withTransaction` boundary is open, else the
28
28
  // pool. Queries and counters stay on the raw `db` for now (reads don't need
@@ -12,7 +12,7 @@
12
12
  * `withLogContext`). The full design — the service-owned `withTransaction`
13
13
  * boundary, the DB↔DB vs DB↔external distinction, the incremental-adoption
14
14
  * caveat, and the serverless db-contract-seam decisions — lives in
15
- * `docs/TRANSACTIONS.md`. This machinery is deliberately adapter-internal:
15
+ * `docs/03-architecture/03-transactions.md`. This machinery is deliberately adapter-internal:
16
16
  * transactions are driver-specific, so `@byline/core` only declares the
17
17
  * `withTransaction` capability on `IDbAdapter`, never the implementation.
18
18
  */
@@ -12,7 +12,7 @@
12
12
  * `withLogContext`). The full design — the service-owned `withTransaction`
13
13
  * boundary, the DB↔DB vs DB↔external distinction, the incremental-adoption
14
14
  * caveat, and the serverless db-contract-seam decisions — lives in
15
- * `docs/TRANSACTIONS.md`. This machinery is deliberately adapter-internal:
15
+ * `docs/03-architecture/03-transactions.md`. This machinery is deliberately adapter-internal:
16
16
  * transactions are driver-specific, so `@byline/core` only declares the
17
17
  * `withTransaction` capability on `IDbAdapter`, never the implementation.
18
18
  */
@@ -37,7 +37,7 @@ export class TXManagerImpl {
37
37
  // `tx` is Drizzle's PgTransaction; it carries the full query-builder
38
38
  // surface every command uses. The cast bridges the one structural gap
39
39
  // to NodePgDatabase — the transaction lacks `$client`, which no command
40
- // touches. See docs/TRANSACTIONS.md.
40
+ // touches. See docs/03-architecture/03-transactions.md.
41
41
  transactionALS.run(tx, fn));
42
42
  }
43
43
  }
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Append-only audit-log writes (docs/AUDIT.md — Workstream 2). A deliberately
9
+ * Append-only audit-log writes (docs/06-auth-and-security/02-auditability.md — Workstream 2). A deliberately
10
10
  * dumb command: it inserts one row and knows nothing about *which* changes
11
11
  * warrant an audit entry — that policy lives in the lifecycle services, which
12
12
  * wrap the mutation + this append in `withTransaction` so they commit
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Audit-log reads (docs/AUDIT.md — Workstreams 3 & 4). Reads run on the pool
9
+ * Audit-log reads (docs/06-auth-and-security/02-auditability.md — Workstreams 3 & 4). Reads run on the pool
10
10
  * directly — they never need to join an audit write's transaction — so this
11
11
  * takes the raw connection rather than the `DBManager`. Access scoping is the
12
12
  * caller's responsibility (the document's own read gate); these queries do no
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Integration tests for the system-wide activity feed (docs/AUDIT.md — W4):
9
+ * Integration tests for the system-wide activity feed (docs/06-auth-and-security/02-auditability.md — W4):
10
10
  * `audit.findAuditLog`, the read-time UNION of the version stream (content
11
11
  * saves → `document.created` / `document.updated`) and the audit log
12
12
  * (everything else). Exercises both sources, the cross-source `occurred_at`
@@ -6,7 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
  /**
9
- * Integration tests for the audit-log storage layer (docs/AUDIT.md — W2):
9
+ * Integration tests for the audit-log storage layer (docs/06-auth-and-security/02-auditability.md — W2):
10
10
  * `audit.append` (write) + `audit.getDocumentAuditLog` (read), and the
11
11
  * load-bearing property that an `append` issued inside `withTransaction`
12
12
  * commits or rolls back **with** the transaction it is part of.
@@ -40,7 +40,7 @@ export declare class CollectionCommands implements ICollectionCommands {
40
40
  * The executor for this call — the ambient transaction when a
41
41
  * `withTransaction` boundary is open, otherwise the pool. Resolved per
42
42
  * access so every `this.db.*` below transparently joins an enclosing
43
- * transaction with no call-site change. See docs/TRANSACTIONS.md.
43
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
44
44
  */
45
45
  private get db();
46
46
  create(path: string, config: CollectionDefinition, opts?: {
@@ -85,7 +85,7 @@ export declare class DocumentCommands implements IDocumentCommands {
85
85
  * The executor for this call — the ambient transaction when a
86
86
  * `withTransaction` boundary is open, otherwise the pool. Resolved per
87
87
  * access so every `this.db.*` below transparently joins an enclosing
88
- * transaction with no call-site change. See docs/TRANSACTIONS.md.
88
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
89
89
  */
90
90
  private get db();
91
91
  /**
@@ -116,7 +116,7 @@ export declare class DocumentCommands implements IDocumentCommands {
116
116
  * `undefined` leaves the existing set untouched (sticky across versions,
117
117
  * like `path`); an empty array clears it (advertise nothing). The locale
118
118
  * values are the advertised content locales themselves, not the default
119
- * locale. See docs/I18N.md.
119
+ * locale. See docs/07-internationalization/index.md.
120
120
  */
121
121
  availableLocales?: string[];
122
122
  locale?: string;
@@ -163,7 +163,7 @@ export declare class DocumentCommands implements IDocumentCommands {
163
163
  * empty array clears the set (advertise nothing). Shared by
164
164
  * `createDocumentVersion` (step 2b, create write path) and the standalone
165
165
  * `setDocumentAvailableLocales` command (the non-versioned admin
166
- * available-locales widget write). See docs/I18N.md.
166
+ * available-locales widget write). See docs/07-internationalization/index.md.
167
167
  */
168
168
  private writeDocumentAvailableLocales;
169
169
  /**
@@ -194,7 +194,7 @@ export declare class DocumentCommands implements IDocumentCommands {
194
194
  * **without** minting a new document version or touching workflow status. The
195
195
  * change is immediate and applies across every version of the document; the
196
196
  * public advertised set remains the intersection with the resolved version's
197
- * completeness ledger. See docs/I18N.md.
197
+ * completeness ledger. See docs/07-internationalization/index.md.
198
198
  */
199
199
  setDocumentAvailableLocales(params: {
200
200
  documentId: string;
@@ -211,7 +211,7 @@ export declare class DocumentCommands implements IDocumentCommands {
211
211
  * so callers must have written them first. Shared by the create write path
212
212
  * (step 6) and `reAnchorDocument` (which recomputes against the new source).
213
213
  * Assumes the version has no ledger rows yet (a freshly-inserted version).
214
- * See docs/I18N.md.
214
+ * See docs/07-internationalization/index.md.
215
215
  */
216
216
  private writeVersionLocaleLedger;
217
217
  /**
@@ -275,7 +275,7 @@ export declare class DocumentCommands implements IDocumentCommands {
275
275
  * identities preserved), and computes that version's ledger against the new
276
276
  * source. `dryRun` performs only the eligibility check and reports the
277
277
  * outcome that *would* result, writing nothing. See
278
- * docs/I18N.md.
278
+ * docs/07-internationalization/index.md.
279
279
  */
280
280
  reAnchorDocument(params: {
281
281
  documentId: string;
@@ -294,7 +294,7 @@ export declare class DocumentCommands implements IDocumentCommands {
294
294
  * "client switched the default content locale, move every fully-translated
295
295
  * document onto it" operation; the `skipped-incomplete` results double as the
296
296
  * outstanding-translation backlog. `dryRun` reports what would happen without
297
- * writing. See docs/I18N.md.
297
+ * writing. See docs/07-internationalization/index.md.
298
298
  */
299
299
  reAnchorDocuments(params: {
300
300
  targetLocale: string;
@@ -320,7 +320,7 @@ export declare class DocumentCommands implements IDocumentCommands {
320
320
  * a version's computed locale set never changes. Returns the number of
321
321
  * `(version, locale)` rows inserted.
322
322
  *
323
- * See docs/I18N.md.
323
+ * See docs/07-internationalization/index.md.
324
324
  */
325
325
  backfillVersionLocales(): Promise<{
326
326
  rowsInserted: number;
@@ -339,7 +339,7 @@ export declare class DocumentCommands implements IDocumentCommands {
339
339
  *
340
340
  * Returns the number of document rows stamped.
341
341
  *
342
- * See docs/I18N.md.
342
+ * See docs/07-internationalization/index.md.
343
343
  */
344
344
  backfillSourceLocales(): Promise<{
345
345
  rowsUpdated: number;
@@ -16,7 +16,7 @@ import { getFirstOrThrow } from './storage-utils.js';
16
16
  * Depth backstop for the document-tree recursive walks (cycle guard, ancestor
17
17
  * walk). The write-path cycle guard prevents true cycles, so this only bounds
18
18
  * recursion against pre-existing pathological state — far deeper than any real
19
- * documentation hierarchy. See docs/DOCUMENT-TREE.md.
19
+ * documentation hierarchy. See docs/04-collections/03-document-trees.md.
20
20
  */
21
21
  const TREE_MAX_DEPTH = 10_000;
22
22
  /**
@@ -31,7 +31,7 @@ export class CollectionCommands {
31
31
  * The executor for this call — the ambient transaction when a
32
32
  * `withTransaction` boundary is open, otherwise the pool. Resolved per
33
33
  * access so every `this.db.*` below transparently joins an enclosing
34
- * transaction with no call-site change. See docs/TRANSACTIONS.md.
34
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
35
35
  */
36
36
  get db() {
37
37
  return this.dbManager.get();
@@ -78,7 +78,7 @@ export class DocumentCommands {
78
78
  * The executor for this call — the ambient transaction when a
79
79
  * `withTransaction` boundary is open, otherwise the pool. Resolved per
80
80
  * access so every `this.db.*` below transparently joins an enclosing
81
- * transaction with no call-site change. See docs/TRANSACTIONS.md.
81
+ * transaction with no call-site change. See docs/03-architecture/03-transactions.md.
82
82
  */
83
83
  get db() {
84
84
  return this.dbManager.get();
@@ -103,7 +103,7 @@ export class DocumentCommands {
103
103
  // source locale rather than the mutable global default. NULL (a row not
104
104
  // yet touched by `backfillSourceLocales`) falls back to the configured
105
105
  // default — the value it was implicitly authored against.
106
- // See docs/I18N.md.
106
+ // See docs/07-internationalization/index.md.
107
107
  let sourceLocale;
108
108
  if (documentId == null) {
109
109
  documentId = uuidv7();
@@ -282,7 +282,7 @@ export class DocumentCommands {
282
282
  // accounts for the per-locale carry-forward in step 5 — not just the
283
283
  // freshly-flattened locale. A version with no localized content at all
284
284
  // records a single `'all'` sentinel (it renders identically in any
285
- // locale). Status-blind by design — see docs/I18N.md.
285
+ // locale). Status-blind by design — see docs/07-internationalization/index.md.
286
286
  await this.writeVersionLocaleLedger(tx, documentVersion.id, sourceLocale);
287
287
  return {
288
288
  document: documentVersion,
@@ -329,7 +329,7 @@ export class DocumentCommands {
329
329
  * empty array clears the set (advertise nothing). Shared by
330
330
  * `createDocumentVersion` (step 2b, create write path) and the standalone
331
331
  * `setDocumentAvailableLocales` command (the non-versioned admin
332
- * available-locales widget write). See docs/I18N.md.
332
+ * available-locales widget write). See docs/07-internationalization/index.md.
333
333
  */
334
334
  async writeDocumentAvailableLocales(tx, args) {
335
335
  await tx
@@ -376,7 +376,7 @@ export class DocumentCommands {
376
376
  * **without** minting a new document version or touching workflow status. The
377
377
  * change is immediate and applies across every version of the document; the
378
378
  * public advertised set remains the intersection with the resolved version's
379
- * completeness ledger. See docs/I18N.md.
379
+ * completeness ledger. See docs/07-internationalization/index.md.
380
380
  */
381
381
  async setDocumentAvailableLocales(params) {
382
382
  await this.db.transaction(async (tx) => {
@@ -397,7 +397,7 @@ export class DocumentCommands {
397
397
  * so callers must have written them first. Shared by the create write path
398
398
  * (step 6) and `reAnchorDocument` (which recomputes against the new source).
399
399
  * Assumes the version has no ledger rows yet (a freshly-inserted version).
400
- * See docs/I18N.md.
400
+ * See docs/07-internationalization/index.md.
401
401
  */
402
402
  async writeVersionLocaleLedger(tx, versionId, sourceLocale) {
403
403
  await tx.execute(sql `
@@ -578,7 +578,7 @@ export class DocumentCommands {
578
578
  * identities preserved), and computes that version's ledger against the new
579
579
  * source. `dryRun` performs only the eligibility check and reports the
580
580
  * outcome that *would* result, writing nothing. See
581
- * docs/I18N.md.
581
+ * docs/07-internationalization/index.md.
582
582
  */
583
583
  async reAnchorDocument(params) {
584
584
  const { documentId, targetLocale, dryRun = false, createdBy } = params;
@@ -660,7 +660,7 @@ export class DocumentCommands {
660
660
  * "client switched the default content locale, move every fully-translated
661
661
  * document onto it" operation; the `skipped-incomplete` results double as the
662
662
  * outstanding-translation backlog. `dryRun` reports what would happen without
663
- * writing. See docs/I18N.md.
663
+ * writing. See docs/07-internationalization/index.md.
664
664
  */
665
665
  async reAnchorDocuments(params) {
666
666
  const { targetLocale, collectionId, dryRun = false } = params;
@@ -721,7 +721,7 @@ export class DocumentCommands {
721
721
  * a version's computed locale set never changes. Returns the number of
722
722
  * `(version, locale)` rows inserted.
723
723
  *
724
- * See docs/I18N.md.
724
+ * See docs/07-internationalization/index.md.
725
725
  */
726
726
  async backfillVersionLocales() {
727
727
  const result = await this.db.execute(sql `
@@ -779,7 +779,7 @@ export class DocumentCommands {
779
779
  *
780
780
  * Returns the number of document rows stamped.
781
781
  *
782
- * See docs/I18N.md.
782
+ * See docs/07-internationalization/index.md.
783
783
  */
784
784
  async backfillSourceLocales() {
785
785
  const result = await this.db
@@ -84,7 +84,7 @@ export declare class DocumentQueries implements IDocumentQueries {
84
84
  * document, or any document read after the global default is switched, falls
85
85
  * back to the locale it was actually authored in) — otherwise the configured
86
86
  * global default, which is correct for not-yet-anchored rows and for
87
- * row-less lookups (findByPath). See docs/I18N.md.
87
+ * row-less lookups (findByPath). See docs/07-internationalization/index.md.
88
88
  */
89
89
  private buildLocaleChain;
90
90
  /**
@@ -115,7 +115,7 @@ export declare class DocumentQueries implements IDocumentQueries {
115
115
  * `availableLocales` — the deliberate
116
116
  * counterpart to the version-grain `_availableVersionLocales` ledger fact;
117
117
  * the public advertised set is their intersection. One indexed query per
118
- * call. See docs/I18N.md.
118
+ * call. See docs/07-internationalization/index.md.
119
119
  */
120
120
  private getAdvertisedLocalesByDocument;
121
121
  /**
@@ -635,10 +635,26 @@ export declare class DocumentQueries implements IDocumentQueries {
635
635
  * the outer read is in published mode), then recurses each nested
636
636
  * filter against the target version's own `td.id`.
637
637
  *
638
- * With no nested filters this reduces to "source has any relation row
639
- * at all on this field pointing at a target that resolves in the
640
- * selected view" useful as a base case but more typically the
641
- * nested list carries a predicate.
638
+ * `hasMany` relations store one row per item at an indexed field name
639
+ * (`<field>.0`, `<field>.1`, …), so the field match switches to a prefix
640
+ * `LIKE`; single relations match the exact name.
641
+ *
642
+ * The `quantifier` selects the set semantics over the relation's
643
+ * (resolving) targets:
644
+ *
645
+ * - `'some'` (default): `EXISTS (… AND nested…)` — at least one target
646
+ * matches. With no nested filters this reduces to "has any resolving
647
+ * target on this field".
648
+ * - `'none'`: `NOT` of the `'some'` form — no target matches. With no
649
+ * nested filters: "has no resolving targets at all".
650
+ * - `'every'`: `NOT EXISTS (… AND NOT (nested…))` — no target *fails*
651
+ * the nested predicate. Vacuously true for documents with no
652
+ * resolving targets (Prisma-style). With no nested filters there is
653
+ * nothing to fail: compile to TRUE.
654
+ *
655
+ * Rows whose target doesn't resolve in the selected view (deleted, or
656
+ * unpublished in published mode) drop out of the JOIN in all three
657
+ * forms — the same visibility rule populate applies.
642
658
  */
643
659
  private buildRelationExists;
644
660
  /**
@@ -105,7 +105,7 @@ export class DocumentQueries {
105
105
  * document, or any document read after the global default is switched, falls
106
106
  * back to the locale it was actually authored in) — otherwise the configured
107
107
  * global default, which is correct for not-yet-anchored rows and for
108
- * row-less lookups (findByPath). See docs/I18N.md.
108
+ * row-less lookups (findByPath). See docs/07-internationalization/index.md.
109
109
  */
110
110
  buildLocaleChain(requestedLocale, sourceLocale) {
111
111
  const floor = sourceLocale ?? this.defaultContentLocale;
@@ -177,7 +177,7 @@ export class DocumentQueries {
177
177
  * `availableLocales` — the deliberate
178
178
  * counterpart to the version-grain `_availableVersionLocales` ledger fact;
179
179
  * the public advertised set is their intersection. One indexed query per
180
- * call. See docs/I18N.md.
180
+ * call. See docs/07-internationalization/index.md.
181
181
  */
182
182
  async getAdvertisedLocalesByDocument(documentIds) {
183
183
  const result = new Map();
@@ -1440,10 +1440,26 @@ export class DocumentQueries {
1440
1440
  * the outer read is in published mode), then recurses each nested
1441
1441
  * filter against the target version's own `td.id`.
1442
1442
  *
1443
- * With no nested filters this reduces to "source has any relation row
1444
- * at all on this field pointing at a target that resolves in the
1445
- * selected view" useful as a base case but more typically the
1446
- * nested list carries a predicate.
1443
+ * `hasMany` relations store one row per item at an indexed field name
1444
+ * (`<field>.0`, `<field>.1`, …), so the field match switches to a prefix
1445
+ * `LIKE`; single relations match the exact name.
1446
+ *
1447
+ * The `quantifier` selects the set semantics over the relation's
1448
+ * (resolving) targets:
1449
+ *
1450
+ * - `'some'` (default): `EXISTS (… AND nested…)` — at least one target
1451
+ * matches. With no nested filters this reduces to "has any resolving
1452
+ * target on this field".
1453
+ * - `'none'`: `NOT` of the `'some'` form — no target matches. With no
1454
+ * nested filters: "has no resolving targets at all".
1455
+ * - `'every'`: `NOT EXISTS (… AND NOT (nested…))` — no target *fails*
1456
+ * the nested predicate. Vacuously true for documents with no
1457
+ * resolving targets (Prisma-style). With no nested filters there is
1458
+ * nothing to fail: compile to TRUE.
1459
+ *
1460
+ * Rows whose target doesn't resolve in the selected view (deleted, or
1461
+ * unpublished in published mode) drop out of the JOIN in all three
1462
+ * forms — the same visibility rule populate applies.
1447
1463
  */
1448
1464
  buildRelationExists(filter, locale, outerScope, readMode, depth) {
1449
1465
  const targetView = readMode === 'published'
@@ -1464,17 +1480,40 @@ export class DocumentQueries {
1464
1480
  path: this.pathProjection(sql.raw(`td${depth}.document_id`), locale, sql.raw(`td${depth}.source_locale`)),
1465
1481
  };
1466
1482
  const nestedConditions = filter.nested.map((nested) => this.buildFilterExists(nested, locale, innerScope, readMode, depth + 1));
1467
- const nestedAnd = nestedConditions.length > 0 ? sql ` AND ${sql.join(nestedConditions, sql ` AND `)}` : sql ``;
1468
- return sql `EXISTS (
1483
+ const quantifier = filter.quantifier ?? 'some';
1484
+ // `every` with nothing to fail is vacuously true for every document —
1485
+ // short-circuit rather than emitting `NOT (TRUE)` noise.
1486
+ if (quantifier === 'every' && nestedConditions.length === 0) {
1487
+ return sql `TRUE`;
1488
+ }
1489
+ // 'some'/'none' assert the nested conjunction on a matching row;
1490
+ // 'every' asserts the *negated* conjunction (a failing row) and wraps
1491
+ // the whole scan in NOT.
1492
+ const nestedAnd = nestedConditions.length === 0
1493
+ ? sql ``
1494
+ : quantifier === 'every'
1495
+ ? sql ` AND NOT (${sql.join(nestedConditions, sql ` AND `)})`
1496
+ : sql ` AND ${sql.join(nestedConditions, sql ` AND `)}`;
1497
+ // hasMany rows are stored at indexed paths (`gallery.0`, `gallery.1`, …)
1498
+ // where `field_name` is the *index segment* ('0', '1', …) and
1499
+ // `parent_path` is the field name — so multi-target rows match on
1500
+ // `parent_path` exactly, single-target rows on `field_name`. (A where
1501
+ // clause only addresses top-level fields, so `parent_path` needs no
1502
+ // prefix handling.)
1503
+ const fieldMatch = filter.hasMany
1504
+ ? sql `${rAlias}.parent_path = ${filter.fieldName}`
1505
+ : sql `${rAlias}.field_name = ${filter.fieldName}`;
1506
+ const existsSql = sql `EXISTS (
1469
1507
  SELECT 1 FROM byline_store_relation ${rAlias}
1470
1508
  JOIN ${targetView} ${tdAlias}
1471
1509
  ON ${tdAlias}.document_id = ${rAlias}.target_document_id
1472
1510
  AND ${tdAlias}.collection_id = ${rAlias}.target_collection_id
1473
1511
  WHERE ${rAlias}.document_version_id = ${outerScope.docVersionId}
1474
- AND ${rAlias}.field_name = ${filter.fieldName}
1512
+ AND ${fieldMatch}
1475
1513
  AND ${rAlias}.target_collection_id = ${filter.targetCollectionId}
1476
1514
  AND (${rAlias}.locale = ${locale} OR ${rAlias}.locale = 'all')${nestedAnd}
1477
1515
  )`;
1516
+ return quantifier === 'some' ? existsSql : sql `NOT ${existsSql}`;
1478
1517
  }
1479
1518
  /**
1480
1519
  * Build a comparison condition for a filter operator.
@@ -7,7 +7,7 @@
7
7
  */
8
8
  /**
9
9
  * Integration tests for request-scoped transaction propagation
10
- * (`withTransaction` / DBManager / TXManager — see docs/TRANSACTIONS.md).
10
+ * (`withTransaction` / DBManager / TXManager — see docs/03-architecture/03-transactions.md).
11
11
  *
12
12
  * Proves the load-bearing guarantee the audit log depends on: multiple
13
13
  * `commands.*` calls wrapped in one `withTransaction` commit or roll back
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@byline/db-postgres",
3
3
  "private": false,
4
4
  "license": "MPL-2.0",
5
- "version": "3.15.1",
5
+ "version": "3.16.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -56,10 +56,9 @@
56
56
  "npm-run-all": "^4.1.5",
57
57
  "pg": "^8.21.0",
58
58
  "uuid": "^14.0.0",
59
- "zod": "^4.4.3",
60
- "@byline/admin": "3.15.1",
61
- "@byline/core": "3.15.1",
62
- "@byline/auth": "3.15.1"
59
+ "@byline/admin": "3.16.0",
60
+ "@byline/auth": "3.16.0",
61
+ "@byline/core": "3.16.0"
63
62
  },
64
63
  "devDependencies": {
65
64
  "@biomejs/biome": "2.4.15",