@getanthill/datastore
Version:
Event-Sourced Datastore
330 lines (327 loc) • 12.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cli = cli;
const util_1 = require("util");
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const promises_1 = require("node:readline/promises");
const node_process_1 = require("node:process");
const ajv_1 = __importDefault(require("ajv"));
const ajv_formats_1 = __importDefault(require("ajv-formats"));
const js_yaml_1 = __importDefault(require("js-yaml"));
const shell_quote_1 = require("shell-quote");
const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
const uniq_1 = __importDefault(require("lodash/uniq"));
const commander_1 = require("commander");
const services_1 = require("../services");
const sdk_1 = require("../sdk");
const runner_1 = __importDefault(require("../sdk/runner"));
const node_assert_1 = require("node:assert");
// CLI sub commands
const admin_1 = __importDefault(require("./admin"));
const data_1 = __importDefault(require("./data"));
const models_1 = __importDefault(require("./models"));
const security_1 = __importDefault(require("./security"));
const { version: CLI_VERSION } = require('../../package.json');
const program = new commander_1.Command('ds');
let isRun = false;
program
.storeOptionsAsProperties(false)
.version(CLI_VERSION, '-v, --vers', 'output the current version')
.description(`Datastore API client
Gitlab:
> https://gitlab.com/getanthill/datastore
Documentation:
> https://datastore.getanthill.org/docs/tutorials/using-the-cli
Examples:
ds models accounts find
ds models accounts find --is_enabled true
ds models accounts find --is_enabled true --sort updated_at --fields email created_at
`);
const datastoreConfigs = JSON.parse(process.env.DATASTORE_CONFIGS ||
JSON.stringify([
{
name: 'default',
config: {
baseUrl: process.env.DATASTORE_API_URL || 'http://localhost:3001',
token: process.env.DATASTORE_ACCESS_TOKEN || 'token',
debug: false,
},
},
]));
const services = (0, services_1.build)();
const datastores = new Map(datastoreConfigs.map((c) => {
(0, node_assert_1.ok)(!!c.name, 'Missing Datastore configuration name');
return [
c.name,
new sdk_1.Datastore({ ...c.config, telemetry: services.telemetry }),
];
}));
services.datastores = datastores;
const validator = new ajv_1.default({
useDefaults: false,
coerceTypes: true,
strict: false,
});
// @ts-ignore
(0, ajv_formats_1.default)(validator);
let modelConfigs = {};
function log(obj, format = process.env.DATASTORE_CLI_FORMAT) {
const _format = isRun === true ? 'cli' : format;
if (_format === 'json') {
console.log(JSON.stringify(obj));
return;
}
if (_format === 'yaml') {
console.log(js_yaml_1.default.dump(obj));
return;
}
console.log((0, util_1.inspect)(obj, false, null, true));
}
function addDatastoreOptions(h) {
const dsNames = Array.from(datastores.keys());
h.addOption(new commander_1.Option('--ds, --datastore <datastore>', 'Datastore to use')
.default(dsNames[0] || 'default')
.choices(dsNames.length === 0 ? ['default'] : dsNames));
}
function stream() {
const h = program.command('stream <model> <source>');
addDatastoreOptions(h);
const dsNames = Array.from(datastores.keys());
h.addOption(new commander_1.Option('--output <output>', 'Output')
.default('entity')
.choices(['entity', 'raw']))
.addOption(new commander_1.Option('--format <format>', 'Response format').choices([
'json',
'yaml',
]))
.addOption(new commander_1.Option('-c, --connector <connector>', 'Connector type').choices([
'http',
'amqp',
]))
.addOption(new commander_1.Option('-q, --queue <queue>', 'Queue name'))
.addOption(new commander_1.Option('-s, --sync <datastore>', 'Sync event to a datastore').choices(dsNames.length === 0 ? ['default'] : dsNames))
.description('Stream entities changes or events');
h.action(async (model, source, cmd) => {
if (!datastores) {
return;
}
try {
const datastore = datastores.get(cmd.datastore);
if (!datastore) {
return;
}
let streamHandler = datastore.streams.streamHTTP.bind(datastore.streams);
if (cmd.connector === 'amqp') {
streamHandler = datastore.streams.streamAMQP.bind(datastore.streams);
}
await streamHandler(async (e, _route, _headers, opts) => {
log(e, cmd.format);
if (source === 'events' && cmd.sync) {
const _model = model === 'all' ? e.model : model;
const event = model === 'all' ? e.entity : e;
const modelConfig = modelConfigs[_model];
datastores
.get(cmd.sync)
?.apply(e.model, event[modelConfig.correlation_field], event.type, event.v, event, { replay: 'true' });
}
typeof opts?.ack === 'function' && (await opts.ack());
}, model, source);
}
catch (err) {
if (err.response) {
log(err.response.data, cmd.format);
return;
}
log(err, cmd.format);
}
});
}
function config() {
const h = program
.command('config')
.addOption(new commander_1.Option('--format <format>', 'Response format').choices([
'json',
'yaml',
]))
.description('Show cli configuration');
h.action(async (cmd) => {
try {
log({
configs: datastoreConfigs,
datastores: Array.from(datastores.keys()),
}, cmd.format);
}
catch (err) {
log(err, cmd.format);
}
});
}
function run() {
const h = program.command('run').description('CLI infinite command');
let cliName;
const commands = new Map();
const defaultOptions = new Map();
function exitOverride(program, sub = '') {
const name = ((sub ? sub + ' ' : '') + program.name()).replace(cliName + ' ', '');
if (sub === '') {
cliName = name;
}
program.exitOverride();
commands.set(name, program);
if (!defaultOptions.has(name)) {
defaultOptions.set(name, {
args: (0, cloneDeep_1.default)(program.args),
// @ts-ignore
_optionValues: (0, cloneDeep_1.default)(program._optionValues),
// @ts-ignore
_optionValueSources: (0, cloneDeep_1.default)(program._optionValueSources),
});
}
for (const prog of program.commands) {
exitOverride(prog, name);
}
}
function reset(program, sub = '') {
const name = ((sub ? sub + ' ' : '') + program.name()).replace(cliName + ' ', '');
const c = defaultOptions.get(name);
program.args = (0, cloneDeep_1.default)(c?.args ?? []);
// @ts-ignore
program._optionValues = (0, cloneDeep_1.default)(c?._optionValues ?? {});
// @ts-ignore
program._optionValueSources = (0, cloneDeep_1.default)(c?._optionValueSources ?? {});
for (const prog of program.commands) {
reset(prog, name);
}
}
h.action(async (cmd) => {
const history = [];
const historyPath = node_os_1.default.homedir() + '/.ds_history';
process.on('beforeExit', () => {
console.log('\nBye!');
});
try {
await node_fs_1.default.promises
.readFile(historyPath)
.then((h) => {
history.push(...h.toString().split('\n').reverse());
})
.catch(() => {
// ...
});
const writeStream = node_fs_1.default.createWriteStream(historyPath, { flags: 'a' });
isRun = true;
exitOverride(program);
const completer = (input) => {
const completions = [...history, ...Array.from(commands.keys())];
const hits = (0, uniq_1.default)(completions
.filter((c) => new RegExp('^' + input, 'i').test(c))
.map((h) => h.replace(new RegExp('^(' + input + '[^\\s]+).*', 'i'), '$1')));
return [hits.length ? hits : completions, input];
};
const readline = (0, promises_1.createInterface)({ input: node_process_1.stdin, output: node_process_1.stdout, history, completer });
let stdin;
console.log(`
Datastore cli@${CLI_VERSION}`);
do {
stdin = await readline.question('> ');
history.push(stdin);
writeStream.write(stdin + '\n');
if (stdin === 'exit') {
break;
}
try {
reset(program);
console.log('');
program.showHelpAfterError(false);
await program.parseAsync((0, shell_quote_1.parse)(stdin), {
from: 'user',
});
}
catch (err2) {
// ...
}
console.log('');
} while (stdin !== 'exit');
readline.close();
}
catch (err) {
log(err, cmd.format);
}
});
}
async function cli(argv = process.argv) {
const _argv = [...process.argv]; // clone argv.
const cleanArgv = _argv.slice(2); // remove node path etc.
const firstArg = cleanArgv // first command ignore lfags.
.filter((v) => !/^--?/.test(v))
.shift();
const isRunCli = cleanArgv.length === 0;
if (isRunCli === true) {
argv.push('run');
}
else if (firstArg !== 'run' &&
!program.commands.some((cmd) => cmd.name() === firstArg)) {
_argv.splice(2, 0, 'help');
}
program
.command('heartbeat')
.description('Check the availability of the service (/heartbeat)')
.action(async (cmd) => {
for (const [name, datastore] of datastores) {
try {
const { data } = await datastore.heartbeat();
log({ datastore: name, response: data }, cmd.format);
}
catch (err) {
console.error('Datastore heartbeat failed', {
datastore: name,
config: datastore.config,
});
}
}
});
for (const [name, datastore] of datastores) {
try {
const dsProgram = new commander_1.Command(name);
dsProgram.summary(`datastore: ${datastore.config.baseUrl}`);
const { data: _models } = await datastore.getModels();
const modelConfigs = _models;
for (const model of Object.values(modelConfigs)) {
try {
model.datastore = name;
dsProgram.addCommand((0, models_1.default)(services, model));
}
catch (errCommand) {
console.log('[Error] Sub command', {
datastore: name,
model: model.name,
});
console.error(errCommand);
}
}
program.addCommand(dsProgram);
}
catch (err) {
// console.error(err);
}
}
config();
program.addCommand((0, admin_1.default)(services));
program.addCommand((0, data_1.default)(services));
program.addCommand((0, security_1.default)(services));
isRunCli === false && program.addCommand((0, runner_1.default)());
isRunCli === false && stream();
if (isRunCli === true) {
console.log(program.helpInformation());
run();
}
return program.parse(argv);
}
if (!module.parent) {
cli();
}
//# sourceMappingURL=index.js.map