@getanthill/datastore
Version:
Event-Sourced Datastore
294 lines (245 loc) • 6.58 kB
text/typescript
import type { AnyObject, GenericType, Services } from '../../typings';
import { omit } from 'lodash';
import { getTokensByRole } from '../middleware';
import * as handlers from './handlers';
import { MessageOptions, Route } from '../../services/broker';
export function wrapper(services: Services, topic: string, handler: Function) {
return async (
event: any,
route?: Route,
headers?: AnyObject,
opts?: MessageOptions,
) => {
try {
await handler(event, route, headers);
typeof opts?.ack === 'function' && (await opts.ack());
} catch (err: any) {
services.telemetry.logger.error('[events#wrapper] Event error', {
err,
});
if (typeof opts?.delivery === 'number' && opts?.delivery > 0) {
services.telemetry.logger.warn('[events#wrapper] Event discarded', {
event,
route,
headers: omit(headers, 'authorization'),
deliver: opts?.delivery,
});
typeof opts?.ack === 'function' && (await opts.ack());
// Publish the message in another dedicated dead messages queue
return;
}
if (err?.details) {
await handlers.publish(services, `${topic}/error`, {
event,
details: err?.details,
});
}
typeof opts?.nack === 'function' && (await opts.nack());
}
};
}
export const created = (services: Services, modelName: string, topic: string) =>
wrapper(
services,
topic,
async (event: any, route: Route, headers: AnyObject = {}) => {
const entity = await handlers.created(
services,
{
params: {
model: modelName,
},
headers,
},
event,
);
return entity.state;
},
);
export const updated = (services: Services, modelName: string, topic: string) =>
wrapper(
services,
topic,
async (event: any, route: Route, headers: AnyObject = {}) => {
const entity = await handlers.updated(
services,
{
params: {
model: modelName,
correlation_id: route.params.correlation_id,
},
headers,
},
event,
);
return entity.state;
},
);
export const patched = (services: Services, modelName: string, topic: string) =>
wrapper(
services,
topic,
async (event: any, route: Route, headers: AnyObject) => {
const entity = await handlers.patched(
services,
{
params: {
model: modelName,
correlation_id: route.params.correlation_id,
},
headers,
},
event,
);
return entity.state;
},
);
export const applied = (services: Services, modelName: string, topic: string) =>
wrapper(
services,
topic,
async (event: any, route: Route, headers: AnyObject = {}) => {
const entity = await handlers.applied(
services,
{
params: {
model: modelName,
correlation_id: route.params.correlation_id,
event_type: route.params.event_type,
},
headers,
},
event,
);
return entity.state;
},
);
const EVENT_HANDLERS_MAPPING = {
CREATED: created,
UPDATED: updated,
ROLLBACKED: null, // Skipped
RESTORED: null, // Skipped
PATCHED: patched,
ARCHIVED: null, // Skipped
DELETED: null, // Skipped
};
async function registerModelEventForMQTT(
services: Services,
model: GenericType,
topic: any,
eventSchema: any,
handler: any,
) {
const { config, mqtt } = services;
if (config.features.mqtt.isEnabled !== true) {
return;
}
const modelConfig = model.getModelConfig();
services.telemetry.logger.debug('[events] Registering model event...', {
protocol: 'mqtt',
name: modelConfig.name,
topic,
});
mqtt.on(
topic,
mqtt.authenticate(
getTokensByRole(config.security.tokens, 'write'),
handler(services, modelConfig.name, topic),
),
);
await mqtt.subscribe(topic, {
type: 'object',
properties: {
body: eventSchema,
},
});
}
async function registerModelEventForAMQP(
services: Services,
model: any,
topic: any,
eventSchema: any,
handler: any,
) {
const { config, amqp } = services;
if (config.features.amqp.isEnabled !== true) {
return;
}
const modelConfig = model.getModelConfig();
services.telemetry.logger.debug('[events] Registering model event...', {
protocol: 'amqp',
name: modelConfig.name,
topic,
});
amqp.on(
topic,
amqp.authenticate(
getTokensByRole(config.security.tokens, 'write'),
handler(services, modelConfig.name, topic),
),
);
await amqp.subscribe(topic, {
type: 'object',
properties: {
body: eventSchema,
},
});
}
async function registerModelEvent(
services: Services,
model: GenericType,
eventName: string,
handler: any,
) {
if (handler === null) {
return;
}
handler = handler || applied;
const modelConfig = model.getModelConfig();
const schema = model.getSchema();
services.telemetry.logger.debug('[events] Registering model...', {
name: modelConfig.name,
});
const event = schema.events[eventName];
let lastVersion;
for (const eventVersion in event) {
lastVersion = eventVersion;
}
if (!lastVersion) {
return;
}
const eventSchema = event[lastVersion];
let topic = `${modelConfig.name}/${eventName}`.toLowerCase();
if (eventName !== 'CREATED') {
topic += `/{${model.getCorrelationField()}}`;
}
await Promise.all([
registerModelEventForMQTT(services, model, topic, eventSchema, handler),
registerModelEventForAMQP(services, model, topic, eventSchema, handler),
]);
}
async function registerModel(services: Services, model: GenericType) {
const schema = model.getSchema();
const events = schema.events || {};
services.telemetry.logger.debug('[events] Registering model...', {
name: model.getModelConfig().name,
});
for (const eventName in events) {
await registerModelEvent(
services,
model,
eventName,
/* @ts-ignore */
EVENT_HANDLERS_MAPPING[eventName],
);
}
}
export default async function register(services: Services) {
services.telemetry.logger.info('[events] Registering...');
for (const [modelName, model] of services.models.MODELS.entries()) {
if (services.models.isInternalModel(modelName) === true) {
continue;
}
await registerModel(services, model);
}
}