-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
371 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface CommandHandler<I = any, O = any> { | ||
handle: (event: I) => Promise<O> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { Class } from 'type-fest' | ||
import { Container, DefaultContainer } from '@rimo/container' | ||
import { storage, SubscriberMetadata, SubscribersMetadata } from '@rimo/metadata' | ||
import { CommandHandler } from './CommandHandler' | ||
import { RunnerCommandHandler } from './RunnerCommandHandler' | ||
|
||
export interface CommandRunnerConfig { | ||
container?: Container | ||
} | ||
|
||
/** | ||
* Runs commands | ||
*/ | ||
export class CommandRunner { | ||
protected container: Container | ||
protected handlers: Map<Class<CommandHandler>, CommandHandler> = new Map() | ||
protected subscribers: Map<Class<CommandHandler>, SubscriberMetadata> = new Map() | ||
|
||
constructor({ container = new DefaultContainer() }: CommandRunnerConfig = {}) { | ||
this.container = container | ||
} | ||
|
||
/** | ||
* Executes commands. | ||
*/ | ||
run = async <T extends CommandHandler>( | ||
Handler: Class<T>, | ||
event: Parameters<T['handle']>['0'] | ||
): Promise<ReturnType<T['handle']>> => { | ||
const handler = await this.container.get(Handler) | ||
|
||
if (handler instanceof RunnerCommandHandler) { | ||
handler.setRunner(this) | ||
} | ||
|
||
await this.emit('middleware', Handler, event) | ||
await this.emit('before', Handler, event) | ||
const result = await handler.handle(event) | ||
await this.emit('after', Handler, result) | ||
|
||
return result | ||
} | ||
|
||
/** | ||
* Serially executes `@BeforeCommand(Handler)` or `@AfterCommand(Handler)` decorated methods for the given `Handler`. | ||
*/ | ||
emit = async <T extends CommandHandler>( | ||
type: keyof SubscribersMetadata, | ||
HandlerClass: Class<T>, | ||
result: ReturnType<T['handle']> | ||
): Promise<void> => { | ||
if (storage.metadata.subscribers[type].has(HandlerClass)) { | ||
const subscribers = storage.metadata.subscribers[type].get( | ||
HandlerClass | ||
) as SubscriberMetadata[] | ||
for (const meta of subscribers) { | ||
await this.container.get(meta.SubscriberClass)[meta.method](result, this) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface Middleware<I = any> { | ||
use: (event: I) => Promise<any> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { CommandHandler } from './CommandHandler' | ||
import { CommandRunner } from './CommandRunner' | ||
|
||
export abstract class RunnerCommandHandler<I = any, O = any> implements CommandHandler<I, O> { | ||
abstract async handle(event: I): Promise<O> | ||
|
||
public runner!: CommandRunner | ||
|
||
// async run<T extends CommandHandler>( | ||
// Handler: Class<T>, | ||
// event: Parameters<T['handle']>['0'] | ||
// ): Promise<ReturnType<T['handle']>> { | ||
// return this.runner.run(Handler, event) | ||
// } | ||
|
||
public setRunner(runner: CommandRunner) { | ||
this.runner = runner | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export * from './CommandHandler' | ||
export * from './CommandRunner' | ||
export * from './RunnerCommandHandler' | ||
export * from './Middleware' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export interface Container { | ||
get(someClass: any): any | ||
} | ||
|
||
export class DefaultContainer { | ||
private instances: Map<any, any> = new Map() | ||
|
||
get<T>(Clazz: any): T { | ||
if (!this.instances.has(Clazz)) { | ||
const handler = new Clazz() | ||
|
||
this.instances.set(Clazz, handler) | ||
|
||
return handler | ||
} | ||
|
||
return this.instances.get(Clazz) as T | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Container' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Class } from 'type-fest' | ||
import { CommandHandler } from '@rimo/command' | ||
import { OnOptions, addSubscriber } from './utils' | ||
|
||
export const AfterCommand = ( | ||
HandlerClass: Class<CommandHandler>, | ||
options?: OnOptions | ||
): MethodDecorator => { | ||
return ( | ||
target: any, | ||
propertyKey: string | symbol, | ||
_descriptor: TypedPropertyDescriptor<any> | ||
): void => { | ||
addSubscriber('after', HandlerClass, options, target.constructor, propertyKey) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Class } from 'type-fest' | ||
import { CommandHandler } from '@rimo/command' | ||
import { OnOptions, addSubscriber } from './utils' | ||
|
||
export const BeforeCommand = ( | ||
HandlerClass: Class<CommandHandler>, | ||
options?: OnOptions | ||
): MethodDecorator => { | ||
return ( | ||
target: any, | ||
propertyKey: string | symbol, | ||
_descriptor: TypedPropertyDescriptor<any> | ||
): void => { | ||
addSubscriber('before', HandlerClass, options, target.constructor, propertyKey) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Class } from 'type-fest' | ||
import { Middleware } from '@rimo/command' | ||
import { OnOptions, addSubscriber } from './utils' | ||
|
||
export const Use = (HandlerClass: Class<Middleware>, options?: OnOptions): MethodDecorator => { | ||
return ( | ||
target: any, | ||
_propertyKey: string | symbol, | ||
_descriptor: TypedPropertyDescriptor<any> | ||
): void => { | ||
addSubscriber('middleware', target.constructor, options, HandlerClass, 'use') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './AfterCommand' | ||
export * from './BeforeCommand' | ||
export * from './Use' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { Class } from 'type-fest' | ||
import { CommandHandler } from '@rimo/command' | ||
import { SubscribersMetadata, SubscriberMetadata, storage } from '@rimo/metadata' | ||
|
||
export interface OnOptions { | ||
priority?: number | ||
} | ||
|
||
const sortByPriority = (arr: { priority: number }[]): void => { | ||
arr.sort((a, b) => (a.priority < b.priority ? 1 : -1)) | ||
} | ||
|
||
export const addSubscriber = ( | ||
kind: keyof SubscribersMetadata, | ||
HandlerClass: Class<CommandHandler>, | ||
options: OnOptions = {}, | ||
SubscriberClass: any, | ||
propertyKey: string | symbol | ||
) => { | ||
let subscribers: SubscriberMetadata[] | ||
|
||
// istanbul ignore next | ||
if (storage.metadata.subscribers[kind].has(HandlerClass)) { | ||
subscribers = storage.metadata.subscribers[kind].get(HandlerClass) as SubscriberMetadata[] | ||
} else { | ||
subscribers = [] | ||
storage.metadata.subscribers[kind].set(HandlerClass, subscribers) | ||
} | ||
|
||
subscribers.push({ | ||
HandlerClass, | ||
SubscriberClass, | ||
method: propertyKey as string, | ||
priority: options.priority || 0, | ||
}) | ||
|
||
sortByPriority(subscribers) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './command' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { Class } from 'type-fest' | ||
import { CommandHandler } from '@rimo/command' | ||
|
||
declare var global: { | ||
__rimo_metadata_storage?: MetadataStorage | ||
} | ||
|
||
export interface SubscriberMetadata { | ||
HandlerClass: Class | ||
SubscriberClass: Class | ||
method: string | symbol | ||
priority: number | ||
} | ||
|
||
export interface SubscribersMetadata { | ||
middleware: Map<Class<CommandHandler>, SubscriberMetadata[]> | ||
before: Map<Class<CommandHandler>, SubscriberMetadata[]> | ||
after: Map<Class<CommandHandler>, SubscriberMetadata[]> | ||
} | ||
|
||
interface Metadata { | ||
subscribers: SubscribersMetadata | ||
} | ||
|
||
export class MetadataStorage { | ||
public metadata: Metadata = { | ||
subscribers: { | ||
middleware: new Map(), | ||
before: new Map(), | ||
after: new Map(), | ||
}, | ||
} | ||
|
||
private constructor() {} | ||
|
||
static getInstance(): MetadataStorage { | ||
if (!global.__rimo_metadata_storage) { | ||
global.__rimo_metadata_storage = new MetadataStorage() | ||
} | ||
|
||
return global.__rimo_metadata_storage | ||
} | ||
} | ||
|
||
export const storage: MetadataStorage = MetadataStorage.getInstance() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Metadata' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
export * from './command' | ||
export * from './container' | ||
export * from './core' | ||
export * from './decorators' | ||
export * from './mapper' | ||
export * from './monad' | ||
export * from './repository' | ||
export * from './service' |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { IsNotEmpty, IsEmail, validate } from 'class-validator' | ||
import { Middleware, CommandRunner, RunnerCommandHandler, CommandHandler } from '@rimo/command' | ||
import { Use, BeforeCommand, AfterCommand } from '@rimo/decorators' | ||
import { Entity } from '@rimo/core' | ||
|
||
import { DefaultContainer } from '@rimo/container' | ||
|
||
/** | ||
* CommandRunner test | ||
*/ | ||
describe('CommandRunner test', () => { | ||
describe('Acceptance test', () => { | ||
let runner: CommandRunner | ||
let watchers: { | ||
before: number[] | ||
after: number[] | ||
} | ||
|
||
beforeEach(() => { | ||
runner = new CommandRunner() | ||
watchers = { | ||
before: [], | ||
after: [], | ||
} | ||
}) | ||
|
||
class User extends Entity<{ email: string }> { | ||
get email() { | ||
return this.props.email | ||
} | ||
} | ||
|
||
class CreateUserInput { | ||
@IsNotEmpty() | ||
@IsEmail() | ||
email!: string | ||
|
||
static populate = (data: any) => { | ||
return Object.assign(new CreateUserInput(), data) | ||
} | ||
} | ||
|
||
class ValidateCommand implements Middleware<CreateUserInput> { | ||
async use(event: CreateUserInput) { | ||
const errors = await validate(event) | ||
if (errors.length > 0) { | ||
throw new Error('validation error') | ||
} | ||
} | ||
} | ||
|
||
class CreateUserCommand extends RunnerCommandHandler<CreateUserInput, User> { | ||
@Use(ValidateCommand) | ||
async handle(input: CreateUserInput): Promise<User> { | ||
return new User(input) | ||
} | ||
} | ||
|
||
class CreateUserSubscriber { | ||
@BeforeCommand(CreateUserCommand, { priority: 0 }) | ||
async beforeCreateUser(input: CreateUserInput) { | ||
watchers.before.push(0) | ||
} | ||
|
||
@BeforeCommand(CreateUserCommand, { priority: 1 }) | ||
async beforeCreateUser2(input: CreateUserInput) { | ||
watchers.before.push(1) | ||
} | ||
|
||
@BeforeCommand(CreateUserCommand, { priority: 2 }) | ||
async beforeCreateUser3(input: CreateUserInput) { | ||
watchers.before.push(2) | ||
} | ||
|
||
@AfterCommand(CreateUserCommand) | ||
async afterCreateUser(user: User) { | ||
watchers.after.push(0) | ||
} | ||
} | ||
|
||
it('should execute middlewares (use)', async () => { | ||
const input = CreateUserInput.populate({}) | ||
await expect(runner.run(CreateUserCommand, input)).rejects.toThrowError() | ||
}) | ||
|
||
it('should notify subscribers', async () => { | ||
const input = CreateUserInput.populate({ email: '[email protected]' }) | ||
const output = await runner.run(CreateUserCommand, input) | ||
expect(watchers.before).toStrictEqual([2, 1, 0]) | ||
expect(watchers.after).toHaveLength(1) | ||
}) | ||
}) | ||
|
||
describe('Custom container', () => { | ||
interface Email { | ||
value: string | ||
} | ||
|
||
class RegisterEmailCommand implements CommandHandler<string, Email> { | ||
async handle(value: string): Promise<Email> { | ||
return { value } | ||
} | ||
} | ||
|
||
it('should run', async () => { | ||
const runner = new CommandRunner({ container: new DefaultContainer() }) | ||
const output = await runner.run(RegisterEmailCommand, '[email protected]') | ||
expect(output.value).toBe('[email protected]') | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.