@syncular/migrations 0.0.4-32 → 0.0.4-34
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/runner.d.ts.map +1 -1
- package/dist/runner.js +107 -50
- package/dist/runner.js.map +1 -1
- package/package.json +2 -1
- package/src/runner.ts +125 -58
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AASH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAwCzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,EAAE,EACpC,OAAO,EAAE,oBAAoB,CAAC,EAAE,CAAC,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAqG9B;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,EAAE,EACvC,EAAE,EAAE,OAAO,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC,EAC/B,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,CAKjB"}
|
package/dist/runner.js
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
4
4
|
import { getMigrationChecksum } from './define.js';
|
|
5
5
|
import { clearAppliedMigrations, ensureTrackingTable, getAppliedMigrations, recordAppliedMigration, } from './tracking.js';
|
|
6
6
|
const DEFAULT_TRACKING_TABLE = 'sync_migration_state';
|
|
7
|
+
const migrationRunQueues = new Map();
|
|
8
|
+
function toErrorMessage(error) {
|
|
9
|
+
return error instanceof Error ? error.message : String(error);
|
|
10
|
+
}
|
|
11
|
+
function isAlreadyExistsSchemaError(error) {
|
|
12
|
+
const message = toErrorMessage(error).toLowerCase();
|
|
13
|
+
return (message.includes('already exists') ||
|
|
14
|
+
(message.includes('relation') && message.includes('exists')));
|
|
15
|
+
}
|
|
16
|
+
async function runWithMigrationQueue(queueKey, task) {
|
|
17
|
+
const previous = migrationRunQueues.get(queueKey) ?? Promise.resolve();
|
|
18
|
+
let release;
|
|
19
|
+
const current = new Promise((resolve) => {
|
|
20
|
+
release = resolve;
|
|
21
|
+
});
|
|
22
|
+
const tail = previous.then(() => current);
|
|
23
|
+
migrationRunQueues.set(queueKey, tail);
|
|
24
|
+
await previous;
|
|
25
|
+
try {
|
|
26
|
+
return await task();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
release();
|
|
30
|
+
if (migrationRunQueues.get(queueKey) === tail) {
|
|
31
|
+
migrationRunQueues.delete(queueKey);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
7
35
|
/**
|
|
8
36
|
* Run pending migrations and track their state.
|
|
9
37
|
*
|
|
@@ -30,59 +58,88 @@ export async function runMigrations(options) {
|
|
|
30
58
|
const { db, migrations } = options;
|
|
31
59
|
const trackingTable = options.trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
32
60
|
const onChecksumMismatch = options.onChecksumMismatch ?? 'error';
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const beforeReset = options.beforeReset;
|
|
62
|
+
// Serialize migration runs per tracking table to avoid duplicate CREATE TABLE
|
|
63
|
+
// races when startup paths invoke migrations concurrently (e.g. React StrictMode).
|
|
64
|
+
return runWithMigrationQueue(`tracking:${trackingTable}`, async () => {
|
|
65
|
+
// Ensure tracking table exists
|
|
66
|
+
await ensureTrackingTable(db, trackingTable);
|
|
67
|
+
// Get already applied migrations
|
|
68
|
+
let applied = await getAppliedMigrations(db, trackingTable);
|
|
69
|
+
let appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
70
|
+
const appliedVersions = [];
|
|
71
|
+
let wasReset = false;
|
|
72
|
+
let recoveredFromSchemaConflict = false;
|
|
73
|
+
// Check for checksum mismatches up-front when reset mode is enabled
|
|
74
|
+
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
75
|
+
const hasMismatch = migrations.migrations.some((migration) => {
|
|
76
|
+
const existing = appliedByVersion.get(migration.version);
|
|
77
|
+
if (!existing)
|
|
78
|
+
return false;
|
|
79
|
+
return existing.checksum !== getMigrationChecksum(migration);
|
|
80
|
+
});
|
|
81
|
+
if (hasMismatch) {
|
|
82
|
+
// Let caller drop application tables first
|
|
83
|
+
await options.beforeReset?.(db);
|
|
84
|
+
// Clear tracking state so all migrations re-run
|
|
85
|
+
await clearAppliedMigrations(db, trackingTable);
|
|
86
|
+
wasReset = true;
|
|
87
|
+
// Refresh applied list (now empty)
|
|
88
|
+
applied = await getAppliedMigrations(db, trackingTable);
|
|
89
|
+
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
90
|
+
}
|
|
57
91
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
for (let index = 0; index < migrations.migrations.length; index += 1) {
|
|
93
|
+
const migration = migrations.migrations[index];
|
|
94
|
+
const existing = appliedByVersion.get(migration.version);
|
|
95
|
+
if (existing) {
|
|
96
|
+
// Migration already applied - verify checksum hasn't changed
|
|
97
|
+
const currentChecksum = getMigrationChecksum(migration);
|
|
98
|
+
if (existing.checksum !== currentChecksum) {
|
|
99
|
+
throw new Error(`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
100
|
+
`Expected checksum ${existing.checksum}, got ${currentChecksum}. ` +
|
|
101
|
+
'Migrations must not be modified after being applied.');
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
68
104
|
}
|
|
69
|
-
|
|
105
|
+
// Run the migration
|
|
106
|
+
try {
|
|
107
|
+
await migration.fn(db);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const canRecoverFromConflict = onChecksumMismatch === 'reset' &&
|
|
111
|
+
typeof beforeReset === 'function' &&
|
|
112
|
+
!recoveredFromSchemaConflict &&
|
|
113
|
+
isAlreadyExistsSchemaError(error);
|
|
114
|
+
if (!canRecoverFromConflict) {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
// Recover once from partially-applied state where app tables exist but
|
|
118
|
+
// migration tracking rows were not committed.
|
|
119
|
+
await beforeReset(db);
|
|
120
|
+
await clearAppliedMigrations(db, trackingTable);
|
|
121
|
+
wasReset = true;
|
|
122
|
+
recoveredFromSchemaConflict = true;
|
|
123
|
+
applied = await getAppliedMigrations(db, trackingTable);
|
|
124
|
+
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
125
|
+
appliedVersions.length = 0;
|
|
126
|
+
index = -1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// Record it as applied
|
|
130
|
+
await recordAppliedMigration(db, trackingTable, {
|
|
131
|
+
version: migration.version,
|
|
132
|
+
name: migration.name,
|
|
133
|
+
checksum: getMigrationChecksum(migration),
|
|
134
|
+
});
|
|
135
|
+
appliedVersions.push(migration.version);
|
|
70
136
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
checksum: getMigrationChecksum(migration),
|
|
78
|
-
});
|
|
79
|
-
appliedVersions.push(migration.version);
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
applied: appliedVersions,
|
|
83
|
-
currentVersion: migrations.currentVersion,
|
|
84
|
-
wasReset,
|
|
85
|
-
};
|
|
137
|
+
return {
|
|
138
|
+
applied: appliedVersions,
|
|
139
|
+
currentVersion: migrations.currentVersion,
|
|
140
|
+
wasReset,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
86
143
|
}
|
|
87
144
|
/**
|
|
88
145
|
* Get the current schema version without running any migrations.
|
package/dist/runner.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAGpB,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/migrations",
|
|
3
|
-
"version": "0.0.4-
|
|
3
|
+
"version": "0.0.4-34",
|
|
4
4
|
"description": "Database migration utilities for Syncular",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"exports": {
|
|
31
31
|
".": {
|
|
32
32
|
"bun": "./src/index.ts",
|
|
33
|
+
"browser": "./src/index.ts",
|
|
33
34
|
"import": {
|
|
34
35
|
"types": "./dist/index.d.ts",
|
|
35
36
|
"default": "./dist/index.js"
|
package/src/runner.ts
CHANGED
|
@@ -12,6 +12,42 @@ import {
|
|
|
12
12
|
import type { RunMigrationsOptions, RunMigrationsResult } from './types';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_TRACKING_TABLE = 'sync_migration_state';
|
|
15
|
+
const migrationRunQueues = new Map<string, Promise<void>>();
|
|
16
|
+
|
|
17
|
+
function toErrorMessage(error: unknown): string {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isAlreadyExistsSchemaError(error: unknown): boolean {
|
|
22
|
+
const message = toErrorMessage(error).toLowerCase();
|
|
23
|
+
return (
|
|
24
|
+
message.includes('already exists') ||
|
|
25
|
+
(message.includes('relation') && message.includes('exists'))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runWithMigrationQueue<T>(
|
|
30
|
+
queueKey: string,
|
|
31
|
+
task: () => Promise<T>
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const previous = migrationRunQueues.get(queueKey) ?? Promise.resolve();
|
|
34
|
+
let release!: () => void;
|
|
35
|
+
const current = new Promise<void>((resolve) => {
|
|
36
|
+
release = resolve;
|
|
37
|
+
});
|
|
38
|
+
const tail = previous.then(() => current);
|
|
39
|
+
migrationRunQueues.set(queueKey, tail);
|
|
40
|
+
|
|
41
|
+
await previous;
|
|
42
|
+
try {
|
|
43
|
+
return await task();
|
|
44
|
+
} finally {
|
|
45
|
+
release();
|
|
46
|
+
if (migrationRunQueues.get(queueKey) === tail) {
|
|
47
|
+
migrationRunQueues.delete(queueKey);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
15
51
|
|
|
16
52
|
/**
|
|
17
53
|
* Run pending migrations and track their state.
|
|
@@ -41,72 +77,103 @@ export async function runMigrations<DB>(
|
|
|
41
77
|
const { db, migrations } = options;
|
|
42
78
|
const trackingTable = options.trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
43
79
|
const onChecksumMismatch = options.onChecksumMismatch ?? 'error';
|
|
80
|
+
const beforeReset = options.beforeReset;
|
|
81
|
+
|
|
82
|
+
// Serialize migration runs per tracking table to avoid duplicate CREATE TABLE
|
|
83
|
+
// races when startup paths invoke migrations concurrently (e.g. React StrictMode).
|
|
84
|
+
return runWithMigrationQueue(`tracking:${trackingTable}`, async () => {
|
|
85
|
+
// Ensure tracking table exists
|
|
86
|
+
await ensureTrackingTable(db, trackingTable);
|
|
87
|
+
|
|
88
|
+
// Get already applied migrations
|
|
89
|
+
let applied = await getAppliedMigrations(db, trackingTable);
|
|
90
|
+
let appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
91
|
+
|
|
92
|
+
const appliedVersions: number[] = [];
|
|
93
|
+
let wasReset = false;
|
|
94
|
+
let recoveredFromSchemaConflict = false;
|
|
95
|
+
|
|
96
|
+
// Check for checksum mismatches up-front when reset mode is enabled
|
|
97
|
+
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
98
|
+
const hasMismatch = migrations.migrations.some((migration) => {
|
|
99
|
+
const existing = appliedByVersion.get(migration.version);
|
|
100
|
+
if (!existing) return false;
|
|
101
|
+
return existing.checksum !== getMigrationChecksum(migration);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (hasMismatch) {
|
|
105
|
+
// Let caller drop application tables first
|
|
106
|
+
await options.beforeReset?.(db);
|
|
107
|
+
// Clear tracking state so all migrations re-run
|
|
108
|
+
await clearAppliedMigrations(db, trackingTable);
|
|
109
|
+
wasReset = true;
|
|
110
|
+
|
|
111
|
+
// Refresh applied list (now empty)
|
|
112
|
+
applied = await getAppliedMigrations(db, trackingTable);
|
|
113
|
+
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
44
116
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Get already applied migrations
|
|
49
|
-
let applied = await getAppliedMigrations(db, trackingTable);
|
|
50
|
-
let appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
51
|
-
|
|
52
|
-
const appliedVersions: number[] = [];
|
|
53
|
-
let wasReset = false;
|
|
54
|
-
|
|
55
|
-
// Check for checksum mismatches up-front when reset mode is enabled
|
|
56
|
-
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
57
|
-
const hasMismatch = migrations.migrations.some((migration) => {
|
|
117
|
+
for (let index = 0; index < migrations.migrations.length; index += 1) {
|
|
118
|
+
const migration = migrations.migrations[index]!;
|
|
58
119
|
const existing = appliedByVersion.get(migration.version);
|
|
59
|
-
if (!existing) return false;
|
|
60
|
-
return existing.checksum !== getMigrationChecksum(migration);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (hasMismatch) {
|
|
64
|
-
// Let caller drop application tables first
|
|
65
|
-
await options.beforeReset?.(db);
|
|
66
|
-
// Clear tracking state so all migrations re-run
|
|
67
|
-
await clearAppliedMigrations(db, trackingTable);
|
|
68
|
-
wasReset = true;
|
|
69
|
-
|
|
70
|
-
// Refresh applied list (now empty)
|
|
71
|
-
applied = await getAppliedMigrations(db, trackingTable);
|
|
72
|
-
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
120
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
121
|
+
if (existing) {
|
|
122
|
+
// Migration already applied - verify checksum hasn't changed
|
|
123
|
+
const currentChecksum = getMigrationChecksum(migration);
|
|
124
|
+
if (existing.checksum !== currentChecksum) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
127
|
+
`Expected checksum ${existing.checksum}, got ${currentChecksum}. ` +
|
|
128
|
+
'Migrations must not be modified after being applied.'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
88
132
|
}
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
133
|
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
// Run the migration
|
|
135
|
+
try {
|
|
136
|
+
await migration.fn(db);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const canRecoverFromConflict =
|
|
139
|
+
onChecksumMismatch === 'reset' &&
|
|
140
|
+
typeof beforeReset === 'function' &&
|
|
141
|
+
!recoveredFromSchemaConflict &&
|
|
142
|
+
isAlreadyExistsSchemaError(error);
|
|
143
|
+
|
|
144
|
+
if (!canRecoverFromConflict) {
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Recover once from partially-applied state where app tables exist but
|
|
149
|
+
// migration tracking rows were not committed.
|
|
150
|
+
await beforeReset(db);
|
|
151
|
+
await clearAppliedMigrations(db, trackingTable);
|
|
152
|
+
wasReset = true;
|
|
153
|
+
recoveredFromSchemaConflict = true;
|
|
154
|
+
applied = await getAppliedMigrations(db, trackingTable);
|
|
155
|
+
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
156
|
+
appliedVersions.length = 0;
|
|
157
|
+
index = -1;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
94
160
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
161
|
+
// Record it as applied
|
|
162
|
+
await recordAppliedMigration(db, trackingTable, {
|
|
163
|
+
version: migration.version,
|
|
164
|
+
name: migration.name,
|
|
165
|
+
checksum: getMigrationChecksum(migration),
|
|
166
|
+
});
|
|
101
167
|
|
|
102
|
-
|
|
103
|
-
|
|
168
|
+
appliedVersions.push(migration.version);
|
|
169
|
+
}
|
|
104
170
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
171
|
+
return {
|
|
172
|
+
applied: appliedVersions,
|
|
173
|
+
currentVersion: migrations.currentVersion,
|
|
174
|
+
wasReset,
|
|
175
|
+
};
|
|
176
|
+
});
|
|
110
177
|
}
|
|
111
178
|
|
|
112
179
|
/**
|