- action add/update: new --dependencies <json> option - parseDependencies accepts JSON, name@version shorthand, or bare names - update is incremental; --dependencies replaces the whole array - docs: deployment/systemd.md + skill SKILL/action-types/development document the npm-deps feature and the service-PATH requirement (spawn npm ENOENT without nvm bin dir on PATH)
583 lines
24 KiB
JavaScript
583 lines
24 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.buildActionConfig = exports.actionCommand = exports.buildTriggerConfig = exports.triggerCommand = exports.workflowCommand = void 0;
|
|
/**
|
|
* `inkreach-teable workflow` — workflow CRUD + lifecycle.
|
|
* `inkreach-teable trigger` — set the trigger node's type & config.
|
|
* `inkreach-teable action` — add/update/delete action nodes.
|
|
*
|
|
* All node-mutating commands operate via PUT /node (whole-graph replacement),
|
|
* so they internally GET the full graph, mutate, then PUT it back.
|
|
*/
|
|
const commander_1 = require("commander");
|
|
const ctx_1 = require("../lib/ctx");
|
|
const output_1 = require("../lib/output");
|
|
// ===================== workflow =====================
|
|
function workflowCommand() {
|
|
const cmd = new commander_1.Command('workflow');
|
|
cmd.description('Manage automations (workflows) — the self-built automation engine');
|
|
cmd
|
|
.command('list')
|
|
.description('List all workflows in the base')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const list = (await ctx.client.listWorkflows(baseId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No workflows found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'isActive', header: 'ACTIVE' },
|
|
{ key: 'hasDraft', header: 'DRAFT' },
|
|
{ key: 'nodes', header: 'NODES' },
|
|
], list.map((w) => ({
|
|
id: w.id,
|
|
name: (0, output_1.truncate)(w.name, 30),
|
|
isActive: w.isActive ? '✓' : '',
|
|
hasDraft: w.hasDraft ? '*' : '',
|
|
nodes: (w.nodes || []).length,
|
|
})));
|
|
process.stdout.write(`\n${list.length} workflow(s). Use 'workflow get <id>' for details.\n`);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <workflowId>')
|
|
.description('Get workflow detail (nodes + edges)')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(wf);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['id', wf.id],
|
|
['name', wf.name],
|
|
['description', wf.description || ''],
|
|
['active', wf.isActive ? 'yes' : 'no'],
|
|
['hasDraft', wf.hasDraft ? 'yes' : 'no'],
|
|
]);
|
|
process.stdout.write('\nNODES:\n');
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'type', header: 'TYPE' },
|
|
{ key: 'category', header: 'CATEGORY' },
|
|
{ key: 'config', header: 'CONFIG' },
|
|
], (wf.nodes || []).map((n) => ({
|
|
id: n.id,
|
|
type: n.type,
|
|
category: n.category,
|
|
config: (0, output_1.truncate)(JSON.stringify(n.config), 40),
|
|
})));
|
|
if (wf.edges?.length) {
|
|
process.stdout.write('\nEDGES:\n');
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'sourceNodeId', header: 'SOURCE' },
|
|
{ key: 'targetNodeId', header: 'TARGET' },
|
|
], (wf.edges || []).map((e) => ({ ...e })));
|
|
}
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create a workflow (auto-includes a recordCreated trigger node)')
|
|
.requiredOption('--name <name>', 'Workflow name')
|
|
.option('--description <desc>', 'Workflow description')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const wf = await ctx.client.createWorkflow(baseId, {
|
|
name: opts.name,
|
|
description: opts.description,
|
|
});
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(wf);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['id', wf.id],
|
|
['name', wf.name],
|
|
['baseId', wf.baseId],
|
|
['', ''],
|
|
['next', `inkreach-teable trigger set ${wf.id} --type <T> ...`],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <workflowId>')
|
|
.description('Update workflow name / description')
|
|
.option('--name <name>', 'New name')
|
|
.option('--description <desc>', 'New description')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const wf = await ctx.client.updateWorkflow(baseId, workflowId, {
|
|
name: opts.name,
|
|
description: opts.description,
|
|
});
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(wf);
|
|
else
|
|
process.stdout.write(`✓ Workflow ${workflowId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <workflowId>')
|
|
.description('Soft-delete a workflow (cascades: base_node, auto-token, trigger unregister)')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
await ctx.client.deleteWorkflow(baseId, workflowId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: workflowId });
|
|
else
|
|
process.stdout.write(`✓ Workflow ${workflowId} deleted\n`);
|
|
}));
|
|
cmd
|
|
.command('activate <workflowId>')
|
|
.description('Activate: snapshot + register trigger + create auto access token')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const res = await ctx.client.activateWorkflow(baseId, workflowId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, ...res });
|
|
else
|
|
(0, output_1.printKv)([['snapshotId', res.snapshotId], ['status', 'activated']]);
|
|
}));
|
|
cmd
|
|
.command('deactivate <workflowId>')
|
|
.description('Deactivate: unregister trigger')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
await ctx.client.deactivateWorkflow(baseId, workflowId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: workflowId });
|
|
else
|
|
process.stdout.write(`✓ Workflow ${workflowId} deactivated\n`);
|
|
}));
|
|
cmd
|
|
.command('test <workflowId>')
|
|
.description('Test-run: execute action nodes with mock input (no trigger, no queue)')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const res = await ctx.client.testWorkflow(baseId, workflowId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(res);
|
|
else {
|
|
process.stdout.write('Test results:\n');
|
|
(0, output_1.printTable)([
|
|
{ key: 'nodeId', header: 'NODE' },
|
|
{ key: 'status', header: 'STATUS' },
|
|
{ key: 'error', header: 'ERROR' },
|
|
], (res.nodeResults || []).map((r) => {
|
|
const row = r;
|
|
return {
|
|
nodeId: row.nodeId,
|
|
status: row.status,
|
|
error: (0, output_1.truncate)(row.error, 50),
|
|
};
|
|
}));
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.workflowCommand = workflowCommand;
|
|
// ===================== trigger =====================
|
|
const TRIGGER_TYPES = [
|
|
'recordCreated', 'recordUpdated', 'recordCondition', 'recordCreatedOrUpdated',
|
|
'formSubmitted', 'scheduledTime', 'buttonClick', 'webhook',
|
|
];
|
|
function triggerCommand() {
|
|
const cmd = new commander_1.Command('trigger');
|
|
cmd.description('Set the trigger node (type + config). Builds correct config keys for you.');
|
|
cmd
|
|
.command('set <workflowId>')
|
|
.description('Set the trigger type and config (replaces the existing trigger node)')
|
|
.requiredOption('--type <type>', `Trigger type: ${TRIGGER_TYPES.join('|')}`)
|
|
.option('--table-id <id>', 'Table id (most record/button triggers)')
|
|
.option('--field-ids <json>', 'Field ids JSON array, e.g. \'["fldXXX"]\' (recordUpdated, buttonClick)')
|
|
.option('--form-id <id>', 'Form view id (formSubmitted)')
|
|
.option('--conditions <json>', 'Filter conditions JSON (recordCondition/recordCreated)')
|
|
.option('--schedule <json>', 'Schedule JSON {timing:{type,interval}} (scheduledTime)')
|
|
.option('--batch <json>', 'Batch config JSON {enabled,batchWindow,batchSize}')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
if (!TRIGGER_TYPES.includes(opts.type)) {
|
|
throw Object.assign(new Error(`Invalid trigger type: ${opts.type}`), { code: 'INVALID_TYPE' });
|
|
}
|
|
const config = buildTriggerConfig(opts.type, {
|
|
tableId: opts.tableId,
|
|
fieldIds: opts.fieldIds,
|
|
formId: opts.formId,
|
|
conditions: opts.conditions,
|
|
schedule: opts.schedule,
|
|
batch: opts.batch,
|
|
});
|
|
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
|
const nodes = (wf.nodes || []).slice();
|
|
// Find the trigger node (there is exactly one) and replace its category + config.
|
|
const triggerIdx = nodes.findIndex((n) => n.type === 'trigger');
|
|
const newTrigger = {
|
|
...nodes[triggerIdx >= 0 ? triggerIdx : 0],
|
|
type: 'trigger',
|
|
category: opts.type,
|
|
config,
|
|
};
|
|
if (triggerIdx >= 0)
|
|
nodes[triggerIdx] = newTrigger;
|
|
else
|
|
nodes.unshift(newTrigger);
|
|
const updated = await ctx.client.replaceNodes(baseId, workflowId, {
|
|
nodes,
|
|
edges: wf.edges || [],
|
|
});
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(updated);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['workflowId', workflowId],
|
|
['triggerType', opts.type],
|
|
['config', JSON.stringify(config)],
|
|
['', ''],
|
|
['next', `inkreach-teable action add ${workflowId} --type script ...`],
|
|
]);
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.triggerCommand = triggerCommand;
|
|
/** Build the trigger node config JSON using the SELF-BUILT contract keys. */
|
|
function buildTriggerConfig(type, opts) {
|
|
const parse = (s) => (s ? JSON.parse(s) : undefined);
|
|
const config = {};
|
|
switch (type) {
|
|
case 'recordCreated':
|
|
case 'recordCreatedOrUpdated': {
|
|
if (opts.tableId)
|
|
config.tableId = opts.tableId;
|
|
if (opts.conditions)
|
|
config.conditions = parse(opts.conditions);
|
|
break;
|
|
}
|
|
case 'recordUpdated': {
|
|
if (opts.tableId)
|
|
config.tableId = opts.tableId;
|
|
// NOTE: key is `fieldIds` (NOT watchFieldIds). [] = watch all fields.
|
|
config.fieldIds = opts.fieldIds ? parse(opts.fieldIds) : [];
|
|
break;
|
|
}
|
|
case 'recordCondition': {
|
|
if (opts.tableId)
|
|
config.tableId = opts.tableId;
|
|
// conditions is required for recordCondition
|
|
config.conditions = opts.conditions ? parse(opts.conditions) : { conjunction: 'and', filterSet: [] };
|
|
break;
|
|
}
|
|
case 'formSubmitted': {
|
|
if (opts.tableId)
|
|
config.tableId = opts.tableId;
|
|
if (opts.formId)
|
|
config.formId = opts.formId;
|
|
break;
|
|
}
|
|
case 'scheduledTime': {
|
|
// Backend ONLY honors timing.type + timing.interval (rest is ignored).
|
|
if (opts.schedule) {
|
|
const sched = parse(opts.schedule);
|
|
if (sched.timing)
|
|
config.timing = sched.timing;
|
|
}
|
|
break;
|
|
}
|
|
case 'buttonClick': {
|
|
if (opts.tableId)
|
|
config.tableId = opts.tableId;
|
|
config.fieldIds = opts.fieldIds ? parse(opts.fieldIds) : [];
|
|
break;
|
|
}
|
|
case 'webhook': {
|
|
// authorization is UI-only; backend does NOT validate it.
|
|
config.authorization = { type: 'none' };
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
if (opts.batch && ['recordCreated', 'recordUpdated', 'recordCondition', 'recordCreatedOrUpdated'].includes(type)) {
|
|
config.batch = parse(opts.batch);
|
|
}
|
|
return config;
|
|
}
|
|
exports.buildTriggerConfig = buildTriggerConfig;
|
|
// ===================== action =====================
|
|
const ACTION_TYPES = [
|
|
'script', 'sendEmail', 'httpRequest', 'createRecord', 'updateRecord', 'getRecords',
|
|
];
|
|
function actionCommand() {
|
|
const cmd = new commander_1.Command('action');
|
|
cmd.description('Add/update/delete action nodes');
|
|
cmd
|
|
.command('add <workflowId>')
|
|
.description('Add an action node after the last existing node')
|
|
.requiredOption('--type <type>', `Action type: ${ACTION_TYPES.join('|')}`)
|
|
.option('--config <json>', 'Full action config JSON (overrides the granular flags below)')
|
|
.option('--code <js>', 'Script JS code (script)')
|
|
.option('--code-file <path>', 'Read script code from a file (script)')
|
|
.option('--timeout <ms>', 'Script timeout ms (script, default 60000)')
|
|
.option('--dependencies <json>', 'npm packages JSON array for script, e.g. \'[{"name":"axios","version":"1.6.0"}]\' (script)')
|
|
.option('--to <email>', 'Recipient (sendEmail)')
|
|
.option('--subject <s>', 'Subject (sendEmail)')
|
|
.option('--body <html>', 'HTML body (sendEmail)')
|
|
.option('--url <u>', 'Request URL (httpRequest)')
|
|
.option('--method <m>', 'HTTP method (httpRequest, default GET)')
|
|
.option('--headers <json>', 'Request headers JSON (httpRequest)')
|
|
.option('--req-body <json>', 'Request body JSON (httpRequest)')
|
|
.option('--target-table <id>', 'Target table id (createRecord/updateRecord/getRecords)')
|
|
.option('--records <json>', 'Records JSON array (createRecord/updateRecord)')
|
|
.option('--query <json>', 'Query JSON (getRecords)')
|
|
.option('--name <n>', 'Optional node display name')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const workflowId = args[0];
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
if (!ACTION_TYPES.includes(opts.type)) {
|
|
throw Object.assign(new Error(`Invalid action type: ${opts.type}`), { code: 'INVALID_TYPE' });
|
|
}
|
|
const config = opts.config ? JSON.parse(opts.config) : buildActionConfig(opts);
|
|
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
|
const nodes = (wf.nodes || []).slice();
|
|
const edges = (wf.edges || []).slice();
|
|
const newNodeId = `node-${Date.now()}`;
|
|
const newNode = {
|
|
id: newNodeId,
|
|
type: 'action',
|
|
category: opts.type,
|
|
config,
|
|
name: opts.name || null,
|
|
};
|
|
// Chain after the last node (trigger → ... → new).
|
|
const lastNode = nodes[nodes.length - 1];
|
|
nodes.push(newNode);
|
|
if (lastNode) {
|
|
edges.push({
|
|
id: `edge-${lastNode.id}-${newNodeId}`,
|
|
sourceNodeId: lastNode.id,
|
|
targetNodeId: newNodeId,
|
|
});
|
|
}
|
|
const updated = await ctx.client.replaceNodes(baseId, workflowId, { nodes, edges });
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(updated);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['workflowId', workflowId],
|
|
['nodeId', newNodeId],
|
|
['type', opts.type],
|
|
['config', JSON.stringify(config)],
|
|
['', ''],
|
|
['next', `inkreach-teable workflow activate ${workflowId}`],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <workflowId> <nodeId>')
|
|
.description('Update an existing action node config')
|
|
.option('--config <json>', 'Full replacement config JSON')
|
|
.option('--code <js>', 'Script JS code (script)')
|
|
.option('--code-file <path>', 'Read script code from a file (script)')
|
|
.option('--timeout <ms>', 'Script timeout ms (script)')
|
|
.option('--dependencies <json>', 'npm packages JSON array for script, e.g. \'[{"name":"axios","version":"1.6.0"}]\' (script; replaces existing deps)')
|
|
.option('--name <n>', 'New node display name')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, nodeId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
|
const nodes = (wf.nodes || []).slice();
|
|
const idx = nodes.findIndex((n) => n.id === nodeId);
|
|
if (idx < 0)
|
|
throw Object.assign(new Error(`Node ${nodeId} not found`), { code: 'NOT_FOUND' });
|
|
const node = nodes[idx];
|
|
let config = node.config || {};
|
|
if (opts.config)
|
|
config = JSON.parse(opts.config);
|
|
else {
|
|
// Granular update: merge only the flags the user actually passed.
|
|
config = { ...config };
|
|
if (opts.code !== undefined || opts['code-file'] !== undefined) {
|
|
config.code = opts['code-file'] ? readFile(opts['code-file']) : opts.code;
|
|
}
|
|
if (opts.timeout !== undefined)
|
|
config.timeout = Number(opts.timeout);
|
|
if (opts.dependencies !== undefined) {
|
|
config.dependencies = parseDependencies(opts.dependencies);
|
|
}
|
|
}
|
|
nodes[idx] = { ...node, config, name: opts.name ?? node.name };
|
|
const updated = await ctx.client.replaceNodes(baseId, workflowId, {
|
|
nodes,
|
|
edges: wf.edges || [],
|
|
});
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(updated);
|
|
else
|
|
process.stdout.write(`✓ Node ${nodeId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <workflowId> <nodeId>')
|
|
.description('Delete an action node and its connected edges')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [workflowId, nodeId] = args;
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const wf = await ctx.client.getWorkflowInBase(baseId, workflowId);
|
|
const nodes = (wf.nodes || []).filter((n) => n.id !== nodeId);
|
|
const edges = (wf.edges || []).filter((e) => e.sourceNodeId !== nodeId && e.targetNodeId !== nodeId);
|
|
const updated = await ctx.client.replaceNodes(baseId, workflowId, { nodes, edges });
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(updated);
|
|
else
|
|
process.stdout.write(`✓ Node ${nodeId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
exports.actionCommand = actionCommand;
|
|
function readFile(p) {
|
|
// inline require to keep imports clean
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
return require('node:fs').readFileSync(p, 'utf8');
|
|
}
|
|
/**
|
|
* Parse `--dependencies` into the ScriptDependency[] shape the backend expects.
|
|
* Accepts:
|
|
* - full JSON: '[{"name":"axios","version":"1.6.0"}]'
|
|
* - name list: 'axios,lodash' (version defaults to 'latest')
|
|
* - name@ver: 'axios@1.6.0,lodash@4.17.21'
|
|
* - empty '[]' or '' to clear deps
|
|
*/
|
|
function parseDependencies(raw) {
|
|
const trimmed = raw.trim();
|
|
if (!trimmed || trimmed === '[]')
|
|
return [];
|
|
// JSON form
|
|
if (trimmed[0] === '[') {
|
|
const parsed = JSON.parse(trimmed);
|
|
if (!Array.isArray(parsed))
|
|
throw new Error('--dependencies JSON must be an array');
|
|
return parsed.map((d) => {
|
|
if (typeof d === 'string')
|
|
return { name: d };
|
|
if (d && typeof d === 'object' && typeof d.name === 'string') {
|
|
return { name: d.name, ...(d.version ? { version: String(d.version) } : {}) };
|
|
}
|
|
throw new Error('--dependencies entries must be strings or {name,version?} objects');
|
|
});
|
|
}
|
|
// Comma list form: axios@1.6.0,lodash
|
|
return trimmed
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean)
|
|
.map((spec) => {
|
|
const at = spec.lastIndexOf('@');
|
|
if (at > 0)
|
|
return { name: spec.slice(0, at), version: spec.slice(at + 1) };
|
|
return { name: spec };
|
|
});
|
|
}
|
|
/** Build action config from granular flags. */
|
|
function buildActionConfig(opts) {
|
|
const type = opts.type;
|
|
// Helper: commander stores --kebab-case flags as camelCase in opts (e.g.
|
|
// --target-table -> opts.targetTable). Accept both forms for safety.
|
|
const pick = (k) => (opts[k] ?? opts[toCamel(k)]);
|
|
const config = {};
|
|
switch (type) {
|
|
case 'script': {
|
|
const codeFile = pick('code-file');
|
|
if (codeFile)
|
|
config.code = readFile(codeFile);
|
|
else if (opts.code)
|
|
config.code = opts.code;
|
|
if (opts.timeout)
|
|
config.timeout = Number(opts.timeout);
|
|
if (opts.dependencies)
|
|
config.dependencies = parseDependencies(opts.dependencies);
|
|
break;
|
|
}
|
|
case 'sendEmail': {
|
|
if (opts.to)
|
|
config.to = opts.to;
|
|
if (opts.subject)
|
|
config.subject = opts.subject;
|
|
if (opts.body)
|
|
config.body = opts.body;
|
|
break;
|
|
}
|
|
case 'httpRequest': {
|
|
if (opts.url)
|
|
config.url = opts.url;
|
|
if (opts.method)
|
|
config.method = opts.method;
|
|
if (opts.headers)
|
|
config.headers = JSON.parse(opts.headers);
|
|
const reqBody = pick('req-body');
|
|
if (reqBody)
|
|
config.body = JSON.parse(reqBody);
|
|
break;
|
|
}
|
|
case 'createRecord':
|
|
case 'updateRecord': {
|
|
const tt = pick('target-table');
|
|
if (tt)
|
|
config.tableId = tt;
|
|
if (opts.records)
|
|
config.records = JSON.parse(opts.records);
|
|
break;
|
|
}
|
|
case 'getRecords': {
|
|
const tt = pick('target-table');
|
|
if (tt)
|
|
config.tableId = tt;
|
|
if (opts.query)
|
|
config.query = JSON.parse(opts.query);
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return config;
|
|
}
|
|
exports.buildActionConfig = buildActionConfig;
|
|
function toCamel(s) {
|
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
}
|