Initial commit - Event Planner application
This commit is contained in:
48
node_modules/kysely/dist/esm/migration/file-migration-provider.d.ts
generated
vendored
Normal file
48
node_modules/kysely/dist/esm/migration/file-migration-provider.d.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Migration, MigrationProvider } from './migrator.js';
|
||||
/**
|
||||
* Reads all migrations from a folder in node.js.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
*
|
||||
* new FileMigrationProvider({
|
||||
* fs,
|
||||
* path,
|
||||
* migrationFolder: 'path/to/migrations/folder'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export declare class FileMigrationProvider implements MigrationProvider {
|
||||
#private;
|
||||
constructor(props: FileMigrationProviderProps);
|
||||
/**
|
||||
* Returns all migrations, old and new.
|
||||
*
|
||||
* For example if you have your migrations in a folder as separate files,
|
||||
* you can implement this method to return all migration in that folder
|
||||
* as {@link Migration} objects.
|
||||
*
|
||||
* Kysely already has a built-in {@link FileMigrationProvider} for node.js
|
||||
* that does exactly that.
|
||||
*
|
||||
* The keys of the returned object are migration names and values are the
|
||||
* migrations. The order of the migrations is determined by the alphabetical
|
||||
* order of the migration names. The items in the object don't need to be
|
||||
* sorted, they are sorted by Kysely.
|
||||
*/
|
||||
getMigrations(): Promise<Record<string, Migration>>;
|
||||
}
|
||||
export interface FileMigrationProviderFS {
|
||||
readdir(path: string): Promise<string[]>;
|
||||
}
|
||||
export interface FileMigrationProviderPath {
|
||||
join(...path: string[]): string;
|
||||
}
|
||||
export interface FileMigrationProviderProps {
|
||||
fs: FileMigrationProviderFS;
|
||||
path: FileMigrationProviderPath;
|
||||
migrationFolder: string;
|
||||
}
|
||||
49
node_modules/kysely/dist/esm/migration/file-migration-provider.js
generated
vendored
Normal file
49
node_modules/kysely/dist/esm/migration/file-migration-provider.js
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
/// <reference types="./file-migration-provider.d.ts" />
|
||||
import { isFunction, isObject } from '../util/object-utils.js';
|
||||
/**
|
||||
* Reads all migrations from a folder in node.js.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
*
|
||||
* new FileMigrationProvider({
|
||||
* fs,
|
||||
* path,
|
||||
* migrationFolder: 'path/to/migrations/folder'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class FileMigrationProvider {
|
||||
#props;
|
||||
constructor(props) {
|
||||
this.#props = props;
|
||||
}
|
||||
async getMigrations() {
|
||||
const migrations = {};
|
||||
const files = await this.#props.fs.readdir(this.#props.migrationFolder);
|
||||
for (const fileName of files) {
|
||||
if (fileName.endsWith('.js') ||
|
||||
(fileName.endsWith('.ts') && !fileName.endsWith('.d.ts')) ||
|
||||
fileName.endsWith('.mjs') ||
|
||||
(fileName.endsWith('.mts') && !fileName.endsWith('.d.mts'))) {
|
||||
const migration = await import(
|
||||
/* webpackIgnore: true */ this.#props.path.join(this.#props.migrationFolder, fileName));
|
||||
const migrationKey = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
// Handle esModuleInterop export's `default` prop...
|
||||
if (isMigration(migration?.default)) {
|
||||
migrations[migrationKey] = migration.default;
|
||||
}
|
||||
else if (isMigration(migration)) {
|
||||
migrations[migrationKey] = migration;
|
||||
}
|
||||
}
|
||||
}
|
||||
return migrations;
|
||||
}
|
||||
}
|
||||
function isMigration(obj) {
|
||||
return isObject(obj) && isFunction(obj.up);
|
||||
}
|
||||
395
node_modules/kysely/dist/esm/migration/migrator.d.ts
generated
vendored
Normal file
395
node_modules/kysely/dist/esm/migration/migrator.d.ts
generated
vendored
Normal file
@@ -0,0 +1,395 @@
|
||||
import type { Kysely } from '../kysely.js';
|
||||
export declare const DEFAULT_MIGRATION_TABLE = "kysely_migration";
|
||||
export declare const DEFAULT_MIGRATION_LOCK_TABLE = "kysely_migration_lock";
|
||||
export declare const DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
|
||||
export declare const MIGRATION_LOCK_ID = "migration_lock";
|
||||
export declare const NO_MIGRATIONS: NoMigrations;
|
||||
export interface Migration {
|
||||
up(db: Kysely<any>): Promise<void>;
|
||||
/**
|
||||
* An optional down method.
|
||||
*
|
||||
* If you don't provide a down method, the migration is skipped when
|
||||
* migrating down.
|
||||
*/
|
||||
down?(db: Kysely<any>): Promise<void>;
|
||||
}
|
||||
/**
|
||||
* A class for running migrations.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* This example uses the {@link FileMigrationProvider} that reads migrations
|
||||
* files from a single folder. You can easily implement your own
|
||||
* {@link MigrationProvider} if you want to provide migrations some
|
||||
* other way.
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import * as Sqlite from 'better-sqlite3'
|
||||
* import {
|
||||
* FileMigrationProvider,
|
||||
* Kysely,
|
||||
* Migrator,
|
||||
* SqliteDialect
|
||||
* } from 'kysely'
|
||||
*
|
||||
* const db = new Kysely<any>({
|
||||
* dialect: new SqliteDialect({
|
||||
* database: Sqlite(':memory:')
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export declare class Migrator {
|
||||
#private;
|
||||
constructor(props: MigratorProps);
|
||||
/**
|
||||
* Returns a {@link MigrationInfo} object for each migration.
|
||||
*
|
||||
* The returned array is sorted by migration name.
|
||||
*/
|
||||
getMigrations(): Promise<ReadonlyArray<MigrationInfo>>;
|
||||
/**
|
||||
* Runs all migrations that have not yet been run.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed. See the examples below.
|
||||
*
|
||||
* This method goes through all possible migrations provided by the provider and runs the
|
||||
* ones whose names come alphabetically after the last migration that has been run. If the
|
||||
* list of executed migrations doesn't match the beginning of the list of possible migrations
|
||||
* an error is returned.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import * as Sqlite from 'better-sqlite3'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* const { error, results } = await migrator.migrateToLatest()
|
||||
*
|
||||
* results?.forEach((it) => {
|
||||
* if (it.status === 'Success') {
|
||||
* console.log(`migration "${it.migrationName}" was executed successfully`)
|
||||
* } else if (it.status === 'Error') {
|
||||
* console.error(`failed to execute migration "${it.migrationName}"`)
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* if (error) {
|
||||
* console.error('failed to run `migrateToLatest`')
|
||||
* console.error(error)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
migrateToLatest(): Promise<MigrationResultSet>;
|
||||
/**
|
||||
* Migrate up/down to a specific migration.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateTo('some_migration')
|
||||
* ```
|
||||
*
|
||||
* If you specify the name of the first migration, this method migrates
|
||||
* down to the first migration, but doesn't run the `down` method of
|
||||
* the first migration. In case you want to migrate all the way down,
|
||||
* you can use a special constant `NO_MIGRATIONS`:
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateTo(NO_MIGRATIONS)
|
||||
* ```
|
||||
*/
|
||||
migrateTo(targetMigrationName: string | NoMigrations): Promise<MigrationResultSet>;
|
||||
/**
|
||||
* Migrate one step up.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateUp()
|
||||
* ```
|
||||
*/
|
||||
migrateUp(): Promise<MigrationResultSet>;
|
||||
/**
|
||||
* Migrate one step down.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateDown()
|
||||
* ```
|
||||
*/
|
||||
migrateDown(): Promise<MigrationResultSet>;
|
||||
}
|
||||
export interface MigratorProps {
|
||||
readonly db: Kysely<any>;
|
||||
readonly provider: MigrationProvider;
|
||||
/**
|
||||
* The name of the internal migration table. Defaults to `kysely_migration`.
|
||||
*
|
||||
* If you do specify this, you need to ALWAYS use the same value. Kysely doesn't
|
||||
* support changing the table on the fly. If you run the migrator even once with a
|
||||
* table name X and then change the table name to Y, kysely will create a new empty
|
||||
* migration table and attempt to run the migrations again, which will obviously
|
||||
* fail.
|
||||
*
|
||||
* If you do specify this, ALWAYS ALWAYS use the same value from the beginning of
|
||||
* the project, to the end of time or prepare to manually migrate the migration
|
||||
* tables.
|
||||
*/
|
||||
readonly migrationTableName?: string;
|
||||
/**
|
||||
* The name of the internal migration lock table. Defaults to `kysely_migration_lock`.
|
||||
*
|
||||
* If you do specify this, you need to ALWAYS use the same value. Kysely doesn't
|
||||
* support changing the table on the fly. If you run the migrator even once with a
|
||||
* table name X and then change the table name to Y, kysely will create a new empty
|
||||
* lock table.
|
||||
*
|
||||
* If you do specify this, ALWAYS ALWAYS use the same value from the beginning of
|
||||
* the project, to the end of time or prepare to manually migrate the migration
|
||||
* tables.
|
||||
*/
|
||||
readonly migrationLockTableName?: string;
|
||||
/**
|
||||
* The schema of the internal migration tables. Defaults to the default schema
|
||||
* on dialects that support schemas.
|
||||
*
|
||||
* If you do specify this, you need to ALWAYS use the same value. Kysely doesn't
|
||||
* support changing the schema on the fly. If you run the migrator even once with a
|
||||
* schema name X and then change the schema name to Y, kysely will create a new empty
|
||||
* migration tables in the new schema and attempt to run the migrations again, which
|
||||
* will obviously fail.
|
||||
*
|
||||
* If you do specify this, ALWAYS ALWAYS use the same value from the beginning of
|
||||
* the project, to the end of time or prepare to manually migrate the migration
|
||||
* tables.
|
||||
*
|
||||
* This only works on postgres and mssql.
|
||||
*/
|
||||
readonly migrationTableSchema?: string;
|
||||
/**
|
||||
* Enforces whether or not migrations must be run in alpha-numeric order.
|
||||
*
|
||||
* When false, migrations must be run in their exact alpha-numeric order.
|
||||
* This is checked against the migrations already run in the database
|
||||
* (`migrationTableName`). This ensures your migrations are always run in
|
||||
* the same order and is the safest option.
|
||||
*
|
||||
* When true, migrations are still run in alpha-numeric order, but
|
||||
* the order is not checked against already-run migrations in the database.
|
||||
* Kysely will simply run all migrations that haven't run yet, in alpha-numeric
|
||||
* order.
|
||||
*/
|
||||
readonly allowUnorderedMigrations?: boolean;
|
||||
/**
|
||||
* A function that compares migration names, used when sorting migrations in
|
||||
* ascending order.
|
||||
*
|
||||
* Default is `name0.localeCompare(name1)`.
|
||||
*/
|
||||
readonly nameComparator?: (name0: string, name1: string) => number;
|
||||
/**
|
||||
* When `true`, don't run migrations in transactions even if the dialect supports transactional DDL.
|
||||
*
|
||||
* Default is `false`.
|
||||
*
|
||||
* This is useful when some migrations include queries that would fail otherwise.
|
||||
*/
|
||||
readonly disableTransactions?: boolean;
|
||||
}
|
||||
/**
|
||||
* All migration methods ({@link Migrator.migrateTo | migrateTo},
|
||||
* {@link Migrator.migrateToLatest | migrateToLatest} etc.) never
|
||||
* throw but return this object instead.
|
||||
*/
|
||||
export interface MigrationResultSet {
|
||||
/**
|
||||
* This is defined if something went wrong.
|
||||
*
|
||||
* An error may have occurred in one of the migrations in which case the
|
||||
* {@link results} list contains an item with `status === 'Error'` to
|
||||
* indicate which migration failed.
|
||||
*
|
||||
* An error may also have occurred before Kysely was able to figure out
|
||||
* which migrations should be executed, in which case the {@link results}
|
||||
* list is undefined.
|
||||
*/
|
||||
readonly error?: unknown;
|
||||
/**
|
||||
* {@link MigrationResult} for each individual migration that was supposed
|
||||
* to be executed by the operation.
|
||||
*
|
||||
* If all went well, each result's `status` is `Success`. If some migration
|
||||
* failed, the failed migration's result's `status` is `Error` and all
|
||||
* results after that one have `status` ´NotExecuted`.
|
||||
*
|
||||
* This property can be undefined if an error occurred before Kysely was
|
||||
* able to figure out which migrations should be executed.
|
||||
*
|
||||
* If this list is empty, there were no migrations to execute.
|
||||
*/
|
||||
readonly results?: MigrationResult[];
|
||||
}
|
||||
type MigrationDirection = 'Up' | 'Down';
|
||||
export interface MigrationResult {
|
||||
readonly migrationName: string;
|
||||
/**
|
||||
* The direction in which this migration was executed.
|
||||
*/
|
||||
readonly direction: MigrationDirection;
|
||||
/**
|
||||
* The execution status.
|
||||
*
|
||||
* - `Success` means the migration was successfully executed. Note that
|
||||
* if any of the later migrations in the {@link MigrationResultSet.results}
|
||||
* list failed (have status `Error`) AND the dialect supports transactional
|
||||
* DDL, even the successfull migrations were rolled back.
|
||||
*
|
||||
* - `Error` means the migration failed. In this case the
|
||||
* {@link MigrationResultSet.error} contains the error.
|
||||
*
|
||||
* - `NotExecuted` means that the migration was supposed to be executed
|
||||
* but wasn't because an earlier migration failed.
|
||||
*/
|
||||
readonly status: 'Success' | 'Error' | 'NotExecuted';
|
||||
}
|
||||
export interface MigrationProvider {
|
||||
/**
|
||||
* Returns all migrations, old and new.
|
||||
*
|
||||
* For example if you have your migrations in a folder as separate files,
|
||||
* you can implement this method to return all migration in that folder
|
||||
* as {@link Migration} objects.
|
||||
*
|
||||
* Kysely already has a built-in {@link FileMigrationProvider} for node.js
|
||||
* that does exactly that.
|
||||
*
|
||||
* The keys of the returned object are migration names and values are the
|
||||
* migrations. The order of the migrations is determined by the alphabetical
|
||||
* order of the migration names. The items in the object don't need to be
|
||||
* sorted, they are sorted by Kysely.
|
||||
*/
|
||||
getMigrations(): Promise<Record<string, Migration>>;
|
||||
}
|
||||
/**
|
||||
* Type for the {@link NO_MIGRATIONS} constant. Never create one of these.
|
||||
*/
|
||||
export interface NoMigrations {
|
||||
readonly __noMigrations__: true;
|
||||
}
|
||||
export interface MigrationInfo {
|
||||
/**
|
||||
* Name of the migration.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The actual migration.
|
||||
*/
|
||||
migration: Migration;
|
||||
/**
|
||||
* When was the migration executed.
|
||||
*
|
||||
* If this is undefined, the migration hasn't been executed yet.
|
||||
*/
|
||||
executedAt?: Date;
|
||||
}
|
||||
export {};
|
||||
608
node_modules/kysely/dist/esm/migration/migrator.js
generated
vendored
Normal file
608
node_modules/kysely/dist/esm/migration/migrator.js
generated
vendored
Normal file
@@ -0,0 +1,608 @@
|
||||
/// <reference types="./migrator.d.ts" />
|
||||
import { NoopPlugin } from '../plugin/noop-plugin.js';
|
||||
import { WithSchemaPlugin } from '../plugin/with-schema/with-schema-plugin.js';
|
||||
import { freeze, getLast, isObject } from '../util/object-utils.js';
|
||||
export const DEFAULT_MIGRATION_TABLE = 'kysely_migration';
|
||||
export const DEFAULT_MIGRATION_LOCK_TABLE = 'kysely_migration_lock';
|
||||
export const DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
|
||||
export const MIGRATION_LOCK_ID = 'migration_lock';
|
||||
export const NO_MIGRATIONS = freeze({ __noMigrations__: true });
|
||||
/**
|
||||
* A class for running migrations.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
* This example uses the {@link FileMigrationProvider} that reads migrations
|
||||
* files from a single folder. You can easily implement your own
|
||||
* {@link MigrationProvider} if you want to provide migrations some
|
||||
* other way.
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import * as Sqlite from 'better-sqlite3'
|
||||
* import {
|
||||
* FileMigrationProvider,
|
||||
* Kysely,
|
||||
* Migrator,
|
||||
* SqliteDialect
|
||||
* } from 'kysely'
|
||||
*
|
||||
* const db = new Kysely<any>({
|
||||
* dialect: new SqliteDialect({
|
||||
* database: Sqlite(':memory:')
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class Migrator {
|
||||
#props;
|
||||
constructor(props) {
|
||||
this.#props = freeze(props);
|
||||
}
|
||||
/**
|
||||
* Returns a {@link MigrationInfo} object for each migration.
|
||||
*
|
||||
* The returned array is sorted by migration name.
|
||||
*/
|
||||
async getMigrations() {
|
||||
const tableExists = await this.#doesTableExist(this.#migrationTable);
|
||||
const executedMigrations = tableExists
|
||||
? await this.#props.db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.selectFrom(this.#migrationTable)
|
||||
.select(['name', 'timestamp'])
|
||||
.$narrowType()
|
||||
.execute()
|
||||
: [];
|
||||
const migrations = await this.#resolveMigrations();
|
||||
return migrations.map(({ name, ...migration }) => {
|
||||
const executed = executedMigrations.find((it) => it.name === name);
|
||||
return {
|
||||
name,
|
||||
migration,
|
||||
executedAt: executed ? new Date(executed.timestamp) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Runs all migrations that have not yet been run.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed. See the examples below.
|
||||
*
|
||||
* This method goes through all possible migrations provided by the provider and runs the
|
||||
* ones whose names come alphabetically after the last migration that has been run. If the
|
||||
* list of executed migrations doesn't match the beginning of the list of possible migrations
|
||||
* an error is returned.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import * as Sqlite from 'better-sqlite3'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* const { error, results } = await migrator.migrateToLatest()
|
||||
*
|
||||
* results?.forEach((it) => {
|
||||
* if (it.status === 'Success') {
|
||||
* console.log(`migration "${it.migrationName}" was executed successfully`)
|
||||
* } else if (it.status === 'Error') {
|
||||
* console.error(`failed to execute migration "${it.migrationName}"`)
|
||||
* }
|
||||
* })
|
||||
*
|
||||
* if (error) {
|
||||
* console.error('failed to run `migrateToLatest`')
|
||||
* console.error(error)
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async migrateToLatest() {
|
||||
return this.#migrate(() => ({ direction: 'Up', step: Infinity }));
|
||||
}
|
||||
/**
|
||||
* Migrate up/down to a specific migration.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateTo('some_migration')
|
||||
* ```
|
||||
*
|
||||
* If you specify the name of the first migration, this method migrates
|
||||
* down to the first migration, but doesn't run the `down` method of
|
||||
* the first migration. In case you want to migrate all the way down,
|
||||
* you can use a special constant `NO_MIGRATIONS`:
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator, NO_MIGRATIONS } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateTo(NO_MIGRATIONS)
|
||||
* ```
|
||||
*/
|
||||
async migrateTo(targetMigrationName) {
|
||||
return this.#migrate(({ migrations, executedMigrations, pendingMigrations, }) => {
|
||||
if (isObject(targetMigrationName) &&
|
||||
targetMigrationName.__noMigrations__ === true) {
|
||||
return { direction: 'Down', step: Infinity };
|
||||
}
|
||||
if (!migrations.find((m) => m.name === targetMigrationName)) {
|
||||
throw new Error(`migration "${targetMigrationName}" doesn't exist`);
|
||||
}
|
||||
const executedIndex = executedMigrations.indexOf(targetMigrationName);
|
||||
const pendingIndex = pendingMigrations.findIndex((m) => m.name === targetMigrationName);
|
||||
if (executedIndex !== -1) {
|
||||
return {
|
||||
direction: 'Down',
|
||||
step: executedMigrations.length - executedIndex - 1,
|
||||
};
|
||||
}
|
||||
else if (pendingIndex !== -1) {
|
||||
return { direction: 'Up', step: pendingIndex + 1 };
|
||||
}
|
||||
else {
|
||||
throw new Error(`migration "${targetMigrationName}" isn't executed or pending`);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Migrate one step up.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateUp()
|
||||
* ```
|
||||
*/
|
||||
async migrateUp() {
|
||||
return this.#migrate(() => ({ direction: 'Up', step: 1 }));
|
||||
}
|
||||
/**
|
||||
* Migrate one step down.
|
||||
*
|
||||
* This method returns a {@link MigrationResultSet} instance and _never_ throws.
|
||||
* {@link MigrationResultSet.error} holds the error if something went wrong.
|
||||
* {@link MigrationResultSet.results} contains information about which migrations
|
||||
* were executed and which failed.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* ```ts
|
||||
* import { promises as fs } from 'node:fs'
|
||||
* import path from 'node:path'
|
||||
* import { FileMigrationProvider, Migrator } from 'kysely'
|
||||
*
|
||||
* const migrator = new Migrator({
|
||||
* db,
|
||||
* provider: new FileMigrationProvider({
|
||||
* fs,
|
||||
* // Path to the folder that contains all your migrations.
|
||||
* migrationFolder: 'some/path/to/migrations',
|
||||
* path,
|
||||
* })
|
||||
* })
|
||||
*
|
||||
* await migrator.migrateDown()
|
||||
* ```
|
||||
*/
|
||||
async migrateDown() {
|
||||
return this.#migrate(() => ({ direction: 'Down', step: 1 }));
|
||||
}
|
||||
async #migrate(getMigrationDirectionAndStep) {
|
||||
try {
|
||||
await this.#ensureMigrationTableSchemaExists();
|
||||
await this.#ensureMigrationTableExists();
|
||||
await this.#ensureMigrationLockTableExists();
|
||||
await this.#ensureLockRowExists();
|
||||
return await this.#runMigrations(getMigrationDirectionAndStep);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof MigrationResultSetError) {
|
||||
return error.resultSet;
|
||||
}
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
get #migrationTableSchema() {
|
||||
return this.#props.migrationTableSchema;
|
||||
}
|
||||
get #migrationTable() {
|
||||
return this.#props.migrationTableName ?? DEFAULT_MIGRATION_TABLE;
|
||||
}
|
||||
get #migrationLockTable() {
|
||||
return this.#props.migrationLockTableName ?? DEFAULT_MIGRATION_LOCK_TABLE;
|
||||
}
|
||||
get #allowUnorderedMigrations() {
|
||||
return (this.#props.allowUnorderedMigrations ?? DEFAULT_ALLOW_UNORDERED_MIGRATIONS);
|
||||
}
|
||||
get #schemaPlugin() {
|
||||
if (this.#migrationTableSchema) {
|
||||
return new WithSchemaPlugin(this.#migrationTableSchema);
|
||||
}
|
||||
return new NoopPlugin();
|
||||
}
|
||||
async #ensureMigrationTableSchemaExists() {
|
||||
if (!this.#migrationTableSchema) {
|
||||
// Use default schema. Nothing to do.
|
||||
return;
|
||||
}
|
||||
const schemaExists = await this.#doesSchemaExist();
|
||||
if (schemaExists) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#createIfNotExists(this.#props.db.schema.createSchema(this.#migrationTableSchema));
|
||||
}
|
||||
catch (error) {
|
||||
const schemaExists = await this.#doesSchemaExist();
|
||||
// At least on PostgreSQL, `if not exists` doesn't guarantee the `create schema`
|
||||
// query doesn't throw if the schema already exits. That's why we check if
|
||||
// the schema exist here and ignore the error if it does.
|
||||
if (!schemaExists) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async #ensureMigrationTableExists() {
|
||||
const tableExists = await this.#doesTableExist(this.#migrationTable);
|
||||
if (tableExists) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#createIfNotExists(this.#props.db.schema
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.createTable(this.#migrationTable)
|
||||
.addColumn('name', 'varchar(255)', (col) => col.notNull().primaryKey())
|
||||
// The migration run time as ISO string. This is not a real date type as we
|
||||
// can't know which data type is supported by all future dialects.
|
||||
.addColumn('timestamp', 'varchar(255)', (col) => col.notNull()));
|
||||
}
|
||||
catch (error) {
|
||||
const tableExists = await this.#doesTableExist(this.#migrationTable);
|
||||
// At least on PostgreSQL, `if not exists` doesn't guarantee the `create table`
|
||||
// query doesn't throw if the table already exits. That's why we check if
|
||||
// the table exist here and ignore the error if it does.
|
||||
if (!tableExists) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async #ensureMigrationLockTableExists() {
|
||||
const tableExists = await this.#doesTableExist(this.#migrationLockTable);
|
||||
if (tableExists) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#createIfNotExists(this.#props.db.schema
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.createTable(this.#migrationLockTable)
|
||||
.addColumn('id', 'varchar(255)', (col) => col.notNull().primaryKey())
|
||||
.addColumn('is_locked', 'integer', (col) => col.notNull().defaultTo(0)));
|
||||
}
|
||||
catch (error) {
|
||||
const tableExists = await this.#doesTableExist(this.#migrationLockTable);
|
||||
// At least on PostgreSQL, `if not exists` doesn't guarantee the `create table`
|
||||
// query doesn't throw if the table already exits. That's why we check if
|
||||
// the table exist here and ignore the error if it does.
|
||||
if (!tableExists) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async #ensureLockRowExists() {
|
||||
const lockRowExists = await this.#doesLockRowExists();
|
||||
if (lockRowExists) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.#props.db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.insertInto(this.#migrationLockTable)
|
||||
.values({ id: MIGRATION_LOCK_ID, is_locked: 0 })
|
||||
.execute();
|
||||
}
|
||||
catch (error) {
|
||||
const lockRowExists = await this.#doesLockRowExists();
|
||||
if (!lockRowExists) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async #doesSchemaExist() {
|
||||
const schemas = await this.#props.db.introspection.getSchemas();
|
||||
return schemas.some((it) => it.name === this.#migrationTableSchema);
|
||||
}
|
||||
async #doesTableExist(tableName) {
|
||||
const schema = this.#migrationTableSchema;
|
||||
const tables = await this.#props.db.introspection.getTables({
|
||||
withInternalKyselyTables: true,
|
||||
});
|
||||
return tables.some((it) => it.name === tableName && (!schema || it.schema === schema));
|
||||
}
|
||||
async #doesLockRowExists() {
|
||||
const lockRow = await this.#props.db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.selectFrom(this.#migrationLockTable)
|
||||
.where('id', '=', MIGRATION_LOCK_ID)
|
||||
.select('id')
|
||||
.executeTakeFirst();
|
||||
return !!lockRow;
|
||||
}
|
||||
async #runMigrations(getMigrationDirectionAndStep) {
|
||||
const adapter = this.#props.db.getExecutor().adapter;
|
||||
const lockOptions = freeze({
|
||||
lockTable: this.#props.migrationLockTableName ?? DEFAULT_MIGRATION_LOCK_TABLE,
|
||||
lockRowId: MIGRATION_LOCK_ID,
|
||||
lockTableSchema: this.#props.migrationTableSchema,
|
||||
});
|
||||
const run = async (db) => {
|
||||
try {
|
||||
await adapter.acquireMigrationLock(db, lockOptions);
|
||||
const state = await this.#getState(db);
|
||||
if (state.migrations.length === 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
const { direction, step } = getMigrationDirectionAndStep(state);
|
||||
if (step <= 0) {
|
||||
return { results: [] };
|
||||
}
|
||||
if (direction === 'Down') {
|
||||
return await this.#migrateDown(db, state, step);
|
||||
}
|
||||
else if (direction === 'Up') {
|
||||
return await this.#migrateUp(db, state, step);
|
||||
}
|
||||
return { results: [] };
|
||||
}
|
||||
finally {
|
||||
await adapter.releaseMigrationLock(db, lockOptions);
|
||||
}
|
||||
};
|
||||
if (adapter.supportsTransactionalDdl && !this.#props.disableTransactions) {
|
||||
return this.#props.db.transaction().execute(run);
|
||||
}
|
||||
else {
|
||||
return this.#props.db.connection().execute(run);
|
||||
}
|
||||
}
|
||||
async #getState(db) {
|
||||
const migrations = await this.#resolveMigrations();
|
||||
const executedMigrations = await this.#getExecutedMigrations(db);
|
||||
this.#ensureNoMissingMigrations(migrations, executedMigrations);
|
||||
if (!this.#allowUnorderedMigrations) {
|
||||
this.#ensureMigrationsInOrder(migrations, executedMigrations);
|
||||
}
|
||||
const pendingMigrations = this.#getPendingMigrations(migrations, executedMigrations);
|
||||
return freeze({
|
||||
migrations,
|
||||
executedMigrations,
|
||||
lastMigration: getLast(executedMigrations),
|
||||
pendingMigrations,
|
||||
});
|
||||
}
|
||||
#getPendingMigrations(migrations, executedMigrations) {
|
||||
return migrations.filter((migration) => {
|
||||
return !executedMigrations.includes(migration.name);
|
||||
});
|
||||
}
|
||||
async #resolveMigrations() {
|
||||
const allMigrations = await this.#props.provider.getMigrations();
|
||||
return Object.keys(allMigrations)
|
||||
.sort()
|
||||
.map((name) => ({
|
||||
...allMigrations[name],
|
||||
name,
|
||||
}));
|
||||
}
|
||||
async #getExecutedMigrations(db) {
|
||||
const executedMigrations = await db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.selectFrom(this.#migrationTable)
|
||||
.select(['name', 'timestamp'])
|
||||
.$narrowType()
|
||||
.execute();
|
||||
const nameComparator = this.#props.nameComparator || ((a, b) => a.localeCompare(b));
|
||||
return (executedMigrations
|
||||
// https://github.com/kysely-org/kysely/issues/843
|
||||
.sort((a, b) => {
|
||||
if (a.timestamp === b.timestamp) {
|
||||
return nameComparator(a.name, b.name);
|
||||
}
|
||||
return (new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||
})
|
||||
.map((it) => it.name));
|
||||
}
|
||||
#ensureNoMissingMigrations(migrations, executedMigrations) {
|
||||
// Ensure all executed migrations exist in the `migrations` list.
|
||||
for (const executed of executedMigrations) {
|
||||
if (!migrations.some((it) => it.name === executed)) {
|
||||
throw new Error(`corrupted migrations: previously executed migration ${executed} is missing`);
|
||||
}
|
||||
}
|
||||
}
|
||||
#ensureMigrationsInOrder(migrations, executedMigrations) {
|
||||
// Ensure the executed migrations are the first ones in the migration list.
|
||||
for (let i = 0; i < executedMigrations.length; ++i) {
|
||||
if (migrations[i].name !== executedMigrations[i]) {
|
||||
throw new Error(`corrupted migrations: expected previously executed migration ${executedMigrations[i]} to be at index ${i} but ${migrations[i].name} was found in its place. New migrations must always have a name that comes alphabetically after the last executed migration.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
async #migrateDown(db, state, step) {
|
||||
const migrationsToRollback = state.executedMigrations
|
||||
.slice()
|
||||
.reverse()
|
||||
.slice(0, step)
|
||||
.map((name) => {
|
||||
return state.migrations.find((it) => it.name === name);
|
||||
});
|
||||
const results = migrationsToRollback.map((migration) => {
|
||||
return {
|
||||
migrationName: migration.name,
|
||||
direction: 'Down',
|
||||
status: 'NotExecuted',
|
||||
};
|
||||
});
|
||||
for (let i = 0; i < results.length; ++i) {
|
||||
const migration = migrationsToRollback[i];
|
||||
try {
|
||||
if (migration.down) {
|
||||
await migration.down(db);
|
||||
await db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.deleteFrom(this.#migrationTable)
|
||||
.where('name', '=', migration.name)
|
||||
.execute();
|
||||
results[i] = {
|
||||
migrationName: migration.name,
|
||||
direction: 'Down',
|
||||
status: 'Success',
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
results[i] = {
|
||||
migrationName: migration.name,
|
||||
direction: 'Down',
|
||||
status: 'Error',
|
||||
};
|
||||
throw new MigrationResultSetError({
|
||||
error,
|
||||
results,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { results };
|
||||
}
|
||||
async #migrateUp(db, state, step) {
|
||||
const migrationsToRun = state.pendingMigrations.slice(0, step);
|
||||
const results = migrationsToRun.map((migration) => {
|
||||
return {
|
||||
migrationName: migration.name,
|
||||
direction: 'Up',
|
||||
status: 'NotExecuted',
|
||||
};
|
||||
});
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const migration = state.pendingMigrations[i];
|
||||
try {
|
||||
await migration.up(db);
|
||||
await db
|
||||
.withPlugin(this.#schemaPlugin)
|
||||
.insertInto(this.#migrationTable)
|
||||
.values({
|
||||
name: migration.name,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
.execute();
|
||||
results[i] = {
|
||||
migrationName: migration.name,
|
||||
direction: 'Up',
|
||||
status: 'Success',
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
results[i] = {
|
||||
migrationName: migration.name,
|
||||
direction: 'Up',
|
||||
status: 'Error',
|
||||
};
|
||||
throw new MigrationResultSetError({
|
||||
error,
|
||||
results,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { results };
|
||||
}
|
||||
async #createIfNotExists(qb) {
|
||||
if (this.#props.db.getExecutor().adapter.supportsCreateIfNotExists) {
|
||||
qb = qb.ifNotExists();
|
||||
}
|
||||
await qb.execute();
|
||||
}
|
||||
}
|
||||
class MigrationResultSetError extends Error {
|
||||
#resultSet;
|
||||
constructor(result) {
|
||||
super();
|
||||
this.#resultSet = result;
|
||||
}
|
||||
get resultSet() {
|
||||
return this.#resultSet;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user