@dungarees/datasource 0.11.4
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/fake.d.ts +35 -0
- package/fake.js +51 -0
- package/fake.test.d.ts +1 -0
- package/fake.test.js +558 -0
- package/helpers.d.ts +2 -0
- package/helpers.js +4 -0
- package/migration/inmemory-migrator.d.ts +2 -0
- package/migration/inmemory-migrator.js +126 -0
- package/migration/kysely-migrator.d.ts +2 -0
- package/migration/kysely-migrator.js +6 -0
- package/migration/migration.d.ts +4 -0
- package/migration/migration.js +5 -0
- package/migration/provider.d.ts +5 -0
- package/migration/provider.js +39 -0
- package/migration/service.d.ts +2 -0
- package/migration/service.js +24 -0
- package/migration/type.d.ts +48 -0
- package/migration/type.js +1 -0
- package/package.json +510 -0
- package/postgres/fake.d.ts +2 -0
- package/postgres/fake.js +45 -0
- package/postgres/service.d.ts +11 -0
- package/postgres/service.js +23 -0
- package/service.d.ts +2 -0
- package/service.js +1 -0
- package/service.test.d.ts +1 -0
- package/service.test.js +37 -0
- package/test-migrations/001-test-migration.d.ts +2 -0
- package/test-migrations/001-test-migration.js +13 -0
- package/test-migrations/002-test-migration.d.ts +2 -0
- package/test-migrations/002-test-migration.js +9 -0
- package/test-migrations/003-test-migration.d.ts +2 -0
- package/test-migrations/003-test-migration.js +9 -0
- package/test-migrations-with-error/001-test-migration.d.ts +2 -0
- package/test-migrations-with-error/001-test-migration.js +13 -0
- package/test-migrations-with-error/002-test-migration.d.ts +2 -0
- package/test-migrations-with-error/002-test-migration.js +10 -0
- package/type.d.ts +5 -0
- package/type.js +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { getMigrationProvider } from './provider.js';
|
|
2
|
+
import { NO_MIGRATIONS, } from 'kysely/migration';
|
|
3
|
+
const byName = (a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
4
|
+
// The applied list is kept in memory because an in-process database has nowhere to persist
|
|
5
|
+
// migration bookkeeping between instances.
|
|
6
|
+
export const createInMemoryMigrator = ({ datasource, ...source }) => {
|
|
7
|
+
const provider = getMigrationProvider(source);
|
|
8
|
+
const applied = [];
|
|
9
|
+
// Per migrator rather than module wide, so two migrators in one test file cannot hand out
|
|
10
|
+
// interleaved timestamps. Only the ordering matters, never the actual instant.
|
|
11
|
+
let executionCounter = 0;
|
|
12
|
+
const getSortedMigrations = async () => Object.entries(await provider.getMigrations())
|
|
13
|
+
.map(([name, migration]) => ({ ...migration, name }))
|
|
14
|
+
.sort((a, b) => byName(a.name, b.name));
|
|
15
|
+
const runMigration = async ({ migration, direction, isSkipped, }) => {
|
|
16
|
+
if (isSkipped) {
|
|
17
|
+
return { migrationName: migration.name, direction, error: undefined, isSkipped };
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
if (direction === 'Up') {
|
|
21
|
+
await migration.up(datasource);
|
|
22
|
+
applied.push({ name: migration.name, executedAt: new Date(executionCounter++) });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await migration.down?.(datasource);
|
|
26
|
+
applied.pop();
|
|
27
|
+
}
|
|
28
|
+
return { migrationName: migration.name, direction, error: undefined, isSkipped };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { migrationName: migration.name, direction, error, isSkipped };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Sequential on purpose: a migration may depend on the one before it, so running the steps
|
|
35
|
+
// concurrently would apply them in an order the author never wrote.
|
|
36
|
+
const runSteps = async (steps) => {
|
|
37
|
+
const outcomes = [];
|
|
38
|
+
for (const step of steps) {
|
|
39
|
+
outcomes.push(await runMigration(step));
|
|
40
|
+
}
|
|
41
|
+
return toResultSet(outcomes);
|
|
42
|
+
};
|
|
43
|
+
const toResultSet = (outcomes) => ({
|
|
44
|
+
error: outcomes.find(({ error }) => error !== undefined)?.error,
|
|
45
|
+
results: outcomes
|
|
46
|
+
.slice()
|
|
47
|
+
.sort((a, b) => byName(a.migrationName, b.migrationName))
|
|
48
|
+
.map(({ migrationName, direction, error, isSkipped }) => ({
|
|
49
|
+
migrationName,
|
|
50
|
+
status: getStatus({ isSkipped, error }),
|
|
51
|
+
direction,
|
|
52
|
+
})),
|
|
53
|
+
});
|
|
54
|
+
const getStatus = ({ isSkipped, error, }) => {
|
|
55
|
+
if (isSkipped) {
|
|
56
|
+
return 'NotExecuted';
|
|
57
|
+
}
|
|
58
|
+
return error !== undefined ? 'Error' : 'Success';
|
|
59
|
+
};
|
|
60
|
+
const isNoMigrations = (target) => target === NO_MIGRATIONS || typeof target !== 'string';
|
|
61
|
+
return {
|
|
62
|
+
migrateToLatest: async () => {
|
|
63
|
+
const migrations = await getSortedMigrations();
|
|
64
|
+
return await runSteps(migrations
|
|
65
|
+
.slice(applied.length)
|
|
66
|
+
.map((migration) => ({ migration, direction: 'Up', isSkipped: false })));
|
|
67
|
+
},
|
|
68
|
+
migrateUp: async () => {
|
|
69
|
+
const migrations = await getSortedMigrations();
|
|
70
|
+
const next = migrations[applied.length];
|
|
71
|
+
return next === undefined
|
|
72
|
+
? { error: undefined, results: [] }
|
|
73
|
+
: await runSteps([{ migration: next, direction: 'Up', isSkipped: false }]);
|
|
74
|
+
},
|
|
75
|
+
migrateDown: async () => {
|
|
76
|
+
const migrations = await getSortedMigrations();
|
|
77
|
+
const last = migrations[applied.length - 1];
|
|
78
|
+
return last === undefined
|
|
79
|
+
? { error: undefined, results: [] }
|
|
80
|
+
: await runSteps([{ migration: last, direction: 'Down', isSkipped: false }]);
|
|
81
|
+
},
|
|
82
|
+
migrateTo: async (target) => {
|
|
83
|
+
const migrations = await getSortedMigrations();
|
|
84
|
+
// NO_MIGRATIONS has no index of its own, and -1 makes every applied migration fall into the
|
|
85
|
+
// "after the target" slice below, which is exactly migrating back to empty.
|
|
86
|
+
const targetIndex = isNoMigrations(target)
|
|
87
|
+
? -1
|
|
88
|
+
: migrations.findIndex(({ name }) => name === target);
|
|
89
|
+
if (targetIndex === -1 && !isNoMigrations(target)) {
|
|
90
|
+
throw new Error(`Migration ${typeof target === 'string' ? target : 'NO_MIGRATIONS'} not found`);
|
|
91
|
+
}
|
|
92
|
+
const currentIndex = applied.length;
|
|
93
|
+
const direction = currentIndex < targetIndex ? 'Up' : 'Down';
|
|
94
|
+
const toRun = direction === 'Up'
|
|
95
|
+
? migrations.slice(currentIndex, targetIndex + 1)
|
|
96
|
+
: migrations.slice(targetIndex + 1, currentIndex).reverse();
|
|
97
|
+
const runSet = new Set(toRun);
|
|
98
|
+
return await runSteps([
|
|
99
|
+
...toRun.map((migration) => ({ migration, direction, isSkipped: false })),
|
|
100
|
+
...migrations
|
|
101
|
+
.filter((migration) => !runSet.has(migration))
|
|
102
|
+
.map((migration) => ({ migration, direction, isSkipped: true })),
|
|
103
|
+
]);
|
|
104
|
+
},
|
|
105
|
+
getMigrations: async () => {
|
|
106
|
+
const migrations = await getSortedMigrations();
|
|
107
|
+
return migrations.map((named) => {
|
|
108
|
+
const executed = applied.find((entry) => entry.name === named.name);
|
|
109
|
+
return {
|
|
110
|
+
name: named.name,
|
|
111
|
+
...(executed !== undefined && { executedAt: executed.executedAt }),
|
|
112
|
+
// Wrapped rather than handed over directly, so `up` and `down` stay attached to the
|
|
113
|
+
// migration they came from instead of being called detached from it.
|
|
114
|
+
migration: {
|
|
115
|
+
up: async (db) => {
|
|
116
|
+
await named.up(db);
|
|
117
|
+
},
|
|
118
|
+
down: async (db) => {
|
|
119
|
+
await named.down?.(db);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { getMigrationProvider } from './provider.js';
|
|
2
|
+
import { Migrator as KyselyMigrator } from 'kysely/migration';
|
|
3
|
+
export const createKyselyMigrator = ({ datasource, ...source }) => new KyselyMigrator({
|
|
4
|
+
db: datasource,
|
|
5
|
+
provider: getMigrationProvider(source),
|
|
6
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const createMigration = ({ up, down }) => ({ up, down });
|
|
2
|
+
// Set by the test environment so a migration that would be slow or destructive against a fixture
|
|
3
|
+
// can be skipped, while still running for real against a real database.
|
|
4
|
+
const TEST_ENV_VAR = 'TEST';
|
|
5
|
+
export const ignoreForTests = async (action, fallbackValue) => (process.env[TEST_ENV_VAR] !== 'true' ? await action() : fallbackValue);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MigrationSource } from './type.ts';
|
|
2
|
+
import type { Migration, MigrationProvider } from 'kysely/migration';
|
|
3
|
+
export declare const createRecordMigrationProvider: (migrations: Record<string, Migration>) => MigrationProvider;
|
|
4
|
+
export declare const createFolderMigrationProvider: (migrationsFolder: string) => MigrationProvider;
|
|
5
|
+
export declare const getMigrationProvider: (source: MigrationSource) => MigrationProvider;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, join } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
const MIGRATION_EXTENSIONS = ['.ts', '.js', '.mjs'];
|
|
5
|
+
export const createRecordMigrationProvider = (migrations) => ({
|
|
6
|
+
getMigrations: async () => await Promise.resolve(migrations),
|
|
7
|
+
});
|
|
8
|
+
// Imported by file url rather than by path: a bare path is resolved relative to this module, and a
|
|
9
|
+
// Windows path is not a valid module specifier at all.
|
|
10
|
+
export const createFolderMigrationProvider = (migrationsFolder) => ({
|
|
11
|
+
getMigrations: async () => {
|
|
12
|
+
const files = await readdir(migrationsFolder);
|
|
13
|
+
const migrationFiles = files
|
|
14
|
+
.filter((name) => MIGRATION_EXTENSIONS.includes(extname(name)) && !name.endsWith('.d.ts'))
|
|
15
|
+
.sort();
|
|
16
|
+
return Object.fromEntries(await Promise.all(migrationFiles.map(async (fileName) => [
|
|
17
|
+
basename(fileName, extname(fileName)),
|
|
18
|
+
await importMigration(join(migrationsFolder, fileName)),
|
|
19
|
+
])));
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const importMigration = async (filePath) => {
|
|
23
|
+
const imported = await import(pathToFileURL(filePath).href);
|
|
24
|
+
// A migration module may default-export the migration or be the migration itself.
|
|
25
|
+
const migration = typeof imported === 'object' && imported !== null && 'default' in imported
|
|
26
|
+
? Reflect.get(imported, 'default')
|
|
27
|
+
: imported;
|
|
28
|
+
if (!isMigration(migration)) {
|
|
29
|
+
throw new Error(`${filePath} does not export a migration with an up function`);
|
|
30
|
+
}
|
|
31
|
+
return migration;
|
|
32
|
+
};
|
|
33
|
+
const isMigration = (value) => typeof value === 'object' &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
'up' in value &&
|
|
36
|
+
typeof Reflect.get(value, 'up') === 'function';
|
|
37
|
+
export const getMigrationProvider = (source) => 'migrations' in source
|
|
38
|
+
? createRecordMigrationProvider(source.migrations)
|
|
39
|
+
: createFolderMigrationProvider(source.migrationsFolder);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NO_MIGRATIONS } from 'kysely/migration';
|
|
2
|
+
export const createMigrator = (runner) => ({
|
|
3
|
+
migrateToLatest: async () => toResultList(await runner.migrateToLatest()),
|
|
4
|
+
migrateToEmpty: async () => toResultList(await runner.migrateTo(NO_MIGRATIONS)),
|
|
5
|
+
migrateTo: async (migrationName) => toResultList(await runner.migrateTo(migrationName)),
|
|
6
|
+
migrateUpStep: async () => toResultList(await runner.migrateUp()),
|
|
7
|
+
migrateDownStep: async () => toResultList(await runner.migrateDown()),
|
|
8
|
+
getMigrations: async () => (await runner.getMigrations()).map(toMigrationDetails),
|
|
9
|
+
});
|
|
10
|
+
// kysely types the failure as unknown, because a migration can throw anything at all.
|
|
11
|
+
const toError = (error) => {
|
|
12
|
+
if (error === undefined) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return error instanceof Error ? error : new Error(JSON.stringify(error));
|
|
16
|
+
};
|
|
17
|
+
const toResultList = ({ error, results }) => ({
|
|
18
|
+
error: toError(error),
|
|
19
|
+
results: results ?? [],
|
|
20
|
+
});
|
|
21
|
+
const toMigrationDetails = ({ name, executedAt }) => ({
|
|
22
|
+
name,
|
|
23
|
+
executedAt,
|
|
24
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AnySchema, Datasource } from '../type.ts';
|
|
2
|
+
import type { Migration, MigrationInfo, MigrationResultSet, NoMigrations } from 'kysely/migration';
|
|
3
|
+
export type MigrationScripts = {
|
|
4
|
+
up: (db: Datasource<AnySchema>) => Promise<void>;
|
|
5
|
+
down: (db: Datasource<AnySchema>) => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
export type MigrationSource = {
|
|
8
|
+
migrationsFolder: string;
|
|
9
|
+
} | {
|
|
10
|
+
migrations: Record<string, Migration>;
|
|
11
|
+
};
|
|
12
|
+
export type MigratorConfig = {
|
|
13
|
+
datasource: Datasource<AnySchema>;
|
|
14
|
+
} & MigrationSource;
|
|
15
|
+
export type MigrationRunner = {
|
|
16
|
+
migrateToLatest: () => Promise<MigrationResultSet>;
|
|
17
|
+
migrateTo: (target: string | NoMigrations) => Promise<MigrationResultSet>;
|
|
18
|
+
migrateUp: () => Promise<MigrationResultSet>;
|
|
19
|
+
migrateDown: () => Promise<MigrationResultSet>;
|
|
20
|
+
getMigrations: () => Promise<ReadonlyArray<MigrationInfo>>;
|
|
21
|
+
};
|
|
22
|
+
export type Migrator = {
|
|
23
|
+
migrateUpStep: () => Promise<MigrationResultList>;
|
|
24
|
+
migrateDownStep: () => Promise<MigrationResultList>;
|
|
25
|
+
migrateTo: (migrationName: string) => Promise<MigrationResultList>;
|
|
26
|
+
migrateToLatest: () => Promise<MigrationResultList>;
|
|
27
|
+
migrateToEmpty: () => Promise<MigrationResultList>;
|
|
28
|
+
getMigrations: () => Promise<MigrationDetails[]>;
|
|
29
|
+
};
|
|
30
|
+
export type MigrationDetails = {
|
|
31
|
+
name: string;
|
|
32
|
+
executedAt: Date | undefined;
|
|
33
|
+
};
|
|
34
|
+
export type MigrationDirection = 'Up' | 'Down';
|
|
35
|
+
export type MigrationStatus = 'Success' | 'Error' | 'NotExecuted';
|
|
36
|
+
export type MigrationResult = {
|
|
37
|
+
migrationName: string;
|
|
38
|
+
status: MigrationStatus;
|
|
39
|
+
direction: MigrationDirection;
|
|
40
|
+
};
|
|
41
|
+
export type MigrationResultList = {
|
|
42
|
+
error: Error | undefined;
|
|
43
|
+
results: MigrationResult[];
|
|
44
|
+
};
|
|
45
|
+
export type IgnoreForTests = {
|
|
46
|
+
<T>(action: () => Promise<T>): Promise<T | undefined>;
|
|
47
|
+
<T>(action: () => Promise<T>, fallbackValue: T): Promise<T>;
|
|
48
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|