From 24b21d0ae95c3219cbf4a84ba06d983594c15574 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Thu, 15 Oct 2026 15:38:45 +0300 Subject: [PATCH] fix(triggers): a trigger file saved from `trigger --json` keeps its action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The read answer wraps the trigger in `item` and names the action `thenAction`; the save request wants `action`. `payments trigger save` and `db trigger create` now unwrap `item` and rename `thenAction`, so read → edit → save no longer saves a trigger without an action (found by the payments docs work, campaign audit/payments). Co-Authored-By: Claude Opus 5.5 --- src/commands/db/trigger/create.ts | 7 +++---- src/commands/payments/trigger/save.ts | 7 +++---- src/lib/triggers.ts | 20 ++++++++++++++++++++ tests/db-routes.test.ts | 9 +++++++++ tests/payments-routes.test.ts | 13 +++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 src/lib/triggers.ts diff --git a/src/commands/db/trigger/create.ts b/src/commands/db/trigger/create.ts index 39e46c5..920909a 100644 --- a/src/commands/db/trigger/create.ts +++ b/src/commands/db/trigger/create.ts @@ -2,6 +2,7 @@ import {Flags} from '@oclif/core' import {BaseCommand} from '../../../base.js' import {readJsonObject, SCHEMA_TRIGGER} from '../../../lib/database.js' +import {triggerFromFile} from '../../../lib/triggers.js' export default class DbTriggerCreate extends BaseCommand { static description = `Create or update a schema trigger from a JSON file @@ -45,10 +46,8 @@ action fails (--no-break-on-error turns it off). Both override the file.` const source = flags.file === '-' ? '-' : `@${flags.file}` const parsed = await readJsonObject(source, 'file') - // Accept the bare trigger or the whole request body ({"trigger": {...}}). - const fromFile = ( - parsed.trigger && typeof parsed.trigger === 'object' ? parsed.trigger : parsed - ) as Record + // The bare trigger, the request body or a `trigger --json` answer. + const fromFile = triggerFromFile(parsed) const trigger: Record = {type: SCHEMA_TRIGGER, ...fromFile} if (flags.schema) trigger.schemaId = flags.schema diff --git a/src/commands/payments/trigger/save.ts b/src/commands/payments/trigger/save.ts index 0a0f6ba..3f35940 100644 --- a/src/commands/payments/trigger/save.ts +++ b/src/commands/payments/trigger/save.ts @@ -2,6 +2,7 @@ import {Flags} from '@oclif/core' import {BaseCommand} from '../../../base.js' import {ON_WEBHOOK_CALL_RECEIVED, PAYMENTS_TRIGGER, readJsonObject} from '../../../lib/payments.js' +import {triggerFromFile} from '../../../lib/triggers.js' export default class PaymentsTriggerSave extends BaseCommand { static description = `Create or update a payment trigger from a JSON file @@ -50,10 +51,8 @@ trigger's place in the queue of the same event.` const source = flags.file === '-' ? '-' : `@${flags.file}` const parsed = await readJsonObject(source, 'file') - // Accept the bare trigger or the whole request body ({"trigger": {...}}). - const fromFile = ( - parsed.trigger && typeof parsed.trigger === 'object' ? parsed.trigger : parsed - ) as Record + // The bare trigger, the request body or a `trigger --json` answer. + const fromFile = triggerFromFile(parsed) const trigger: Record = {type: PAYMENTS_TRIGGER, when: ON_WEBHOOK_CALL_RECEIVED, ...fromFile} if (flags.id) trigger.triggerId = flags.id diff --git a/src/lib/triggers.ts b/src/lib/triggers.ts new file mode 100644 index 0000000..9cf50c2 --- /dev/null +++ b/src/lib/triggers.ts @@ -0,0 +1,20 @@ +/** + * The trigger of a `--file` for `db trigger create` / `payments trigger save`. + * + * Accepts the bare trigger, the whole save request (`{"trigger": {...}}`) or + * the read answer of `… trigger --json` (`{"item": {...}}`). A read + * answer names the action `thenAction`; the save request names it `action`, + * so a file saved from a read is renamed here — without it the round trip + * saved a trigger with no action. + */ +export function triggerFromFile(parsed: Record): Record { + const inner = [parsed.trigger, parsed.item].find((v) => v && typeof v === 'object' && !Array.isArray(v)) + const trigger = {...((inner ?? parsed) as Record)} + + if (trigger.action === undefined && trigger.thenAction !== undefined) { + trigger.action = trigger.thenAction + } + + delete trigger.thenAction + return trigger +} diff --git a/tests/db-routes.test.ts b/tests/db-routes.test.ts index a2b7ad3..a15fbf0 100644 --- a/tests/db-routes.test.ts +++ b/tests/db-routes.test.ts @@ -386,6 +386,15 @@ describe('db trigger', () => { }) }) + it('create takes a `db trigger --json` answer: item unwrapped, thenAction saved as action', async () => { + const {action, ...rest} = trigger as Record + const read = join(dir, 'read-trigger.json') + writeFileSync(read, JSON.stringify({item: {...rest, thenAction: action}})) + expect(await bodyOf(['trigger', 'create', '--file', read])).toEqual({ + trigger: {type: 'Schema', ...rest, action}, + }) + }) + it('create refuses a negative --order and sends nothing', async () => { const {error} = await runCommand(['db', 'trigger', 'create', '--file', TRIGGER_FILE, '--order', '-1', ...globalArgs]) expect(error?.message).toMatch(/order/) diff --git a/tests/payments-routes.test.ts b/tests/payments-routes.test.ts index 6592da3..2336cba 100644 --- a/tests/payments-routes.test.ts +++ b/tests/payments-routes.test.ts @@ -219,6 +219,19 @@ describe('payments trigger save', () => { expect(body?.trigger).toMatchObject({type: 'Payments', name: 'Paid'}) }) + it('takes a `payments trigger --json` answer: item unwrapped, thenAction saved as action', async () => { + const file = join(dir, 'read.json') + writeFileSync(file, JSON.stringify({item: {name: 'Paid', thenAction: {type: 'WebhookCall'}, events: ['invoice.paid']}})) + const {body} = await callOf(['trigger', 'save', '--file', file]) + expect(body?.trigger).toEqual({ + type: 'Payments', + when: 'OnWebhookCallReceived', + name: 'Paid', + action: {type: 'WebhookCall'}, + events: ['invoice.paid'], + }) + }) + it('lets the flags override the file', async () => { const {body} = await callOf([ 'trigger', 'save', '--file', triggerFile({order: 5}), '--id', ID,