"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 ', '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 ' for details.\n`); } })); cmd .command('get ') .description('Get workflow detail (nodes + edges)') .option('--base-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 ', 'Workflow name') .option('--description ', 'Workflow description') .option('--base-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 ...`], ]); } })); cmd .command('update ') .description('Update workflow name / description') .option('--name ', 'New name') .option('--description ', 'New description') .option('--base-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 ') .description('Soft-delete a workflow (cascades: base_node, auto-token, trigger unregister)') .option('--base-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 ') .description('Activate: snapshot + register trigger + create auto access token') .option('--base-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 ') .description('Deactivate: unregister trigger') .option('--base-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 ') .description('Test-run: execute action nodes with mock input (no trigger, no queue)') .option('--base-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 ') .description('Set the trigger type and config (replaces the existing trigger node)') .requiredOption('--type ', `Trigger type: ${TRIGGER_TYPES.join('|')}`) .option('--table-id ', 'Table id (most record/button triggers)') .option('--field-ids ', 'Field ids JSON array, e.g. \'["fldXXX"]\' (recordUpdated, buttonClick)') .option('--form-id ', 'Form view id (formSubmitted)') .option('--conditions ', 'Filter conditions JSON (recordCondition/recordCreated)') .option('--schedule ', 'Schedule JSON {timing:{type,interval}} (scheduledTime)') .option('--batch ', 'Batch config JSON {enabled,batchWindow,batchSize}') .option('--base-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 ') .description('Add an action node after the last existing node') .requiredOption('--type ', `Action type: ${ACTION_TYPES.join('|')}`) .option('--config ', 'Full action config JSON (overrides the granular flags below)') .option('--code ', 'Script JS code (script)') .option('--code-file ', 'Read script code from a file (script)') .option('--timeout ', 'Script timeout ms (script, default 60000)') .option('--to ', 'Recipient (sendEmail)') .option('--subject ', 'Subject (sendEmail)') .option('--body ', 'HTML body (sendEmail)') .option('--url ', 'Request URL (httpRequest)') .option('--method ', 'HTTP method (httpRequest, default GET)') .option('--headers ', 'Request headers JSON (httpRequest)') .option('--req-body ', 'Request body JSON (httpRequest)') .option('--target-table ', 'Target table id (createRecord/updateRecord/getRecords)') .option('--records ', 'Records JSON array (createRecord/updateRecord)') .option('--query ', 'Query JSON (getRecords)') .option('--name ', 'Optional node display name') .option('--base-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 ') .description('Update an existing action node config') .option('--config ', 'Full replacement config JSON') .option('--code ', 'Script JS code (script)') .option('--code-file ', 'Read script code from a file (script)') .option('--name ', 'New node display name') .option('--base-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 if (opts.code !== undefined || opts['code-file'] !== undefined) { config = { ...config, code: opts['code-file'] ? readFile(opts['code-file']) : opts.code }; } 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 ') .description('Delete an action node and its connected edges') .option('--base-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'); } /** 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); 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()); }