-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmigrate.ts
173 lines (152 loc) · 5.47 KB
/
migrate.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import {
extname,
readLines,
relative,
resolve,
StringReader,
toFileUrl,
walk,
} from "./deps.ts";
export interface MigrationFile {
id: number;
path: string;
}
export interface Migration {
id: number;
path?: string;
appliedPath?: string;
appliedAt?: Date;
createdAt: Date;
updatedAt: Date;
}
export function assertMigrationFile(
migration: Migration,
): asserts migration is Migration & MigrationFile {
if (!migration.path) throw new Error("no path for migration");
}
export const MIGRATION_FILENAME = /^(\d+)(?:_.+)?.(sql|js|ts|json)$/;
/** Migration query configuration. */
export interface MigrationQueryConfig {
text: string;
args?: unknown[];
}
export type MigrationQuery = string | MigrationQueryConfig;
/** JSON representation of a migration. */
export interface MigrationJSON {
queries: MigrationQuery[];
disableTransaction?: boolean;
}
/** A script for generating migration queries. */
export interface MigrationScript<GenerateOptions = unknown> {
generateQueries(options?: GenerateOptions):
| Iterable<MigrationQuery>
| AsyncIterable<MigrationQuery>;
disableTransaction?: boolean;
}
export interface MigrationPlan {
queries: Iterable<MigrationQuery> | AsyncIterable<MigrationQuery>;
useTransaction: boolean;
}
export interface MigrateOptions<GenerateOptions = unknown> {
migrationsDir?: string;
generateOptions?: GenerateOptions;
}
export interface MigrateLockOptions {
signal?: AbortSignal;
}
export interface MigrateLock {
release(): Promise<void>;
}
/** Base class for object used to apply migrations. */
export abstract class Migrate<GenerateOptions = unknown> {
migrationsDir: string;
generateOptions?: GenerateOptions;
constructor(options: MigrateOptions<GenerateOptions>) {
this.migrationsDir = resolve(options.migrationsDir ?? "./migrations");
this.generateOptions = options.generateOptions;
}
/** Creates the migration table. */
abstract init(): Promise<void>;
/** Connects the client. */
abstract connect(): Promise<void>;
/** Ends the client connection. */
abstract end(): Promise<void>;
/**
* Acquires an advisory lock for the migrate client.
* This can be used to ensure only one instance of the migrate script runs at a time.
* Without locking, it's possible that a migration may get applied multiple times.
* The lock should be acquired before getting the list of unapplied migrations and
* the lock should be released after the migrations are applied.
* With the options, you can override the default advisory lock id and specify an abort signal.
* The abort signal is only used to abort attempts to acquire it,
* it will not release an already acquired lock.
*/
abstract lock(options?: MigrateLockOptions): Promise<MigrateLock>;
/** Get the current date from the client plus the optional offset in milliseconds. */
abstract now(offset?: number): Promise<Date>;
/**
* Loads all migration's current path values into the migration table.
* If file was deleted, path will be set to null.
*/
abstract load(): Promise<void>;
/** Gets all loaded migrations, sorted by id. */
abstract getAll(): Promise<Migration[]>;
/** Gets all loaded migrations that have not been applied yet, sorted by id. */
abstract getUnapplied(): Promise<Migration[]>;
/** Applies a migration. */
abstract apply(migration: Migration): Promise<void>;
/** Resolves the relative migration path in the migrations directory. */
resolve(migration: Migration): string {
assertMigrationFile(migration);
return resolve(this.migrationsDir, migration.path);
}
/** Gets id and path for all migration files, sorted by id. */
async getFiles(): Promise<MigrationFile[]> {
const migrations = new Map<number, MigrationFile>();
for await (const entry of walk(this.migrationsDir)) {
const match = entry.isFile && entry.name.match(MIGRATION_FILENAME);
if (!match) continue;
const id = parseInt(match[1]);
const path = relative(this.migrationsDir, entry.path);
if (migrations.has(id)) {
throw new Error(`migration id collision on ${id}`);
}
migrations.set(id, { id, path });
}
return [...migrations.values()].sort((a, b) => a.id - b.id);
}
/** Gets a migration plan for a migration from its file. */
async getPlan(migration: Migration): Promise<MigrationPlan> {
assertMigrationFile(migration);
const path = this.resolve(migration);
let useTransaction = true;
let queries: Iterable<MigrationQuery> | AsyncIterable<MigrationQuery>;
const ext = extname(path).toLowerCase();
if (ext === ".sql") {
const query = await Deno.readTextFile(path);
for await (const line of readLines(new StringReader(query))) {
if (line.slice(0, 2) !== "--") break;
if (line === "-- migrate disableTransaction") useTransaction = false;
}
queries = [query];
} else if (ext === ".json") {
const migration: MigrationJSON = JSON.parse(
await Deno.readTextFile(path),
);
({ queries } = migration);
useTransaction = !migration.disableTransaction;
} else {
const { generateQueries, disableTransaction }: MigrationScript<
GenerateOptions
> = await import(toFileUrl(path).href);
if (!generateQueries) {
throw new Error(
"migration script must export generateQueries function",
);
}
queries = generateQueries(this.generateOptions);
useTransaction = !disableTransaction;
}
return { queries, useTransaction };
}
}