@byline/db-postgres 3.15.2 → 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.
- package/dist/database/schema/auth.js +1 -1
- package/dist/database/schema/index.js +10 -10
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/lib/db-manager.d.ts +1 -1
- package/dist/lib/db-manager.js +2 -2
- package/dist/modules/audit/audit-commands.d.ts +1 -1
- package/dist/modules/audit/audit-queries.d.ts +1 -1
- package/dist/modules/audit/tests/activity-feed.test.js +1 -1
- package/dist/modules/audit/tests/audit-log.test.js +1 -1
- package/dist/modules/storage/storage-commands.d.ts +10 -10
- package/dist/modules/storage/storage-commands.js +12 -12
- package/dist/modules/storage/storage-queries.d.ts +22 -6
- package/dist/modules/storage/storage-queries.js +48 -9
- package/dist/modules/storage/tests/storage-transactions.test.js +1 -1
- package/package.json +4 -5
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
package/dist/lib/db-manager.d.ts
CHANGED
|
@@ -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/
|
|
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
|
*/
|
package/dist/lib/db-manager.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
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/
|
|
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/
|
|
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
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
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
|
|
1468
|
-
|
|
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 ${
|
|
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/
|
|
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.
|
|
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
|
-
"
|
|
60
|
-
"@byline/auth": "3.
|
|
61
|
-
"@byline/core": "3.
|
|
62
|
-
"@byline/admin": "3.15.2"
|
|
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",
|