I'm currently using this with decent volume in production. When I feel it's proven to be fully stable, I'll mark a "stable" veresion of this library. Until then, my commits & NPM publishes may not be up to date. Feel free to contribute / mark issues or feature requests. Star this and I'll most likely go through each project with the most interest to stabilize and clean up.
Other notes: I'll most likely remove the events in favor of allowing people to just use my type-events
library. It's felt better for me so far.
type-clean
A simple (& lightly opinionated) clean architecture framework.
type-clean tries to be nothing special. It provides a simple and clean way of running and scribing to commands/use-cases.
Installation
yarn add type-clean
or
npm install type-clean
Usage
import { CommandRunner, Command, On } from 'type-clean';
// ./models/User.ts
class User {
email: string;
}
// ./commands/CreateUser.ts
class CreateUser implements Command {
async handle(fields: Partial<User>): Promise<User> {
return Object.assign(new User(), { ...fields });
}
}
// ./commands/UserSubscribe.ts
class UserSubscribe implements Command {
async handle(email: string): Promise<string> {
// subscribe user to 3rd party email system
}
}
// ./commands/SendEmail.ts
class SendEmail implements Command {
async handle(email: string): Promise<string> {
// send some email
}
}
// ./workflows/CreateUserWorkflow.ts
class CreateUserWorkflow {
@On(CreateUser)
async onCreateUser(user: User, runner: CommandRunner): Promise<void> {
await runner.run(UserSubscribe, user.email);
await runner.run(SendEmail, user.email);
}
}
// ./app.ts
import { route } from 'some-framework';
const runner = new CommandRunner();
route.post('/user', async (body: Partial<User>): Promise<User> => {
return await runner.run(CreateUser, body);
});
In this example, we are awaiting 3rd party user subscribes and sending email. In a real-world app, you'd want to use true CQRS
, but sometimes that's too much. It might be best to simply use this module and emit events to SQS
/ SNS
and handle the subscription asynchronously.
Using alternative DI. (i.e. inversify)
import { Container, injectable, inject } from 'inversify';
import { CommandRunner } from 'type-clean';
import { UserRepository } from './UserRepository';
@injectable()
class CreateUser implements Command {
constructor(@inject(UserRepository) private repo: UserRepository) {}
async handle(fields: Partial<User>): Promise<User> {
return this.repo.create(fields);
}
}
const container = new Container();
container.bind(UserRepository).toSelf().inSingletonScope();
container.bind(CreateUser).toSelf().inSingletonScope();
const runner = new CommandRunner({ container });
// ...
Events
There are a few ways of executing other logic before or after events are ran.
-
@Use()
: acts as a middleware. -
@BeforeCommand(Command)
: Gets called after@Use
& before the command is handled. -
@AfterCommand(Command)
: Gets called after the command is handled with the handler result.
import { Use, BeforeCommand, AfterCommand, Middleware } from 'type-clean';
import { validate } from 'class-validator';
class ValidateCommand implements Middleware {
async use(input: CreateUserInput): Promise<User> {
await validate(input);
}
}
class CreateUserCommand implements Command {
@Use(ValidateCommand)
async handle(input: CreateUserInput): Promise<User> {
// ...
}
}
class CreateUserSubscriber {
@BeforeCommand(CreateUserCommand)
async beforeCreateUser(input: CreateUserInput) {
console.log(`Creating user ${input.name}`);
}
@AfterCommand(CreateUserCommand)
async afterCreateUser(user: User) {
console.log(`Created user ${user.id}`);
}
}
Examples
You can run examples by installing ts-node
globally and running ts-node examples/type-graphql
;