@syncular/migrations 0.0.1-60
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/define.d.ts +30 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +98 -0
- package/dist/define.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/runner.d.ts +32 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +97 -0
- package/dist/runner.js.map +1 -0
- package/dist/tracking.d.ts +28 -0
- package/dist/tracking.d.ts.map +1 -0
- package/dist/tracking.js +62 -0
- package/dist/tracking.js.map +1 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/define.ts +123 -0
- package/src/index.ts +10 -0
- package/src/runner.ts +123 -0
- package/src/tracking.ts +86 -0
- package/src/types.ts +75 -0
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration definition
|
|
3
|
+
*/
|
|
4
|
+
import type { DefinedMigrations, MigrationRecord, ParsedMigration } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Define versioned migrations with automatic version parsing and sorting.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* export const migrations = defineMigrations({
|
|
11
|
+
* v1: async (db) => {
|
|
12
|
+
* await db.schema.createTable('tasks')
|
|
13
|
+
* .addColumn('id', 'text', col => col.primaryKey())
|
|
14
|
+
* .addColumn('title', 'text', col => col.notNull())
|
|
15
|
+
* .execute();
|
|
16
|
+
* },
|
|
17
|
+
* v2: async (db) => {
|
|
18
|
+
* await db.schema.alterTable('tasks')
|
|
19
|
+
* .addColumn('priority', 'integer', col => col.defaultTo(0))
|
|
20
|
+
* .execute();
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function defineMigrations<DB = unknown, T extends MigrationRecord<DB> = MigrationRecord<DB>>(versionedMigrations: T): DefinedMigrations<DB>;
|
|
26
|
+
/**
|
|
27
|
+
* Get the checksum for a migration.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getMigrationChecksum<DB>(migration: ParsedMigration<DB>): string;
|
|
30
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,iBAAiB,EAEjB,eAAe,EACf,eAAe,EAChB,MAAM,SAAS,CAAC;AAuCjB;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,GAAG,OAAO,EACZ,CAAC,SAAS,eAAe,CAAC,EAAE,CAAC,GAAG,eAAe,CAAC,EAAE,CAAC,EACnD,mBAAmB,EAAE,CAAC,GAAG,iBAAiB,CAAC,EAAE,CAAC,CA0C/C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EACrC,SAAS,EAAE,eAAe,CAAC,EAAE,CAAC,GAC7B,MAAM,CAER"}
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration definition
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Parse a version key (e.g., 'v1', 'v2', '1', '2') into a version number.
|
|
6
|
+
*/
|
|
7
|
+
function parseVersionKey(key) {
|
|
8
|
+
// Support both 'v1' and '1' formats
|
|
9
|
+
const match = key.match(/^v?(\d+)$/i);
|
|
10
|
+
if (!match)
|
|
11
|
+
return null;
|
|
12
|
+
const version = Number.parseInt(match[1], 10);
|
|
13
|
+
return Number.isNaN(version) ? null : version;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a function source string for checksum comparison.
|
|
17
|
+
* Strips comments and collapses whitespace so that formatting-only
|
|
18
|
+
* changes don't break checksums.
|
|
19
|
+
*/
|
|
20
|
+
function normalizeSource(source) {
|
|
21
|
+
return source
|
|
22
|
+
.replace(/\/\/[^\n]*/g, '') // strip // comments
|
|
23
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // strip /* */ comments
|
|
24
|
+
.replace(/\s+/g, ' ') // collapse whitespace
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compute a simple checksum for a migration function.
|
|
29
|
+
* Used to detect if a migration has changed after being applied.
|
|
30
|
+
*/
|
|
31
|
+
function computeChecksum(fn) {
|
|
32
|
+
const fnStr = normalizeSource(fn.toString());
|
|
33
|
+
let hash = 0;
|
|
34
|
+
for (let i = 0; i < fnStr.length; i++) {
|
|
35
|
+
hash = (hash * 31 + fnStr.charCodeAt(i)) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
return hash.toString(16).padStart(8, '0');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Define versioned migrations with automatic version parsing and sorting.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* export const migrations = defineMigrations({
|
|
45
|
+
* v1: async (db) => {
|
|
46
|
+
* await db.schema.createTable('tasks')
|
|
47
|
+
* .addColumn('id', 'text', col => col.primaryKey())
|
|
48
|
+
* .addColumn('title', 'text', col => col.notNull())
|
|
49
|
+
* .execute();
|
|
50
|
+
* },
|
|
51
|
+
* v2: async (db) => {
|
|
52
|
+
* await db.schema.alterTable('tasks')
|
|
53
|
+
* .addColumn('priority', 'integer', col => col.defaultTo(0))
|
|
54
|
+
* .execute();
|
|
55
|
+
* },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function defineMigrations(versionedMigrations) {
|
|
60
|
+
const migrations = [];
|
|
61
|
+
for (const [key, fn] of Object.entries(versionedMigrations)) {
|
|
62
|
+
const version = parseVersionKey(key);
|
|
63
|
+
if (version === null) {
|
|
64
|
+
throw new Error(`Invalid migration key "${key}": must be a version number (e.g., 'v1', 'v2', '1', '2')`);
|
|
65
|
+
}
|
|
66
|
+
if (version < 1) {
|
|
67
|
+
throw new Error(`Invalid migration version ${version}: versions must be >= 1`);
|
|
68
|
+
}
|
|
69
|
+
migrations.push({
|
|
70
|
+
version,
|
|
71
|
+
name: key,
|
|
72
|
+
fn,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Sort by version number
|
|
76
|
+
migrations.sort((a, b) => a.version - b.version);
|
|
77
|
+
// Check for duplicate versions
|
|
78
|
+
for (let i = 1; i < migrations.length; i++) {
|
|
79
|
+
if (migrations[i].version === migrations[i - 1].version) {
|
|
80
|
+
throw new Error(`Duplicate migration version ${migrations[i].version}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const currentVersion = migrations.length > 0 ? migrations[migrations.length - 1].version : 0;
|
|
84
|
+
return {
|
|
85
|
+
migrations,
|
|
86
|
+
currentVersion,
|
|
87
|
+
getMigration(version) {
|
|
88
|
+
return migrations.find((m) => m.version === version);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get the checksum for a migration.
|
|
94
|
+
*/
|
|
95
|
+
export function getMigrationChecksum(migration) {
|
|
96
|
+
return computeChecksum(migration.fn);
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.js","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Versioned migration system
|
|
3
|
+
*
|
|
4
|
+
* Provides migration definition, tracking, and execution.
|
|
5
|
+
*/
|
|
6
|
+
export * from './define';
|
|
7
|
+
export * from './runner';
|
|
8
|
+
export * from './tracking';
|
|
9
|
+
export * from './types';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Versioned migration system
|
|
3
|
+
*
|
|
4
|
+
* Provides migration definition, tracking, and execution.
|
|
5
|
+
*/
|
|
6
|
+
export * from './define';
|
|
7
|
+
export * from './runner';
|
|
8
|
+
export * from './tracking';
|
|
9
|
+
export * from './types';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC"}
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration runner
|
|
3
|
+
*/
|
|
4
|
+
import type { RunMigrationsOptions, RunMigrationsResult } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* Run pending migrations and track their state.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
11
|
+
*
|
|
12
|
+
* const migrations = defineMigrations({
|
|
13
|
+
* v1: async (db) => { ... },
|
|
14
|
+
* v2: async (db) => { ... },
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const result = await runMigrations({
|
|
18
|
+
* db,
|
|
19
|
+
* migrations,
|
|
20
|
+
* trackingTable: 'sync_migration_state', // optional
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* console.log(`Applied versions: ${result.applied.join(', ')}`);
|
|
24
|
+
* console.log(`Current version: ${result.currentVersion}`);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function runMigrations<DB>(options: RunMigrationsOptions<DB>): Promise<RunMigrationsResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Get the current schema version without running any migrations.
|
|
30
|
+
*/
|
|
31
|
+
export declare function getSchemaVersion<DB>(db: import('kysely').Kysely<DB>, trackingTable?: string): Promise<number>;
|
|
32
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +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;AAIzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,aAAa,CAAC,EAAE,EACpC,OAAO,EAAE,oBAAoB,CAAC,EAAE,CAAC,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAsE9B;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
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration runner
|
|
3
|
+
*/
|
|
4
|
+
import { getMigrationChecksum } from './define';
|
|
5
|
+
import { clearAppliedMigrations, ensureTrackingTable, getAppliedMigrations, recordAppliedMigration, } from './tracking';
|
|
6
|
+
const DEFAULT_TRACKING_TABLE = 'sync_migration_state';
|
|
7
|
+
/**
|
|
8
|
+
* Run pending migrations and track their state.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
13
|
+
*
|
|
14
|
+
* const migrations = defineMigrations({
|
|
15
|
+
* v1: async (db) => { ... },
|
|
16
|
+
* v2: async (db) => { ... },
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* const result = await runMigrations({
|
|
20
|
+
* db,
|
|
21
|
+
* migrations,
|
|
22
|
+
* trackingTable: 'sync_migration_state', // optional
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* console.log(`Applied versions: ${result.applied.join(', ')}`);
|
|
26
|
+
* console.log(`Current version: ${result.currentVersion}`);
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function runMigrations(options) {
|
|
30
|
+
const { db, migrations } = options;
|
|
31
|
+
const trackingTable = options.trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
32
|
+
const onChecksumMismatch = options.onChecksumMismatch ?? 'error';
|
|
33
|
+
// Ensure tracking table exists
|
|
34
|
+
await ensureTrackingTable(db, trackingTable);
|
|
35
|
+
// Get already applied migrations
|
|
36
|
+
let applied = await getAppliedMigrations(db, trackingTable);
|
|
37
|
+
let appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
38
|
+
const appliedVersions = [];
|
|
39
|
+
let wasReset = false;
|
|
40
|
+
// Check for checksum mismatches up-front when reset mode is enabled
|
|
41
|
+
if (onChecksumMismatch === 'reset' && applied.length > 0) {
|
|
42
|
+
const hasMismatch = migrations.migrations.some((migration) => {
|
|
43
|
+
const existing = appliedByVersion.get(migration.version);
|
|
44
|
+
if (!existing)
|
|
45
|
+
return false;
|
|
46
|
+
return existing.checksum !== getMigrationChecksum(migration);
|
|
47
|
+
});
|
|
48
|
+
if (hasMismatch) {
|
|
49
|
+
// Let caller drop application tables first
|
|
50
|
+
await options.beforeReset?.(db);
|
|
51
|
+
// Clear tracking state so all migrations re-run
|
|
52
|
+
await clearAppliedMigrations(db, trackingTable);
|
|
53
|
+
wasReset = true;
|
|
54
|
+
// Refresh applied list (now empty)
|
|
55
|
+
applied = await getAppliedMigrations(db, trackingTable);
|
|
56
|
+
appliedByVersion = new Map(applied.map((m) => [m.version, m]));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const migration of migrations.migrations) {
|
|
60
|
+
const existing = appliedByVersion.get(migration.version);
|
|
61
|
+
if (existing) {
|
|
62
|
+
// Migration already applied - verify checksum hasn't changed
|
|
63
|
+
const currentChecksum = getMigrationChecksum(migration);
|
|
64
|
+
if (existing.checksum !== currentChecksum) {
|
|
65
|
+
throw new Error(`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
66
|
+
`Expected checksum ${existing.checksum}, got ${currentChecksum}. ` +
|
|
67
|
+
'Migrations must not be modified after being applied.');
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
// Run the migration
|
|
72
|
+
await migration.fn(db);
|
|
73
|
+
// Record it as applied
|
|
74
|
+
await recordAppliedMigration(db, trackingTable, {
|
|
75
|
+
version: migration.version,
|
|
76
|
+
name: migration.name,
|
|
77
|
+
checksum: getMigrationChecksum(migration),
|
|
78
|
+
});
|
|
79
|
+
appliedVersions.push(migration.version);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
applied: appliedVersions,
|
|
83
|
+
currentVersion: migrations.currentVersion,
|
|
84
|
+
wasReset,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get the current schema version without running any migrations.
|
|
89
|
+
*/
|
|
90
|
+
export async function getSchemaVersion(db, trackingTable) {
|
|
91
|
+
const tableName = trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
92
|
+
const applied = await getAppliedMigrations(db, tableName);
|
|
93
|
+
if (applied.length === 0)
|
|
94
|
+
return 0;
|
|
95
|
+
return applied[applied.length - 1].version;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration tracking table helpers
|
|
3
|
+
*/
|
|
4
|
+
import { type Kysely } from 'kysely';
|
|
5
|
+
import type { MigrationStateRow } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Ensure the migration tracking table exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensureTrackingTable<DB>(db: Kysely<DB>, tableName: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Get all applied migrations from the tracking table.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getAppliedMigrations<DB, TTableName extends string>(db: Kysely<DB>, tableName: TTableName): Promise<MigrationStateRow[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Record a migration as applied in the tracking table.
|
|
16
|
+
*/
|
|
17
|
+
export declare function recordAppliedMigration<DB, TTableName extends string>(db: Kysely<DB>, tableName: TTableName, migration: Omit<MigrationStateRow, 'applied_at'>): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Clear all rows from the migration tracking table.
|
|
20
|
+
* Used when resetting the database after a checksum mismatch.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clearAppliedMigrations<DB>(db: Kysely<DB>, tableName: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the current schema version from the tracking table.
|
|
25
|
+
* Returns 0 if no migrations have been applied.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getCurrentVersion<DB, TTableName extends string>(db: Kysely<DB>, tableName: TTableName): Promise<number>;
|
|
28
|
+
//# sourceMappingURL=tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking.d.ts","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,SAAS,MAAM,EACtE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAU9B;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,UAAU,SAAS,MAAM,EACxE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,SAAS,EAAE,UAAU,EACrB,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAC7C,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,UAAU,SAAS,MAAM,EACnE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,MAAM,CAAC,CAIjB"}
|
package/dist/tracking.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration tracking table helpers
|
|
3
|
+
*/
|
|
4
|
+
import { sql } from 'kysely';
|
|
5
|
+
/**
|
|
6
|
+
* Ensure the migration tracking table exists.
|
|
7
|
+
*/
|
|
8
|
+
export async function ensureTrackingTable(db, tableName) {
|
|
9
|
+
await db.schema
|
|
10
|
+
.createTable(tableName)
|
|
11
|
+
.ifNotExists()
|
|
12
|
+
.addColumn('version', 'integer', (col) => col.primaryKey())
|
|
13
|
+
.addColumn('name', 'text', (col) => col.notNull())
|
|
14
|
+
.addColumn('applied_at', 'text', (col) => col.notNull())
|
|
15
|
+
.addColumn('checksum', 'text', (col) => col.notNull())
|
|
16
|
+
.execute();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Get all applied migrations from the tracking table.
|
|
20
|
+
*/
|
|
21
|
+
export async function getAppliedMigrations(db, tableName) {
|
|
22
|
+
await ensureTrackingTable(db, tableName);
|
|
23
|
+
const result = await sql `
|
|
24
|
+
select version, name, applied_at, checksum
|
|
25
|
+
from ${sql.table(tableName)}
|
|
26
|
+
order by version asc
|
|
27
|
+
`.execute(db);
|
|
28
|
+
return result.rows;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Record a migration as applied in the tracking table.
|
|
32
|
+
*/
|
|
33
|
+
export async function recordAppliedMigration(db, tableName, migration) {
|
|
34
|
+
await ensureTrackingTable(db, tableName);
|
|
35
|
+
await sql `
|
|
36
|
+
insert into ${sql.table(tableName)} (version, name, applied_at, checksum)
|
|
37
|
+
values (
|
|
38
|
+
${migration.version},
|
|
39
|
+
${migration.name},
|
|
40
|
+
${new Date().toISOString()},
|
|
41
|
+
${migration.checksum}
|
|
42
|
+
)
|
|
43
|
+
`.execute(db);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clear all rows from the migration tracking table.
|
|
47
|
+
* Used when resetting the database after a checksum mismatch.
|
|
48
|
+
*/
|
|
49
|
+
export async function clearAppliedMigrations(db, tableName) {
|
|
50
|
+
await sql `delete from ${sql.table(tableName)}`.execute(db);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the current schema version from the tracking table.
|
|
54
|
+
* Returns 0 if no migrations have been applied.
|
|
55
|
+
*/
|
|
56
|
+
export async function getCurrentVersion(db, tableName) {
|
|
57
|
+
const applied = await getAppliedMigrations(db, tableName);
|
|
58
|
+
if (applied.length === 0)
|
|
59
|
+
return 0;
|
|
60
|
+
return applied[applied.length - 1].version;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=tracking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tracking.js","sourceRoot":"","sources":["../src/tracking.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAe,GAAG,EAAE,MAAM,QAAQ,CAAC;AAG1C;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAc,EACd,SAAiB,EACF;IACf,MAAM,EAAE,CAAC,MAAM;SACZ,WAAW,CAAC,SAAS,CAAC;SACtB,WAAW,EAAE;SACb,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC;SAC1D,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;SACjD,SAAS,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;SACvD,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;SACrD,OAAO,EAAE,CAAC;AAAA,CACd;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,EAAc,EACd,SAAqB,EACS;IAC9B,MAAM,mBAAmB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAEzC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAmB;;WAElC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;;GAE5B,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAEd,OAAO,MAAM,CAAC,IAAI,CAAC;AAAA,CACpB;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,EAAc,EACd,SAAqB,EACrB,SAAgD,EACjC;IACf,MAAM,mBAAmB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAEzC,MAAM,GAAG,CAAA;kBACO,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;;QAE9B,SAAS,CAAC,OAAO;QACjB,SAAS,CAAC,IAAI;QACd,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACxB,SAAS,CAAC,QAAQ;;GAEvB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAAA,CACf;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,EAAc,EACd,SAAiB,EACF;IACf,MAAM,GAAG,CAAA,eAAe,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;AAAA,CAC5D;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,EAAc,EACd,SAAqB,EACJ;IACjB,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC1D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC,OAAO,CAAC;AAAA,CAC7C"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Type definitions
|
|
3
|
+
*/
|
|
4
|
+
import type { Kysely } from 'kysely';
|
|
5
|
+
/**
|
|
6
|
+
* A single migration function that modifies the database schema.
|
|
7
|
+
*/
|
|
8
|
+
export type MigrationFn<DB = unknown> = (db: Kysely<DB>) => Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Record of versioned migrations keyed by version string (e.g., 'v1', 'v2').
|
|
11
|
+
*/
|
|
12
|
+
export type MigrationRecord<DB = unknown> = Record<string, MigrationFn<DB>>;
|
|
13
|
+
/**
|
|
14
|
+
* Parsed migration with version number and function.
|
|
15
|
+
*/
|
|
16
|
+
export interface ParsedMigration<DB = unknown> {
|
|
17
|
+
version: number;
|
|
18
|
+
name: string;
|
|
19
|
+
fn: MigrationFn<DB>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Result of defineMigrations() - contains migrations and metadata.
|
|
23
|
+
*/
|
|
24
|
+
export interface DefinedMigrations<DB = unknown> {
|
|
25
|
+
/** Sorted list of migrations */
|
|
26
|
+
migrations: ParsedMigration<DB>[];
|
|
27
|
+
/** Current (latest) schema version */
|
|
28
|
+
currentVersion: number;
|
|
29
|
+
/** Get migration by version number */
|
|
30
|
+
getMigration(version: number): ParsedMigration<DB> | undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Migration state row stored in the tracking table.
|
|
34
|
+
*/
|
|
35
|
+
export interface MigrationStateRow {
|
|
36
|
+
version: number;
|
|
37
|
+
name: string;
|
|
38
|
+
applied_at: string;
|
|
39
|
+
checksum: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Options for running migrations.
|
|
43
|
+
*/
|
|
44
|
+
export interface RunMigrationsOptions<DB = unknown> {
|
|
45
|
+
/** Kysely database instance */
|
|
46
|
+
db: Kysely<DB>;
|
|
47
|
+
/** Defined migrations from defineMigrations() */
|
|
48
|
+
migrations: DefinedMigrations<DB>;
|
|
49
|
+
/** Name of the tracking table (default: 'sync_migration_state') */
|
|
50
|
+
trackingTable?: string;
|
|
51
|
+
/** What to do when a migration's checksum doesn't match. Default: 'error' */
|
|
52
|
+
onChecksumMismatch?: 'error' | 'reset';
|
|
53
|
+
/** Called before clearing tracking state and re-running migrations.
|
|
54
|
+
* Use this to drop application tables so migrations can recreate them. */
|
|
55
|
+
beforeReset?: (db: Kysely<DB>) => Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Result of running migrations.
|
|
59
|
+
*/
|
|
60
|
+
export interface RunMigrationsResult {
|
|
61
|
+
/** Versions that were applied in this run */
|
|
62
|
+
applied: number[];
|
|
63
|
+
/** Current schema version after migration */
|
|
64
|
+
currentVersion: number;
|
|
65
|
+
/** True if a checksum mismatch triggered a full reset */
|
|
66
|
+
wasReset: boolean;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC;;GAEG;AACH,MAAM,MAAM,WAAW,CAAC,EAAE,GAAG,OAAO,IAAI,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1E;;GAEG;AACH,MAAM,MAAM,eAAe,CAAC,EAAE,GAAG,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC;AAE5E;;GAEG;AACH,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,EAAE,GAAG,OAAO;IAC7C,gCAAgC;IAChC,UAAU,EAAE,eAAe,CAAC,EAAE,CAAC,EAAE,CAAC;IAClC,sCAAsC;IACtC,cAAc,EAAE,MAAM,CAAC;IACvB,sCAAsC;IACtC,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC;CAChE;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB,CAAC,EAAE,GAAG,OAAO;IAChD,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,iDAAiD;IACjD,UAAU,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAC;IAClC,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,kBAAkB,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IACvC;+EAC2E;IAC3E,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAC;IACvB,yDAAyD;IACzD,QAAQ,EAAE,OAAO,CAAC;CACnB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncular/migrations",
|
|
3
|
+
"version": "0.0.1-60",
|
|
4
|
+
"description": "Database migration utilities for Syncular",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benjamin Kniffler",
|
|
7
|
+
"homepage": "https://syncular.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/syncular/syncular.git",
|
|
11
|
+
"directory": "packages/migrations"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/syncular/syncular/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sync",
|
|
18
|
+
"offline-first",
|
|
19
|
+
"realtime",
|
|
20
|
+
"database",
|
|
21
|
+
"typescript",
|
|
22
|
+
"migrations",
|
|
23
|
+
"database"
|
|
24
|
+
],
|
|
25
|
+
"private": false,
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "bun test --pass-with-no-tests",
|
|
41
|
+
"tsgo": "tsgo --noEmit",
|
|
42
|
+
"build": "rm -rf dist && tsgo",
|
|
43
|
+
"release": "bun pm pack --destination . && npm publish ./*.tgz --tag latest && rm -f ./*.tgz"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"kysely": "^0.28.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@syncular/config": "0.0.0",
|
|
50
|
+
"kysely": "*"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"src"
|
|
55
|
+
]
|
|
56
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration definition
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
DefinedMigrations,
|
|
7
|
+
MigrationFn,
|
|
8
|
+
MigrationRecord,
|
|
9
|
+
ParsedMigration,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parse a version key (e.g., 'v1', 'v2', '1', '2') into a version number.
|
|
14
|
+
*/
|
|
15
|
+
function parseVersionKey(key: string): number | null {
|
|
16
|
+
// Support both 'v1' and '1' formats
|
|
17
|
+
const match = key.match(/^v?(\d+)$/i);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const version = Number.parseInt(match[1]!, 10);
|
|
20
|
+
return Number.isNaN(version) ? null : version;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a function source string for checksum comparison.
|
|
25
|
+
* Strips comments and collapses whitespace so that formatting-only
|
|
26
|
+
* changes don't break checksums.
|
|
27
|
+
*/
|
|
28
|
+
function normalizeSource(source: string): string {
|
|
29
|
+
return source
|
|
30
|
+
.replace(/\/\/[^\n]*/g, '') // strip // comments
|
|
31
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // strip /* */ comments
|
|
32
|
+
.replace(/\s+/g, ' ') // collapse whitespace
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compute a simple checksum for a migration function.
|
|
38
|
+
* Used to detect if a migration has changed after being applied.
|
|
39
|
+
*/
|
|
40
|
+
function computeChecksum<DB>(fn: MigrationFn<DB>): string {
|
|
41
|
+
const fnStr = normalizeSource(fn.toString());
|
|
42
|
+
let hash = 0;
|
|
43
|
+
for (let i = 0; i < fnStr.length; i++) {
|
|
44
|
+
hash = (hash * 31 + fnStr.charCodeAt(i)) >>> 0;
|
|
45
|
+
}
|
|
46
|
+
return hash.toString(16).padStart(8, '0');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Define versioned migrations with automatic version parsing and sorting.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* export const migrations = defineMigrations({
|
|
55
|
+
* v1: async (db) => {
|
|
56
|
+
* await db.schema.createTable('tasks')
|
|
57
|
+
* .addColumn('id', 'text', col => col.primaryKey())
|
|
58
|
+
* .addColumn('title', 'text', col => col.notNull())
|
|
59
|
+
* .execute();
|
|
60
|
+
* },
|
|
61
|
+
* v2: async (db) => {
|
|
62
|
+
* await db.schema.alterTable('tasks')
|
|
63
|
+
* .addColumn('priority', 'integer', col => col.defaultTo(0))
|
|
64
|
+
* .execute();
|
|
65
|
+
* },
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function defineMigrations<
|
|
70
|
+
DB = unknown,
|
|
71
|
+
T extends MigrationRecord<DB> = MigrationRecord<DB>,
|
|
72
|
+
>(versionedMigrations: T): DefinedMigrations<DB> {
|
|
73
|
+
const migrations: ParsedMigration<DB>[] = [];
|
|
74
|
+
|
|
75
|
+
for (const [key, fn] of Object.entries(versionedMigrations)) {
|
|
76
|
+
const version = parseVersionKey(key);
|
|
77
|
+
if (version === null) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Invalid migration key "${key}": must be a version number (e.g., 'v1', 'v2', '1', '2')`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (version < 1) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid migration version ${version}: versions must be >= 1`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
migrations.push({
|
|
88
|
+
version,
|
|
89
|
+
name: key,
|
|
90
|
+
fn,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sort by version number
|
|
95
|
+
migrations.sort((a, b) => a.version - b.version);
|
|
96
|
+
|
|
97
|
+
// Check for duplicate versions
|
|
98
|
+
for (let i = 1; i < migrations.length; i++) {
|
|
99
|
+
if (migrations[i]!.version === migrations[i - 1]!.version) {
|
|
100
|
+
throw new Error(`Duplicate migration version ${migrations[i]!.version}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const currentVersion =
|
|
105
|
+
migrations.length > 0 ? migrations[migrations.length - 1]!.version : 0;
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
migrations,
|
|
109
|
+
currentVersion,
|
|
110
|
+
getMigration(version: number) {
|
|
111
|
+
return migrations.find((m) => m.version === version);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the checksum for a migration.
|
|
118
|
+
*/
|
|
119
|
+
export function getMigrationChecksum<DB>(
|
|
120
|
+
migration: ParsedMigration<DB>
|
|
121
|
+
): string {
|
|
122
|
+
return computeChecksum(migration.fn);
|
|
123
|
+
}
|
package/src/index.ts
ADDED
package/src/runner.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration runner
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getMigrationChecksum } from './define';
|
|
6
|
+
import {
|
|
7
|
+
clearAppliedMigrations,
|
|
8
|
+
ensureTrackingTable,
|
|
9
|
+
getAppliedMigrations,
|
|
10
|
+
recordAppliedMigration,
|
|
11
|
+
} from './tracking';
|
|
12
|
+
import type { RunMigrationsOptions, RunMigrationsResult } from './types';
|
|
13
|
+
|
|
14
|
+
const DEFAULT_TRACKING_TABLE = 'sync_migration_state';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run pending migrations and track their state.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { defineMigrations, runMigrations } from '@syncular/migrations';
|
|
22
|
+
*
|
|
23
|
+
* const migrations = defineMigrations({
|
|
24
|
+
* v1: async (db) => { ... },
|
|
25
|
+
* v2: async (db) => { ... },
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* const result = await runMigrations({
|
|
29
|
+
* db,
|
|
30
|
+
* migrations,
|
|
31
|
+
* trackingTable: 'sync_migration_state', // optional
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* console.log(`Applied versions: ${result.applied.join(', ')}`);
|
|
35
|
+
* console.log(`Current version: ${result.currentVersion}`);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export async function runMigrations<DB>(
|
|
39
|
+
options: RunMigrationsOptions<DB>
|
|
40
|
+
): Promise<RunMigrationsResult> {
|
|
41
|
+
const { db, migrations } = options;
|
|
42
|
+
const trackingTable = options.trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
43
|
+
const onChecksumMismatch = options.onChecksumMismatch ?? 'error';
|
|
44
|
+
|
|
45
|
+
// Ensure tracking table exists
|
|
46
|
+
await ensureTrackingTable(db, trackingTable);
|
|
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) => {
|
|
58
|
+
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
|
+
|
|
76
|
+
for (const migration of migrations.migrations) {
|
|
77
|
+
const existing = appliedByVersion.get(migration.version);
|
|
78
|
+
|
|
79
|
+
if (existing) {
|
|
80
|
+
// Migration already applied - verify checksum hasn't changed
|
|
81
|
+
const currentChecksum = getMigrationChecksum(migration);
|
|
82
|
+
if (existing.checksum !== currentChecksum) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Migration v${migration.version} (${migration.name}) has changed since it was applied. ` +
|
|
85
|
+
`Expected checksum ${existing.checksum}, got ${currentChecksum}. ` +
|
|
86
|
+
'Migrations must not be modified after being applied.'
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Run the migration
|
|
93
|
+
await migration.fn(db);
|
|
94
|
+
|
|
95
|
+
// Record it as applied
|
|
96
|
+
await recordAppliedMigration(db, trackingTable, {
|
|
97
|
+
version: migration.version,
|
|
98
|
+
name: migration.name,
|
|
99
|
+
checksum: getMigrationChecksum(migration),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
appliedVersions.push(migration.version);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
applied: appliedVersions,
|
|
107
|
+
currentVersion: migrations.currentVersion,
|
|
108
|
+
wasReset,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the current schema version without running any migrations.
|
|
114
|
+
*/
|
|
115
|
+
export async function getSchemaVersion<DB>(
|
|
116
|
+
db: import('kysely').Kysely<DB>,
|
|
117
|
+
trackingTable?: string
|
|
118
|
+
): Promise<number> {
|
|
119
|
+
const tableName = trackingTable ?? DEFAULT_TRACKING_TABLE;
|
|
120
|
+
const applied = await getAppliedMigrations(db, tableName);
|
|
121
|
+
if (applied.length === 0) return 0;
|
|
122
|
+
return applied[applied.length - 1]!.version;
|
|
123
|
+
}
|
package/src/tracking.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Migration tracking table helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Kysely, sql } from 'kysely';
|
|
6
|
+
import type { MigrationStateRow } from './types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Ensure the migration tracking table exists.
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureTrackingTable<DB>(
|
|
12
|
+
db: Kysely<DB>,
|
|
13
|
+
tableName: string
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
await db.schema
|
|
16
|
+
.createTable(tableName)
|
|
17
|
+
.ifNotExists()
|
|
18
|
+
.addColumn('version', 'integer', (col) => col.primaryKey())
|
|
19
|
+
.addColumn('name', 'text', (col) => col.notNull())
|
|
20
|
+
.addColumn('applied_at', 'text', (col) => col.notNull())
|
|
21
|
+
.addColumn('checksum', 'text', (col) => col.notNull())
|
|
22
|
+
.execute();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get all applied migrations from the tracking table.
|
|
27
|
+
*/
|
|
28
|
+
export async function getAppliedMigrations<DB, TTableName extends string>(
|
|
29
|
+
db: Kysely<DB>,
|
|
30
|
+
tableName: TTableName
|
|
31
|
+
): Promise<MigrationStateRow[]> {
|
|
32
|
+
await ensureTrackingTable(db, tableName);
|
|
33
|
+
|
|
34
|
+
const result = await sql<MigrationStateRow>`
|
|
35
|
+
select version, name, applied_at, checksum
|
|
36
|
+
from ${sql.table(tableName)}
|
|
37
|
+
order by version asc
|
|
38
|
+
`.execute(db);
|
|
39
|
+
|
|
40
|
+
return result.rows;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Record a migration as applied in the tracking table.
|
|
45
|
+
*/
|
|
46
|
+
export async function recordAppliedMigration<DB, TTableName extends string>(
|
|
47
|
+
db: Kysely<DB>,
|
|
48
|
+
tableName: TTableName,
|
|
49
|
+
migration: Omit<MigrationStateRow, 'applied_at'>
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
await ensureTrackingTable(db, tableName);
|
|
52
|
+
|
|
53
|
+
await sql`
|
|
54
|
+
insert into ${sql.table(tableName)} (version, name, applied_at, checksum)
|
|
55
|
+
values (
|
|
56
|
+
${migration.version},
|
|
57
|
+
${migration.name},
|
|
58
|
+
${new Date().toISOString()},
|
|
59
|
+
${migration.checksum}
|
|
60
|
+
)
|
|
61
|
+
`.execute(db);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear all rows from the migration tracking table.
|
|
66
|
+
* Used when resetting the database after a checksum mismatch.
|
|
67
|
+
*/
|
|
68
|
+
export async function clearAppliedMigrations<DB>(
|
|
69
|
+
db: Kysely<DB>,
|
|
70
|
+
tableName: string
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
await sql`delete from ${sql.table(tableName)}`.execute(db);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the current schema version from the tracking table.
|
|
77
|
+
* Returns 0 if no migrations have been applied.
|
|
78
|
+
*/
|
|
79
|
+
export async function getCurrentVersion<DB, TTableName extends string>(
|
|
80
|
+
db: Kysely<DB>,
|
|
81
|
+
tableName: TTableName
|
|
82
|
+
): Promise<number> {
|
|
83
|
+
const applied = await getAppliedMigrations(db, tableName);
|
|
84
|
+
if (applied.length === 0) return 0;
|
|
85
|
+
return applied[applied.length - 1]!.version;
|
|
86
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/migrations - Type definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Kysely } from 'kysely';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single migration function that modifies the database schema.
|
|
9
|
+
*/
|
|
10
|
+
export type MigrationFn<DB = unknown> = (db: Kysely<DB>) => Promise<void>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Record of versioned migrations keyed by version string (e.g., 'v1', 'v2').
|
|
14
|
+
*/
|
|
15
|
+
export type MigrationRecord<DB = unknown> = Record<string, MigrationFn<DB>>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parsed migration with version number and function.
|
|
19
|
+
*/
|
|
20
|
+
export interface ParsedMigration<DB = unknown> {
|
|
21
|
+
version: number;
|
|
22
|
+
name: string;
|
|
23
|
+
fn: MigrationFn<DB>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of defineMigrations() - contains migrations and metadata.
|
|
28
|
+
*/
|
|
29
|
+
export interface DefinedMigrations<DB = unknown> {
|
|
30
|
+
/** Sorted list of migrations */
|
|
31
|
+
migrations: ParsedMigration<DB>[];
|
|
32
|
+
/** Current (latest) schema version */
|
|
33
|
+
currentVersion: number;
|
|
34
|
+
/** Get migration by version number */
|
|
35
|
+
getMigration(version: number): ParsedMigration<DB> | undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Migration state row stored in the tracking table.
|
|
40
|
+
*/
|
|
41
|
+
export interface MigrationStateRow {
|
|
42
|
+
version: number;
|
|
43
|
+
name: string;
|
|
44
|
+
applied_at: string;
|
|
45
|
+
checksum: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for running migrations.
|
|
50
|
+
*/
|
|
51
|
+
export interface RunMigrationsOptions<DB = unknown> {
|
|
52
|
+
/** Kysely database instance */
|
|
53
|
+
db: Kysely<DB>;
|
|
54
|
+
/** Defined migrations from defineMigrations() */
|
|
55
|
+
migrations: DefinedMigrations<DB>;
|
|
56
|
+
/** Name of the tracking table (default: 'sync_migration_state') */
|
|
57
|
+
trackingTable?: string;
|
|
58
|
+
/** What to do when a migration's checksum doesn't match. Default: 'error' */
|
|
59
|
+
onChecksumMismatch?: 'error' | 'reset';
|
|
60
|
+
/** Called before clearing tracking state and re-running migrations.
|
|
61
|
+
* Use this to drop application tables so migrations can recreate them. */
|
|
62
|
+
beforeReset?: (db: Kysely<DB>) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of running migrations.
|
|
67
|
+
*/
|
|
68
|
+
export interface RunMigrationsResult {
|
|
69
|
+
/** Versions that were applied in this run */
|
|
70
|
+
applied: number[];
|
|
71
|
+
/** Current schema version after migration */
|
|
72
|
+
currentVersion: number;
|
|
73
|
+
/** True if a checksum mismatch triggered a full reset */
|
|
74
|
+
wasReset: boolean;
|
|
75
|
+
}
|