@beardedtim/archie
v0.1.14
Published
A way to build _processors_ that _do things_ and result in _some context_ being set across them.
Downloads
19
Readme
Archie
A way to build processors that do things and result in some context being set across them.
Usage
You can find full-fledged examples inside of
/examples
if you want to see how this might look in real-life
PING -> PONG
Here is an example of a system sending messages to itself in order to respond to a specific question. This proves that each invocation is a new closure/ran each time and that the systems can be nested.
enum Actions {
PING = "PING",
PONG = "PONG",
}
RootSystem.when(Actions.PING)
.validate(always(Promise.resolve(true)))
.do(async (ctx, action) => {
const pingSymbol = Symbol("New Ping");
const result = await RootSystem.handle(Actions.PONG, {
id: pingSymbol,
});
ctx.set("body", result.get("body") === pingSymbol);
});
RootSystem.when(Actions.PONG)
.validate(always(Promise.resolve(true)))
.do(async (ctx, action) => {
ctx.set("body", action.payload.id);
});
const pingSuccessful = await RootSystem.handle(Actions.PING, {});
console.log(pingSuccessful.get("body")); // true
Express Integration
/**
* You can manually handle the triggering of the system
* via some external event, such as an Express Request Handler
*/
const healthcheckRouter: RequestHandler = async (req, res, next) => {
const ctx = await System.handle(
Actions.HEALTHCHECK,
Plugins.Express.actionFromRequest(req).payload
);
res.json(ctx.get("body"));
};
/**
* And attach it to the external system manuall
*/
Server.get("/healthcheck", healthcheckRouter);
/**
* Or you can use a plugin and just have the System
* generically handle the external request
*/
Server.use(Plugins.Express.middleware(System));
System.when("/:foo")
.where(async (action) => action.payload.method === "get")
.do(async (ctx, action) => {
console.log("I am doing anything that starts with /:foo and is a GET request", action.meta);
});
Nested Systems
import { System, Log } from "@beardedtim/archie";
import { randomUUID } from "crypto";
const CruddySystem = new System({
name: "Cruddy",
usePattern: true,
// ignore validation for demo purposes
useManualActionValidation: true
});
const UserSystem = new System({
name: "Cruddy::User",
// ignore validation for demo purposes
useManualActionValidation: true
});
UserSystem.when("CREATE").do(async (ctx, action) => {
if (!action.payload.body) {
console.log(action, "action");
throw new Error("Missing Body for Creating of User");
}
ctx.set("body", {
id: "123-456",
name: action.payload.body.name,
});
});
CruddySystem.beforeAll().do(async (ctx, action) => {
const traceId = randomUUID({ disableEntropyCache: true });
ctx.set("trace-id", traceId);
Log.trace({ action, traceId }, "Handling");
});
CruddySystem.afterAll().do(async (ctx, action) => {
const traceId = ctx.get("trace-id");
Log.trace({ action, traceId }, "Handled");
});
CruddySystem.when("foobar").do(async (ctx, action) => {
Log.debug({ action }, "I am the event handler");
// Systems can call Systems
const result = await UserSystem.handle("CREATE", {
body: {
name: "Tim",
},
});
ctx.set("body", {
data: result.get("body"),
});
});
const main = async () => {
const result = await CruddySystem.handle("foobar", {
some: "payload",
});
console.log(result.get("body"), result.get("trace-id"));
};
main();
Helpers
validateByJSONSchema
This allows you to say that the action.payload
value will
match a specific JSON Schema
const healthcheckSchema = {
type: "object",
required: ["hello"],
properties: {
hello: {
type: "string",
},
},
};
/**
* When some ACTION occours
*/
System.when(Actions.HEALTHCHECK)
.validate(Helpers.validateByJSONScema(healthcheckSchema))
/**
* Do some list of things
*/
.do(async (ctx, action) => {
console.log("System handling HEALTHCHECK action", action);
console.log("Maybe we go out and check Database connections, or whatever");
ctx.set("body", {
data: {
healthy: true,
},
});
});
Generating Docs
A System comes built with the ability to have docs generated for it. Right now, the docs are sparse but the interfaces are built and ready for consuming.
import { Doc } from '@beardedtim/archie'
/**
* Somehow build up a UserSystem
*/
const UserDocs = new Doc(UserSystem);
console.log(UserDocs.generate());
console.log();
prints something like
# System: User
- Uses Patterns: No
## Actions
- Action CREATE
- validateUserCreation
- createUser
- Action READ_ONE
- readUserById
to the console.
NOTE If you do not name the functions you pass to do
, they will be a blank line.
Always make the function you pass to do
a named function (const foo = () => { ... }
)
if you intend on using the Documentation Generation in any serious way.
Demo
git clone [email protected]:beardedtim/archie.git
npm i
yarn ts-node demo/index.ts
Reasoning
I wanted a way to build an action processor that held some state between the pipeline processors. I also wanted to remove any concept of HTTP/WS/Whatever from the unit of value that I think action processing offers.
Instead of tying the middleware processor to some HTTP library like Express, I am trying to build our system in a way that is agnostic from the outside world in a meaningful way. I am trying to keep the unit of value as the modification of context given some action and action handlers.
Terminology
Action
An Action is an internal representation of some request or external thing that can be handled within the system.
Context
A Context is some shared state between Action Handlers
Action Handler
An Action Handler is responsible for taking in the tuple (Context, Action) and doing something with that, including setting some shared value on Context.
System
A System is a grouping of Action Handlers, Preware, and Postware. It is responsible for taking in some request, processing it, and returning some output.
Plugins
A way to interact with external things such as Express in an abstract way.