Security News
Input Validation Vulnerabilities Dominate MITRE's 2024 CWE Top 25 List
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
@carpasse/dapi
Advanced tools
Simple library to create complex systems out of pure functions
npm install -D @carpasse/dapi
To create an DapiWrapper
you need to create an DapiDefinition
object and pass it to the createDapi
factory function.
import {createDapi} from '@carpasse/';
type Dependencies = {
db: OracleClient,
logger: Logger
}
type UserData = {
name: string,
email: string
}
// Pure function that takes its dependencies in the first argument aka DapiFn
const insertUser = async ({db, logger}: Dependencies, data: UserData) => {
logger.info(`Inserting user`, {data});
const entity = await db.insert('User', {
...data,
id: randomUUID()
});
return entity;
}
export const createDapiUser = (dependencies: Dependencies) => createDapi({
dependencies,
fns: {
// dictionary of pure functions that accept the above passed dependencies as their first argument
create: insertUser,
// ...
},
type: 'Entity'
name: 'User'
}, EventEmitter);
export type DapiUser = ReturnType<typeof createUserApi>;
import {createDapiUser} from './user';
import config from './config';
import {createDb} from './db';
import {createLogger} from './logger';
const user = createDapiUser({
db: await createDb(config.db),
logger: await createLogger(config.logger)
});
user.create({
name: 'John Doe',
email: 'johndoe@random.company.com'
});
// or
const {create: createUser} = createDapiUser({
db: await createDb(config.db),
logger: await createLogger(config.logger)
});
createUser({
name: 'John Doe',
email: 'johndoe@random.company.com'
});
dapi
?For a system to be maintainable, it needs to be testable. If your system is hard to test, it is hard to maintain and evolve. The problem is that code is hard to test because it is usually designed to be tested. It is designed to be executed.
We are getting a bit philosophical here, but my above statements goes against the obvious fact that we write code to be executed and writing code with execution in mind should be the priority.
So, what is the simplest execution logic you can think of? A function. A function is a piece of code that takes some input and returns some output. It is the simplest form of execution logic. And for a function to be easily testable, it needs to be pure.
A pure function is a function that has no side effects and always returns the same output for the same input.
The problem with pure functions is that they usually have a set of dependencies they rely on to do their job. For example, they may need to access a database, or they may need to send an email. This is where the dependency injection comes in handy.
Dependency injection is a technique whereby you split the responsibility of creating the dependencies from the responsibility of using them. By doing so, you can easily change them as your understanding of the system evolves. It also make the code easier to test because you can easily mock the dependencies.
Lets create systems that are composed of pure functions that accept as their first argument all the dependencies they needs to do their job. This way, we can easily mock the dependencies when we test them.
From now on, we will call these functions DapiFns.
import {randomUUID} from 'crypto';
export type Dependencies = {
db: OracleClient,
logger: Logger
}
export type UserData = {
name: string,
email: string
}
// Pure function that takes its dependencies in the first argument i.e. a DapiFn
export const createUser = async ({db, logger}: Dependencies, data: UserData) => {
logger.info(`Creating user`, {data});
const entity = await db.insert('User', {
...data,
id: randomUUID()
});
return entity;
}
The problem with this approach is that it is very verbose. You need to pass the dependencies to every function you call. createUser
function is going to be called by another function that will also needs other dependencies. This means that you need to pass the dependencies to the caller and the caller needs to pass the dependencies to the callee.
This is not ideal. The caller should not know about the dependencies of the callee. It should only know about the dependencies it needs to do its job. It also makes the code harder to test because you need to mock the dependencies of the caller and the callee.
import {
createUser,
type Dependencies as UserDependencies,
type UserData
} from './createUser';
export const createCustomer = async (
{db, logger, mqBroker}: {mqBroker: RabbitMQ} & UserDependencies,
data: UserData) => {
logger.info(`Creating customer`, {data});
const newUser = await createUser({db, logger, mqBroker}, 'customer', data);
mqBroker.publish('customer.created', {data: newUser});
}
Ideally you should pass a createUser
fn with its dependencies already set to the createCustomer
fn. This way, the createCustomer
fn does not need to know about the dependencies of the createPerson
fn. It only needs to know about the dependencies it needs to do its job.
import {createDapi, DapiFn} from '@carpasse/';
import type {DapiUser, UserData} from './user';
import type {Logger} from './logger';
import type {RabbitMQ} from './mqBroker';
export type CustomerDeps = {
user: DapiUser,
logger: Logger,
mqBroker: RabbitMQ
}
export const createCustomer= async ({user: {create}, logger}: CustomerDeps, data: UserData) => {
logger.info(`Creating customer`, {data});
const newUser = await create('customer', data);
mqBroker.publish('customer.created', {data: newUser});
};
export const createDapiCustomer = (dependencies: CustomerDeps) => createDapi({
dependencies,
fns: {
create: createCustomer
},
type: 'Entity'
name: 'Customer'
}, EventEmitter);
This is where the dapi package comes in handy. The dapi package is a library that allows you to loosely bind the dependencies to the functions by creating an DapiWrapper around a group of functions that share the same dependencies (DapiFns). This way, you can easily pass the DapiWrapper instance as a dependency and the caller does not need to know about the dependencies of the callee.
It means that the dependencies are not bound to the functions. The dapi wrapper will pass them to the function on execution. By doing so you are able to modify dependencies on runtime. Or decorate the dapi fns or add hooks to them.
Following on the previous example, we could do:
import {createLogger} from './logger';
import {createDapiUser} from './user';
import {createDapiCustomer} from './customer';
import {profile} from './profiler';
const start = async (config: Config) => {
const logger = createLogger(config.logger);
const {mqBroker, db} = createServiceClients(config);
const user = createDapiUser({db, logger});
const customer = createDapiCustomer({user, logger, mqBroker});
config.on('db.config.update', async (dbConfig) => {
const newDb = await createDb(dbConfig);
await db.close();
user.updateDependencies({db}); // From this point on the user Dapi fns will receive the new db client on their first argument
});
if(config.profile.customer) {
customer.decorate('create', (create, deps, ...args) => {
return profile(deps.logger, create(deps, ...args));
});
}
// you could also add hooks to the DapiFns
customer.addPreHook('create', (create, deps, ...args) => {
deps.logger.info(`Create customer start` data);
});
customer.addPostHook('create', (createCustomer, deps, ...args) => {
deps.logger.info(`Create customer end` data);
});
// ... logic to start the server
}
Represents an DapiWrapper
definition with dependencies and fns.
DEPENDENCIES
- The type of dependencies required by the DAPI
functions dictionary.DAPI
- A dictionary of pure functions the DapiWrapper will be composed of. All functions must accepts as their first argument a DEPENDENCIES
type obj. /**
* Represents an `DapiWrapper` definition with dependencies and commands.
* @template DEPENDENCIES - The type of dependencies required by the DapiFns.
* @template DAPI - The pure functions the `Dapiwrapper` will be composed of.
*/
interface interface DapiDefinition<DEPENDENCIES, DAPI extends DapiFns<DEPENDENCIES>> {
dependencies: DEPENDENCIES;
fns: DAPI;
type: string;
name?: string;
}
dependencies
- The dependencies required by the DapiFns
.fns
- The pure functions the DAPI wrapper will be composed of.type
- The type of the DapiWrapper
.name
- The name of the DapiWrapper
.DapiMixin
Mixin fn that creates a DapiWrapper
class that extends the passed SuperClass class and wraps the passed DapiFns dictionary.
DapiMixin<T extends DapiDefinition>
(definition: T, SuperClass?: SuperClass): DapiWrapper<T>
definition
- An DapiDefinition
objectSuperClass
] - An optional SuperClass class the returned DapiWrapper
class will extend from.DapiWrapper
class.createDapi
Factory function that creates an DapiWrapper instance of the passed the DapiFns
. it accepts a DapiDefinition
object and an optional SuperClass class.
createDapi<T extends DapiDefinition>
(definition: T, SuperClass?: SuperClass): DapiWrapper<T>
definition
- An DapiDefinition
objectSuperClass
] - An optional SuperClass class the returned DapiWrapper
class will extend from.DapiWrapper
class instance.TypeError
- If the definition is not an objectTypeError
- If the definition has no dependencies
propertyTypeError
- If the definition has no fns
propertyTypeError
- If the definition has no type
propertyDapiWrapper
Inner class created by createDapi
factory that wraps the DapiDefinition.fns
fns and injects them the passed DapiDefinition.dependencies
at runtime. It also provides methods to modify the dependencies and to hook and decorate the dapi fns.
Every instance of the DapiWrapper class creates a Facade of the DapiDefinition.fns
fns. This means that every instance of the DapiWrapper
class will have the same methods as the DapiDefinition.fns
functions dictionary. The only difference is that the instance methods will omit the first argument of the DapiDefinition.fns
. This is because the DapiWrapper class injects the dependencies at runtime.
The class also allows to decorate the Dapi functions with decorators and hooks.
DEPENDENCIES
- The type of dependencies required by the DAPI
fns dictionary.DAPI
- A dictionary of pure functions the DapiWrapper
will be composed of. All functions must accepts as their first argument a DEPENDENCIES
type obj.T
- The constructor type of the class being enhanced.type
Readonly - Same as the DapiDefinition.type
property.name
Readonly - Same as the DapiDefinition.name
property.getDependencies
Dependencies getter.
Syntax:
getDependencies(): DEPENDENCIES
setDependencies
Dependencies setter.
Syntax:
setDependencies(newDeps: DEPENDENCIES): void
Parameters:
newDeps
- The new dependencies to be set.Throws:
TypeError
- If the new dependencies are not defined.updateDependencies
Partially updates the dependencies of the `DapiWrapper`` instance.
Syntax:
updateDependencies(newDeps: Partial<DEPENDENCIES>): void
Parameters:
newDeps
- A partial of the new dependencies to be updated.Throws:
TypeError
- If the new dependencies are not defined.toJSON
Returns a JSON representation of the DapiWrapper
instance.
Syntax:
toJSON(...args: ParamsExtract<JSON['stringify'], [Parameters<JSON['stringify']>[0]]>)
Parameters:
replacer
- An array of strings and numbers that acts as an approved list for selecting the object properties that will be stringified.space
- Adds indentation, white space, and line break characters to the return-value JSON text to make it easier to read.toString
Returns the DapiWrapper
definition stringified for reading.
Syntax:
toString(): string
addDecorator
Adds a decorator to a DapiFn
of the DapiWrapper
instance.
Syntax:
addDecorator<KEY extends keyof DAPI>
(key: KEY, decorator: DecoratorFn<DAPI[KEY], DapiWrapper>): () => void
Parameters:
key
- The key of the DapiFn
function to decorate. Must be a key of the DapiDefinition.fns
fns dictionary.decorator
- The decorator function to be added to the method. Each decorator must accept the following arguments
fn
- The function to be decorated.deps
- The dependencies of the DapiDefinition.fns
.args
- The arguments passed to the method.Returns:
Throws:
TypeError
- If the key is not a property of the Dapi functions dictionary. i.e. Not a key of the DapiDefinition.fns
dictionary.Example
import {createDapi} from '@carpasse/';
import {profile} from './profiler';
import {createPerson} from './person';
const instance = createDapi({
dependencies: {db, logger},
fns: {
createPerson
},
type: 'Entity'
name: 'Person'
});
instance.addDecorator('createPerson', (createPerson, deps, ...args) => {
return profile(deps.logger, createPerson(deps, ...args));
});
Please note that the decorator function must call the decorated DapiDefinition fn and return the result of its call.
removeDecorator
Removes a decorator from the DapiWrapper
instance.
Syntax:
removeDecorator<KEY extends keyof DAPI>
(key: KEY, decorator: DecoratorFn<DAPI[KEY], DApiWrapper>)
Parameters:
key
- The name of the decorated method. Must be a key of the DapiDefinition.fns
fns dictionary.decorator
- The decorator function to be removed from the method.addHook
Adds a hook to a DapiFn
of the DapiWrapper
instance. Hooks are functions that are called before (hookType
= 'pre'
) or after (hookType
= 'post'
) the DapiFn
is called.
Syntax:
addHook<KEY extends keyof API>(
hookType: 'pre' | 'post',
key: KEY,
hook: HookFn<API[KEY], ApiWrapper>
): () => void
Parameters:
hookType
- The type of hook to be added. Can be either pre
or post
.key
- The name of the method to be hooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be added to the method. Each hook must accept the following arguments
fn
- The function to be hooked.deps
- The dependencies of the DapiDefinition.fns
.args
- The arguments passed to the method.Returns
Throws:
TypeError
- If the key is not a property of the Dapi functions dictionary. i.e. Not a key of the DapiDefinition.fns
dictionary.Example
const instance = createDapi({
dependencies: {db, logger},
fns: {
createPerson
},
type: 'Entity'
name: 'Person'
});
instance.addHook('pre', 'createPerson', (createPerson, deps, ...args) => {
deps.logger.info(`Create person start` data);
});
instance.addHook('post', 'createPerson', (createPerson, deps, ...args) => {
deps.logger.info(`Create person end` data);
});
removeHook
Removes a decorator from the DapiWrapper
instance.
Syntax
removeHook<KEY extends keyof DAPI>(
hookType: 'pre' | 'post',
key: KEY,
hook: HookFn<DAPI[KEY], DapiWrapper>
): void
Parameters
hookType
- The type of hook to be removed. Can be either pre
or post
.key
- The name of the method to be unhooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be removed from the method.addPreHook
Adds a pre hook to DapiDefinition['api']
function calls. Pre hooks are functions that are called before the decorated function is called.
Syntax:
addPreHook<KEY extends keyof DAPI>(
key: KEY,
hook: HookFn<DAPI[KEY], DapiWrapper>
): () => void
Parameters:
key
- The name of the method to be hooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be added to the method. Each hook must accept the following arguments
fn
- The function to be hooked.deps
- The dependencies of the DapiDefinition.fns
.args
- The arguments passed to the method.Returns
Throws:
TypeError
- If the key is not a property of the Dapi functions dictionary. i.e. Not a key of the DapiDefinition.fns
dictionary.removePreHook
Removes a pre hook from a Facade method of the DapiWrapper
instance.
Syntax
removePreHook<KEY extends keyof DAPI>(
key: KEY,
hook: HookFn<DAPI[KEY], DapiWrapper>
): void
Parameters
key
- The name of the method to be unhooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be removed from the method.addPostHook
Adds a post hook to DapiDefinition['api']
function calls. Post hooks are functions that are called after the decorated function is called.
Syntax
addPostHook<KEY extends keyof API>(
key: KEY,
hook: HookFn<API[KEY], ApiWrapper>
): () => void
Parameters:
key
- The name of the method to be hooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be added to the method. Each hook must accept the following arguments
fn
- The function to be hooked.deps
- The dependencies of the DapiDefinition.fns
.args
- The arguments passed to the method.Returns
Throws:
TypeError
- If the key is not a property of the Dapi functions dictionary. i.e. Not a key of the DapiDefinition.fns
dictionary.removePostHook
Removes a post hook from the DapiWrapper
instance.
Syntax
removePostHook<KEY extends keyof DAPI>(
key: KEY,
hook: HookFn<DAPI[KEY], DapiWrapper>
): void
Parameters
key
- The name of the method to be unhooked. Must be a key of the DapiDefinition.fns
fns dictionary.hook
- The hook function to be removed from the method.Every instance of the DapiWrapper class creates a Facade of the DapiDefinition.fns
fns. This means that every instance of the class will have the same methods as the DapiDefinition.fns
functions dictionary. The only difference is that the instance methods will omit the first argument of the DapiDefinition.fns
. This is because the
DapiWrapper class injects the dependencies at runtime.
Example
const instance = createDapi({
dependencies: {db, logger},
fns: {
createPerson
},
type: 'Entity'
name: 'Person'
});
instance.createPerson({name: 'John Doe'});
FAQs
Simple library to create complex systems out of pure functions
The npm package @carpasse/dapi receives a total of 4 weekly downloads. As such, @carpasse/dapi popularity was classified as not popular.
We found that @carpasse/dapi demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.
Research
Security News
A threat actor's playbook for exploiting the npm ecosystem was exposed on the dark web, detailing how to build a blockchain-powered botnet.