UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

578 lines (577 loc) 20.6 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 (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __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.version = exports.updateMany = exports.entity = exports.events = exports.count = exports.restore = exports.patch = exports.update = exports.create = exports.get = exports.find = void 0; const commander_1 = require("commander"); const ajv_1 = __importDefault(require("ajv")); const ajv_formats_1 = __importDefault(require("ajv-formats")); const merge_1 = __importDefault(require("lodash/merge")); const omit_1 = __importDefault(require("lodash/omit")); const pick_1 = __importDefault(require("lodash/pick")); const utils = __importStar(require("./utils")); const constants_1 = require("../constants"); const validator = new ajv_1.default({ useDefaults: false, coerceTypes: true, strict: false, }); // @ts-ignore (0, ajv_formats_1.default)(validator); function addSchemaFields(h, { correlation_field, schema, }) { h.option(`--${correlation_field} <${correlation_field}...>`, 'Correlation field'); h.option(`--json <json>`, 'JSON Query', JSON.parse); for (const field in schema.model.properties) { const { type, description } = schema.model.properties[field]; if (type === 'array') { h.option(`--${field} <${field}...>`, description); } else { h.option(`--${field} <${field}>`, description); } } } function buildQuery(config, cmd, schema) { const { debug, dryRun, json, ...args } = cmd; const query = (0, merge_1.default)((0, pick_1.default)(args, Object.keys({ [config.correlation_field]: { type: 'string', description: 'Correlation field', }, ...schema.properties, })), json); const isValid = validator.validate((0, omit_1.default)(schema, 'required'), query); if (debug === true) { utils.log({ dryRun, args, json, query, isValid }, cmd.format); } if (cmd.skipValidation === false && !isValid) { utils.log({ err: 'Query is invalid', dryRun, args, json, query, isValid, details: validator.errors, }, cmd.format); throw new Error('Query is invalid'); } return query; } function find(services, config) { const model = config.name; return async (cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const { page, pageSize, ...args } = cmd; const query = buildQuery(config, args, config.schema.model); query._must_hash = cmd.skipHash === false; if (cmd.fields) { query._fields = cmd.fields.reduce((s, c) => ({ ...s, [c]: 1, }), {}); } if (cmd.sort) { query._sort = cmd.sort.reduce((s, c) => ({ [c.replace(/^[+-]/, '')]: c[0] === '-' ? -1 : 1, }), {}); } if (cmd.dryRun === true) { utils.log(query, cmd.format); return; } let entities = []; if (cmd.source === 'entities') { const { data } = await datastore.find(model, query, page, pageSize, { // @ts-ignore 'with-response-validation': `${cmd.skipValidation === false}`, }); entities = data; } else { const { data } = await datastore.allEvents(model, query, page, pageSize, {}); entities = data; } if (cmd.decrypt === true) { const { data: decryptedData } = await datastore.decrypt(model, entities); entities = decryptedData; } utils.log(entities, cmd.format); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.find = find; function get(services, config) { const model = config.name; return async (correlationId, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } if (cmd.dryRun === true) { utils.log({ [config.correlation_field]: correlationId, }, cmd.format); return; } let entity; const { data: obj } = await datastore.get(model, correlationId); entity = obj; if (cmd.decrypt === true) { const { data: [decrypted], } = await datastore.decrypt(model, [entity]); entity = decrypted; } utils.log(entity, cmd.format); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.get = get; function create(services, config) { const model = config.name; const schema = config.schema; return async (cmd) => { const v = Object.keys(schema.events.CREATED).sort().pop(); if (!v) { return; } try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const payload = buildQuery(config, cmd, schema.events.CREATED[v]); if (cmd.dryRun === false) { const { data } = await datastore.create(model, payload); utils.log(data, cmd.format); } else { utils.log(payload, cmd.format); } } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.create = create; function update(services, config) { const model = config.name; const schema = config.schema; return async (correlationId, cmd) => { const v = Object.keys(schema.events.CREATED).sort().pop(); if (!v) { return; } try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const payload = buildQuery(config, cmd, schema.events.UPDATED[v]); if (cmd.dryRun === false) { // @ts-ignore const { data } = await datastore.update(model, correlationId, payload, { upsert: cmd.upsert, }); utils.log(data, cmd.format); } else { utils.log(payload, cmd.format); } } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.update = update; function patch(services, config) { const model = config.name; return async (correlationId, _patches, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const patches = _patches.map((p) => JSON.parse(p)); if (cmd.dryRun === false) { const { data } = await datastore.patch(model, correlationId, patches); utils.log(data, cmd.format); } else { utils.log(patches, cmd.format); } } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.patch = patch; function restore(services, config) { const model = config.name; return async (correlationId, version, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } if (cmd.dryRun === false) { const { data } = await datastore.restore(model, correlationId, version); utils.log(data, cmd.format); } else { utils.log({ correlation_id: correlationId, version }, cmd.format); } } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.restore = restore; function count(services, config) { const model = config.name; return async (cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const { page, pageSize, ...args } = cmd; const query = buildQuery(config, args, config.schema.model); if (cmd.dryRun === false) { const c = await datastore.count(model, { ...query, _must_hash: cmd.skipHash === false, }, cmd.source); utils.log(c, cmd.format); } else { utils.log(query, cmd.format); } } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.count = count; function events(services, config) { const model = config.name; return async (correlationId, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } const { page, pageSize } = cmd; if (cmd.dryRun === true) { utils.log({ [config.correlation_field]: correlationId, }, cmd.format); return; } let events = []; const { data } = await datastore.events(model, correlationId, page, pageSize); events = data; if (cmd.decrypt === true) { const { data: decryptedEvents } = await datastore.decrypt(model, events); events = decryptedEvents; } utils.log(events, cmd.format); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.events = events; function entity(services, config) { const model = config.name; return async (verb, correlationId, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } if (cmd.dryRun === true) { utils.log({ [config.correlation_field]: correlationId, }, cmd.format); return; } let res; if (verb === 'data') { res = await datastore.data(model, correlationId, cmd.onlyModels); } else { /* @ts-ignore */ res = await datastore[verb](model, correlationId, true, cmd.onlyModels); } utils.log(res.data, cmd.format); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.entity = entity; function updateMany(services, config) { const model = config.name; return async (query, update, cmd) => { try { const datastore = services.datastores.get(config.datastore); const _update = JSON.parse(update); let stats; await datastore?.updateOverwhelmingly(model, JSON.parse(query), async () => _update, (_stats) => { stats = _stats; Math.floor(stats.progress * 100) % 5 === 0 && utils.log(stats, cmd.format); }, 100); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.updateMany = updateMany; function version(services, config) { const model = config.name; return async (correlationId, version, cmd) => { try { const datastore = services.datastores.get(config.datastore); if (!datastore) { return; } if (cmd.dryRun === true) { utils.log({ [config.correlation_field]: correlationId, }, cmd.format); return; } let res; const versionNumber = parseInt(version, 10); const isVersionISOString = constants_1.REGEXP_DATE_ISO_STRING_8601.test(version); if (isVersionISOString === true) { res = await datastore.at(model, correlationId, version); } else { res = await datastore.version(model, correlationId, versionNumber); } utils.log(res.data, cmd.format); } catch (err) { if (err.response) { utils.log(err.response.data, cmd.format); return; } utils.log(err, cmd.format); } }; } exports.version = version; function register(services, config) { const program = new commander_1.Command(config.name); const summary = config.description?.split('\n', 2)[0] ?? ''; program .summary(`${config.datastore}@${config.name} (v${config.version})${summary ? ' - ' + summary : ''}`) .description(`datastore:\t${config.datastore} version:\t${config.version} created_at:\t${config.created_at} updated_at:\t${config.updated_at} description: ${(config.description || config.name || '').split('\n').shift()} `); let c = program .command('schema') .description(`Returns the JSON schema associated to ${config.name}`) .action(() => { utils.log(config.schema.model); }); // Find c = program .command(`find`) .description(`Get available entities for ${config.name}`); addSchemaFields(c, config); utils.addStandardFields(c); c.addOption(new commander_1.Option('-s, --source <source>', 'Count source: entities or events') .default('entities') .choices(['entities', 'events'])); c.option('--fields <fields...>', 'Fields to return from the entity'); c.option('--sort <sorts...>', 'Sort entities on some specific fields'); c.option('--decrypt', 'Must try to decrypt entities', false); c.option('--skip-hash', 'Remove the hash request', false); c.option('--skip-validation', 'Skip query validation', false); utils.addPaginationFields(c); c.action(find(services, config)); // Get c = program .command(`get`) .argument('<correlation_id>', 'Entity correlation ID') .description(`Get one entity for ${config.name}`); addSchemaFields(c, config); utils.addStandardFields(c); c.option('--decrypt', 'Must try to decrypt entities').action(get(services, config)); // Create c = program .command(`create`) .description(`Create a new entity for ${config.name}`); addSchemaFields(c, config); utils.addStandardFields(c); c.option('--skip-validation', 'Skip query validation', false); c.action(create(services, config)); // Update c = program .command(`update`) .argument('<correlation_id>', 'Entity correlation ID') .description(`Update a single entity for ${config.name}`); c.option('--upsert', 'Upsert the entity if not exists', false); addSchemaFields(c, config); utils.addStandardFields(c); c.option('--skip-validation', 'Skip query validation', false); c.action(update(services, config)); // UpdateMany c = program .command(`update:many`) .argument('<query>', 'Matching query') .argument('<update>', 'Update query') .description(`Update all entities for ${config.name} matching the query`); c.action(updateMany(services, config)); // Patch c = program .command(`patch`) .argument('<correlation_id>', 'Entity correlation ID') .argument('<patches...>', 'JSON Patch') .description(`Patch a single entity for ${config.name}`); utils.addStandardFields(c); c.action(patch(services, config)); // Restore c = program .command(`restore`) .argument('<correlation_id>', 'Entity correlation ID') .argument('<version>', 'Entity version to restore') .description(`Restore a single entity for ${config.name} to a given version`); utils.addStandardFields(c); c.action(restore(services, config)); // Count c = program .command(`count`) .description(`Get count of entities for ${config.name} matching your query`); addSchemaFields(c, config); utils.addStandardFields(c); c.addOption(new commander_1.Option('-s, --source <source>', 'Count source: entities or events') .default('entities') .choices(['entities', 'events'])); c.option('--skip-hash', 'Remove the hash request', true); c.option('--skip-validation', 'Skip query validation', false); c.action(count(services, config)); // Events c = program .command(`events`) .argument('<correlation_id>', 'Entity correlation ID') .description(`Get events associated to a ${config.name}`); utils.addPaginationFields(c); utils.addStandardFields(c); c.option('--decrypt', 'Must try to decrypt entities').action(events(services, config)); // Entity c = program .command('entity') .addArgument(new commander_1.Argument('<verb>', 'Verb to apply on the entity').choices([ 'data', 'archive', 'unarchive', 'delete', ])) .argument('<correlation_id>', 'Entity correlation ID') .description(`Apply a specific method to data related to a single entity for ${config.name}`); c.option('--only-models <models...>', 'Models to retrieve data from'); utils.addStandardFields(c); c.action(entity(services, config)); // Version c = program .command('version') .argument('<correlation_id>', 'Entity correlation ID') .addArgument(new commander_1.Argument('<version>', 'Version')) .description(`Get entity at version`); utils.addStandardFields(c); c.action(version(services, config)); return program; } exports.default = register; //# sourceMappingURL=models.js.map