import type { Adapter } from "../adapters/adapter.ts" ; import { Colors , joinPath } from "../../deps.ts" ; import { Schema } from "./schema.ts" ; import { createMigrationTimestamp } from "../utils/date.ts" ; export interface Migration { up ( schema ) : Promise < void > ; down ( schema ) : Promise < void > ; } interface MigrationInfo { migration : Migration ; name : string ; isExecuted : boolean ; batch : number ; } /**
 * Run migration classes
 */ export class MigrationRunner { constructor ( /** Migration files directory */ private migrationDir , /** The database adapter to perform queries */ private adapter , /** The version of currently used Cotton */ private version ) { } param name public async createMigrationFile ( name ) { if ( ! name . match ( /^[A-Za-z]+$/ ) ) { throw new Error ( `Invalid name for migration '${ name }', name can only contain letters!` ) ; } await this . ensureMigrationDir ( ) ; const timestamp = createMigrationTimestamp ( ) ; const fileName = `${ this . migrationDir }/${ timestamp + "_" + name }.ts` ; await Deno . writeTextFile ( fileName , `import { Schema } from "https://deno.land/x/cotton@${ this . version }/mod.ts";\n\nexport async function up(schema: Schema) {\n  // Do something...\n}\n\nexport async function down(schema: Schema) {\n  // Do something...\n}\n` ) ; console . log ( `${ Colors . green ( "Created:" ) } ${ fileName }` ) ; return fileName ; } /**
     * Get all migration classes
     */ private async getAllMigrationFiles ( ) { const migrations = [ ] ; for await ( const file of Deno . readDir ( this . migrationDir ) ) { if ( file . isFile ) { let migration ; try { const fileContent = await import ( "file://" + joinPath ( this . migrationDir , file . name ) ) ; if ( typeof fileContent . up !== "function" || typeof fileContent . down !== "function" ) { throw new Error ( ) ; } migration = { up : fileContent . up , down : fileContent . down } ; } catch { throw new Error ( `Failed to load '${ file . name }' migration file!` ) ; } migrations . push ( { migration , name : file . name . split ( "." ) [ 0 ] , batch : 0 , isExecuted : false } ) ; } } return migrations . sort ( ( a , b ) => ( a . name > b . name ) ? 1 : - 1 ) ; } /**
     * Get all available migrations
     */ public async getAllMigrations ( ) { await this . createMigrationsTable ( ) ; const migrations = await this . adapter . table ( "migrations" ) . select ( "name" , "batch" ) . execute ( ) ; const migrationFiles = await this . getAllMigrationFiles ( ) ; for ( const migration of migrations ) { const migrationIndex = migrationFiles . findIndex ( ( item ) => item . name === migration . name ) ; if ( migrationIndex === - 1 ) { throw new Error ( `Migration '${ migration . name }' is missing!` ) ; } migrationFiles [ migrationIndex ] . isExecuted = true ; migrationFiles [ migrationIndex ] . batch = migration . batch as number ; } const lastBatch = migrationFiles . reduce ( ( prev , current ) => ( prev . batch > current . batch ) ? prev : current ) ; return { migrations : migrationFiles , lastBatch : lastBatch . batch } ; } /**
     * Execute a new batch of migrations
     */ public async applyMigrations ( ) { const { migrations , lastBatch } = await this . getAllMigrations ( ) ; for ( const migration of migrations ) { if ( ! migration . isExecuted ) { try { console . log ( `${ Colors . yellow ( "Migrating:" ) } ${ migration . name }` ) ; await migration . migration . up ( new Schema ( this . adapter ) ) ; await this . adapter . table ( "migrations" ) . insert ( { name : migration . name , batch : lastBatch + 1 } ) . execute ( ) ; console . log ( `${ Colors . green ( "Migrated:" ) }  ${ migration . name }` ) ; } catch { throw new Error ( `Failed to apply migration '${ migration . name }'!` ) ; } } } } param steps public async revertMigrations ( steps ? ) { let { migrations , lastBatch } = await this . getAllMigrations ( ) ; if ( typeof steps === "number" && migrations . length ) { migrations = migrations . slice ( Math . max ( migrations . length - steps , 0 ) ) ; } else if ( lastBatch > 0 ) { migrations = migrations . filter ( ( migration ) => migration . batch === lastBatch ) ; } else { throw new Error ( "No migration to revert!" ) ; } for ( const migration of migrations ) { try { console . log ( `${ Colors . yellow ( "Reverting:" ) } ${ migration . name }` ) ; await migration . migration . down ( new Schema ( this . adapter ) ) ; await this . adapter . table ( "migrations" ) . where ( "name" , migration . name ) . delete ( ) . execute ( ) ; console . log ( `${ Colors . green ( "Reverted:" ) }  ${ migration . name }` ) ; } catch { throw new Error ( `Failed to revert migration '${ migration . name }'!` ) ; } } } /**
     * Create the `migrations` table if it doesn't exist yet
     */ public async createMigrationsTable ( ) { const schema = new Schema ( this . adapter ) ; if ( ! await schema . hasTable ( "migrations" ) ) { await schema . createTable ( "migrations" , ( table ) => { table . id ( ) ; table . varchar ( "name" , 255 ) . unique ( ) . notNull ( ) ; table . integer ( "batch" ) . notNull ( ) ; } ) ; console . log ( `${ Colors . green ( "Migration table created successfully!" ) }` ) ; } } /**
     * Ensures that the migration directory exists.
     * If the directory structure does not exist, it is created. Like mkdir -p.
     * Requires the `--allow-read` and `--allow-write` flag.
     */ public async ensureMigrationDir ( ) { try { const fileInfo = await Deno . lstat ( this . migrationDir ) ; if ( ! fileInfo . isDirectory ) throw null ; } catch ( err ) { if ( err instanceof Deno . errors . NotFound || err === null ) { await Deno . mkdir ( this . migrationDir , { recursive : true } ) ; return ; } throw err ; } } }