AsyncPipe; table schemata; migrations; File logging
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-07-25 09:15:01 -05:00
parent e86cf420df
commit fcce28081b
42 changed files with 3139 additions and 56 deletions

View File

@@ -0,0 +1,48 @@
import {Directive, OptionDefinition} from '../../cli'
import {Injectable} from '../../di'
import {stringToPascal} from '../../util'
import {templateMigration} from '../template/migration'
/**
* CLI directive that creates migration classes from template.
*/
@Injectable()
export class CreateMigrationDirective extends Directive {
getDescription(): string {
return 'create a new migration'
}
getKeywords(): string | string[] {
return ['create-migration', 'make-migration']
}
getOptions(): OptionDefinition[] {
return [
'{description} | Description of what the migration does',
]
}
getHelpText(): string {
return [
'Creates a new migration file in `src/app/migrations`.',
'To use, specify a string describing what the migration does. For example:',
'./ex create-migration "Add version column to sessions table"',
].join('\n\n')
}
async handle(): Promise<void> {
const description = this.option('description')
const className = `${stringToPascal(description)}Migration`
const fileName = `${(new Date()).toISOString()}_${className}.migration.ts`
const path = this.app().path('..', 'src', 'app', 'migrations', fileName)
// Create the migrations directory, if it doesn't already exist
await path.concat('..').mkdir()
// Render the template
const rendered = await templateMigration.render(className, className, path)
await path.write(rendered)
this.success(`Created migration: ${className}`)
}
}

View File

@@ -0,0 +1,117 @@
import {Directive, OptionDefinition} from '../../cli'
import {Container, Inject, Injectable} from '../../di'
import {EventBus} from '../../event/EventBus'
import {Migrator} from '../migrations/Migrator'
import {Migrations} from '../services/Migrations'
import {ApplyingMigrationEvent} from '../migrations/events/ApplyingMigrationEvent'
import {AppliedMigrationEvent} from '../migrations/events/AppliedMigrationEvent'
import {EventSubscription} from '../../event/types'
import {NothingToMigrateError} from '../migrations/NothingToMigrateError'
/**
* CLI directive that applies migrations using the default Migrator.
* @fixme Support dry run mode
*/
@Injectable()
export class MigrateDirective extends Directive {
@Inject()
protected readonly bus!: EventBus
@Inject('injector')
protected readonly injector!: Container
/** Event bus subscriptions. */
protected subscriptions: EventSubscription[] = []
getKeywords(): string | string[] {
return ['migrate']
}
getDescription(): string {
return 'apply pending migrations'
}
getOptions(): OptionDefinition[] {
return [
'--package -p {name} | apply migrations for a specific namespace',
'--identifier -i {name} | apply a specific migration, by identifier',
]
}
getHelpText(): string {
return [
'Migrations are single-run code patches used to track changes to things like database schemata.',
'',
'You can create migrations in your app using the ./ex command and they can be applied and rolled-back.',
'',
'./ex migrate:create "Add version column to sessions table"',
'',
'Modules and packages can also register their own migrations. These are run by default.',
'',
'To run the migrations for a specific package, and no others, use the --package option. Example:',
'',
'./ex migrate --package @extollo',
'',
].join('\n')
}
async handle(): Promise<void> {
await this.registerListeners()
const namespace = this.option('package')
const identifier = this.option('identifier')
let identifiers
if ( namespace ) {
identifiers = (this.injector.make<Migrations>(Migrations))
.all(namespace)
.map(id => `${namespace}:${id}`)
}
if ( identifier ) {
if ( !identifiers ) {
identifiers = [identifier]
}
identifiers = identifiers.filter(x => x === identifier)
}
let error
try {
await (this.injector.make<Migrator>(Migrator)).migrate(identifiers)
} catch (e) {
if ( e instanceof NothingToMigrateError ) {
this.info(e.message)
} else {
error = e
this.error(e)
}
} finally {
await this.removeListeners()
}
if ( error ) {
throw error
}
}
/**
* Register event bus listeners to print messages for the user.
* @protected
*/
protected async registerListeners(): Promise<void> {
this.subscriptions.push(await this.bus.subscribe(ApplyingMigrationEvent, event => {
this.info(`Applying migration ${event.migration.identifier}...`)
}))
this.subscriptions.push(await this.bus.subscribe(AppliedMigrationEvent, event => {
this.success(`Applied migration: ${event.migration.identifier}`)
}))
}
/** Remove event bus listeners before finish. */
protected async removeListeners(): Promise<void> {
await Promise.all(this.subscriptions.map(x => x.unsubscribe()))
this.subscriptions = []
}
}

View File

@@ -0,0 +1,102 @@
import {Directive, OptionDefinition} from '../../cli'
import {Container, Inject, Injectable} from '../../di'
import {EventBus} from '../../event/EventBus'
import {Migrator} from '../migrations/Migrator'
import {Migrations} from '../services/Migrations'
import {RollingBackMigrationEvent} from '../migrations/events/RollingBackMigrationEvent'
import {RolledBackMigrationEvent} from '../migrations/events/RolledBackMigrationEvent'
import {EventSubscription} from '../../event/types'
import {NothingToMigrateError} from '../migrations/NothingToMigrateError'
/**
* CLI directive that undoes applied migrations using the default Migrator.
* @fixme Support dry run mode
*/
@Injectable()
export class RollbackDirective extends Directive {
@Inject()
protected readonly bus!: EventBus
@Inject('injector')
protected readonly injector!: Container
@Inject()
protected readonly migrations!: Migrations
/** Event bus subscriptions. */
protected subscriptions: EventSubscription[] = []
getKeywords(): string | string[] {
return ['rollback']
}
getDescription(): string {
return 'roll-back applied migrations'
}
getOptions(): OptionDefinition[] {
return [
'--identifier -i {name} | roll-back a specific migration, by identifier',
]
}
getHelpText(): string {
return [
'Use this command to undo one or more migrations that were applied.',
'',
'By default, the command will undo all of the migrations applied the last time the migrate command was run.',
'',
'To undo a specific migration, pass its identifier using the --identifier option.',
'',
].join('\n')
}
async handle(): Promise<void> {
await this.registerListeners()
const identifier = this.option('identifier')
let identifiers
if ( identifier ) {
identifiers = [identifier]
}
let error
try {
await (this.injector.make<Migrator>(Migrator)).rollback(identifiers)
} catch (e) {
if ( e instanceof NothingToMigrateError ) {
this.info(e.message)
} else {
error = e
this.error(e)
}
} finally {
await this.removeListeners()
}
if ( error ) {
throw error
}
}
/**
* Register event-bus listeners to print messages for the user.
* @protected
*/
protected async registerListeners(): Promise<void> {
this.subscriptions.push(await this.bus.subscribe(RollingBackMigrationEvent, event => {
this.info(`Rolling-back migration ${event.migration.identifier}...`)
}))
this.subscriptions.push(await this.bus.subscribe(RolledBackMigrationEvent, event => {
this.success(`Rolled-back migration: ${event.migration.identifier}`)
}))
}
/** Remove event bus listeners before finish. */
protected async removeListeners(): Promise<void> {
await Promise.all(this.subscriptions.map(x => x.unsubscribe()))
this.subscriptions = []
}
}