@syncular/migrations 0.0.6-95 → 0.1.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/README.md +51 -8
- package/dist/checksum.d.ts +7 -0
- package/dist/checksum.d.ts.map +1 -0
- package/dist/checksum.js +29 -0
- package/dist/checksum.js.map +1 -0
- package/dist/define.d.ts +22 -14
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +39 -145
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/naming.d.ts +38 -0
- package/dist/naming.d.ts.map +1 -0
- package/dist/naming.js +60 -0
- package/dist/naming.js.map +1 -0
- package/dist/runner.d.ts +8 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +50 -17
- package/dist/runner.js.map +1 -1
- package/dist/tracking.d.ts.map +1 -1
- package/dist/tracking.js +11 -3
- package/dist/tracking.js.map +1 -1
- package/dist/types.d.ts +15 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +12 -4
- package/src/checksum.ts +54 -0
- package/src/define.ts +50 -171
- package/src/index.ts +3 -1
- package/src/naming.ts +106 -0
- package/src/runner.ts +94 -19
- package/src/tracking.ts +11 -3
- package/src/types.ts +18 -9
package/src/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @syncular/migrations - Versioned migration system
|
|
3
3
|
*
|
|
4
|
-
* Provides migration definition, tracking, and execution.
|
|
4
|
+
* Provides migration definition, tracking, naming, and execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export * from './checksum';
|
|
7
8
|
export * from './define';
|
|
9
|
+
export * from './naming';
|
|
8
10
|
export * from './runner';
|
|
9
11
|
export * from './tracking';
|
|
10
12
|
export * from './types';
|
package/src/naming.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration tracking table naming helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type MigrationTrackingTableNamePart =
|
|
6
|
+
| string
|
|
7
|
+
| number
|
|
8
|
+
| null
|
|
9
|
+
| undefined
|
|
10
|
+
| false;
|
|
11
|
+
|
|
12
|
+
export interface CreateMigrationTrackingTableNameOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Optional namespace prefix placed before the scope segments.
|
|
15
|
+
* Defaults to `sync`.
|
|
16
|
+
*/
|
|
17
|
+
namespace?:
|
|
18
|
+
| MigrationTrackingTableNamePart
|
|
19
|
+
| readonly MigrationTrackingTableNamePart[];
|
|
20
|
+
/**
|
|
21
|
+
* Optional suffix segments appended after `migration_state`.
|
|
22
|
+
*/
|
|
23
|
+
suffix?:
|
|
24
|
+
| MigrationTrackingTableNamePart
|
|
25
|
+
| readonly MigrationTrackingTableNamePart[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_MIGRATION_TRACKING_TABLE = 'sync_migration_state';
|
|
29
|
+
|
|
30
|
+
function normalizeTrackingTableNamePart(
|
|
31
|
+
part: MigrationTrackingTableNamePart
|
|
32
|
+
): string | null {
|
|
33
|
+
if (part === null || part === undefined || part === false) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalized = String(part)
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
41
|
+
.replace(/^_+|_+$/g, '')
|
|
42
|
+
.replace(/_+/g, '_');
|
|
43
|
+
|
|
44
|
+
return normalized.length > 0 ? normalized : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeTrackingTableNameParts(
|
|
48
|
+
input:
|
|
49
|
+
| MigrationTrackingTableNamePart
|
|
50
|
+
| readonly MigrationTrackingTableNamePart[]
|
|
51
|
+
| undefined
|
|
52
|
+
): string[] {
|
|
53
|
+
if (Array.isArray(input)) {
|
|
54
|
+
return input
|
|
55
|
+
.map((part) => normalizeTrackingTableNamePart(part))
|
|
56
|
+
.filter((part): part is string => part !== null);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const normalized = normalizeTrackingTableNamePart(
|
|
60
|
+
input as MigrationTrackingTableNamePart
|
|
61
|
+
);
|
|
62
|
+
return normalized ? [normalized] : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a stable migration tracking table name.
|
|
67
|
+
*
|
|
68
|
+
* The generated name always includes the `migration_state` suffix and
|
|
69
|
+
* normalizes segments to lowercase snake_case.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* createMigrationTrackingTableName()
|
|
73
|
+
* // => 'sync_migration_state'
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* createMigrationTrackingTableName('server')
|
|
77
|
+
* // => 'sync_server_migration_state'
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* createMigrationTrackingTableName(['spaces', 'billing'], {
|
|
81
|
+
* suffix: ['prod', 'v2'],
|
|
82
|
+
* })
|
|
83
|
+
* // => 'sync_spaces_billing_migration_state_prod_v2'
|
|
84
|
+
*/
|
|
85
|
+
export function createMigrationTrackingTableName(
|
|
86
|
+
scope?:
|
|
87
|
+
| MigrationTrackingTableNamePart
|
|
88
|
+
| readonly MigrationTrackingTableNamePart[],
|
|
89
|
+
options: CreateMigrationTrackingTableNameOptions = {}
|
|
90
|
+
): string {
|
|
91
|
+
const namespace =
|
|
92
|
+
options.namespace === undefined ? ['sync'] : options.namespace;
|
|
93
|
+
const segments = [
|
|
94
|
+
...normalizeTrackingTableNameParts(namespace),
|
|
95
|
+
...normalizeTrackingTableNameParts(scope),
|
|
96
|
+
'migration',
|
|
97
|
+
'state',
|
|
98
|
+
...normalizeTrackingTableNameParts(options.suffix),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
if (segments.length === 0) {
|
|
102
|
+
return DEFAULT_MIGRATION_TRACKING_TABLE;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return segments.join('_');
|
|
106
|
+
}
|
package/src/runner.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* @syncular/migrations - Migration runner
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DISABLED_MIGRATION_CHECKSUM,
|
|
7
|
+
DISABLED_MIGRATION_CHECKSUM_ALGORITHM,
|
|
8
|
+
getMigrationChecksumAlgorithm,
|
|
9
|
+
getStoredDeterministicChecksum,
|
|
10
|
+
} from './checksum';
|
|
11
|
+
import { DEFAULT_MIGRATION_TRACKING_TABLE } from './naming';
|
|
6
12
|
import {
|
|
7
13
|
clearAppliedMigrations,
|
|
8
14
|
ensureTrackingTable,
|
|
@@ -11,13 +17,15 @@ import {
|
|
|
11
17
|
removeAppliedMigration,
|
|
12
18
|
} from './tracking';
|
|
13
19
|
import type {
|
|
20
|
+
DefinedMigrations,
|
|
21
|
+
MigrationChecksumAlgorithm,
|
|
22
|
+
ParsedMigration,
|
|
14
23
|
RunMigrationsOptions,
|
|
15
24
|
RunMigrationsResult,
|
|
16
25
|
RunMigrationsToVersionOptions,
|
|
17
26
|
RunMigrationsToVersionResult,
|
|
18
27
|
} from './types';
|
|
19
28
|
|
|
20
|
-
const DEFAULT_TRACKING_TABLE = 'sync_migration_state';
|
|
21
29
|
const migrationRunQueues = new Map<string, Promise<void>>();
|
|
22
30
|
|
|
23
31
|
function toErrorMessage(error: unknown): string {
|
|
@@ -32,6 +40,39 @@ function isAlreadyExistsSchemaError(error: unknown): boolean {
|
|
|
32
40
|
);
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
function isDeterministicMigration<DB>(migration: ParsedMigration<DB>): boolean {
|
|
44
|
+
return migration.checksum === 'deterministic';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getDeterministicMigrations<DB>(
|
|
48
|
+
migrations: DefinedMigrations<DB>
|
|
49
|
+
): ParsedMigration<DB>[] {
|
|
50
|
+
return migrations.migrations.filter(isDeterministicMigration);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getStoredChecksumForMigration<DB>(
|
|
54
|
+
options: RunMigrationsOptions<DB>,
|
|
55
|
+
migration: ParsedMigration<DB>
|
|
56
|
+
): Promise<string> {
|
|
57
|
+
return getStoredDeterministicChecksum(migration, options.checksums);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function getChecksumForAlgorithm<DB>(
|
|
61
|
+
options: RunMigrationsOptions<DB>,
|
|
62
|
+
migration: ParsedMigration<DB>,
|
|
63
|
+
algorithm: MigrationChecksumAlgorithm
|
|
64
|
+
): Promise<string> {
|
|
65
|
+
if (algorithm === DISABLED_MIGRATION_CHECKSUM_ALGORITHM) {
|
|
66
|
+
return DISABLED_MIGRATION_CHECKSUM;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (algorithm === 'sql_trace_v1') {
|
|
70
|
+
return await getStoredChecksumForMigration(options, migration);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(`Unsupported migration checksum algorithm: ${algorithm}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
35
76
|
async function runWithMigrationQueue<T>(
|
|
36
77
|
queueKey: string,
|
|
37
78
|
task: () => Promise<T>
|
|
@@ -63,8 +104,14 @@ async function runWithMigrationQueue<T>(
|
|
|
63
104
|
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
64
105
|
*
|
|
65
106
|
* const migrations = defineMigrations({
|
|
66
|
-
* v1:
|
|
67
|
-
*
|
|
107
|
+
* v1: {
|
|
108
|
+
* up: async (db) => { ... },
|
|
109
|
+
* down: async (db) => { ... },
|
|
110
|
+
* },
|
|
111
|
+
* v2: {
|
|
112
|
+
* up: async (db) => { ... },
|
|
113
|
+
* down: async (db) => { ... },
|
|
114
|
+
* },
|
|
68
115
|
* });
|
|
69
116
|
*
|
|
70
117
|
* const result = await runMigrations({
|
|
@@ -98,7 +145,8 @@ export async function runMigrationsToVersion<DB>(
|
|
|
98
145
|
options: RunMigrationsToVersionOptions<DB>
|
|
99
146
|
): Promise<RunMigrationsToVersionResult> {
|
|
100
147
|
const { db, migrations, targetVersion } = options;
|
|
101
|
-
const trackingTable =
|
|
148
|
+
const trackingTable =
|
|
149
|
+
options.trackingTable ?? DEFAULT_MIGRATION_TRACKING_TABLE;
|
|
102
150
|
const onChecksumMismatch = options.onChecksumMismatch ?? 'error';
|
|
103
151
|
const beforeReset = options.beforeReset;
|
|
104
152
|
if (!Number.isInteger(targetVersion) || targetVersion < 0) {
|
|
@@ -126,14 +174,28 @@ export async function runMigrationsToVersion<DB>(
|
|
|
126
174
|
const revertedVersions: number[] = [];
|
|
127
175
|
let wasReset = false;
|
|
128
176
|
let recoveredFromSchemaConflict = false;
|
|
177
|
+
const deterministicMigrations = getDeterministicMigrations(migrations);
|
|
129
178
|
|
|
130
179
|
// Check for checksum mismatches up-front when reset mode is enabled
|
|
131
180
|
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
132
|
-
|
|
181
|
+
let hasMismatch = false;
|
|
182
|
+
|
|
183
|
+
for (const migration of deterministicMigrations) {
|
|
133
184
|
const existing = appliedByVersion.get(migration.version);
|
|
134
|
-
if (!existing)
|
|
135
|
-
|
|
136
|
-
|
|
185
|
+
if (!existing) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
190
|
+
options,
|
|
191
|
+
migration,
|
|
192
|
+
existing.checksum_algorithm
|
|
193
|
+
);
|
|
194
|
+
if (existing.checksum !== currentChecksum) {
|
|
195
|
+
hasMismatch = true;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
137
199
|
|
|
138
200
|
if (hasMismatch) {
|
|
139
201
|
// Let caller drop application tables first
|
|
@@ -153,8 +215,18 @@ export async function runMigrationsToVersion<DB>(
|
|
|
153
215
|
if (!existing) {
|
|
154
216
|
continue;
|
|
155
217
|
}
|
|
156
|
-
|
|
157
|
-
if (
|
|
218
|
+
|
|
219
|
+
if (migration.checksum === 'disabled') {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const currentChecksum = await getChecksumForAlgorithm(
|
|
224
|
+
options,
|
|
225
|
+
migration,
|
|
226
|
+
existing.checksum_algorithm
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
if (existing.checksum !== currentChecksum) {
|
|
158
230
|
throw new Error(
|
|
159
231
|
`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
160
232
|
`Stored checksum ${existing.checksum} is not compatible with current checksum ${currentChecksum}. ` +
|
|
@@ -202,10 +274,19 @@ export async function runMigrationsToVersion<DB>(
|
|
|
202
274
|
continue;
|
|
203
275
|
}
|
|
204
276
|
|
|
277
|
+
const checksum = await getStoredChecksumForMigration(
|
|
278
|
+
options,
|
|
279
|
+
migration
|
|
280
|
+
);
|
|
281
|
+
|
|
205
282
|
await recordAppliedMigration(db, trackingTable, {
|
|
206
283
|
version: migration.version,
|
|
207
284
|
name: migration.name,
|
|
208
|
-
checksum
|
|
285
|
+
checksum,
|
|
286
|
+
checksum_algorithm: getMigrationChecksumAlgorithm(
|
|
287
|
+
migration,
|
|
288
|
+
options.checksums
|
|
289
|
+
),
|
|
209
290
|
});
|
|
210
291
|
appliedVersions.push(migration.version);
|
|
211
292
|
}
|
|
@@ -221,12 +302,6 @@ export async function runMigrationsToVersion<DB>(
|
|
|
221
302
|
`Cannot revert migration v${version}: migration is not defined in current migration set.`
|
|
222
303
|
);
|
|
223
304
|
}
|
|
224
|
-
if (typeof migration.down !== 'function') {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`Cannot revert migration v${version} (${migration.name}): down migration is not defined.`
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
305
|
await migration.down(db);
|
|
231
306
|
await removeAppliedMigration(db, trackingTable, version);
|
|
232
307
|
revertedVersions.push(version);
|
|
@@ -249,7 +324,7 @@ export async function getSchemaVersion<DB>(
|
|
|
249
324
|
db: import('kysely').Kysely<DB>,
|
|
250
325
|
trackingTable?: string
|
|
251
326
|
): Promise<number> {
|
|
252
|
-
const tableName = trackingTable ??
|
|
327
|
+
const tableName = trackingTable ?? DEFAULT_MIGRATION_TRACKING_TABLE;
|
|
253
328
|
const applied = await getAppliedMigrations(db, tableName);
|
|
254
329
|
if (applied.length === 0) return 0;
|
|
255
330
|
return applied[applied.length - 1]!.version;
|
package/src/tracking.ts
CHANGED
|
@@ -19,6 +19,7 @@ export async function ensureTrackingTable<DB>(
|
|
|
19
19
|
.addColumn('name', 'text', (col) => col.notNull())
|
|
20
20
|
.addColumn('applied_at', 'text', (col) => col.notNull())
|
|
21
21
|
.addColumn('checksum', 'text', (col) => col.notNull())
|
|
22
|
+
.addColumn('checksum_algorithm', 'text', (col) => col.notNull())
|
|
22
23
|
.execute();
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -32,7 +33,7 @@ export async function getAppliedMigrations<DB, TTableName extends string>(
|
|
|
32
33
|
await ensureTrackingTable(db, tableName);
|
|
33
34
|
|
|
34
35
|
const result = await sql<MigrationStateRow>`
|
|
35
|
-
select version, name, applied_at, checksum
|
|
36
|
+
select version, name, applied_at, checksum, checksum_algorithm
|
|
36
37
|
from ${sql.table(tableName)}
|
|
37
38
|
order by version asc
|
|
38
39
|
`.execute(db);
|
|
@@ -51,12 +52,19 @@ export async function recordAppliedMigration<DB, TTableName extends string>(
|
|
|
51
52
|
await ensureTrackingTable(db, tableName);
|
|
52
53
|
|
|
53
54
|
await sql`
|
|
54
|
-
insert into ${sql.table(tableName)} (
|
|
55
|
+
insert into ${sql.table(tableName)} (
|
|
56
|
+
version,
|
|
57
|
+
name,
|
|
58
|
+
applied_at,
|
|
59
|
+
checksum,
|
|
60
|
+
checksum_algorithm
|
|
61
|
+
)
|
|
55
62
|
values (
|
|
56
63
|
${migration.version},
|
|
57
64
|
${migration.name},
|
|
58
65
|
${new Date().toISOString()},
|
|
59
|
-
${migration.checksum}
|
|
66
|
+
${migration.checksum},
|
|
67
|
+
${migration.checksum_algorithm}
|
|
60
68
|
)
|
|
61
69
|
`.execute(db);
|
|
62
70
|
}
|
package/src/types.ts
CHANGED
|
@@ -9,6 +9,12 @@ import type { Kysely } from 'kysely';
|
|
|
9
9
|
*/
|
|
10
10
|
export type MigrationFn<DB = unknown> = (db: Kysely<DB>) => Promise<void>;
|
|
11
11
|
|
|
12
|
+
export type MigrationChecksumMode = 'deterministic' | 'disabled';
|
|
13
|
+
|
|
14
|
+
export type MigrationChecksumAlgorithm = 'sql_trace_v1' | 'disabled';
|
|
15
|
+
|
|
16
|
+
export type MigrationChecksums = Record<string, string>;
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
19
|
* A reversible migration definition.
|
|
14
20
|
*/
|
|
@@ -16,16 +22,14 @@ export interface ReversibleMigrationDefinition<DB = unknown> {
|
|
|
16
22
|
/** Apply schema/data changes for this version. */
|
|
17
23
|
up: MigrationFn<DB>;
|
|
18
24
|
/** Revert schema/data changes for this version. */
|
|
19
|
-
down
|
|
25
|
+
down: MigrationFn<DB>;
|
|
26
|
+
/** Controls whether this migration participates in checksum validation. */
|
|
27
|
+
checksum?: MigrationChecksumMode;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
/**
|
|
23
|
-
* A migration definition can be a single "up" function or
|
|
24
|
-
* an object with explicit up/down handlers.
|
|
25
|
-
*/
|
|
30
|
+
/** A migration definition must explicitly provide both up/down handlers. */
|
|
26
31
|
export type MigrationDefinition<DB = unknown> =
|
|
27
|
-
|
|
28
|
-
| ReversibleMigrationDefinition<DB>;
|
|
32
|
+
ReversibleMigrationDefinition<DB>;
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* Record of versioned migrations keyed by version string (e.g., 'v1', 'v2').
|
|
@@ -43,8 +47,10 @@ export interface ParsedMigration<DB = unknown> {
|
|
|
43
47
|
name: string;
|
|
44
48
|
/** Up migration function. */
|
|
45
49
|
up: MigrationFn<DB>;
|
|
46
|
-
/**
|
|
47
|
-
down
|
|
50
|
+
/** Down migration function. */
|
|
51
|
+
down: MigrationFn<DB>;
|
|
52
|
+
/** Controls whether this migration participates in checksum validation. */
|
|
53
|
+
checksum: MigrationChecksumMode;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -67,6 +73,7 @@ export interface MigrationStateRow {
|
|
|
67
73
|
name: string;
|
|
68
74
|
applied_at: string;
|
|
69
75
|
checksum: string;
|
|
76
|
+
checksum_algorithm: MigrationChecksumAlgorithm;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
/**
|
|
@@ -77,6 +84,8 @@ export interface RunMigrationsOptions<DB = unknown> {
|
|
|
77
84
|
db: Kysely<DB>;
|
|
78
85
|
/** Defined migrations from defineMigrations() */
|
|
79
86
|
migrations: DefinedMigrations<DB>;
|
|
87
|
+
/** Generated deterministic checksums for this migration set. */
|
|
88
|
+
checksums?: MigrationChecksums;
|
|
80
89
|
/** Name of the tracking table (default: 'sync_migration_state') */
|
|
81
90
|
trackingTable?: string;
|
|
82
91
|
/** What to do when a migration's checksum doesn't match. Default: 'error' */
|