UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

294 lines (245 loc) 6.58 kB
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); } }