@proteinjs/db 1.34.4 → 1.35.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/CHANGELOG.md +22 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/Db.d.ts +10 -0
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +42 -2
- package/dist/src/Db.js.map +1 -1
- package/dist/src/MigrationRunner.d.ts +26 -0
- package/dist/src/MigrationRunner.d.ts.map +1 -1
- package/dist/src/MigrationRunner.js +82 -0
- package/dist/src/MigrationRunner.js.map +1 -1
- package/dist/src/source/SourceRecordLoader.d.ts +8 -1
- package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
- package/dist/src/source/SourceRecordLoader.js +10 -1
- package/dist/src/source/SourceRecordLoader.js.map +1 -1
- package/dist/src/tables/MigrationTable.d.ts +24 -0
- package/dist/src/tables/MigrationTable.d.ts.map +1 -1
- package/dist/src/tables/MigrationTable.js +1 -0
- package/dist/src/tables/MigrationTable.js.map +1 -1
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +1 -1
- package/package.json +2 -2
- package/src/Db.ts +19 -0
- package/src/MigrationRunner.ts +62 -0
- package/src/source/SourceRecordLoader.ts +10 -1
- package/src/tables/MigrationTable.ts +24 -0
- package/tsconfig.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "ab6c2ca7ce824c1c1f22fdc7853eb93cb5334172"
|
|
70
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -145,8 +145,27 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
145
145
|
return defaultTransactionContextFactory;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Boot-time reconciliation, in THE load-bearing order:
|
|
150
|
+
* 1. ensure the database exists;
|
|
151
|
+
* 2. pre-schema-sync migrations ({@link MigrationRunner.runPreSchemaSyncMigrations}) — data
|
|
152
|
+
* repairs new schema invariants depend on (e.g. deduplicating rows before a unique-index
|
|
153
|
+
* backfill, which fails loudly over violating data); an immediate no-op unless a migration
|
|
154
|
+
* declares {@link Migration.preSchemaSync};
|
|
155
|
+
* 3. schema sync — create/alter every registered table (the DDL);
|
|
156
|
+
* 4. source-record sync.
|
|
157
|
+
*/
|
|
148
158
|
async init(): Promise<void> {
|
|
149
159
|
await this.dbDriver.createDbIfNotExists();
|
|
160
|
+
// Resolved lazily, on purpose: Db sits at the BOTTOM of this package's module graph
|
|
161
|
+
// (Table -> Record -> Columns -> ReferenceArray -> Db), while MigrationRunner sits above it
|
|
162
|
+
// (it loads MigrationTable, whose class `extends Table`). A top-level import here closes
|
|
163
|
+
// that loop downward — an entry module that starts anywhere in the lower chain would then
|
|
164
|
+
// evaluate MigrationTable's `extends Table` while Table.ts is still mid-load (ES5
|
|
165
|
+
// downlevel: "Class extends value undefined is not a constructor"). A call-time import
|
|
166
|
+
// keeps the edge runtime-only, when the whole graph is complete.
|
|
167
|
+
const { MigrationRunner } = await import('./MigrationRunner');
|
|
168
|
+
await new MigrationRunner().runPreSchemaSyncMigrations(this.dbDriver.getTableManager());
|
|
150
169
|
await this.dbDriver.getTableManager().loadTables();
|
|
151
170
|
await new SourceRecordLoader().load();
|
|
152
171
|
}
|
package/src/MigrationRunner.ts
CHANGED
|
@@ -2,9 +2,12 @@ import { Moment, moment } from './opt/moment';
|
|
|
2
2
|
import { Db, getDb, getDbAsSystem } from './Db';
|
|
3
3
|
import { Table } from './Table';
|
|
4
4
|
import { SourceRecordRepo } from './source/SourceRecordRepo';
|
|
5
|
+
import { SourceRecordLoader } from './source/SourceRecordLoader';
|
|
6
|
+
import { getSourceRecordLoaders } from './source/SourceRecord';
|
|
5
7
|
import { MigrationRunnerService, getMigrationRunnerService } from './services/MigrationRunnerService';
|
|
6
8
|
import { Migration, MigrationTable } from './tables/MigrationTable';
|
|
7
9
|
import { QueryBuilderFactory } from './QueryBuilderFactory';
|
|
10
|
+
import { TableManager } from './schema/TableManager';
|
|
8
11
|
import { Service } from '@proteinjs/service';
|
|
9
12
|
import { Logger } from '@proteinjs/logger';
|
|
10
13
|
|
|
@@ -92,6 +95,65 @@ export class MigrationRunner implements MigrationRunnerService {
|
|
|
92
95
|
return migration;
|
|
93
96
|
}
|
|
94
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Pre-schema-sync phase — called by {@link Db.init} between database creation and schema sync:
|
|
100
|
+
* runs every source-declared migration flagged {@link Migration.preSchemaSync}, so data repairs
|
|
101
|
+
* a new schema invariant depends on (e.g. deduplicating rows a new unique index would reject —
|
|
102
|
+
* TableManager's unique-index preflight fails loudly over violating data) land BEFORE the DDL
|
|
103
|
+
* that needs them. The ordinary series ({@link runPendingMigrations}) runs after init — too
|
|
104
|
+
* late for this class by construction.
|
|
105
|
+
*
|
|
106
|
+
* ZERO-COST WHEN UNUSED: no flagged migrations -> immediate return (no ledger IO, no DDL) —
|
|
107
|
+
* the common boot pays nothing.
|
|
108
|
+
*
|
|
109
|
+
* Bootstrap: the phase runs before schema sync, so it fronts the two pieces it needs itself —
|
|
110
|
+
* the migration TABLE's own schema (framework-owned, never data-hazardous) and the migration
|
|
111
|
+
* table's source-record sync (ledger rows + SourceRecordRepo registration, which
|
|
112
|
+
* {@link ensureMigrationRun}'s resolveMigration reads). The full loadTables/source sync that
|
|
113
|
+
* follows re-reconciles both idempotently.
|
|
114
|
+
*
|
|
115
|
+
* Runs through {@link ensureMigrationRun} — full ledger semantics: skip on 'success', retry on
|
|
116
|
+
* 'failure'/'running' (a crashed earlier boot). Multiple flagged migrations run in series
|
|
117
|
+
* ordered by id (deterministic; their ledger rows may not exist yet, so created-order cannot
|
|
118
|
+
* apply). A non-success outcome THROWS: Db.init must fail as loudly as the schema sync it
|
|
119
|
+
* protects would have — the boot crash / deploy-Job failure names the migration instead of an
|
|
120
|
+
* opaque index-backfill error, and the recorded failure row retries on the next boot.
|
|
121
|
+
*/
|
|
122
|
+
async runPreSchemaSyncMigrations(tableManager: TableManager): Promise<void> {
|
|
123
|
+
const migrationTable: Table<Migration> = new MigrationTable();
|
|
124
|
+
const flagged = getSourceRecordLoaders<Migration>()
|
|
125
|
+
// db >=1.34.4: declarations are { source, loader } pairs — the loader carries table/record.
|
|
126
|
+
.filter(({ loader }) => loader.table.name === migrationTable.name && (loader.record as Migration).preSchemaSync)
|
|
127
|
+
.map(({ loader }) => loader.record as Migration)
|
|
128
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
129
|
+
if (flagged.length === 0) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const contradictions = flagged.filter((migration) => migration.manual);
|
|
134
|
+
if (contradictions.length > 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Migration(s) declare both preSchemaSync and manual — a contradiction (the pre-schema-sync phase ` +
|
|
137
|
+
`exists to run unattended before DDL): ${contradictions.map((migration) => migration.id).join(', ')}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await tableManager.loadTable(migrationTable);
|
|
142
|
+
await new SourceRecordLoader().load(migrationTable);
|
|
143
|
+
this.logger.info({
|
|
144
|
+
message: `Running ${flagged.length} pre-schema-sync migration${flagged.length === 1 ? '' : 's'} before schema sync`,
|
|
145
|
+
obj: { ids: flagged.map((migration) => migration.id) },
|
|
146
|
+
});
|
|
147
|
+
for (const migration of flagged) {
|
|
148
|
+
const outcome = await this.ensureMigrationRun(migration.id);
|
|
149
|
+
if (outcome.status !== 'success') {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Pre-schema-sync migration (${outcome.id}) failed; schema sync not attempted: ${outcome.failureMessage}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
95
157
|
/**
|
|
96
158
|
* Deploy-path API (plans/POST_RELEASE_QUEUE.md 27f): the deploy pipeline's migration Job calls
|
|
97
159
|
* this AFTER `new Db().init()` (schema sync + source-record sync — every source-declared
|
|
@@ -61,12 +61,21 @@ export class SourceRecordLoader {
|
|
|
61
61
|
* boot carries its authority — clean up explicitly); two builds of one package at the SAME
|
|
62
62
|
* version with differing sets (uncommitted local skew) are last-writer-wins, since versions
|
|
63
63
|
* cannot order them.
|
|
64
|
+
*
|
|
65
|
+
* With no argument, every source-record table is synced (the `Db.init` full pass). Passing
|
|
66
|
+
* `onlyTable` scopes the sync to that one table — the pre-schema-sync migration phase uses
|
|
67
|
+
* this to land the migration ledger's rows before the full schema sync has run (see
|
|
68
|
+
* {@link MigrationRunner.runPreSchemaSyncMigrations}); the later full pass re-reconciles the
|
|
69
|
+
* same rows idempotently under the same ownership model.
|
|
64
70
|
*/
|
|
65
|
-
async load(): Promise<SourceRecordLoadSummary> {
|
|
71
|
+
async load(onlyTable?: Table<any>): Promise<SourceRecordLoadSummary> {
|
|
66
72
|
const { tables, buildSources } = await this.getDeclarations();
|
|
67
73
|
const db = getDbAsSystem();
|
|
68
74
|
const summary: SourceRecordLoadSummary = {};
|
|
69
75
|
for (const tableName in tables) {
|
|
76
|
+
if (onlyTable && tableName !== onlyTable.name) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
70
79
|
const { table, records } = tables[tableName];
|
|
71
80
|
// 'id' unless the table declares a natural key (validated: unique-indexed, present and
|
|
72
81
|
// unambiguous across declarations).
|
|
@@ -14,6 +14,29 @@ export interface Migration extends SourceRecord {
|
|
|
14
14
|
* schema, not in deploy-pipeline prose.
|
|
15
15
|
*/
|
|
16
16
|
manual?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Runs during `Db.init()` BEFORE schema sync (the pre-schema-sync phase —
|
|
19
|
+
* {@link MigrationRunner.runPreSchemaSyncMigrations}), instead of after init like the
|
|
20
|
+
* deploy-gated series. This is the class for data repairs a NEW SCHEMA INVARIANT depends on —
|
|
21
|
+
* e.g. deduplicating rows before a unique index lands: schema sync's unique-index preflight
|
|
22
|
+
* fails loudly over violating data ({@link DuplicateValuesForUniqueIndexError}), and the
|
|
23
|
+
* ordinary series (deploy Job, after init) is too late by construction.
|
|
24
|
+
*
|
|
25
|
+
* Contract for this class (stricter than the ordinary automated class):
|
|
26
|
+
* - IDEMPOTENT and tolerant of CONCURRENT duplicate runs: every booting replica and the deploy
|
|
27
|
+
* Job each run init — two actors can observe the row un-applied and both run the body.
|
|
28
|
+
* - TABLE-EXISTENCE tolerant: on a fresh database the body runs before ANY schema sync, so its
|
|
29
|
+
* target tables may not exist yet (check and no-op — a fresh database has nothing to repair).
|
|
30
|
+
* - Never `manual` (a contradiction — the phase exists to run unattended before DDL; declaring
|
|
31
|
+
* both fails init loudly).
|
|
32
|
+
* A failure fails `Db.init()` loudly (recorded on the ledger row, retried next boot) — exactly
|
|
33
|
+
* the failure the schema sync would otherwise hit, but named and retryable.
|
|
34
|
+
*
|
|
35
|
+
* The phase reads the SOURCE declaration (like {@link runPendingMigrations} reads
|
|
36
|
+
* `source.manual`); the column mirrors it into the ledger so the Migrations page shows why a
|
|
37
|
+
* row ran at boot.
|
|
38
|
+
*/
|
|
39
|
+
preSchemaSync?: boolean;
|
|
17
40
|
/**
|
|
18
41
|
* Ledger-owned state (like `status` — never declared on a source record): stamped `true` by the
|
|
19
42
|
* deploy-gated series ({@link MigrationRunner.runPendingMigrations}) when the row's source class
|
|
@@ -47,6 +70,7 @@ export class MigrationTable extends Table<Migration> {
|
|
|
47
70
|
public columns = withSourceRecordColumns<Migration>({
|
|
48
71
|
description: new StringColumn('description', {}, 4000),
|
|
49
72
|
manual: new BooleanColumn('manual'),
|
|
73
|
+
preSchemaSync: new BooleanColumn('pre_schema_sync'),
|
|
50
74
|
retired: new BooleanColumn('retired'),
|
|
51
75
|
status: new StringColumn('status', { defaultValue: async () => 'proposed' }),
|
|
52
76
|
failureMessage: new StringColumn('failure_message', {}, 4000),
|
package/tsconfig.json
CHANGED