@byline/db-postgres 2.7.0 → 3.0.1
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/index.d.ts +190 -0
- package/dist/database/schema/index.js +60 -1
- package/dist/index.d.ts +55 -4
- package/dist/index.js +5 -1
- package/dist/modules/storage/storage-commands.d.ts +141 -3
- package/dist/modules/storage/storage-commands.js +388 -10
- package/dist/modules/storage/storage-queries.d.ts +90 -12
- package/dist/modules/storage/storage-queries.js +285 -48
- package/dist/modules/storage/tests/storage-document-available-locales.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-document-available-locales.test.js +198 -0
- package/dist/modules/storage/tests/storage-locale-fallback.test.d.ts +8 -0
- package/dist/modules/storage/tests/storage-locale-fallback.test.js +595 -0
- package/package.json +4 -4
|
@@ -260,6 +260,25 @@ export declare const documents: import("drizzle-orm/pg-core").PgTableWithColumns
|
|
|
260
260
|
}, {}, {
|
|
261
261
|
pgColumnBuilderBrand: "PgCustomColumnBuilderBrand";
|
|
262
262
|
}>;
|
|
263
|
+
source_locale: import("drizzle-orm/pg-core").PgColumn<{
|
|
264
|
+
name: "source_locale";
|
|
265
|
+
tableName: "byline_documents";
|
|
266
|
+
dataType: "string";
|
|
267
|
+
columnType: "PgVarchar";
|
|
268
|
+
data: string;
|
|
269
|
+
driverParam: string;
|
|
270
|
+
notNull: true;
|
|
271
|
+
hasDefault: false;
|
|
272
|
+
isPrimaryKey: false;
|
|
273
|
+
isAutoincrement: false;
|
|
274
|
+
hasRuntimeDefault: false;
|
|
275
|
+
enumValues: [string, ...string[]];
|
|
276
|
+
baseColumn: never;
|
|
277
|
+
identity: undefined;
|
|
278
|
+
generated: undefined;
|
|
279
|
+
}, {}, {
|
|
280
|
+
length: 10;
|
|
281
|
+
}>;
|
|
263
282
|
};
|
|
264
283
|
dialect: "pg";
|
|
265
284
|
}>;
|
|
@@ -591,6 +610,143 @@ export declare const documentPaths: import("drizzle-orm/pg-core").PgTableWithCol
|
|
|
591
610
|
};
|
|
592
611
|
dialect: "pg";
|
|
593
612
|
}>;
|
|
613
|
+
export declare const documentAvailableLocales: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
614
|
+
name: "byline_document_available_locales";
|
|
615
|
+
schema: undefined;
|
|
616
|
+
columns: {
|
|
617
|
+
created_at: import("drizzle-orm/pg-core").PgColumn<{
|
|
618
|
+
name: string;
|
|
619
|
+
tableName: "byline_document_available_locales";
|
|
620
|
+
dataType: "date";
|
|
621
|
+
columnType: "PgTimestamp";
|
|
622
|
+
data: Date;
|
|
623
|
+
driverParam: string;
|
|
624
|
+
notNull: true;
|
|
625
|
+
hasDefault: true;
|
|
626
|
+
isPrimaryKey: false;
|
|
627
|
+
isAutoincrement: false;
|
|
628
|
+
hasRuntimeDefault: false;
|
|
629
|
+
enumValues: undefined;
|
|
630
|
+
baseColumn: never;
|
|
631
|
+
identity: undefined;
|
|
632
|
+
generated: undefined;
|
|
633
|
+
}, {}, {}>;
|
|
634
|
+
updated_at: import("drizzle-orm/pg-core").PgColumn<{
|
|
635
|
+
name: string;
|
|
636
|
+
tableName: "byline_document_available_locales";
|
|
637
|
+
dataType: "date";
|
|
638
|
+
columnType: "PgTimestamp";
|
|
639
|
+
data: Date;
|
|
640
|
+
driverParam: string;
|
|
641
|
+
notNull: true;
|
|
642
|
+
hasDefault: true;
|
|
643
|
+
isPrimaryKey: false;
|
|
644
|
+
isAutoincrement: false;
|
|
645
|
+
hasRuntimeDefault: false;
|
|
646
|
+
enumValues: undefined;
|
|
647
|
+
baseColumn: never;
|
|
648
|
+
identity: undefined;
|
|
649
|
+
generated: undefined;
|
|
650
|
+
}, {}, {}>;
|
|
651
|
+
document_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
652
|
+
name: "document_id";
|
|
653
|
+
tableName: "byline_document_available_locales";
|
|
654
|
+
dataType: "string";
|
|
655
|
+
columnType: "PgUUID";
|
|
656
|
+
data: string;
|
|
657
|
+
driverParam: string;
|
|
658
|
+
notNull: true;
|
|
659
|
+
hasDefault: false;
|
|
660
|
+
isPrimaryKey: false;
|
|
661
|
+
isAutoincrement: false;
|
|
662
|
+
hasRuntimeDefault: false;
|
|
663
|
+
enumValues: undefined;
|
|
664
|
+
baseColumn: never;
|
|
665
|
+
identity: undefined;
|
|
666
|
+
generated: undefined;
|
|
667
|
+
}, {}, {}>;
|
|
668
|
+
locale: import("drizzle-orm/pg-core").PgColumn<{
|
|
669
|
+
name: "locale";
|
|
670
|
+
tableName: "byline_document_available_locales";
|
|
671
|
+
dataType: "string";
|
|
672
|
+
columnType: "PgVarchar";
|
|
673
|
+
data: string;
|
|
674
|
+
driverParam: string;
|
|
675
|
+
notNull: true;
|
|
676
|
+
hasDefault: false;
|
|
677
|
+
isPrimaryKey: false;
|
|
678
|
+
isAutoincrement: false;
|
|
679
|
+
hasRuntimeDefault: false;
|
|
680
|
+
enumValues: [string, ...string[]];
|
|
681
|
+
baseColumn: never;
|
|
682
|
+
identity: undefined;
|
|
683
|
+
generated: undefined;
|
|
684
|
+
}, {}, {
|
|
685
|
+
length: 10;
|
|
686
|
+
}>;
|
|
687
|
+
collection_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
688
|
+
name: "collection_id";
|
|
689
|
+
tableName: "byline_document_available_locales";
|
|
690
|
+
dataType: "string";
|
|
691
|
+
columnType: "PgUUID";
|
|
692
|
+
data: string;
|
|
693
|
+
driverParam: string;
|
|
694
|
+
notNull: true;
|
|
695
|
+
hasDefault: false;
|
|
696
|
+
isPrimaryKey: false;
|
|
697
|
+
isAutoincrement: false;
|
|
698
|
+
hasRuntimeDefault: false;
|
|
699
|
+
enumValues: undefined;
|
|
700
|
+
baseColumn: never;
|
|
701
|
+
identity: undefined;
|
|
702
|
+
generated: undefined;
|
|
703
|
+
}, {}, {}>;
|
|
704
|
+
};
|
|
705
|
+
dialect: "pg";
|
|
706
|
+
}>;
|
|
707
|
+
export declare const documentVersionLocales: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
708
|
+
name: "byline_document_version_locales";
|
|
709
|
+
schema: undefined;
|
|
710
|
+
columns: {
|
|
711
|
+
document_version_id: import("drizzle-orm/pg-core").PgColumn<{
|
|
712
|
+
name: "document_version_id";
|
|
713
|
+
tableName: "byline_document_version_locales";
|
|
714
|
+
dataType: "string";
|
|
715
|
+
columnType: "PgUUID";
|
|
716
|
+
data: string;
|
|
717
|
+
driverParam: string;
|
|
718
|
+
notNull: true;
|
|
719
|
+
hasDefault: false;
|
|
720
|
+
isPrimaryKey: false;
|
|
721
|
+
isAutoincrement: false;
|
|
722
|
+
hasRuntimeDefault: false;
|
|
723
|
+
enumValues: undefined;
|
|
724
|
+
baseColumn: never;
|
|
725
|
+
identity: undefined;
|
|
726
|
+
generated: undefined;
|
|
727
|
+
}, {}, {}>;
|
|
728
|
+
locale: import("drizzle-orm/pg-core").PgColumn<{
|
|
729
|
+
name: "locale";
|
|
730
|
+
tableName: "byline_document_version_locales";
|
|
731
|
+
dataType: "string";
|
|
732
|
+
columnType: "PgVarchar";
|
|
733
|
+
data: string;
|
|
734
|
+
driverParam: string;
|
|
735
|
+
notNull: true;
|
|
736
|
+
hasDefault: false;
|
|
737
|
+
isPrimaryKey: false;
|
|
738
|
+
isAutoincrement: false;
|
|
739
|
+
hasRuntimeDefault: false;
|
|
740
|
+
enumValues: [string, ...string[]];
|
|
741
|
+
baseColumn: never;
|
|
742
|
+
identity: undefined;
|
|
743
|
+
generated: undefined;
|
|
744
|
+
}, {}, {
|
|
745
|
+
length: 10;
|
|
746
|
+
}>;
|
|
747
|
+
};
|
|
748
|
+
dialect: "pg";
|
|
749
|
+
}>;
|
|
594
750
|
export declare const documentRelationships: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
595
751
|
name: "byline_document_relationships";
|
|
596
752
|
schema: undefined;
|
|
@@ -854,6 +1010,23 @@ export declare const currentDocumentsView: import("drizzle-orm/pg-core").PgViewW
|
|
|
854
1010
|
identity: undefined;
|
|
855
1011
|
generated: undefined;
|
|
856
1012
|
}, {}, {}>;
|
|
1013
|
+
source_locale: import("drizzle-orm/pg-core").PgColumn<{
|
|
1014
|
+
name: "source_locale";
|
|
1015
|
+
tableName: "byline_current_documents";
|
|
1016
|
+
dataType: "string";
|
|
1017
|
+
columnType: "PgVarchar";
|
|
1018
|
+
data: string;
|
|
1019
|
+
driverParam: string;
|
|
1020
|
+
notNull: true;
|
|
1021
|
+
hasDefault: false;
|
|
1022
|
+
isPrimaryKey: false;
|
|
1023
|
+
isAutoincrement: false;
|
|
1024
|
+
hasRuntimeDefault: false;
|
|
1025
|
+
enumValues: [string, ...string[]];
|
|
1026
|
+
baseColumn: never;
|
|
1027
|
+
identity: undefined;
|
|
1028
|
+
generated: undefined;
|
|
1029
|
+
}, {}, {}>;
|
|
857
1030
|
}>;
|
|
858
1031
|
export declare const currentPublishedDocumentsView: import("drizzle-orm/pg-core").PgViewWithSelection<"byline_current_published_documents", false, {
|
|
859
1032
|
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
@@ -1060,6 +1233,23 @@ export declare const currentPublishedDocumentsView: import("drizzle-orm/pg-core"
|
|
|
1060
1233
|
identity: undefined;
|
|
1061
1234
|
generated: undefined;
|
|
1062
1235
|
}, {}, {}>;
|
|
1236
|
+
source_locale: import("drizzle-orm/pg-core").PgColumn<{
|
|
1237
|
+
name: "source_locale";
|
|
1238
|
+
tableName: "byline_current_published_documents";
|
|
1239
|
+
dataType: "string";
|
|
1240
|
+
columnType: "PgVarchar";
|
|
1241
|
+
data: string;
|
|
1242
|
+
driverParam: string;
|
|
1243
|
+
notNull: true;
|
|
1244
|
+
hasDefault: false;
|
|
1245
|
+
isPrimaryKey: false;
|
|
1246
|
+
isAutoincrement: false;
|
|
1247
|
+
hasRuntimeDefault: false;
|
|
1248
|
+
enumValues: [string, ...string[]];
|
|
1249
|
+
baseColumn: never;
|
|
1250
|
+
identity: undefined;
|
|
1251
|
+
generated: undefined;
|
|
1252
|
+
}, {}, {}>;
|
|
1063
1253
|
}>;
|
|
1064
1254
|
export declare const textStore: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
1065
1255
|
name: "byline_store_text";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { eq, relations, sql } from 'drizzle-orm';
|
|
9
|
-
import { bigint, boolean, customType, date, decimal, index, integer, jsonb, pgTable, pgView, real, text, time, timestamp, unique, uuid, varchar, } from 'drizzle-orm/pg-core';
|
|
9
|
+
import { bigint, boolean, customType, date, decimal, index, integer, jsonb, pgTable, pgView, primaryKey, real, text, time, timestamp, unique, uuid, varchar, } from 'drizzle-orm/pg-core';
|
|
10
10
|
import { createdAt, timestamps } from './common.js';
|
|
11
11
|
/**
|
|
12
12
|
* `varchar(...)` with explicit byte-wise (C) collation.
|
|
@@ -63,6 +63,16 @@ export const documents = pgTable('byline_documents', {
|
|
|
63
63
|
// comparison — the fractional-index algorithm requires this. See
|
|
64
64
|
// `varcharByteSorted` above and docs/COLLECTIONS.md (Orderable collections).
|
|
65
65
|
order_key: varcharByteSorted('order_key', { length: 128 }),
|
|
66
|
+
// The content locale this document was first authored in — its per-document
|
|
67
|
+
// data anchor. Set once at creation (= the global default content locale at
|
|
68
|
+
// that moment) and immutable in normal operation; changed only by the
|
|
69
|
+
// deliberate re-anchor operation. Re-bases the fallback floor, the path
|
|
70
|
+
// locale, and the completeness ledger off the mutable global config onto
|
|
71
|
+
// the document's own truth, so switching `i18n.content.defaultLocale` no
|
|
72
|
+
// longer silently re-interprets existing data. Backfilled by
|
|
73
|
+
// `backfillSourceLocales()` (boot-auto via initBylineCore).
|
|
74
|
+
//
|
|
75
|
+
source_locale: varchar('source_locale', { length: 10 }).notNull(),
|
|
66
76
|
...timestamps,
|
|
67
77
|
}, (table) => [
|
|
68
78
|
index('idx_documents_collection').on(table.collection_id),
|
|
@@ -129,6 +139,46 @@ export const documentPaths = pgTable('byline_document_paths', {
|
|
|
129
139
|
// Reverse lookup by document.
|
|
130
140
|
index('idx_document_paths_document_id').on(table.document_id),
|
|
131
141
|
]);
|
|
142
|
+
// Document → advertised content locales. One row per (logical document,
|
|
143
|
+
// advertised locale) — the editorial "advertise these locales" set an editor
|
|
144
|
+
// curates per document. The deliberate counterpart to the derived,
|
|
145
|
+
// version-grained `byline_document_version_locales` ledger: this is intent
|
|
146
|
+
// ("I want these advertised"), the ledger is fact ("this version is complete
|
|
147
|
+
// in these"). Document-grain and sticky across versions — editorial intent
|
|
148
|
+
// carries forward across edits and survives restore. Surfaced on reads as
|
|
149
|
+
// `availableLocales`; the public advertised set is the intersection with the
|
|
150
|
+
// ledger's `_availableVersionLocales`. Replaced wholesale on write (the lifecycle
|
|
151
|
+
// deletes then re-inserts the set), never appended. See docs/I18N.md.
|
|
152
|
+
export const documentAvailableLocales = pgTable('byline_document_available_locales', {
|
|
153
|
+
document_id: uuid('document_id')
|
|
154
|
+
.notNull()
|
|
155
|
+
.references(() => documents.id, { onDelete: 'cascade' }),
|
|
156
|
+
locale: varchar('locale', { length: 10 }).notNull(),
|
|
157
|
+
collection_id: uuid('collection_id')
|
|
158
|
+
.notNull()
|
|
159
|
+
.references(() => collections.id, { onDelete: 'cascade' }),
|
|
160
|
+
...timestamps,
|
|
161
|
+
}, (table) => [
|
|
162
|
+
// One row per (logical document, advertised locale).
|
|
163
|
+
primaryKey({ columns: [table.document_id, table.locale] }),
|
|
164
|
+
// Reverse lookup by document for the read projection.
|
|
165
|
+
index('idx_document_available_locales_document_id').on(table.document_id),
|
|
166
|
+
]);
|
|
167
|
+
// Document version → available content locales. One row per (version, locale)
|
|
168
|
+
// for every locale the version's content is *complete* in — path-coverage
|
|
169
|
+
// against the default content locale: a locale is recorded only when it covers
|
|
170
|
+
// every localized field path the default locale has. A version with no
|
|
171
|
+
// localized content at all gets a single `'all'` sentinel row (it renders
|
|
172
|
+
// identically in any locale). Computed status-blind at write time and frozen
|
|
173
|
+
// on the immutable version, so restore / point-in-time reads stay consistent.
|
|
174
|
+
// Drives `localeFallback: 'strict'` reads via an indexed EXISTS gate without
|
|
175
|
+
// scanning the store_* tables. See docs/I18N.md.
|
|
176
|
+
export const documentVersionLocales = pgTable('byline_document_version_locales', {
|
|
177
|
+
document_version_id: uuid('document_version_id')
|
|
178
|
+
.notNull()
|
|
179
|
+
.references(() => documentVersions.id, { onDelete: 'cascade' }),
|
|
180
|
+
locale: varchar('locale', { length: 10 }).notNull(),
|
|
181
|
+
}, (table) => [primaryKey({ columns: [table.document_version_id, table.locale] })]);
|
|
132
182
|
// Document Relationships (Parent/Child) - Many-to-Many
|
|
133
183
|
export const documentRelationships = pgTable('byline_document_relationships', {
|
|
134
184
|
// Note: These reference the logical `document_id`, not the version `id`.
|
|
@@ -194,6 +244,12 @@ export const currentDocumentsView = pgView('byline_current_documents').as((qb) =
|
|
|
194
244
|
created_by: sq.created_by,
|
|
195
245
|
change_summary: sq.change_summary,
|
|
196
246
|
order_key: documents.order_key,
|
|
247
|
+
// The document's content-locale anchor, projected here so locale-aware
|
|
248
|
+
// read paths (`buildLocaleChain` / `pathProjection` / field-fallback)
|
|
249
|
+
// re-base onto the per-document source rather than the mutable global
|
|
250
|
+
// default — a primary-key join, already present for `order_key`.
|
|
251
|
+
// See docs/I18N.md.
|
|
252
|
+
source_locale: documents.source_locale,
|
|
197
253
|
})
|
|
198
254
|
.from(sq)
|
|
199
255
|
.innerJoin(documents, eq(documents.id, sq.document_id))
|
|
@@ -237,6 +293,9 @@ export const currentPublishedDocumentsView = pgView('byline_current_published_do
|
|
|
237
293
|
created_by: sq.created_by,
|
|
238
294
|
change_summary: sq.change_summary,
|
|
239
295
|
order_key: documents.order_key,
|
|
296
|
+
// See `currentDocumentsView` — the per-document content-locale anchor,
|
|
297
|
+
// carried for locale-aware reads. PK join, already present.
|
|
298
|
+
source_locale: documents.source_locale,
|
|
240
299
|
})
|
|
241
300
|
.from(sq)
|
|
242
301
|
.innerJoin(documents, eq(documents.id, sq.document_id))
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type { CollectionDefinition, IDbAdapter } from '@byline/core';
|
|
|
9
9
|
import { type NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
10
10
|
import pg from 'pg';
|
|
11
11
|
import * as schema from './database/schema/index.js';
|
|
12
|
+
import { type ReAnchorReport, type ReAnchorResult } from './modules/storage/storage-commands.js';
|
|
13
|
+
export type { ReAnchorReport, ReAnchorResult, ReAnchorStatus, } from './modules/storage/storage-commands.js';
|
|
12
14
|
/**
|
|
13
15
|
* Public return type of `pgAdapter`. Extends `IDbAdapter` with concrete
|
|
14
16
|
* Drizzle + pg handles so integrations that need the raw database (the
|
|
@@ -23,16 +25,65 @@ export interface PgAdapter extends IDbAdapter {
|
|
|
23
25
|
drizzle: NodePgDatabase<typeof schema>;
|
|
24
26
|
/** The pg connection pool — exposed for housekeeping and teardown. */
|
|
25
27
|
pool: pg.Pool;
|
|
28
|
+
/**
|
|
29
|
+
* One-time maintenance: populate the version-locale availability ledger
|
|
30
|
+
* (`byline_document_version_locales`) for versions written before it
|
|
31
|
+
* existed, so `localeFallback: 'strict'` reads can see pre-existing
|
|
32
|
+
* documents. Idempotent; uses the configured default content locale. Kept
|
|
33
|
+
* off the core `IDbAdapter` contract (no service depends on it) — see
|
|
34
|
+
* docs/I18N.md.
|
|
35
|
+
*/
|
|
36
|
+
backfillVersionLocales(): Promise<{
|
|
37
|
+
rowsInserted: number;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* One-time maintenance: stamp `byline_documents.source_locale` for documents
|
|
41
|
+
* created before the column existed, setting NULL rows to the configured
|
|
42
|
+
* default content locale (the anchor they were implicitly authored against).
|
|
43
|
+
* Idempotent; run automatically at boot by `initBylineCore` (also exposed on
|
|
44
|
+
* the core `IDbAdapter` contract as an optional method) — see
|
|
45
|
+
* docs/I18N.md.
|
|
46
|
+
*/
|
|
47
|
+
backfillSourceLocales(): Promise<{
|
|
48
|
+
rowsUpdated: number;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Re-anchor a single document's content source locale to `targetLocale`
|
|
52
|
+
* (its fallback floor, path locale, and completeness yardstick). Refuses
|
|
53
|
+
* unless the document is complete in the target. Writes a new immutable
|
|
54
|
+
* version. `dryRun` reports the would-be outcome without writing. Off the
|
|
55
|
+
* core `IDbAdapter` contract (maintenance/admin operation) — see
|
|
56
|
+
* docs/I18N.md.
|
|
57
|
+
*/
|
|
58
|
+
reAnchorDocument(params: {
|
|
59
|
+
documentId: string;
|
|
60
|
+
targetLocale: string;
|
|
61
|
+
dryRun?: boolean;
|
|
62
|
+
}): Promise<ReAnchorResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Bulk re-anchor every fully-translated document (optionally within one
|
|
65
|
+
* collection) onto `targetLocale`, skipping and reporting the rest. Each
|
|
66
|
+
* document is its own transaction — idempotent and resumable. This is the
|
|
67
|
+
* "switched the default content locale, move everything that's ready" command.
|
|
68
|
+
*/
|
|
69
|
+
reAnchorDocuments(params: {
|
|
70
|
+
targetLocale: string;
|
|
71
|
+
collectionId?: string;
|
|
72
|
+
dryRun?: boolean;
|
|
73
|
+
}): Promise<ReAnchorReport>;
|
|
26
74
|
}
|
|
27
75
|
export declare const pgAdapter: ({ connectionString, collections, defaultContentLocale, max, idleTimeoutMillis, connectionTimeoutMillis, }: {
|
|
28
76
|
connectionString: string;
|
|
29
77
|
collections: CollectionDefinition[];
|
|
30
78
|
/**
|
|
31
79
|
* The installation's default content locale, sourced from
|
|
32
|
-
* `ServerConfig.i18n.content.defaultLocale`. Used by the storage layer
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
80
|
+
* `ServerConfig.i18n.content.defaultLocale`. Used by the storage layer as
|
|
81
|
+
* the **fallback** anchor only: new documents are stamped with it as their
|
|
82
|
+
* `source_locale`, and it is the floor for row-less lookups (findByPath) and
|
|
83
|
+
* for documents whose `source_locale` is not yet backfilled. Per-document
|
|
84
|
+
* reads and writes otherwise re-base onto each document's own `source_locale`
|
|
85
|
+
* (carried on the current-documents views), so changing this value does not
|
|
86
|
+
* re-interpret existing data. See docs/I18N.md.
|
|
36
87
|
*/
|
|
37
88
|
defaultContentLocale: string;
|
|
38
89
|
/**
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { drizzle } from 'drizzle-orm/node-postgres';
|
|
|
9
9
|
import pg from 'pg';
|
|
10
10
|
import * as schema from './database/schema/index.js';
|
|
11
11
|
import { createCounterCommands } from './modules/counters/counters-commands.js';
|
|
12
|
-
import { createCommandBuilders } from './modules/storage/storage-commands.js';
|
|
12
|
+
import { createCommandBuilders, } from './modules/storage/storage-commands.js';
|
|
13
13
|
import { createQueryBuilders } from './modules/storage/storage-queries.js';
|
|
14
14
|
export const pgAdapter = ({ connectionString, collections, defaultContentLocale, max = 20, idleTimeoutMillis = 2000, connectionTimeoutMillis = 30000, }) => {
|
|
15
15
|
const pool = new pg.Pool({
|
|
@@ -30,5 +30,9 @@ export const pgAdapter = ({ connectionString, collections, defaultContentLocale,
|
|
|
30
30
|
queries: queryBuilders,
|
|
31
31
|
drizzle: db,
|
|
32
32
|
pool,
|
|
33
|
+
backfillVersionLocales: () => commandBuilders.documents.backfillVersionLocales(),
|
|
34
|
+
backfillSourceLocales: () => commandBuilders.documents.backfillSourceLocales(),
|
|
35
|
+
reAnchorDocument: (params) => commandBuilders.documents.reAnchorDocument(params),
|
|
36
|
+
reAnchorDocuments: (params) => commandBuilders.documents.reAnchorDocuments(params),
|
|
33
37
|
};
|
|
34
38
|
};
|
|
@@ -9,6 +9,29 @@ import type { CollectionDefinition, ICollectionCommands, IDocumentCommands } fro
|
|
|
9
9
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
10
10
|
import type * as schema from '../../database/schema/index.js';
|
|
11
11
|
type DatabaseConnection = NodePgDatabase<typeof schema>;
|
|
12
|
+
/** Outcome of re-anchoring a single document's content source locale. */
|
|
13
|
+
export type ReAnchorStatus = 'reanchored' | 'skipped-incomplete' | 'already-anchored' | 'not-found';
|
|
14
|
+
export interface ReAnchorResult {
|
|
15
|
+
documentId: string;
|
|
16
|
+
status: ReAnchorStatus;
|
|
17
|
+
/** The document's source locale before the operation (when known). */
|
|
18
|
+
fromLocale?: string;
|
|
19
|
+
/** The target source locale. */
|
|
20
|
+
toLocale: string;
|
|
21
|
+
/** The new version id written on a successful re-anchor. */
|
|
22
|
+
newVersionId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ReAnchorReport {
|
|
25
|
+
targetLocale: string;
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
total: number;
|
|
28
|
+
reanchored: number;
|
|
29
|
+
skippedIncomplete: number;
|
|
30
|
+
alreadyAnchored: number;
|
|
31
|
+
notFound: number;
|
|
32
|
+
/** Per-document outcomes, for logging / inspection. */
|
|
33
|
+
results: ReAnchorResult[];
|
|
34
|
+
}
|
|
12
35
|
/**
|
|
13
36
|
* CollectionCommands
|
|
14
37
|
*/
|
|
@@ -70,11 +93,20 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
70
93
|
documentData: any;
|
|
71
94
|
/**
|
|
72
95
|
* Optional. When provided, upserts a row into byline_document_paths
|
|
73
|
-
* keyed by (document_id,
|
|
74
|
-
* lifecycle for non-
|
|
75
|
-
*
|
|
96
|
+
* keyed by (document_id, <document source_locale>). Omitted by the
|
|
97
|
+
* lifecycle for non-source-locale (translation) saves so the existing
|
|
98
|
+
* path row is left untouched.
|
|
76
99
|
*/
|
|
77
100
|
path?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Optional. When provided, replaces the document's advertised-locale set
|
|
103
|
+
* in byline_document_available_locales wholesale (delete-then-insert).
|
|
104
|
+
* `undefined` leaves the existing set untouched (sticky across versions,
|
|
105
|
+
* like `path`); an empty array clears it (advertise nothing). The locale
|
|
106
|
+
* values are the advertised content locales themselves, not the default
|
|
107
|
+
* locale. See docs/I18N.md.
|
|
108
|
+
*/
|
|
109
|
+
availableLocales?: string[];
|
|
78
110
|
locale?: string;
|
|
79
111
|
status?: string;
|
|
80
112
|
createdBy?: string;
|
|
@@ -97,6 +129,112 @@ export declare class DocumentCommands implements IDocumentCommands {
|
|
|
97
129
|
};
|
|
98
130
|
fieldCount: number;
|
|
99
131
|
}>;
|
|
132
|
+
/**
|
|
133
|
+
* writeVersionLocaleLedger
|
|
134
|
+
*
|
|
135
|
+
* Compute and insert a version's `byline_document_version_locales` rows: a
|
|
136
|
+
* locale is recorded when it covers every localized field path the version's
|
|
137
|
+
* `sourceLocale` has (path-coverage), and a version with no localized content
|
|
138
|
+
* records a single `'all'` sentinel. Reads the version's persisted store rows,
|
|
139
|
+
* so callers must have written them first. Shared by the create write path
|
|
140
|
+
* (step 6) and `reAnchorDocument` (which recomputes against the new source).
|
|
141
|
+
* Assumes the version has no ledger rows yet (a freshly-inserted version).
|
|
142
|
+
* See docs/I18N.md.
|
|
143
|
+
*/
|
|
144
|
+
private writeVersionLocaleLedger;
|
|
145
|
+
/**
|
|
146
|
+
* copyAllVersionStoreRows
|
|
147
|
+
*
|
|
148
|
+
* Copy every store row — all eight tables, all locales, including the `meta`
|
|
149
|
+
* identity rows (so block / array-item `_id`s are preserved) — from one
|
|
150
|
+
* document version to another, verbatim. New `id`s are minted; the target
|
|
151
|
+
* `document_version_id` is rebound; timestamps are refreshed. The target
|
|
152
|
+
* version is assumed fresh (no rows), so no conflict handling is needed.
|
|
153
|
+
* Used by `reAnchorDocument` to snapshot the current version into the new
|
|
154
|
+
* re-anchored one without re-flattening (lossless, identity-preserving).
|
|
155
|
+
*/
|
|
156
|
+
private copyAllVersionStoreRows;
|
|
157
|
+
/**
|
|
158
|
+
* reAnchorDocument
|
|
159
|
+
*
|
|
160
|
+
* Change a single document's content source locale to `targetLocale` — its
|
|
161
|
+
* fallback floor, path locale, and completeness yardstick. Refuses unless the
|
|
162
|
+
* document is **complete** in the target (the current version's ledger covers
|
|
163
|
+
* it, or the document is locale-agnostic) — never manufactures a primary
|
|
164
|
+
* language with holes. In one transaction: flips `source_locale`, moves the
|
|
165
|
+
* path row onto the new locale (keeping the slug), writes a **new version**
|
|
166
|
+
* that is a verbatim copy of the current one (immutable version event,
|
|
167
|
+
* identities preserved), and computes that version's ledger against the new
|
|
168
|
+
* source. `dryRun` performs only the eligibility check and reports the
|
|
169
|
+
* outcome that *would* result, writing nothing. See
|
|
170
|
+
* docs/I18N.md.
|
|
171
|
+
*/
|
|
172
|
+
reAnchorDocument(params: {
|
|
173
|
+
documentId: string;
|
|
174
|
+
targetLocale: string;
|
|
175
|
+
dryRun?: boolean;
|
|
176
|
+
}): Promise<ReAnchorResult>;
|
|
177
|
+
/**
|
|
178
|
+
* reAnchorDocuments
|
|
179
|
+
*
|
|
180
|
+
* Bulk re-anchor: walk every (non-deleted) logical document — optionally
|
|
181
|
+
* scoped to one collection — and re-anchor each that is complete in
|
|
182
|
+
* `targetLocale`, skipping (and reporting) the rest. Each document runs in its
|
|
183
|
+
* own transaction via `reAnchorDocument`, so one failure or skip never rolls
|
|
184
|
+
* back the others; the command is idempotent and resumable. This is the
|
|
185
|
+
* "client switched the default content locale, move every fully-translated
|
|
186
|
+
* document onto it" operation; the `skipped-incomplete` results double as the
|
|
187
|
+
* outstanding-translation backlog. `dryRun` reports what would happen without
|
|
188
|
+
* writing. See docs/I18N.md.
|
|
189
|
+
*/
|
|
190
|
+
reAnchorDocuments(params: {
|
|
191
|
+
targetLocale: string;
|
|
192
|
+
collectionId?: string;
|
|
193
|
+
dryRun?: boolean;
|
|
194
|
+
}): Promise<ReAnchorReport>;
|
|
195
|
+
/**
|
|
196
|
+
* backfillVersionLocales
|
|
197
|
+
*
|
|
198
|
+
* One-time maintenance: populate `byline_document_version_locales` for
|
|
199
|
+
* versions written *before* the ledger existed (i.e. before the migration
|
|
200
|
+
* that added it). Going forward `createDocumentVersion` step 6 keeps the
|
|
201
|
+
* ledger current; this fills the historical gap so `localeFallback:
|
|
202
|
+
* 'strict'` reads can see pre-existing documents.
|
|
203
|
+
*
|
|
204
|
+
* Same path-coverage rule as the write path, applied set-wise across every
|
|
205
|
+
* version in one statement. The `canonical` anchor is each document's own
|
|
206
|
+
* `source_locale` (joined via `byline_document_versions` → `byline_documents`),
|
|
207
|
+
* falling back to the adapter's configured default content locale for rows
|
|
208
|
+
* not yet stamped by `backfillSourceLocales` — mirroring the per-document
|
|
209
|
+
* anchor the write path uses, rather than a single global locale. Idempotent
|
|
210
|
+
* — safe to re-run (PK + `ON CONFLICT DO NOTHING`); versions are immutable, so
|
|
211
|
+
* a version's computed locale set never changes. Returns the number of
|
|
212
|
+
* `(version, locale)` rows inserted.
|
|
213
|
+
*
|
|
214
|
+
* See docs/I18N.md.
|
|
215
|
+
*/
|
|
216
|
+
backfillVersionLocales(): Promise<{
|
|
217
|
+
rowsInserted: number;
|
|
218
|
+
}>;
|
|
219
|
+
/**
|
|
220
|
+
* backfillSourceLocales
|
|
221
|
+
*
|
|
222
|
+
* One-time maintenance: stamp `byline_documents.source_locale` for documents
|
|
223
|
+
* created *before* the column existed. Sets every row whose `source_locale`
|
|
224
|
+
* is still NULL to the adapter's configured default content locale — the
|
|
225
|
+
* anchor those documents were implicitly authored against (a static SQL
|
|
226
|
+
* migration cannot know the configured default, mirroring
|
|
227
|
+
* `backfillVersionLocales`). Idempotent: only touches NULL rows, so re-runs
|
|
228
|
+
* and rows already stamped by the write path are left alone. Must run before
|
|
229
|
+
* the follow-up migration that sets the column NOT NULL.
|
|
230
|
+
*
|
|
231
|
+
* Returns the number of document rows stamped.
|
|
232
|
+
*
|
|
233
|
+
* See docs/I18N.md.
|
|
234
|
+
*/
|
|
235
|
+
backfillSourceLocales(): Promise<{
|
|
236
|
+
rowsUpdated: number;
|
|
237
|
+
}>;
|
|
100
238
|
/**
|
|
101
239
|
* setDocumentStatus
|
|
102
240
|
*
|