Initial commit - Event Planner application

This commit is contained in:
mberlin
2026-03-18 14:55:56 -03:00
commit 86d779eb4d
7548 changed files with 1006324 additions and 0 deletions

View 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;
}

View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileMigrationProvider = void 0;
const object_utils_js_1 = require("../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'
* })
* ```
*/
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 Promise.resolve(`${
/* webpackIgnore: true */ this.#props.path.join(this.#props.migrationFolder, fileName)}`).then(s => require(s));
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;
}
}
exports.FileMigrationProvider = FileMigrationProvider;
function isMigration(obj) {
return (0, object_utils_js_1.isObject)(obj) && (0, object_utils_js_1.isFunction)(obj.up);
}

395
node_modules/kysely/dist/cjs/migration/migrator.d.ts generated vendored Normal file
View 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 {};

611
node_modules/kysely/dist/cjs/migration/migrator.js generated vendored Normal file
View File

@@ -0,0 +1,611 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Migrator = exports.NO_MIGRATIONS = exports.MIGRATION_LOCK_ID = exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS = exports.DEFAULT_MIGRATION_LOCK_TABLE = exports.DEFAULT_MIGRATION_TABLE = void 0;
const noop_plugin_js_1 = require("../plugin/noop-plugin.js");
const with_schema_plugin_js_1 = require("../plugin/with-schema/with-schema-plugin.js");
const object_utils_js_1 = require("../util/object-utils.js");
exports.DEFAULT_MIGRATION_TABLE = 'kysely_migration';
exports.DEFAULT_MIGRATION_LOCK_TABLE = 'kysely_migration_lock';
exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS = false;
exports.MIGRATION_LOCK_ID = 'migration_lock';
exports.NO_MIGRATIONS = (0, object_utils_js_1.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,
* })
* })
* ```
*/
class Migrator {
#props;
constructor(props) {
this.#props = (0, object_utils_js_1.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 ((0, object_utils_js_1.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 ?? exports.DEFAULT_MIGRATION_TABLE;
}
get #migrationLockTable() {
return this.#props.migrationLockTableName ?? exports.DEFAULT_MIGRATION_LOCK_TABLE;
}
get #allowUnorderedMigrations() {
return (this.#props.allowUnorderedMigrations ?? exports.DEFAULT_ALLOW_UNORDERED_MIGRATIONS);
}
get #schemaPlugin() {
if (this.#migrationTableSchema) {
return new with_schema_plugin_js_1.WithSchemaPlugin(this.#migrationTableSchema);
}
return new noop_plugin_js_1.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: exports.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', '=', exports.MIGRATION_LOCK_ID)
.select('id')
.executeTakeFirst();
return !!lockRow;
}
async #runMigrations(getMigrationDirectionAndStep) {
const adapter = this.#props.db.getExecutor().adapter;
const lockOptions = (0, object_utils_js_1.freeze)({
lockTable: this.#props.migrationLockTableName ?? exports.DEFAULT_MIGRATION_LOCK_TABLE,
lockRowId: exports.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 (0, object_utils_js_1.freeze)({
migrations,
executedMigrations,
lastMigration: (0, object_utils_js_1.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();
}
}
exports.Migrator = Migrator;
class MigrationResultSetError extends Error {
#resultSet;
constructor(result) {
super();
this.#resultSet = result;
}
get resultSet() {
return this.#resultSet;
}
}