@getanthill/datastore
Version:
Event-Sourced Datastore
367 lines • 13.2 kB
JavaScript
;
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