sqlmorpheus 0.2.6 → 0.2.7

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 CHANGED
@@ -1,3 +1,124 @@
1
- Ever wanted to use a migration tool but couldn't because is uses a different stack than your project and would add unnecessary overhead or would simply not work in your setup at all? SQLMorpheus is an automatic migration library that is as unopinionated as it gets:
2
-
3
- - It does not use its own database framework
1
+ # SqlMorpheus
2
+
3
+ SQLMorpheus is a standalone SQL migration tool that automatically calculates the differences between a database and a defined state and updates the database structure accordingly. It is not bundled with any ORM or limited to a specific SQL dialect or SQL library, making it very flexible and easy to integrate into any (new or preexisting) project.
4
+
5
+ Its stand-out features are:
6
+ - Database **changes are calculated at runtime**, allowing SqlMorpheus to be used with different database states without expecting any existing structure or having to recompile your app.
7
+ - SQLMorpheus **does not bundle its own SQL library** but allows you to define database access yourself using any library you want. This not only prevents unnecessary overhead, it also makes SQLMorpheus very flexible and easy to migrate between database flavours.
8
+ - While SQLMorpheus can work without (almost) any manual input, it optionally **allows for destructive changes to be confirmed** to prevent possible data loss.
9
+ - It **keeps a history of changes** (and adds an interface for backups before migrations) and allows viewing all changes made and even roll back to a specific version.
10
+
11
+
12
+ ## How to install?
13
+ To install SqlMorpheus, run
14
+ ```bash
15
+ npm install sqlmorpheus
16
+ ```
17
+
18
+ ## How to use?
19
+
20
+ Because SqlMorpheus relies on the framework you choose for database access, it needs a little bit more setup code. But you only need to copy & paste a few simple lines of code:
21
+
22
+ ### 1. Access the database
23
+ For SqlMorpheus to be able to access your database, you need to provide its `runMigration` call (see bellow) with a `dbAccess` object with three callback functions: `runReadStatement()`, `runWriteStatement()` and `runTransaction()`.
24
+ Here is a full example for SQLite using better-sqlite3
25
+ For other SQL types you can copy one of the provided examples (see [MySQL](/examples/DatabaseAccess_for_MySQL.md), [Postgres](/examples/DatabaseAccess_for_Postgres.md), [SQLite](/examples/DatabaseAccess_for_SQLite.md))
26
+
27
+ ```typescript
28
+ import {DatabaseAccess, runMigration} from "sqlmorpheus";
29
+ import BetterSqlite3 from "better-sqlite3";
30
+
31
+ const db = new BetterSqlite3(":memory:");
32
+ const dbAccess: DatabaseAccess = {
33
+ runReadStatement: async (query: string) => db.prepare(query).all(),
34
+ runWriteStatement: async (query: string) => db.prepare(query).run(),
35
+ runTransaction: async (query: string) => {
36
+ const transaction = db.transaction(() => db.exec(query));
37
+ transaction();
38
+ }
39
+ }
40
+ ```
41
+ <details>
42
+ <summary>Optional: `createBackup()` in DatabaseAccess.</summary>
43
+
44
+ On top of the three mandatory methods, you can also define `createBackup()`. This callback is called whenever the `version` in `DatabaseInstructions` has changed and SqlMorpheus needs to change the database. An example implementation for a backup in SQLite would be:
45
+
46
+ ```typescript
47
+ const dbAccess: DatabaseAccess = {
48
+ ...,
49
+ createBackup: async (backupName: string) => {
50
+ await db.backup(`backups/${backupName}.sqlite`)
51
+ }
52
+ }
53
+ ```
54
+ </details>
55
+
56
+
57
+ ### 2. Define the database structure
58
+ This part might require the most work from you, but should be familiar to you if you have already used other migration libraries: You need to tell SqlMorpheus what structure it should mold your database into. To make this process as simple as possible, SqlMorpheus provides a convenient `TableObj.create()` builder (see [TableObj example](/examples/database_structure_using_TableObj.md)) or, if you prefer, you can also use decorators (see [decorator example](/examples/database_structure_using_decorators.md)).
59
+ Here is an example using `TableObj.create()`:
60
+ ```typescript
61
+ import {DatabaseAccess, runMigration} from "sqlmorpheus";
62
+ const usersTable = TableObj.create("User", {
63
+ userId: [0, {primaryKey: true}],
64
+ name: "Default name",
65
+ email: [null, {dataType: "string"}]
66
+ });
67
+ const messagesTable = TableObj.create("Message", {
68
+ messageId: [0, {primaryKey: true}],
69
+ from: 0,
70
+ to: 0,
71
+ content: ""
72
+ })
73
+ .foreignKey("from", usersTable, "userId", {onDelete: "CASCADE"})
74
+ .foreignKey("to", usersTable, "userId", {onDelete: "CASCADE"});
75
+ ```
76
+
77
+ ### 3. Create the database instructions and run the migration
78
+ Now we combine everything and call `runMigration()`.
79
+
80
+ Example:
81
+ ```typescript
82
+ const instructions: DatabaseInstructions = {
83
+ dialect: "Sqlite",
84
+ tables: [userTable, messagesTable], // usersTable and messagesTable are defined above
85
+ version: 1
86
+ };
87
+
88
+ await runMigration(dbAccess, instructions); // dbAccess is defined above
89
+ ```
90
+ <details>
91
+ <summary>More information about `DatabaseInstructions`.</summary>
92
+
93
+ #### `dialect`
94
+ This property tells SqlMorpheus which SQL dialect your database uses.
95
+
96
+ #### `tables`
97
+ This property contains all tables that should be created in the database.
98
+
99
+ #### `version`
100
+ SqlMorpheus will only compare the database structure when the database version is different to the version property defined in DatabaseInstructions. The version property is also used to decide which manual migrations defined in `preMigration()` should be executed.
101
+
102
+ #### `preMigration()`
103
+ This method is run before database migrations (and comparisons) are executed. This is also the place where you allow specific destructive migrations or tell SqlMorpheus that you want to rename a table or column (rather than removing and adding it – which would be the default behaviour).
104
+
105
+ #### `postMigration()`
106
+ This method is run after the migration was finished. You will very rarely need it.
107
+
108
+ > All properties are documented in [DatabaseInstructions](/src/lib/typings/DatabaseInstructions.ts).
109
+ </details>
110
+
111
+ ## When does SqlMorpheus check for migrations?
112
+ SqlMorpheus will only compare the database when the [version property](#version) is different to the version saved in the database itself (or if the database has no version defined at all).
113
+
114
+ ## How to rename a table or column?
115
+ SqlMorpheus has no way of telling if you want to rename a table / column or want to remove it and add a completely new one. To solve this, it relies on your input to tell it what to do (by default it just removes and adds a table / column).
116
+ To tell it to rename a table / column, you call `migration.renameTable(VERSION, "OLD_TABLENAME", "NEW_TABLENAME");` in `preMigration()` ([example](/examples/example_renaming.md))
117
+
118
+ ## rollback your database
119
+ SqlMorpheus saves its changes into the database (into a table called `__sqlmorpheus_migrations`) which allows you to roll back the database to a previous state with a single command:
120
+
121
+ ```typescript
122
+ // `dbAccess` and `instructions` are defined above
123
+ await rollback(dbAccess, TARGET_VERSION, instructions.dialect);
124
+ ```
package/dist/index.d.mts CHANGED
@@ -1,7 +1,8 @@
1
1
  interface DatabaseAccess {
2
- createBackup?(backupName: string): Promise<void>;
3
- runGetStatement(query: string): Promise<unknown>;
4
- runMultipleWriteStatements(query: string): Promise<void>;
2
+ createBackup?(backupName: string): Promise<any>;
3
+ runReadStatement(query: string): Promise<unknown>;
4
+ runWriteStatement(query: string): Promise<any>;
5
+ runTransaction(query: string): Promise<any>;
5
6
  }
6
7
 
7
8
  type Class<T> = new (...args: never[]) => T;
@@ -12,14 +13,16 @@ type TableClassInterface = {
12
13
  [key: string]: any;
13
14
  };
14
15
 
16
+ declare const ALLOWED = "allowed";
17
+ declare const USED = "allowedAndUsed";
18
+ type AllowedMigrationState = typeof ALLOWED | typeof USED;
15
19
  interface AllowedMigrations {
16
- recreateTable?: boolean;
17
- dropTable?: boolean;
18
- dropColumn?: boolean;
19
- alterPrimaryKey?: boolean;
20
- removeForeignKey?: boolean;
21
- alterForeignKey?: boolean;
22
- continueWithoutRollback?: boolean;
20
+ recreateTable?: Record<string, AllowedMigrationState>;
21
+ dropTable?: Record<string, AllowedMigrationState>;
22
+ dropColumn?: Record<string, AllowedMigrationState>;
23
+ alterPrimaryKey?: Record<string, AllowedMigrationState>;
24
+ removeForeignKey?: Record<string, AllowedMigrationState>;
25
+ alterForeignKey?: Record<string, AllowedMigrationState>;
23
26
  }
24
27
 
25
28
  interface RenameData {
@@ -34,7 +37,6 @@ interface MigrationInstructions {
34
37
  */
35
38
  renamedColumns: RenameData[];
36
39
  allowedMigrations: AllowedMigrations;
37
- usedMigrations: Record<string, boolean>;
38
40
  }
39
41
 
40
42
  interface ColumnInfo {
@@ -81,14 +83,6 @@ declare class Migrations {
81
83
  * @param fromVersion - The version of the actual database.
82
84
  */
83
85
  reset(dbInstructions: DatabaseInstructions, fromVersion: number): void;
84
- /**
85
- * Retrieves the migration instructions for the specified table.
86
- * @see getEntryFromTableName
87
- *
88
- * @param table - The {@link TableInput} object from which the table name will be retrieved.
89
- * @return The migration instructions corresponding to the given table input.
90
- */
91
- private getEntry;
92
86
  /**
93
87
  * Retrieves the migration instructions for the specified table.
94
88
  *
@@ -96,13 +90,6 @@ declare class Migrations {
96
90
  * @return The migration instructions corresponding to the given table input.
97
91
  */
98
92
  private getEntryFromTableName;
99
- /**
100
- * Retrieves the table name from the given table input.
101
- *
102
- * @param table - The {@link TableInput} object.
103
- * @return The name of the table derived from either {@link TableObj} or a class.
104
- */
105
- private getTableName;
106
93
  /**
107
94
  * Checks whether a provided version is relevant based on database version ({@link fromVersion}) and target version ({@link toVersion}).
108
95
  * Also makes sure that version checks happen in order.
@@ -136,8 +123,9 @@ declare class Migrations {
136
123
  * @see allowMigration()
137
124
  * @param tableName - The name of the table to check the migration against.
138
125
  * @param type - The type of destructive migration being checked.
126
+ * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
139
127
  */
140
- compareWithAllowedMigration(tableName: string, type: keyof AllowedMigrations): void;
128
+ compareWithAllowedMigration(tableName: string, type: keyof AllowedMigrations, column?: string): void;
141
129
  /**
142
130
  * Retrieves the migration data containing instructions for data migration.
143
131
  * Only used internally.
@@ -205,10 +193,13 @@ declare class Migrations {
205
193
  * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
206
194
  *
207
195
  * @param version - The target version for which the specific migration is allowed. Must be provided to make sure this method will only have an effect when specified.
208
- * @param table - Either a {@link TableObj} or a class decorated with @TableClass.
209
- * @param allowedMigrations - The keys representing the migrations to be allowed. Multiple migrations can be provided
196
+ * @param tableName - The current table name in the database at the time of the migration version.
197
+ * @param allowedMigration - The keys representing the migrations to be allowed.
198
+ * @param column - The column name for which the migration is allowed. Only needed for "dropColumn", "removeForeignKey" and "alterForeignKey".
199
+ *
200
+ * @throws Error if the provided column name is not valid for the specified migration.
210
201
  */
211
- allowMigration(version: number, table: TableInput, ...allowedMigrations: (keyof AllowedMigrations)[]): void;
202
+ allowMigration(version: number, tableName: string, allowedMigration: keyof AllowedMigrations, column?: string): void;
212
203
  /**
213
204
  * When you change the name of a table, SqlMorpheus will assume the original table was meant to be dropped
214
205
  * and the new table was meant to be added, which would lead to data loss.
@@ -216,10 +207,10 @@ declare class Migrations {
216
207
  * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
217
208
  *
218
209
  * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
219
- * @param oldTableName - The current name in the database at the time of the migration.
220
- * @param newTableName - The new table name or its equivalent input format.
210
+ * @param oldTableName - The current name in the database at the time of the migration version.
211
+ * @param newTableName - The new table name at the time of the migration version.
221
212
  */
222
- renameTable(version: number, oldTableName: string, newTableName: TableInput): void;
213
+ renameTable(version: number, oldTableName: string, newTableName: string): void;
223
214
  /**
224
215
  * When you change the name of a column, SqlMorpheus will assume the original column was meant to be removed
225
216
  * and the new column was meant to be added, which would lead to data loss.
@@ -227,11 +218,11 @@ declare class Migrations {
227
218
  * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
228
219
  *
229
220
  * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
230
- * @param table Either a {@link TableObj} or a class decorated with @TableClass containing the column to be renamed.
221
+ * @param tableName - The current table name in the database at the time of the migration version.
231
222
  * @param oldColumn The current name in the database at the time of the migration.
232
223
  * @param newColumn The new name to assign to the column.
233
224
  */
234
- renameColumn(version: number, table: TableInput, oldColumn: string, newColumn: string): void;
225
+ renameColumn(version: number, tableName: string, oldColumn: string, newColumn: string): void;
235
226
  /**
236
227
  * Should not be needed most of the time.
237
228
  * Marks a table to be recreated
@@ -239,9 +230,9 @@ declare class Migrations {
239
230
  * This method will be ignored if {@link version} is smaller than the current database version or higher than the target version of {@link DatabaseInstructions}.
240
231
  *
241
232
  * @param version - The target version for which the table should be renamed. Must be provided to make sure this method will only have an effect when specified.
242
- * @param table Either a {@link TableObj} or a class decorated with @TableClass.
233
+ * @param tableName - The current table name in the database at the time of the migration version.
243
234
  */
244
- recreateTable(version: number, table: TableInput): void;
235
+ recreateTable(version: number, tableName: string): void;
245
236
  }
246
237
  type PublicMigrations = Pick<Migrations, "recreateTable" | "renameTable" | "renameColumn" | "allowMigration">;
247
238
 
@@ -263,12 +254,17 @@ interface InputTableStructure extends TableStructure {
263
254
  columns: Record<string, InputColumnInfo>;
264
255
  }
265
256
 
266
- type DataFormat = Record<string, unknown>;
267
- type GetColumns<T> = T extends TableObjBuilderDefinition<infer Item, any> ? Item : never;
268
- type TableObjBuilderDefinition<T extends DataFormat, TPick extends keyof TableObj<T>> = Pick<TableObj<T>, TPick>;
269
- type NewTableObjBuilder<T extends DataFormat> = TableObjBuilderDefinition<T, "primaryKey" | "foreignKey" | "dataType" | "maxCharacterLength">;
270
- type TableObjBuilder<T extends DataFormat> = TableObjBuilderDefinition<T, "foreignKey" | "dataType" | "maxCharacterLength">;
271
- type TableObjInput = TableObjBuilderDefinition<any, any>;
257
+ type AllowedTypes = boolean | number | bigint | string | null | Date;
258
+
259
+ interface ColumnOptions {
260
+ primaryKey?: boolean;
261
+ dataType?: DataTypeOptions;
262
+ maxCharacterLength?: number;
263
+ }
264
+ type DataFormat = Record<string, AllowedTypes | [AllowedTypes, ColumnOptions]>;
265
+ type GetColumns<T> = T extends PublicTableObj<infer Item> ? Item : never;
266
+ type PublicTableObj<T extends DataFormat> = Pick<TableObj<T>, "foreignKey" | "sealType">;
267
+ type TableObjInput = PublicTableObj<DataFormat>;
272
268
  /**
273
269
  * Use this class to define tables for DatabaseInstructions
274
270
  * (as an alternative for Class decorators)
@@ -278,44 +274,68 @@ declare class TableObj<T extends DataFormat> {
278
274
  readonly IS_TABLE_OBJ = true;
279
275
  readonly tableName: string;
280
276
  tableStructure: InputTableStructure;
281
- static getEmptyColumnEntry(key: string): InputColumnInfo;
282
- static getColumnEntry(key: string, value: unknown): InputColumnInfo | null;
283
277
  private constructor();
284
- primaryKey(key: keyof T): TableObjBuilder<T>;
285
- foreignKey<TOther extends TableObjBuilderDefinition<DataFormat, any>>(fromColumn: keyof T, toTable: TOther, toColumn: keyof GetColumns<TOther>, options?: {
278
+ private applyOptions;
279
+ /**
280
+ * Creates a foreign key relationship between the current table and a target table.
281
+ *
282
+ * @param fromColumn - The column in the current table that will serve as the foreign key.
283
+ * @param toTable - The target table object that the foreign key will reference.
284
+ * @param toColumn - The column in the target table that the foreign key will reference.
285
+ * @param options - Optional configurations for the foreign key, specifying actions on update or delete.
286
+ */
287
+ foreignKey<TOther extends Partial<TableObj<DataFormat>>>(fromColumn: keyof T, toTable: TOther, toColumn: keyof GetColumns<TOther>, options?: {
286
288
  onUpdate?: ForeignKeyActions;
287
289
  onDelete?: ForeignKeyActions;
288
- }): TableObjBuilder<T>;
289
- dataType(column: keyof T, type: DataTypeOptions): TableObjBuilder<T>;
290
- maxCharacterLength(column: keyof T, max: number): TableObjBuilder<T>;
291
- static create<T extends Record<string, unknown>>(tableName: string, columns: T): NewTableObjBuilder<T>;
292
- static isTableObj(obj: unknown): obj is TableObj<any>;
290
+ }): PublicTableObj<T>;
291
+ /**
292
+ * An optional helper function to deal with `TS2589: Type instantiation is excessively deep and possibly infinite. when used as method parameter`
293
+ * Does nothing in runtime
294
+ */
295
+ sealType(): TableObjInput;
296
+ static create<T extends DataFormat>(tableName: string, columns: T): PublicTableObj<T>;
293
297
  }
294
298
 
299
+ type LoggerMode = "silent" | "error" | "warn" | "log" | "debug";
300
+
295
301
  type TableInput = TableObjInput | Class<TableClassInterface>;
296
302
  interface DatabaseInstructions {
297
303
  dialect: "Sqlite" | "MsSql" | "MySql" | "Postgres";
298
304
  tables: TableInput[];
299
305
  version: number;
300
- configPath: string;
301
- throwIfNotAllowed: boolean;
302
- loggerMode?: "silent" | "noLog" | "normal" | "debug";
303
- alwaysAllowedMigrations?: AllowedMigrations;
306
+ /**
307
+ * Usually SqlMorpheus throws an exception if it needs to execute a destructive database change and is not explicitly confirmed in
308
+ * (using `allowMigration()` in `preMigration()`).
309
+ * If true, no exception will be thrown and an error message will be logged to the console instead.
310
+ */
311
+ doNotThrowIfNotAllowed?: boolean;
312
+ /**
313
+ * Defines how much should be logged to the console.
314
+ */
315
+ loggerMode?: LoggerMode;
316
+ /**
317
+ * Usually SqlMorpheus throws an exception if it needs to execute a destructive database change and is not explicitly allowed to do so
318
+ * (using `allowMigration()` in `preMigration()`).
319
+ * if defined, the confirmation will be skipped for each migration type defined in `alwaysAllowedMigrations`.
320
+ */
321
+ alwaysAllowedMigrations?: (keyof AllowedMigrations)[];
304
322
  /**
305
323
  * Custom migrations that will run before any database changes are applied.
306
- * @param migrations The migration object that will be validated after this.
307
- * @param fromVersion Version of the current database.
308
- * @param toVersion Version of the database after update.
324
+ * @param migrations - The migration object that will be validated after this.
325
+ * @param dbAccess - The database access object to interact with the database.
326
+ * @param fromVersion - Version of the current database.
327
+ * @param toVersion - Version of the database after update.
309
328
  * @return custom SQL that should be executed before migrations are executed.
310
329
  */
311
- preMigration?(migrations: PublicMigrations, fromVersion: number, toVersion: number): SqlChanges | void;
330
+ preMigration?(migrations: PublicMigrations, dbAccess: DatabaseAccess, fromVersion: number, toVersion: number): SqlChanges | void;
312
331
  /**
313
332
  * Custom migrations that will run after database changes have been applied (but before the transaction is finished)
314
- * @param fromVersion Version of the current database.
315
- * @param toVersion Version of the database after update.
316
- * @return custom SQL that should be executed before migrations are executed.
333
+ * @param dbAccess - The database access object to interact with the database.
334
+ * @param fromVersion - Version of the current database.
335
+ * @param toVersion - Version of the database after update.
336
+ * @return custom SQL that should be executed after migrations were executed.
317
337
  */
318
- postMigration?(fromVersion: number, toVersion: number): SqlChanges;
338
+ postMigration?(dbAccess: DatabaseAccess, fromVersion: number, toVersion: number): SqlChanges;
319
339
  }
320
340
 
321
341
  /**
@@ -348,6 +368,13 @@ declare function ForeignKey<TOther extends TableClassInterface>(toTable: Class<T
348
368
  }): (table: any, context: any) => void;
349
369
 
350
370
  declare function runMigration(db: DatabaseAccess, dbInstructions: DatabaseInstructions): Promise<void>;
351
- declare function rollback(db: DatabaseAccess, dbInstructions: DatabaseInstructions, toVersion: number): Promise<number>;
371
+ /**
372
+ * Rolls back the database to the provided toVersion
373
+ * @param dbAccess - Access to the database.
374
+ * @param toVersion - the version to which the database should be upgraded to.
375
+ * @param dialect - the dialect that should be used for the database. Usually is the value defined in DatabaseInstructions
376
+ * @param loggerMode - defines how much should be logged to the console.
377
+ */
378
+ declare function rollback(dbAccess: DatabaseAccess, toVersion: number, dialect: DatabaseInstructions["dialect"], loggerMode?: DatabaseInstructions["loggerMode"]): Promise<number>;
352
379
 
353
380
  export { DataType, type DatabaseAccess, type DatabaseInstructions, ForeignKey, type PublicMigrations, type SqlChanges, TableClass, TableObj, rollback, runMigration };