@proteinjs/db 1.34.3 → 1.35.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/CHANGELOG.md +22 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.d.ts.map +1 -1
- package/dist/generated/test/index.js +3 -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/SourceRecord.d.ts +35 -2
- package/dist/src/source/SourceRecord.d.ts.map +1 -1
- package/dist/src/source/SourceRecord.js +8 -1
- package/dist/src/source/SourceRecord.js.map +1 -1
- package/dist/src/source/SourceRecordLoader.d.ts +105 -7
- package/dist/src/source/SourceRecordLoader.d.ts.map +1 -1
- package/dist/src/source/SourceRecordLoader.js +358 -94
- package/dist/src/source/SourceRecordLoader.js.map +1 -1
- package/dist/src/tables/MigrationTable.d.ts +26 -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/dist/test/reusable/SourceRecordSyncTests.d.ts.map +1 -1
- package/dist/test/reusable/SourceRecordSyncTests.js +803 -27
- package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
- package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts +16 -0
- package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/sourceRecordSyncTestTables.js +23 -1
- package/dist/test/util/tables/sourceRecordSyncTestTables.js.map +1 -1
- package/generated/index.ts +1 -1
- package/generated/test/index.ts +3 -1
- package/package.json +4 -4
- package/src/Db.ts +19 -0
- package/src/MigrationRunner.ts +62 -0
- package/src/source/SourceRecord.ts +42 -4
- package/src/source/SourceRecordLoader.ts +342 -48
- package/src/tables/MigrationTable.ts +24 -0
- package/test/reusable/SourceRecordSyncTests.ts +462 -11
- package/test/util/tables/sourceRecordSyncTestTables.ts +20 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.0",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"test": "jest --passWithNoTests"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@proteinjs/db-query": "^1.7.
|
|
44
|
+
"@proteinjs/db-query": "^1.7.2",
|
|
45
45
|
"@proteinjs/logger": "^1.0.21",
|
|
46
|
-
"@proteinjs/reflection": "^1.
|
|
46
|
+
"@proteinjs/reflection": "^1.2.0",
|
|
47
47
|
"@proteinjs/serializer": "^1.1.10",
|
|
48
48
|
"@proteinjs/server-api": "^3.0.11",
|
|
49
49
|
"@proteinjs/service": "^1.5.1",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "521d5d1ca2f86f0ed2bef0309b0dde30506f132b"
|
|
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
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { Loadable, SourceRepository } from '@proteinjs/reflection';
|
|
2
2
|
import { Columns, Table, getTables } from '../Table';
|
|
3
3
|
import { Record as DbRecord, withRecordColumns } from '../Record';
|
|
4
|
-
import { BooleanColumn } from '../Columns';
|
|
4
|
+
import { BooleanColumn, StringColumn } from '../Columns';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* A source record declaration paired with its owning source: the package that compiled the
|
|
8
|
+
* declaration into this build (from the declaration's reflection qualified name). The source is
|
|
9
|
+
* the ownership grain of the sync — {@link SourceRecordLoader} stamps it on every row it writes
|
|
10
|
+
* and prunes only within it, so servers running different builds against one shared database
|
|
11
|
+
* never delete each other's rows.
|
|
12
|
+
*/
|
|
13
|
+
export type SourceRecordLoaderDeclaration<T extends SourceRecord = SourceRecord> = {
|
|
14
|
+
source: string;
|
|
15
|
+
loader: SourceRecordLoader<T>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getSourceRecordLoaders = <T extends SourceRecord = SourceRecord>(): SourceRecordLoaderDeclaration<T>[] =>
|
|
19
|
+
SourceRepository.get()
|
|
20
|
+
.objectsWithNames<SourceRecordLoader<T>>('@proteinjs/db/SourceRecordLoader')
|
|
21
|
+
.map(({ packageName, object }) => ({ source: packageName, loader: object }));
|
|
8
22
|
|
|
9
23
|
export function getSourceRecordTables() {
|
|
10
24
|
const tables = getTables();
|
|
@@ -31,11 +45,31 @@ export function isSourceRecordTable(table: Table<any>) {
|
|
|
31
45
|
|
|
32
46
|
export interface SourceRecord extends DbRecord {
|
|
33
47
|
isLoadedFromSource?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The package whose declaration owns this row (the declaring loader's package, from its
|
|
50
|
+
* reflection qualified name). Stamped by {@link SourceRecordLoader} on every row it writes;
|
|
51
|
+
* the removed-reconcile prunes only rows whose `sourcePackage` matches a package the running
|
|
52
|
+
* build actually declares from — so a build never deletes rows owned by a package it does not
|
|
53
|
+
* carry (e.g. another server's types on a shared database). Rows written before this column
|
|
54
|
+
* existed carry NULL until their owning package's next boot adopts and stamps them.
|
|
55
|
+
*/
|
|
56
|
+
sourcePackage?: string;
|
|
57
|
+
/**
|
|
58
|
+
* The declaring package's version at the time this row was last stamped (from the package's
|
|
59
|
+
* own package.json, resolved at runtime). This is the ordering WITHIN a package that makes
|
|
60
|
+
* version skew safe on a shared database: a boot never prunes, flags, or rewrites a row stamped
|
|
61
|
+
* by a strictly NEWER version of the same package, so an older build cannot delete the types a
|
|
62
|
+
* newer build added or churn the ones it redefined. NULL (legacy rows, or builds whose package
|
|
63
|
+
* version could not be resolved) carries no ordering and keeps the last-writer-wins semantics.
|
|
64
|
+
*/
|
|
65
|
+
sourcePackageVersion?: string;
|
|
34
66
|
}
|
|
35
67
|
|
|
36
68
|
const getSourceRecordColumns = (hideFromUi = true) => {
|
|
37
69
|
return {
|
|
38
70
|
isLoadedFromSource: new BooleanColumn('is_loaded_from_source', { ui: { hidden: hideFromUi } }),
|
|
71
|
+
sourcePackage: new StringColumn('source_package', { ui: { hidden: hideFromUi } }),
|
|
72
|
+
sourcePackageVersion: new StringColumn('source_package_version', { ui: { hidden: hideFromUi } }),
|
|
39
73
|
};
|
|
40
74
|
};
|
|
41
75
|
|
|
@@ -78,7 +112,11 @@ type OptionalProperties<T> = Pick<
|
|
|
78
112
|
*
|
|
79
113
|
* On Db.init, the record will be inserted if it doesn't exist, and updated if it does exist to mirror what is in source.
|
|
80
114
|
*
|
|
81
|
-
* If the SourceRecordLoader is deleted from source, the record will be deleted from the db on server startup
|
|
115
|
+
* If the SourceRecordLoader is deleted from source, the record will be deleted from the db on server startup (per the
|
|
116
|
+
* table's `onSourceRemoved` policy) — by the next boot of a build that still carries the declaring package at its
|
|
117
|
+
* version or newer. Ownership is per package: a boot only reconciles rows owned by packages it carries, and never
|
|
118
|
+
* rows a newer version of the same package stamped; a package removed from every build leaves its rows behind.
|
|
119
|
+
* This will also be the behavior if id is changed - the record with the old id will be deleted.
|
|
82
120
|
*/
|
|
83
121
|
export interface SourceRecordLoader<T extends SourceRecord> extends Loadable {
|
|
84
122
|
table: Table<T>;
|