UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

169 lines (143 loc) 3.78 kB
import type { JSONSchemaType, ValidateFunction } from 'ajv'; import type { AnyObject, Telemetry } from '../typings'; import assert from 'node:assert'; import { EventEmitter } from 'events'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import cloneDeep from 'lodash/cloneDeep'; import merge from 'lodash/merge'; export interface Route { original: string; topic: string; regexp: RegExp; paramNames: string[]; params: { [key: string]: string }; validate: ValidateFunction; } export interface MessageOptions { source: 'amqp' | 'mqtt'; original: any; delivery: number; ack: () => Promise<void>; nack: () => Promise<void>; } const DEFAULT_SPEC = { asyncapi: '2.2.0', info: { title: 'MQTT API', version: '0.1.0', // termsOfService: '', contact: {}, license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT', }, }, tags: [], channels: {}, components: { schemas: { authorization: { type: 'string', description: 'Authorization token', }, }, }, }; export default class BrokerClient extends EventEmitter { static REGEXP_MATCH_PATH_PARAMETERS = /\{([a-z_]+)\}/g; protected config: any; protected telemetry?: Telemetry; protected ajv: Ajv; protected spec: any; protected topics: Map<string, any> = new Map(); constructor(config: any, telemetry?: Telemetry) { super(); this.config = config; this.telemetry = telemetry; this.ajv = new Ajv({ useDefaults: true, coerceTypes: false, strict: false, }); addFormats(this.ajv); this.spec = merge(cloneDeep(DEFAULT_SPEC), config.spec); } logOnInvalid(event: any, route: any) { const err = new assert.AssertionError({ message: 'Event schema validation error', expected: null, actual: { event, errors: route.validate.errors, }, }); /* @ts-ignore */ this.telemetry?.logger[this.config.logLevelOnInvalidMessage ?? 'error']( '[services#broker] Message is invalid', { err, event, schema: route.validate.schema, errors: route.validate.errors, acked: true, }, ); } mapTopic(topic: string, schema: JSONSchemaType<AnyObject>): Route { const topicWithNamespace = this.topicWithNamespace(topic); return { original: topic, topic: topicWithNamespace.replace( BrokerClient.REGEXP_MATCH_PATH_PARAMETERS, '+', ), regexp: new RegExp( topicWithNamespace.replace( BrokerClient.REGEXP_MATCH_PATH_PARAMETERS, '([^\\/]+)', ), ), paramNames: ( topic.match(BrokerClient.REGEXP_MATCH_PATH_PARAMETERS) ?? [] ).map((p) => p.slice(1, -1)), params: {}, validate: this.ajv.compile(schema), }; } parseTopic(topic: string, route: Route): Route { const values = route.regexp.exec(topic)!.slice(1); const params: { [key: string]: string } = {}; values.forEach((v, i) => (params[route.paramNames[i]] = v)); return { ...route, params, }; } getRoute(topic: string): Route | null { for (const entry of this.topics.values()) { if (entry.regexp.test(topic)) { return entry; } } return null; } topicWithNamespace(topic: string): string { return ( (this.config.namespace !== '' ? this.config.namespace + '/' : '') + topic ); } addChannelToSpec(topic: string, schema: any) { this.spec.channels[this.topicWithNamespace(topic)] = { publish: { message: { payload: { type: 'object', additionalProperties: false, properties: schema, }, }, }, }; } }