Refactor event bus and queue system; detect cycles in DI realization and make
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
506fb55c74
commit
6d1cf18680
@ -1,10 +1,10 @@
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {Awaitable, Maybe} from '../../util'
|
||||
import {Authenticatable, AuthenticatableRepository} from '../types'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {UserAuthenticatedEvent} from '../event/UserAuthenticatedEvent'
|
||||
import {UserFlushedEvent} from '../event/UserFlushedEvent'
|
||||
import {Bus} from '../../support/bus'
|
||||
|
||||
/**
|
||||
* Base-class for a context that authenticates users and manages security.
|
||||
@ -12,7 +12,7 @@ import {UserFlushedEvent} from '../event/UserFlushedEvent'
|
||||
@Injectable()
|
||||
export abstract class SecurityContext {
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
protected readonly bus!: Bus
|
||||
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
@ -40,7 +40,7 @@ export abstract class SecurityContext {
|
||||
*/
|
||||
async authenticateOnce(user: Authenticatable): Promise<void> {
|
||||
this.authenticatedUser = user
|
||||
await this.bus.dispatch(new UserAuthenticatedEvent(user, this))
|
||||
await this.bus.push(new UserAuthenticatedEvent(user, this))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,7 +50,7 @@ export abstract class SecurityContext {
|
||||
async authenticate(user: Authenticatable): Promise<void> {
|
||||
this.authenticatedUser = user
|
||||
await this.persist()
|
||||
await this.bus.dispatch(new UserAuthenticatedEvent(user, this))
|
||||
await this.bus.push(new UserAuthenticatedEvent(user, this))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +60,7 @@ export abstract class SecurityContext {
|
||||
const user = this.authenticatedUser
|
||||
if ( user ) {
|
||||
this.authenticatedUser = undefined
|
||||
await this.bus.dispatch(new UserFlushedEvent(user, this))
|
||||
await this.bus.push(new UserFlushedEvent(user, this))
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export abstract class SecurityContext {
|
||||
if ( user ) {
|
||||
this.authenticatedUser = undefined
|
||||
await this.persist()
|
||||
await this.bus.dispatch(new UserFlushedEvent(user, this))
|
||||
await this.bus.push(new UserFlushedEvent(user, this))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ export class SessionSecurityContext extends SecurityContext {
|
||||
const user = await this.repository.getByIdentifier(identifier)
|
||||
if ( user ) {
|
||||
this.authenticatedUser = user
|
||||
await this.bus.dispatch(new UserAuthenticationResumedEvent(user, this))
|
||||
await this.bus.push(new UserAuthenticationResumedEvent(user, this))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,12 @@
|
||||
import {Event} from '../../event/Event'
|
||||
import {SecurityContext} from '../context/SecurityContext'
|
||||
import {Awaitable, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
import {BaseEvent} from '../../support/bus'
|
||||
|
||||
/**
|
||||
* Event fired when a user is authenticated.
|
||||
*/
|
||||
export class AuthenticationEvent extends Event {
|
||||
export abstract class AuthenticationEvent extends BaseEvent {
|
||||
constructor(
|
||||
public readonly user: Authenticatable,
|
||||
public readonly context: SecurityContext,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
user: await this.user.dehydrate(),
|
||||
contextName: this.context.name,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): Awaitable<void> { // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
// TODO fill this in
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,6 @@ import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
/**
|
||||
* Event fired when a user is authenticated.
|
||||
*/
|
||||
export class UserAuthenticatedEvent extends AuthenticationEvent {}
|
||||
export class UserAuthenticatedEvent extends AuthenticationEvent {
|
||||
public readonly eventName = '@extollo/lib:UserAuthenticatedEvent'
|
||||
}
|
||||
|
@ -3,4 +3,6 @@ import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
/**
|
||||
* Event raised when a user is re-authenticated to a security context
|
||||
*/
|
||||
export class UserAuthenticationResumedEvent extends AuthenticationEvent {}
|
||||
export class UserAuthenticationResumedEvent extends AuthenticationEvent {
|
||||
public readonly eventName = '@extollo/lib:UserAuthenticationResumedEvent'
|
||||
}
|
||||
|
@ -3,4 +3,6 @@ import {AuthenticationEvent} from './AuthenticationEvent'
|
||||
/**
|
||||
* Event fired when a user is unauthenticated.
|
||||
*/
|
||||
export class UserFlushedEvent extends AuthenticationEvent {}
|
||||
export class UserFlushedEvent extends AuthenticationEvent {
|
||||
public readonly eventName = '@extollo/lib:UserFlushedEvent'
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ export * from './event/UserAuthenticatedEvent'
|
||||
export * from './event/UserAuthenticationResumedEvent'
|
||||
export * from './event/UserFlushedEvent'
|
||||
|
||||
export * from './serial/AuthenticationEventSerializer'
|
||||
|
||||
export * from './repository/orm/ORMUser'
|
||||
export * from './repository/orm/ORMUserRepository'
|
||||
|
||||
|
54
src/auth/serial/AuthenticationEventSerializer.ts
Normal file
54
src/auth/serial/AuthenticationEventSerializer.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {BaseSerializer, ObjectSerializer, SerialPayload} from '../../support/bus'
|
||||
import {AuthenticationEvent} from '../event/AuthenticationEvent'
|
||||
import {ErrorWithContext, JSONState} from '../../util'
|
||||
import {Authenticatable} from '../types'
|
||||
import {StaticInstantiable} from '../../di'
|
||||
import {SecurityContext} from '../context/SecurityContext'
|
||||
import {UserAuthenticatedEvent} from '../event/UserAuthenticatedEvent'
|
||||
import {UserAuthenticationResumedEvent} from '../event/UserAuthenticationResumedEvent'
|
||||
import {UserFlushedEvent} from '../event/UserFlushedEvent'
|
||||
|
||||
export interface AuthenticationEventSerialPayload extends JSONState {
|
||||
user: SerialPayload<Authenticatable, JSONState>
|
||||
eventName: string
|
||||
}
|
||||
|
||||
@ObjectSerializer()
|
||||
export class AuthenticationEventSerializer extends BaseSerializer<AuthenticationEvent, AuthenticationEventSerialPayload> {
|
||||
protected async decodeSerial(serial: AuthenticationEventSerialPayload): Promise<AuthenticationEvent> {
|
||||
const user = await this.getSerialization().decode(serial.user)
|
||||
const context = await this.getRequest().make(SecurityContext)
|
||||
|
||||
const EventClass = this.getEventClass(serial.eventName)
|
||||
return new EventClass(user, context)
|
||||
}
|
||||
|
||||
protected async encodeActual(actual: AuthenticationEvent): Promise<AuthenticationEventSerialPayload> {
|
||||
return {
|
||||
eventName: actual.eventName,
|
||||
user: await this.getSerialization().encode(actual.user),
|
||||
}
|
||||
}
|
||||
|
||||
protected getName(): string {
|
||||
return '@extollo/lib:AuthenticationEventSerializer'
|
||||
}
|
||||
|
||||
matchActual(some: AuthenticationEvent): boolean {
|
||||
return some instanceof AuthenticationEvent
|
||||
}
|
||||
|
||||
protected getEventClass(name: string): StaticInstantiable<AuthenticationEvent> {
|
||||
if ( name === '@extollo/lib:UserAuthenticatedEvent' ) {
|
||||
return UserAuthenticatedEvent
|
||||
} else if ( name === '@extollo/lib:UserAuthenticationResumedEvent' ) {
|
||||
return UserAuthenticationResumedEvent
|
||||
} else if ( name === '@extollo/lib:UserFlushedEvent' ) {
|
||||
return UserFlushedEvent
|
||||
}
|
||||
|
||||
throw new ErrorWithContext('Unable to map event name to AuthenticationEvent implementation', {
|
||||
eventName: name,
|
||||
})
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ export const CLIDirective = (): ClassDecorator => {
|
||||
if ( isInstantiableOf(target, Directive) ) {
|
||||
logIfDebugging('extollo.cli.decorators', 'Registering CLIDirective blueprint:', target)
|
||||
ContainerBlueprint.getContainerBlueprint()
|
||||
.onResolve<CommandLine>(CommandLine, cli => {
|
||||
.onResolve<CommandLine>(CommandLine, (cli: CommandLine) => {
|
||||
cli.registerDirective(target as Instantiable<Directive>)
|
||||
})
|
||||
} else {
|
||||
|
39
src/cli/directive/queue/WorkDirective.ts
Normal file
39
src/cli/directive/queue/WorkDirective.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import {Directive, OptionDefinition} from '../../Directive'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Queue} from '../../../support/bus'
|
||||
|
||||
@Injectable()
|
||||
export class WorkDirective extends Directive {
|
||||
@Inject()
|
||||
protected readonly queue!: Queue
|
||||
|
||||
getDescription(): string {
|
||||
return 'pop a single item from the queue and execute it'
|
||||
}
|
||||
|
||||
getKeywords(): string | string[] {
|
||||
return 'queue-work'
|
||||
}
|
||||
|
||||
getOptions(): OptionDefinition[] {
|
||||
return []
|
||||
}
|
||||
|
||||
async handle(): Promise<void> {
|
||||
try {
|
||||
const queueable = await this.queue.pop()
|
||||
if ( !queueable ) {
|
||||
this.info('There are no items in the queue.')
|
||||
return
|
||||
}
|
||||
|
||||
this.info(`Fetched 1 item from the queue`)
|
||||
await queueable.execute()
|
||||
this.success('Executed 1 item from the queue')
|
||||
} catch (e: unknown) {
|
||||
this.error('Failed to execute queueable:')
|
||||
this.error(e)
|
||||
process.exitCode = 1
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import {TemplateDirective} from '../directive/TemplateDirective'
|
||||
import {RunDirective} from '../directive/RunDirective'
|
||||
import {RoutesDirective} from '../directive/RoutesDirective'
|
||||
import {RouteDirective} from '../directive/RouteDirective'
|
||||
import {WorkDirective} from '../directive/queue/WorkDirective'
|
||||
|
||||
/**
|
||||
* Unit that takes the place of the final unit in the application that handles
|
||||
@ -46,6 +47,7 @@ export class CommandLineApplication extends Unit {
|
||||
this.cli.registerDirective(RunDirective)
|
||||
this.cli.registerDirective(RoutesDirective)
|
||||
this.cli.registerDirective(RouteDirective)
|
||||
this.cli.registerDirective(WorkDirective)
|
||||
|
||||
const argv = process.argv.slice(2)
|
||||
const match = this.cli.getDirectives()
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
TypedDependencyKey,
|
||||
} from './types'
|
||||
import {AbstractFactory} from './factory/AbstractFactory'
|
||||
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
|
||||
import {collect, Collection, ErrorWithContext, globalRegistry, logIfDebugging} from '../util'
|
||||
import {Factory} from './factory/Factory'
|
||||
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
|
||||
import {ClosureFactory} from './factory/ClosureFactory'
|
||||
@ -25,6 +25,27 @@ export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resol
|
||||
* A container of resolve-able dependencies that are created via inversion-of-control.
|
||||
*/
|
||||
export class Container {
|
||||
/**
|
||||
* Set to true when we're realizing a container.
|
||||
* Used to prevent infinite recursion when `getContainer()` is accidentally called
|
||||
* from somewhere within the `realizeContainer()` call.
|
||||
*/
|
||||
protected static realizingContainer = false
|
||||
|
||||
/**
|
||||
* List of dependency keys currently being `make`'d as a reverse stack.
|
||||
* This is used to detect dependency cycles.
|
||||
* @protected
|
||||
*/
|
||||
protected static makeStack?: Collection<DependencyKey>
|
||||
|
||||
/**
|
||||
* The 100 most recent dependency keys that were `make`'d. Used to help with
|
||||
* debugging cyclic dependency errors.
|
||||
* @protected
|
||||
*/
|
||||
protected static makeHistory?: Collection<DependencyKey>
|
||||
|
||||
/**
|
||||
* Given a Container instance, apply the ContainerBlueprint to it.
|
||||
* @param container
|
||||
@ -34,6 +55,10 @@ export class Container {
|
||||
.resolve()
|
||||
.map(factory => container.registerFactory(factory))
|
||||
|
||||
ContainerBlueprint.getContainerBlueprint()
|
||||
.resolveConstructable()
|
||||
.map((factory: StaticClass<AbstractFactory<any>, any>) => console.log(factory)) // eslint-disable-line no-console
|
||||
|
||||
ContainerBlueprint.getContainerBlueprint()
|
||||
.resolveConstructable()
|
||||
.map((factory: StaticClass<AbstractFactory<any>, any>) => container.registerFactory(container.make(factory)))
|
||||
@ -54,8 +79,14 @@ export class Container {
|
||||
public static getContainer(): Container {
|
||||
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
|
||||
if ( !existing ) {
|
||||
if ( this.realizingContainer ) {
|
||||
throw new ErrorWithContext('Attempted getContainer call during container realization.')
|
||||
}
|
||||
|
||||
this.realizingContainer = true
|
||||
const container = Container.realizeContainer(new Container())
|
||||
globalRegistry.setGlobal('extollo/injector', container)
|
||||
this.realizingContainer = false
|
||||
return container
|
||||
}
|
||||
|
||||
@ -403,13 +434,92 @@ export class Container {
|
||||
* @param {...any} parameters
|
||||
*/
|
||||
make<T>(target: DependencyKey, ...parameters: any[]): T {
|
||||
if ( this.hasKey(target) ) {
|
||||
return this.resolveAndCreate(target, ...parameters)
|
||||
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
|
||||
return this.produceFactory(new Factory(target), parameters)
|
||||
} else {
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
if ( !Container.makeStack ) {
|
||||
Container.makeStack = new Collection()
|
||||
}
|
||||
|
||||
if ( !Container.makeHistory ) {
|
||||
Container.makeHistory = new Collection()
|
||||
}
|
||||
|
||||
Container.makeStack.push(target)
|
||||
|
||||
if ( Container.makeHistory.length > 100 ) {
|
||||
Container.makeHistory = Container.makeHistory.slice(1, 100)
|
||||
}
|
||||
Container.makeHistory.push(target)
|
||||
|
||||
this.checkForMakeCycles()
|
||||
|
||||
if ( this.hasKey(target) ) {
|
||||
const realized = this.resolveAndCreate(target, ...parameters)
|
||||
Container.makeStack.pop()
|
||||
return realized
|
||||
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
|
||||
const realized = this.produceFactory(new Factory(target), parameters)
|
||||
Container.makeStack.pop()
|
||||
return realized
|
||||
}
|
||||
|
||||
Container.makeStack.pop()
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the `makeStack` for duplicates and throw an error if a dependency cycle is
|
||||
* detected. This is used to prevent infinite mutual recursion when cyclic dependencies
|
||||
* occur.
|
||||
* @protected
|
||||
*/
|
||||
protected checkForMakeCycles(): void {
|
||||
if ( !Container.makeStack ) {
|
||||
Container.makeStack = new Collection()
|
||||
}
|
||||
|
||||
if ( !Container.makeHistory ) {
|
||||
Container.makeHistory = new Collection()
|
||||
}
|
||||
|
||||
if ( Container.makeStack.unique().length !== Container.makeStack.length ) {
|
||||
const displayKey = (key: DependencyKey) => {
|
||||
if ( typeof key === 'string' ) {
|
||||
return 'key: `' + key + '`'
|
||||
} else {
|
||||
return `key: ${key.name}`
|
||||
}
|
||||
}
|
||||
|
||||
const makeStack = Container.makeStack
|
||||
.reverse()
|
||||
.map(displayKey)
|
||||
|
||||
const makeHistory = Container.makeHistory
|
||||
.reverse()
|
||||
.map(displayKey)
|
||||
|
||||
console.error('Make Stack:') // eslint-disable-line no-console
|
||||
console.error(makeStack.join('\n')) // eslint-disable-line no-console
|
||||
console.error('Make History:') // eslint-disable-line no-console
|
||||
console.error(makeHistory.join('\n')) // eslint-disable-line no-console
|
||||
throw new ErrorWithContext('Cyclic dependency chain detected', {
|
||||
makeStack,
|
||||
makeHistory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of the dependency key using this container, ignoring any pre-existing instances
|
||||
* in this container.
|
||||
* @param key
|
||||
* @param parameters
|
||||
*/
|
||||
makeNew<T>(key: TypedDependencyKey<T>, ...parameters: any[]): T {
|
||||
if ( isInstantiable(key) ) {
|
||||
return this.produceFactory(new Factory(key), parameters)
|
||||
}
|
||||
|
||||
throw new TypeError(`Invalid or unknown make target: ${key}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,13 +0,0 @@
|
||||
import {Dispatchable} from './types'
|
||||
import {Awaitable, JSONState} from '../util'
|
||||
|
||||
/**
|
||||
* Abstract class representing an event that may be fired.
|
||||
*/
|
||||
export abstract class Event implements Dispatchable {
|
||||
|
||||
|
||||
abstract dehydrate(): Awaitable<JSONState>
|
||||
|
||||
abstract rehydrate(state: JSONState): Awaitable<void>
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import {Instantiable, Singleton, StaticClass} from '../di'
|
||||
import {Bus, Dispatchable, EventSubscriber, EventSubscriberEntry, EventSubscription} from './types'
|
||||
import {Awaitable, Collection, uuid4} from '../util'
|
||||
|
||||
/**
|
||||
* A non-queued bus implementation that executes subscribers immediately in the main thread.
|
||||
*/
|
||||
@Singleton()
|
||||
export class EventBus implements Bus {
|
||||
/**
|
||||
* Collection of subscribers, by their events.
|
||||
* @protected
|
||||
*/
|
||||
protected subscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
|
||||
|
||||
subscribe<T extends Dispatchable>(event: StaticClass<T, Instantiable<T>>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription> {
|
||||
const entry: EventSubscriberEntry<T> = {
|
||||
id: uuid4(),
|
||||
event,
|
||||
subscriber,
|
||||
}
|
||||
|
||||
this.subscribers.push(entry)
|
||||
return this.buildSubscription(entry.id)
|
||||
}
|
||||
|
||||
unsubscribe<T extends Dispatchable>(subscriber: EventSubscriber<T>): Awaitable<void> {
|
||||
this.subscribers = this.subscribers.where('subscriber', '!=', subscriber)
|
||||
}
|
||||
|
||||
async dispatch(event: Dispatchable): Promise<void> {
|
||||
const eventClass: StaticClass<typeof event, typeof event> = event.constructor as StaticClass<Dispatchable, Dispatchable>
|
||||
await this.subscribers.where('event', '=', eventClass)
|
||||
.promiseMap(entry => entry.subscriber(event))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an EventSubscription object for the subscriber of the given ID.
|
||||
* @param id
|
||||
* @protected
|
||||
*/
|
||||
protected buildSubscription(id: string): EventSubscription {
|
||||
let subscribed = true
|
||||
return {
|
||||
unsubscribe: (): Awaitable<void> => {
|
||||
if ( subscribed ) {
|
||||
this.subscribers = this.subscribers.where('id', '!=', id)
|
||||
subscribed = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import {EventBus} from './EventBus'
|
||||
import {Collection} from '../util'
|
||||
import {Bus, Dispatchable} from './types'
|
||||
|
||||
/**
|
||||
* A non-queued bus implementation that executes subscribers immediately in the main thread.
|
||||
* This bus also supports "propagating" events along to any other connected buses.
|
||||
* Such behavior is useful, e.g., if we want to have a semi-isolated request-
|
||||
* level bus whose events still reach the global EventBus instance.
|
||||
*/
|
||||
export class PropagatingEventBus extends EventBus {
|
||||
protected recipients: Collection<Bus> = new Collection<Bus>()
|
||||
|
||||
async dispatch(event: Dispatchable): Promise<void> {
|
||||
await super.dispatch(event)
|
||||
await this.recipients.promiseMap(bus => bus.dispatch(event))
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given bus to receive events fired on this bus.
|
||||
* @param recipient
|
||||
*/
|
||||
connect(recipient: Bus): void {
|
||||
if ( !this.recipients.includes(recipient) ) {
|
||||
this.recipients.push(recipient)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import {Awaitable, Rehydratable} from '../util'
|
||||
import {Instantiable, StaticClass} from '../di'
|
||||
|
||||
/**
|
||||
* A closure that should be executed with the given event is fired.
|
||||
*/
|
||||
export type EventSubscriber<T extends Dispatchable> = (event: T) => Awaitable<void>
|
||||
|
||||
/**
|
||||
* An object used to track event subscriptions internally.
|
||||
*/
|
||||
export interface EventSubscriberEntry<T extends Dispatchable> {
|
||||
/** Globally unique ID of this subscription. */
|
||||
id: string
|
||||
|
||||
/** The event class subscribed to. */
|
||||
event: StaticClass<T, Instantiable<T>>
|
||||
|
||||
/** The closure to execute when the event is fired. */
|
||||
subscriber: EventSubscriber<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* An object returned upon subscription, used to unsubscribe.
|
||||
*/
|
||||
export interface EventSubscription {
|
||||
/**
|
||||
* Unsubscribe the associated listener from the event bus.
|
||||
*/
|
||||
unsubscribe(): Awaitable<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* An instance of something that can be fired on an event bus.
|
||||
*/
|
||||
export interface Dispatchable extends Rehydratable {
|
||||
shouldQueue?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* An event-driven bus that manages subscribers and dispatched items.
|
||||
*/
|
||||
export interface Bus {
|
||||
subscribe<T extends Dispatchable>(eventClass: StaticClass<T, Instantiable<T>>, subscriber: EventSubscriber<T>): Awaitable<EventSubscription>
|
||||
unsubscribe<T extends Dispatchable>(subscriber: EventSubscriber<T>): Awaitable<void>
|
||||
dispatch(event: Dispatchable): Awaitable<void>
|
||||
}
|
28
src/http/kernel/module/ClearRequestEventBusHTTPModule.ts
Normal file
28
src/http/kernel/module/ClearRequestEventBusHTTPModule.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {Bus} from '../../../support/bus'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that creates a request-specific event bus
|
||||
* and injects it into the request container.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ClearRequestEventBusHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected bus!: Bus
|
||||
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).first()
|
||||
}
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const requestBus = request.make<Bus>(Bus)
|
||||
await requestBus.down()
|
||||
|
||||
// FIXME disconnect request bus from global event bus
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
@ -2,8 +2,7 @@ import {HTTPKernelModule} from '../HTTPKernelModule'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {HTTPKernel} from '../HTTPKernel'
|
||||
import {Request} from '../../lifecycle/Request'
|
||||
import {EventBus} from '../../../event/EventBus'
|
||||
import {PropagatingEventBus} from '../../../event/PropagatingEventBus'
|
||||
import {Bus} from '../../../support/bus'
|
||||
|
||||
/**
|
||||
* HTTP kernel module that creates a request-specific event bus
|
||||
@ -12,17 +11,18 @@ import {PropagatingEventBus} from '../../../event/PropagatingEventBus'
|
||||
@Injectable()
|
||||
export class InjectRequestEventBusHTTPModule extends HTTPKernelModule {
|
||||
@Inject()
|
||||
protected bus!: EventBus
|
||||
protected bus!: Bus
|
||||
|
||||
public static register(kernel: HTTPKernel): void {
|
||||
kernel.register(this).first()
|
||||
}
|
||||
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const bus = <PropagatingEventBus> this.make(PropagatingEventBus)
|
||||
bus.connect(this.bus)
|
||||
const requestBus = this.container().makeNew<Bus>(Bus)
|
||||
await requestBus.up()
|
||||
await requestBus.connect(this.bus)
|
||||
|
||||
request.purge(EventBus).registerProducer(EventBus, () => bus)
|
||||
request.purge(Bus).registerProducer(Bus, () => requestBus)
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,10 @@ export * from './util'
|
||||
export * from './lib'
|
||||
export * from './di'
|
||||
|
||||
export * from './event/types'
|
||||
export * from './event/Event'
|
||||
export * from './event/EventBus'
|
||||
export * from './event/PropagatingEventBus'
|
||||
|
||||
export * from './service/Logging'
|
||||
|
||||
export * from './support/bus/index'
|
||||
|
||||
export * from './lifecycle/RunLevelErrorHandler'
|
||||
export * from './lifecycle/Application'
|
||||
export * from './lifecycle/AppClass'
|
||||
@ -30,6 +27,7 @@ export * from './http/kernel/module/AbstractResolvedRouteHandlerHTTPModule'
|
||||
export * from './http/kernel/module/ExecuteResolvedRoutePreflightHTTPModule'
|
||||
export * from './http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule'
|
||||
export * from './http/kernel/module/ExecuteResolvedRoutePostflightHTTPModule'
|
||||
export * from './http/kernel/module/ClearRequestEventBusHTTPModule'
|
||||
|
||||
export * from './http/kernel/HTTPKernel'
|
||||
export * from './http/kernel/HTTPKernelModule'
|
||||
@ -89,7 +87,6 @@ export * from './support/cache/MemoryCache'
|
||||
export * from './support/cache/RedisCache'
|
||||
export * from './support/cache/CacheFactory'
|
||||
export * from './support/NodeModules'
|
||||
export * from './support/queue/Queue'
|
||||
|
||||
export * from './service/Queueables'
|
||||
|
||||
|
@ -3,9 +3,9 @@ import {QueryResult} from '../types'
|
||||
import {SQLDialect} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Inject, Injectable} from '../../di'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {QueryExecutedEvent} from './event/QueryExecutedEvent'
|
||||
import {Schema} from '../schema/Schema'
|
||||
import {Bus} from '../../support/bus'
|
||||
|
||||
/**
|
||||
* Error thrown when a connection is used before it is ready.
|
||||
@ -25,7 +25,7 @@ export class ConnectionNotReadyError extends ErrorWithContext {
|
||||
@Injectable()
|
||||
export abstract class Connection extends AppClass {
|
||||
@Inject()
|
||||
protected bus!: EventBus
|
||||
protected readonly bus!: Bus
|
||||
|
||||
constructor(
|
||||
/**
|
||||
@ -82,6 +82,6 @@ export abstract class Connection extends AppClass {
|
||||
*/
|
||||
protected async queryExecuted(query: string): Promise<void> {
|
||||
const event = new QueryExecutedEvent(this.name, this, query)
|
||||
await this.bus.dispatch(event)
|
||||
await this.bus.push(event)
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +1,17 @@
|
||||
import {Event} from '../../../event/Event'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {InvalidJSONStateError, JSONState} from '../../../util'
|
||||
import {Connection} from '../Connection'
|
||||
import {DatabaseService} from '../../DatabaseService'
|
||||
import {BaseEvent} from '../../../support/bus'
|
||||
|
||||
/**
|
||||
* Event fired when a query is executed.
|
||||
*/
|
||||
@Injectable()
|
||||
export class QueryExecutedEvent extends Event {
|
||||
@Inject()
|
||||
protected database!: DatabaseService
|
||||
|
||||
/**
|
||||
* The name of the connection where the query was executed.
|
||||
* @protected
|
||||
*/
|
||||
public connectionName!: string
|
||||
|
||||
/**
|
||||
* The connection where the query was executed.
|
||||
*/
|
||||
public connection!: Connection
|
||||
|
||||
/**
|
||||
* The query that was executed.
|
||||
*/
|
||||
public query!: string
|
||||
|
||||
export class QueryExecutedEvent extends BaseEvent {
|
||||
constructor(
|
||||
connectionName?: string,
|
||||
connection?: Connection,
|
||||
query?: string,
|
||||
public readonly connectionName: string,
|
||||
public readonly connection: Connection,
|
||||
public readonly query: string,
|
||||
) {
|
||||
super()
|
||||
if ( connectionName ) {
|
||||
this.connectionName = connectionName
|
||||
}
|
||||
|
||||
if ( connection ) {
|
||||
this.connection = connection
|
||||
}
|
||||
|
||||
if ( query ) {
|
||||
this.query = query
|
||||
}
|
||||
}
|
||||
|
||||
async dehydrate(): Promise<JSONState> {
|
||||
return {
|
||||
connectionName: this.connectionName,
|
||||
query: this.query,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: JSONState): void {
|
||||
if ( !state.connectionName || !state.query ) {
|
||||
throw new InvalidJSONStateError('Missing connectionName or query from QueryExecutedEvent state.')
|
||||
}
|
||||
|
||||
this.query = String(state.query)
|
||||
this.connectionName = String(state.connectionName)
|
||||
this.connection = this.database.get(this.connectionName)
|
||||
}
|
||||
|
||||
eventName = '@extollo/lib.QueryExecutedEvent'
|
||||
}
|
||||
|
38
src/orm/connection/event/QueryExecutedEventSerializer.ts
Normal file
38
src/orm/connection/event/QueryExecutedEventSerializer.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {BaseSerializer} from '../../../support/bus'
|
||||
import {QueryExecutedEvent} from './QueryExecutedEvent'
|
||||
import {Awaitable, JSONState} from '../../../util'
|
||||
import {Container, Inject, Injectable} from '../../../di'
|
||||
import {DatabaseService} from '../../DatabaseService'
|
||||
import {ObjectSerializer} from '../../../support/bus/serial/decorators'
|
||||
|
||||
export interface QueryExecutedEventSerialPayload extends JSONState {
|
||||
connectionName: string
|
||||
query: string
|
||||
}
|
||||
|
||||
@ObjectSerializer()
|
||||
@Injectable()
|
||||
export class QueryExecutedEventSerializer extends BaseSerializer<QueryExecutedEvent, QueryExecutedEventSerialPayload> {
|
||||
@Inject()
|
||||
protected readonly injector!: Container
|
||||
|
||||
protected decodeSerial(serial: QueryExecutedEventSerialPayload): Awaitable<QueryExecutedEvent> {
|
||||
const connection = this.injector.make<DatabaseService>(DatabaseService).get(serial.connectionName)
|
||||
return new QueryExecutedEvent(serial.connectionName, connection, serial.query)
|
||||
}
|
||||
|
||||
protected encodeActual(actual: QueryExecutedEvent): Awaitable<QueryExecutedEventSerialPayload> {
|
||||
return {
|
||||
connectionName: actual.connectionName,
|
||||
query: actual.query,
|
||||
}
|
||||
}
|
||||
|
||||
protected getName(): string {
|
||||
return '@extollo/lib.QueryExecutedEventSerializer'
|
||||
}
|
||||
|
||||
matchActual(some: QueryExecutedEvent): boolean {
|
||||
return some instanceof QueryExecutedEvent
|
||||
}
|
||||
}
|
@ -1,13 +1,12 @@
|
||||
import {Directive, OptionDefinition} from '../../cli'
|
||||
import {Directive, OptionDefinition, CLIDirective} from '../../cli'
|
||||
import {Container, Inject, Injectable} from '../../di'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {Collection} from '../../util'
|
||||
import {Bus, EventHandlerSubscription} from '../../support/bus'
|
||||
import {Migrations} from '../services/Migrations'
|
||||
import {Migrator} from '../migrations/Migrator'
|
||||
import {ApplyingMigrationEvent} from '../migrations/events/ApplyingMigrationEvent'
|
||||
import {AppliedMigrationEvent} from '../migrations/events/AppliedMigrationEvent'
|
||||
import {EventSubscription} from '../../event/types'
|
||||
import {NothingToMigrateError} from '../migrations/NothingToMigrateError'
|
||||
import {CLIDirective} from '../../cli/decorators'
|
||||
|
||||
/**
|
||||
* CLI directive that applies migrations using the default Migrator.
|
||||
@ -17,13 +16,13 @@ import {CLIDirective} from '../../cli/decorators'
|
||||
@CLIDirective()
|
||||
export class MigrateDirective extends Directive {
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
protected readonly bus!: Bus
|
||||
|
||||
@Inject('injector')
|
||||
protected readonly injector!: Container
|
||||
|
||||
/** Event bus subscriptions. */
|
||||
protected subscriptions: EventSubscription[] = []
|
||||
protected subscriptions: Collection<EventHandlerSubscription> = new Collection()
|
||||
|
||||
getKeywords(): string | string[] {
|
||||
return ['migrate']
|
||||
@ -113,7 +112,6 @@ export class MigrateDirective extends Directive {
|
||||
|
||||
/** Remove event bus listeners before finish. */
|
||||
protected async removeListeners(): Promise<void> {
|
||||
await Promise.all(this.subscriptions.map(x => x.unsubscribe()))
|
||||
this.subscriptions = []
|
||||
await this.subscriptions.awaitMapCall('unsubscribe')
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import {Directive, OptionDefinition} from '../../cli'
|
||||
import {Directive, OptionDefinition, CLIDirective} from '../../cli'
|
||||
import {Container, Inject, Injectable} from '../../di'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {Collection} from '../../util'
|
||||
import {Bus, EventHandlerSubscription} from '../../support/bus'
|
||||
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'
|
||||
import {CLIDirective} from '../../cli/decorators'
|
||||
|
||||
/**
|
||||
* CLI directive that undoes applied migrations using the default Migrator.
|
||||
@ -17,7 +16,7 @@ import {CLIDirective} from '../../cli/decorators'
|
||||
@CLIDirective()
|
||||
export class RollbackDirective extends Directive {
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
protected readonly bus!: Bus
|
||||
|
||||
@Inject('injector')
|
||||
protected readonly injector!: Container
|
||||
@ -26,7 +25,7 @@ export class RollbackDirective extends Directive {
|
||||
protected readonly migrations!: Migrations
|
||||
|
||||
/** Event bus subscriptions. */
|
||||
protected subscriptions: EventSubscription[] = []
|
||||
protected subscriptions: Collection<EventHandlerSubscription> = new Collection()
|
||||
|
||||
getKeywords(): string | string[] {
|
||||
return ['rollback']
|
||||
@ -98,7 +97,6 @@ export class RollbackDirective extends Directive {
|
||||
|
||||
/** Remove event bus listeners before finish. */
|
||||
protected async removeListeners(): Promise<void> {
|
||||
await Promise.all(this.subscriptions.map(x => x.unsubscribe()))
|
||||
this.subscriptions = []
|
||||
await this.subscriptions.awaitMapCall('unsubscribe')
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,12 @@ import {Container, Inject, Injectable} from '../../di'
|
||||
import {Awaitable, collect, ErrorWithContext} from '../../util'
|
||||
import {Migration} from './Migration'
|
||||
import {Migrations} from '../services/Migrations'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {ApplyingMigrationEvent} from './events/ApplyingMigrationEvent'
|
||||
import {AppliedMigrationEvent} from './events/AppliedMigrationEvent'
|
||||
import {RollingBackMigrationEvent} from './events/RollingBackMigrationEvent'
|
||||
import {RolledBackMigrationEvent} from './events/RolledBackMigrationEvent'
|
||||
import {NothingToMigrateError} from './NothingToMigrateError'
|
||||
import {Bus} from '../../support/bus'
|
||||
|
||||
/**
|
||||
* Manages single-run patches/migrations.
|
||||
@ -18,7 +18,7 @@ export abstract class Migrator {
|
||||
protected readonly migrations!: Migrations
|
||||
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
protected readonly bus!: Bus
|
||||
|
||||
@Inject('injector')
|
||||
protected readonly injector!: Container
|
||||
@ -193,31 +193,11 @@ export abstract class Migrator {
|
||||
* @protected
|
||||
*/
|
||||
protected async filterAppliedMigrations(identifiers: string[]): Promise<string[]> {
|
||||
return collect(identifiers)
|
||||
.partialMap(identifier => {
|
||||
const migration = this.migrations.get(identifier)
|
||||
if ( migration ) {
|
||||
return {
|
||||
identifier,
|
||||
migration,
|
||||
}
|
||||
}
|
||||
})
|
||||
.asyncPipe()
|
||||
.tap(coll => {
|
||||
return coll.promiseMap(async group => {
|
||||
return {
|
||||
...group,
|
||||
has: await this.has(group.migration),
|
||||
}
|
||||
})
|
||||
})
|
||||
.tap(coll => {
|
||||
return coll.filter(group => !group.has)
|
||||
.pluck<string>('identifier')
|
||||
.all()
|
||||
})
|
||||
.resolve()
|
||||
const ids = await collect(identifiers)
|
||||
.toAsync()
|
||||
.filterOut(async id => this.has(this.migrations.getOrFail(id)))
|
||||
|
||||
return ids.all()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -226,31 +206,11 @@ export abstract class Migrator {
|
||||
* @protected
|
||||
*/
|
||||
protected async filterPendingMigrations(identifiers: string[]): Promise<string[]> {
|
||||
return collect(identifiers)
|
||||
.partialMap(identifier => {
|
||||
const migration = this.migrations.get(identifier)
|
||||
if ( migration ) {
|
||||
return {
|
||||
identifier,
|
||||
migration,
|
||||
}
|
||||
}
|
||||
})
|
||||
.asyncPipe()
|
||||
.tap(coll => {
|
||||
return coll.promiseMap(async group => {
|
||||
return {
|
||||
...group,
|
||||
has: await this.has(group.migration),
|
||||
}
|
||||
})
|
||||
})
|
||||
.tap(coll => {
|
||||
return coll.filter(group => group.has)
|
||||
.pluck<string>('identifier')
|
||||
.all()
|
||||
})
|
||||
.resolve()
|
||||
const ids = await collect(identifiers)
|
||||
.toAsync()
|
||||
.filter(async id => this.has(this.migrations.getOrFail(id)))
|
||||
|
||||
return ids.all()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -260,7 +220,7 @@ export abstract class Migrator {
|
||||
*/
|
||||
protected async applying(migration: Migration): Promise<void> {
|
||||
const event = <ApplyingMigrationEvent> this.injector.make(ApplyingMigrationEvent, migration)
|
||||
await this.bus.dispatch(event)
|
||||
await this.bus.push(event)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,7 +230,7 @@ export abstract class Migrator {
|
||||
*/
|
||||
protected async applied(migration: Migration): Promise<void> {
|
||||
const event = <AppliedMigrationEvent> this.injector.make(AppliedMigrationEvent, migration)
|
||||
await this.bus.dispatch(event)
|
||||
await this.bus.push(event)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -280,7 +240,7 @@ export abstract class Migrator {
|
||||
*/
|
||||
protected async rollingBack(migration: Migration): Promise<void> {
|
||||
const event = <RollingBackMigrationEvent> this.injector.make(RollingBackMigrationEvent, migration)
|
||||
await this.bus.dispatch(event)
|
||||
await this.bus.push(event)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,6 +250,6 @@ export abstract class Migrator {
|
||||
*/
|
||||
protected async rolledBack(migration: Migration): Promise<void> {
|
||||
const event = <RolledBackMigrationEvent> this.injector.make(RolledBackMigrationEvent, migration)
|
||||
await this.bus.dispatch(event)
|
||||
await this.bus.push(event)
|
||||
}
|
||||
}
|
||||
|
@ -5,4 +5,6 @@ import {MigrationEvent} from './MigrationEvent'
|
||||
* Event fired after a migration is applied.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AppliedMigrationEvent extends MigrationEvent {}
|
||||
export class AppliedMigrationEvent extends MigrationEvent {
|
||||
eventName = '@extollo/lib.AppliedMigrationEvent'
|
||||
}
|
||||
|
@ -5,4 +5,6 @@ import {MigrationEvent} from './MigrationEvent'
|
||||
* Event fired before a migration is applied.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ApplyingMigrationEvent extends MigrationEvent {}
|
||||
export class ApplyingMigrationEvent extends MigrationEvent {
|
||||
eventName = '@extollo/lib.ApplyingMigrationEvent'
|
||||
}
|
||||
|
@ -1,49 +1,13 @@
|
||||
import {Event} from '../../../event/Event'
|
||||
import {Migration} from '../Migration'
|
||||
import {Inject, Injectable} from '../../../di'
|
||||
import {Migrations} from '../../services/Migrations'
|
||||
import {ErrorWithContext} from '../../../util'
|
||||
import {BaseEvent} from '../../../support/bus'
|
||||
|
||||
/**
|
||||
* Generic base-class for migration-related events.
|
||||
*/
|
||||
@Injectable()
|
||||
export abstract class MigrationEvent extends Event {
|
||||
@Inject()
|
||||
protected readonly migrations!: Migrations
|
||||
|
||||
/** The migration relevant to this event. */
|
||||
private internalMigration: Migration
|
||||
|
||||
/**
|
||||
* Get the relevant migration.
|
||||
*/
|
||||
public get migration(): Migration {
|
||||
return this.internalMigration
|
||||
}
|
||||
|
||||
export abstract class MigrationEvent extends BaseEvent {
|
||||
constructor(
|
||||
migration: Migration,
|
||||
public readonly migration: Migration,
|
||||
) {
|
||||
super()
|
||||
this.internalMigration = migration
|
||||
}
|
||||
|
||||
dehydrate(): {identifier: string} {
|
||||
return {
|
||||
identifier: this.migration.identifier,
|
||||
}
|
||||
}
|
||||
|
||||
rehydrate(state: {identifier: string}): void {
|
||||
const migration = this.migrations.get(state.identifier)
|
||||
|
||||
if ( !migration ) {
|
||||
throw new ErrorWithContext(`Unable to find migration with identifier: ${state.identifier}`, {
|
||||
identifier: state.identifier,
|
||||
})
|
||||
}
|
||||
|
||||
this.internalMigration = migration
|
||||
}
|
||||
}
|
||||
|
59
src/orm/migrations/events/MigrationEventSerializer.ts
Normal file
59
src/orm/migrations/events/MigrationEventSerializer.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {BaseSerializer} from '../../../support/bus'
|
||||
import {ObjectSerializer} from '../../../support/bus/serial/decorators'
|
||||
import {Injectable, Instantiable} from '../../../di'
|
||||
import {Awaitable, ErrorWithContext, JSONState} from '../../../util'
|
||||
import {MigrationEvent} from './MigrationEvent'
|
||||
import {Migrations} from '../../services/Migrations'
|
||||
import {AppliedMigrationEvent} from './AppliedMigrationEvent'
|
||||
import {ApplyingMigrationEvent} from './ApplyingMigrationEvent'
|
||||
import {RolledBackMigrationEvent} from './RolledBackMigrationEvent'
|
||||
import {RollingBackMigrationEvent} from './RollingBackMigrationEvent'
|
||||
|
||||
export interface MigrationEventSerialPayload extends JSONState {
|
||||
identifier: string
|
||||
eventType: string
|
||||
}
|
||||
|
||||
@ObjectSerializer()
|
||||
@Injectable()
|
||||
export class MigrationEventSerializer extends BaseSerializer<MigrationEvent, MigrationEventSerialPayload> {
|
||||
protected decodeSerial(serial: MigrationEventSerialPayload): Awaitable<MigrationEvent> {
|
||||
const migration = this.make<Migrations>(Migrations).get(serial.identifier)
|
||||
if ( !migration ) {
|
||||
throw new ErrorWithContext(`Invalid serialized migration identifier: ${serial.identifier}`)
|
||||
}
|
||||
|
||||
return (new (this.stringToEvent(serial.eventType))(migration))
|
||||
}
|
||||
|
||||
protected encodeActual(actual: MigrationEvent): Awaitable<MigrationEventSerialPayload> {
|
||||
return {
|
||||
identifier: actual.migration.identifier,
|
||||
eventType: actual.eventName,
|
||||
}
|
||||
}
|
||||
|
||||
protected getName(): string {
|
||||
return '@extollo/lib.MigrationEventSerializer'
|
||||
}
|
||||
|
||||
matchActual(some: MigrationEvent): boolean {
|
||||
return some instanceof MigrationEvent
|
||||
}
|
||||
|
||||
private stringToEvent(name: string): Instantiable<MigrationEvent> {
|
||||
if ( name === '@extollo/lib.AppliedMigrationEvent' ) {
|
||||
return AppliedMigrationEvent
|
||||
} else if ( name === '@extollo/lib.ApplyingMigrationEvent' ) {
|
||||
return ApplyingMigrationEvent
|
||||
} else if ( name === '@extollo/lib.RollingBackMigrationEvent' ) {
|
||||
return RollingBackMigrationEvent
|
||||
} else if ( name === '@extollo/lib.RolledBackMigrationEvent' ) {
|
||||
return RolledBackMigrationEvent
|
||||
}
|
||||
|
||||
throw new ErrorWithContext(`Invalid migration event name: ${name}`, {
|
||||
name,
|
||||
})
|
||||
}
|
||||
}
|
@ -5,4 +5,6 @@ import {MigrationEvent} from './MigrationEvent'
|
||||
* Event fired after a migration has been rolled-back.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RolledBackMigrationEvent extends MigrationEvent {}
|
||||
export class RolledBackMigrationEvent extends MigrationEvent {
|
||||
eventName = '@extollo/lib.RolledBackMigrationEvent'
|
||||
}
|
||||
|
@ -5,4 +5,6 @@ import {MigrationEvent} from './MigrationEvent'
|
||||
* Event fired before a migration is rolled back.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RollingBackMigrationEvent extends MigrationEvent {}
|
||||
export class RollingBackMigrationEvent extends MigrationEvent {
|
||||
eventName = '@extollo/lib.RollingBackMigrationEvent'
|
||||
}
|
||||
|
@ -1,14 +1,12 @@
|
||||
import {ModelKey, QueryRow, QuerySource} from '../types'
|
||||
import {Container, Inject, Instantiable, isInstantiable, StaticClass} from '../../di'
|
||||
import {Container, Inject, Instantiable, isInstantiable} from '../../di'
|
||||
import {DatabaseService} from '../DatabaseService'
|
||||
import {ModelBuilder} from './ModelBuilder'
|
||||
import {getFieldsMeta, ModelField} from './Field'
|
||||
import {deepCopy, Collection, Awaitable, uuid4, isKeyof, Pipeline} from '../../util'
|
||||
import {deepCopy, Collection, uuid4, isKeyof, Pipeline} from '../../util'
|
||||
import {EscapeValueObject} from '../dialect/SQLDialect'
|
||||
import {AppClass} from '../../lifecycle/AppClass'
|
||||
import {Logging} from '../../service/Logging'
|
||||
import {Connection} from '../connection/Connection'
|
||||
import {Bus, Dispatchable, EventSubscriber, EventSubscriberEntry, EventSubscription} from '../../event/types'
|
||||
import {ModelRetrievedEvent} from './events/ModelRetrievedEvent'
|
||||
import {ModelSavingEvent} from './events/ModelSavingEvent'
|
||||
import {ModelSavedEvent} from './events/ModelSavedEvent'
|
||||
@ -16,23 +14,21 @@ import {ModelUpdatingEvent} from './events/ModelUpdatingEvent'
|
||||
import {ModelUpdatedEvent} from './events/ModelUpdatedEvent'
|
||||
import {ModelCreatingEvent} from './events/ModelCreatingEvent'
|
||||
import {ModelCreatedEvent} from './events/ModelCreatedEvent'
|
||||
import {EventBus} from '../../event/EventBus'
|
||||
import {Relation, RelationValue} from './relation/Relation'
|
||||
import {HasOne} from './relation/HasOne'
|
||||
import {HasMany} from './relation/HasMany'
|
||||
import {HasOneOrMany} from './relation/HasOneOrMany'
|
||||
import {Scope, ScopeClosure} from './scope/Scope'
|
||||
import {LocalBus} from '../../support/bus/LocalBus'
|
||||
import {ModelEvent} from './events/ModelEvent'
|
||||
|
||||
/**
|
||||
* Base for classes that are mapped to tables in a database.
|
||||
*/
|
||||
export abstract class Model<T extends Model<T>> extends AppClass implements Bus {
|
||||
export abstract class Model<T extends Model<T>> extends LocalBus<ModelEvent<T>> {
|
||||
@Inject()
|
||||
protected readonly logging!: Logging
|
||||
|
||||
@Inject()
|
||||
protected readonly bus!: EventBus
|
||||
|
||||
/**
|
||||
* The name of the connection this model should run through.
|
||||
* @type string
|
||||
@ -100,12 +96,6 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
*/
|
||||
protected originalSourceRow?: QueryRow
|
||||
|
||||
/**
|
||||
* Collection of event subscribers, by their events.
|
||||
* @protected
|
||||
*/
|
||||
protected modelEventBusSubscribers: Collection<EventSubscriberEntry<any>> = new Collection<EventSubscriberEntry<any>>()
|
||||
|
||||
/**
|
||||
* Cache of relation instances by property accessor.
|
||||
* This is used by the `@Relation()` decorator to cache Relation instances.
|
||||
@ -257,7 +247,7 @@ export abstract class Model<T extends Model<T>> extends AppClass implements Bus
|
||||
this.setFieldFromObject(field.modelKey, field.databaseKey, row)
|
||||
})
|
||||
|
||||
await this.dispatch(new ModelRetrievedEvent<T>(this as any))
|
||||
await this.push(new ModelRetrievedEvent<T>(this as any))
|
||||
return this
|
||||
}
|
||||
|
||||