UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

367 lines 13.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildContext = buildContext; exports.getHandler = getHandler; exports.load = load; exports.prepare = prepare; exports.init = init; exports.sendEvents = sendEvents; exports.run = run; exports.assert = assert; exports.tearDown = tearDown; exports.cli = cli; const path_1 = __importDefault(require("path")); const telemetry = __importStar(require("@getanthill/telemetry")); const commander_1 = require("commander"); const get_1 = __importDefault(require("lodash/get")); const cloneDeep_1 = __importDefault(require("lodash/cloneDeep")); const merge_1 = __importDefault(require("lodash/merge")); const setup_1 = __importDefault(require("../../setup")); const services_1 = __importDefault(require("../../services")); const runner = __importStar(require("../runner")); const utils = __importStar(require("../../utils")); const things_json_1 = __importDefault(require("../../templates/examples/things.json")); const expect_1 = __importDefault(require("expect")); const CLI_VERSION = '0.2.0'; const program = new commander_1.Command(); program .storeOptionsAsProperties(false) .version(CLI_VERSION, '-v, --version', 'output the current version'); function mapEntityValues(ctx, entity, iteration, cloneIndex) { const clonedEntity = (0, cloneDeep_1.default)(entity); utils.mapValuesDeep(clonedEntity, (v) => { if (typeof v !== 'string') { return v; } // Entity mapping const [id, ...fragments] = v.slice(1, -1).split('.'); if (ctx.entities.has(id)) { return (0, get_1.default)(ctx.entities.get(id), fragments); } return v .replace(/\{(i|iteration)\}/g, iteration.toString()) .replace(/\{(c|clone_index)\}/g, cloneIndex.toString()) .replace(/\{(i|iteration)-1\}/g, `${Math.max(0, iteration - 1)}`) .replace(/\{uuid\}/g, setup_1.default.uuid()); }); return clonedEntity; } function mockDate(str) { if (str === undefined) { return; } const constantDate = new Date(str); // @ts-ignore global._Date = Date; // @ts-ignore global.Date = class extends Date { constructor() { super(); return constantDate; } static now() { return constantDate.getTime(); } }; } function unmockDate() { /* @ts-ignore */ if (global._Date === undefined) { return; } /* @ts-ignore */ global.Date = global._Date; /* @ts-ignore */ delete global._Date; } async function buildContext(initialContext) { let ctx = { telemetry, ...initialContext, }; ctx = { ...ctx, ...(await prepare(ctx)), }; ctx.projections = await init(ctx); await sendEvents(ctx, ctx.data.imports, 0); return ctx; } async function getHandler(ctx, handlerType) { /* @ts-ignore */ const handler = runner[handlerType](); return handler(ctx.projections.map((projection) => `/projections?projection_id=${projection.projection_id}&${ctx.data.config.runner_params || ''}`), { exitTimeout: 100, verbose: true, cwd: path_1.default.resolve(__dirname, '..'), maxReconnectionAttempts: 1000, reconnectionInterval: 100, connectionMaxLifeSpanInSeconds: 3600, pageSize: 10, }, {}); } async function load(dir) { return require(dir); } async function prepare(ctx) { ctx.telemetry.logger.info('[validator] Preparing datastores...'); const instances = new Map(); const entities = new Map(); const modelConfigs = new Map(); const datastores = Object.keys(ctx.data.config.datastores); services_1.default.datastores = new Map(); for (const d of datastores) { ctx.telemetry.logger.debug('[validator] Starting datastore...', { datastore: d, }); const instance = await setup_1.default.startApi((0, merge_1.default)({ security: { tokens: [ { id: 'admin', level: 'admin', token: 'token', }, ], }, features: { api: { admin: true, updateSpecOnModelsChange: true, }, cache: { isEnabled: false, }, }, }, ctx.data.config.datastores[d].config)); instances.set(d, instance); services_1.default.datastores.set(d, instance[5]); const _modelConfigs = ctx.data.config.datastores[d].modelConfigs || []; for (const modelConfig of _modelConfigs) { ctx.telemetry.logger.debug('[validator] Importing model...', { datastore: d, model: modelConfig.name, }); await instance[5].createModel(modelConfig); } await instance[7].restart(); for (const modelConfig of _modelConfigs) { ctx.telemetry.logger.debug('[validator] Creating model indexes...', { datastore: d, model: modelConfig.name, }); await instance[5].createModelIndexes(modelConfig); } const { data: initializedModelConfigs } = await instance[5].getModels(); ctx.telemetry.logger.debug('[validator] Available models...', { datastore: d, models: Object.keys(initializedModelConfigs), }); modelConfigs.set(d, initializedModelConfigs); } return { instances, entities, modelConfigs, datastores, }; } async function init(ctx) { ctx.telemetry.logger.info('[validator] Initializing projections...'); const sourceInstance = ctx.instances.get(ctx.data.projections[0]?.from?.datastore || ctx.data.projections[0]?.triggers[0].datastore); try { sourceInstance[5].config.debug = true; const { data: ddd } = await sourceInstance[5].createModel({ ...things_json_1.default, db: 'datastore', name: 'projections', correlation_field: 'projection_id', schema: { ...things_json_1.default.schema, model: { additionalProperties: true, properties: {}, }, }, }); await sourceInstance[7].restart(); } catch (err) { if (err?.response?.data?.status !== 409) { throw err; } // Model already initialized } const projections = []; for (const projection of ctx.data.projections) { const { data: _projection } = await sourceInstance[5].create('projections', projection); projections.push(_projection); } return projections; } async function sendEvents(ctx, events, waitTimeout = 3000) { for (const event of events) { const sdk = ctx.instances.get(event.datastore)[5]; const modelConfigs = ctx.modelConfigs.get(event.datastore); const repeat = event.repeat || 1; const clone = event.clone || 1; const sleep = event.sleep || 100; const entities = (0, cloneDeep_1.default)(event.entities || []); for (let i = 1; i < clone; i++) { entities.push(...event.entities); } ctx.telemetry.logger.info('[validator] Sending events...', { entities_count: entities.length, repeat, clone, sleep, }); mockDate(event.date ?? ctx.data.config.imports?.date); for (let iteration = 0; iteration < repeat; iteration++) { const _entities = entities.map((entity, cloneIndex) => ({ ...entity, idempotency: mapEntityValues(ctx, entity.idempotency, iteration, cloneIndex), entity: mapEntityValues(ctx, entity.entity, iteration, cloneIndex), ...mapEntityValues(ctx, { id: entity.id, }, iteration, cloneIndex), })); await sdk.import(_entities, modelConfigs, { dryRun: false, }, ctx.entities); await new Promise((resolve) => setTimeout(resolve, sleep)); } unmockDate(); } await new Promise((resolve) => setTimeout(resolve, waitTimeout)); } async function run(ctx, handlerType) { handlerType === 'replay' && (await sendEvents(ctx, ctx.data.events, 0)); await getHandler(ctx, handlerType); handlerType === 'start' && (await sendEvents(ctx, ctx.data.events, ctx.data.config.exit_timeout)); } async function assert(ctx, handlerType) { let withErrors = false; ctx.telemetry.logger.info('[validator] Checking results...'); const assertions = ctx.data.assertions.filter((assertion) => assertion.handler === undefined || assertion.handler === handlerType); for (const assertion of assertions) { if (assertion.entities.length === 0) { continue; } const res = await ctx.instances .get(assertion.datastore)[5] .find(assertion.model, assertion.query || {}); try { if (assertion.log === true) { ctx.telemetry.logger.info('[validator] Assertion', { data: res.data }); } (0, expect_1.default)(res.data).toEqual(expect_1.default.arrayContaining([ expect_1.default.objectContaining(mapEntityValues(ctx, assertion.entities[0], 0, 0)), ])); } catch (err) { console.error(err); withErrors = true; ctx.telemetry.logger.error('[validator] ❌ Assertion failure', { err, data: res.data, assertion, details: err?.response?.data, }); } } if (withErrors === true) { throw new Error('Tests failed'); } ctx.telemetry.logger.info('[validator] ✅ All checks passed'); } async function tearDown(ctx) { ctx.telemetry.logger.info('[validator] Stopping...'); for (const ds of services_1.default.datastores.values()) { ds.closeAll(); } for (const d of ctx.datastores || []) { const instance = ctx.instances.get(d); ctx.telemetry.logger.debug('[validator] Dropping database...', { datastore: d, }); await setup_1.default.teardownDb(instance[1]); ctx.telemetry.logger.debug('[validator] Stopping datastore...', { datastore: d, }); await setup_1.default.stopApi(instance[7]); } } async function validator(dir, handlerType, cmd) { let ctx = { telemetry, }; try { const data = await load(dir); ctx = await buildContext({ data }); await run(ctx, handlerType); await assert(ctx, handlerType); } catch (err) { if (err?.response?.data) { console.error(err?.response?.data); } else { console.error(err); } } finally { await tearDown(ctx); } } async function cli(argv = process.argv) { program .argument('<path>', 'Path of the projections entries') .argument('[handler_type]', 'Handler type: start, replay', 'start') .option('-v, --verbosity <verbosity>', 'Logger level verbosity. 0: ALL 100: NOTHING]', '50') .option('-p, --projections <projections...>', 'List of path projections') .action(validator); return program.parse(argv); } if (!module.parent) { cli(); } //# sourceMappingURL=validator.js.map