From 0ddc032a5de6874b5362d50ff5c7e41e4a82b1d0 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Thu, 8 Oct 2026 12:17:36 +0300 Subject: [PATCH 1/2] feat(db): db trigger create --order and --break-on-error The gateway added two fields to every trigger: order (lower runs first; no order runs after the numbered ones) and breakOnError (stop the later triggers of the same event when this one fails). The file already passed them through; the new flags set them from the terminal and win over the file, like --schema and --id. A negative order is refused before sending. @norbix.ai/ts is bumped to ^4.18.0, whose trigger types carry both fields. Co-Authored-By: Claude Opus 5.5 --- docs/database.md | 11 ++++++++- package-lock.json | 8 +++---- package.json | 2 +- src/commands/db/trigger/create.ts | 18 +++++++++++++- tests/__snapshots__/schema.test.ts.snap | 2 ++ tests/db-routes.test.ts | 32 +++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 7 deletions(-) diff --git a/docs/database.md b/docs/database.md index 81b0040..73ea24b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -187,7 +187,7 @@ webhook …) when records of a schema are inserted, updated or deleted. |---|---|---| | `norbix db triggers [--schema ] [--page-size ] [--after ]` | list triggers | `GET /schemas/triggers` | | `norbix db trigger [--schema ]` | show one trigger | `GET /schemas/triggers/{id}` | -| `norbix db trigger create --file [--schema ] [--id ]` | create (or, with `--id`, update) a trigger | `POST /schemas/triggers` | +| `norbix db trigger create --file [--schema ] [--id ] [--order ] [--[no-]break-on-error]` | create (or, with `--id`, update) a trigger | `POST /schemas/triggers` | | `norbix db trigger enable ` | turn it on | `PATCH /schemas/triggers/{triggerId}/enable` | | `norbix db trigger disable [--yes]` | turn it off | `PATCH /schemas/triggers/{triggerId}/disable` | | `norbix db trigger delete [--yes]` | delete it | `DELETE /schemas/triggers/{triggerId}` | @@ -208,8 +208,17 @@ file. The action names an integration and a template of the e-mail, push or SMS module: look them up with `norbix email integrations` / `norbix email templates` (and the push / sms equivalents). +When several triggers fire for the same record event they run as a queue. +`--order ` sets this trigger's place (0 or more, lower runs first; a trigger +without an order runs after the numbered ones, equal places run by name) and +`--break-on-error` stops the later triggers of the event when this one's action +fails (`--no-break-on-error` turns it off). Both win over `order` / +`breakOnError` in the file; `db trigger ` shows them back. An update +replaces the whole trigger, so keep the rest of the file as it was. + ```bash norbix db trigger create --file notify-on-order.json --schema 66b2f0a1c3d4e5f6a7b8c9d0 +norbix db trigger create --file notify-on-order.json --id 66b2f0a1c3d4e5f6a7b8c9d1 --order 1 --break-on-error norbix db trigger disable 66b2f0a1c3d4e5f6a7b8c9d1 --yes ``` diff --git a/package-lock.json b/package-lock.json index 87add8b..ccdf03d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@inquirer/prompts": "^8.7.2", - "@norbix.ai/ts": "^4.17.0", + "@norbix.ai/ts": "^4.18.0", "@oclif/core": "^5.0.0", "@oclif/plugin-autocomplete": "^4.0.0", "@oclif/plugin-help": "^7.0.0" @@ -860,9 +860,9 @@ } }, "node_modules/@norbix.ai/ts": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.17.0.tgz", - "integrity": "sha512-Wo4DOnyaW91UUz3gbTUyvgxHC8dHPxL4W9FGmdBgPXBNtAgSwReaPuOEqLXXrsQ0F79mkfvrsxQR0ir0BqqbPw==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@norbix.ai/ts/-/ts-4.18.0.tgz", + "integrity": "sha512-uW4230ae45kx0uyPSfZjKk2ki+iUHvWQwReTBjq0Z+gAnA5PDv49Pk4uWiOriFCAgnLf+pq4IrS0lSktmDU5OQ==", "license": "MIT", "engines": { "node": ">=18" diff --git a/package.json b/package.json index a0568b2..e56ee71 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@inquirer/prompts": "^8.7.2", - "@norbix.ai/ts": "^4.17.0", + "@norbix.ai/ts": "^4.18.0", "@oclif/core": "^5.0.0", "@oclif/plugin-autocomplete": "^4.0.0", "@oclif/plugin-help": "^7.0.0" diff --git a/src/commands/db/trigger/create.ts b/src/commands/db/trigger/create.ts index 6adbfb8..39e46c5 100644 --- a/src/commands/db/trigger/create.ts +++ b/src/commands/db/trigger/create.ts @@ -9,13 +9,19 @@ export default class DbTriggerCreate extends BaseCommand { The file holds the trigger: name, schemaId, the record events it reacts to, and an action ({type, ...}) — the same shape \`db trigger --json\` shows. "type" is set to "Schema" when the file leaves it out. --schema sets schemaId, ---id sets triggerId (update instead of create).` +--id sets triggerId (update instead of create). + +When several triggers fire for the same record event they run as a queue: +--order sets the trigger's place (lower runs first; no order = after the +numbered ones) and --break-on-error stops the later triggers when this one's +action fails (--no-break-on-error turns it off). Both override the file.` static examples = [ '<%= config.bin %> db trigger create --file trigger.json', '<%= config.bin %> db trigger create --file trigger.json --schema 66b2f0a1c3d4e5f6a7b8c9d0', 'cat trigger.json | <%= config.bin %> db trigger create --file -', '<%= config.bin %> db trigger create --file trigger.json --dry-run', + '<%= config.bin %> db trigger create --file trigger.json --id 66b2f0a1c3d4e5f6a7b8c9d0 --order 1 --break-on-error', ] static flags = { @@ -23,6 +29,14 @@ and an action ({type, ...}) — the same shape \`db trigger --json\` shows. file: Flags.string({required: true, description: 'Trigger JSON file (or `-` for stdin)'}), schema: Flags.string({description: 'Schema ID (overrides schemaId in the file)'}), id: Flags.string({description: 'Trigger ID — set it to update instead of create'}), + order: Flags.integer({ + min: 0, + description: 'Place in the queue of the event: lower runs first, 0 or more (overrides "order" in the file)', + }), + 'break-on-error': Flags.boolean({ + allowNo: true, + description: 'Stop the later triggers of the same event when this one fails (overrides "breakOnError" in the file)', + }), } async run(): Promise { @@ -39,6 +53,8 @@ and an action ({type, ...}) — the same shape \`db trigger --json\` shows. const trigger: Record = {type: SCHEMA_TRIGGER, ...fromFile} if (flags.schema) trigger.schemaId = flags.schema if (flags.id) trigger.triggerId = flags.id + if (flags.order !== undefined) trigger.order = flags.order + if (flags['break-on-error'] !== undefined) trigger.breakOnError = flags['break-on-error'] // The schema-trigger fields are not on the generated base type, so the // body is cast: the server picks the shape from `type`. diff --git a/tests/__snapshots__/schema.test.ts.snap b/tests/__snapshots__/schema.test.ts.snap index af2bbed..39d86c4 100644 --- a/tests/__snapshots__/schema.test.ts.snap +++ b/tests/__snapshots__/schema.test.ts.snap @@ -869,6 +869,8 @@ exports[`norbix schema > is stable (snapshot of ids, flags and markers) 1`] = ` "file:string", "schema:string", "id:string", + "order:string", + "break-on-error:boolean", ], "id": "db trigger create", "supportsDryRun": true, diff --git a/tests/db-routes.test.ts b/tests/db-routes.test.ts index 5ca99d8..f9e8bb1 100644 --- a/tests/db-routes.test.ts +++ b/tests/db-routes.test.ts @@ -357,6 +357,38 @@ describe('db trigger', () => { }) }) + // The queue of one record event: order (lower runs first) and breakOnError + // (stop the later triggers when this one fails) — gateway trigger-order-break. + it('create sends --order and --break-on-error, and they win over the file', async () => { + const ordered = join(dir, 'ordered-trigger.json') + writeFileSync(ordered, JSON.stringify({...trigger, order: 5, breakOnError: false})) + expect(await bodyOf(['trigger', 'create', '--file', ordered, '--order', '0', '--break-on-error'])).toEqual({ + trigger: {type: 'Schema', ...trigger, order: 0, breakOnError: true}, + }) + }) + + it('create keeps order and breakOnError from the file when no flag is given', async () => { + const ordered = join(dir, 'ordered-trigger-2.json') + writeFileSync(ordered, JSON.stringify({...trigger, order: 2, breakOnError: true})) + expect(await bodyOf(['trigger', 'create', '--file', ordered])).toEqual({ + trigger: {type: 'Schema', ...trigger, order: 2, breakOnError: true}, + }) + }) + + it('create --no-break-on-error turns the file\'s breakOnError off', async () => { + const ordered = join(dir, 'ordered-trigger-3.json') + writeFileSync(ordered, JSON.stringify({...trigger, order: 2, breakOnError: true})) + expect(await bodyOf(['trigger', 'create', '--file', ordered, '--no-break-on-error'])).toEqual({ + trigger: {type: 'Schema', ...trigger, order: 2, breakOnError: false}, + }) + }) + + 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/) + expect(calls).toHaveLength(0) + }) + it('create reports a file that is not JSON and sends nothing', async () => { const bad = join(dir, 'bad.json') writeFileSync(bad, 'not-json') From 1c441996fa87b031208048e94b8e76153aa21970 Mon Sep 17 00:00:00 2001 From: CodeMash Date: Thu, 8 Oct 2026 12:35:47 +0300 Subject: [PATCH 2/2] =?UTF-8?q?docs(tasks):=20trigger=20order=20and=20brea?= =?UTF-8?q?k-on-failure=20across=20the=20SDKs=20and=20the=20CLI=20?= =?UTF-8?q?=E2=80=94=20plan,=20shipped=20pull=20requests,=20evidence,=20fi?= =?UTF-8?q?ndings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/tasks/trigger-order-sdks.md | 101 +++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/tasks/trigger-order-sdks.md diff --git a/docs/tasks/trigger-order-sdks.md b/docs/tasks/trigger-order-sdks.md new file mode 100644 index 0000000..2bd22ef --- /dev/null +++ b/docs/tasks/trigger-order-sdks.md @@ -0,0 +1,101 @@ +# SDKs + CLI: trigger "order" and "break on failure" +This file: /Users/djovaisas/Projects/norbix/worktrees/cli/feat/trigger-order/docs/tasks/trigger-order-sdks.md (branch feat/trigger-order; after the merge: norbix-code/cli main, `docs/tasks/trigger-order-sdks.md`) +Gateway half (shipped): /Users/djovaisas/Projects/norbix/worktrees/gateway/feat/trigger-order-break/docs/tasks/trigger-order-break.md (merged into refactoringV2, 251fbc72a) + +## Goal +Every SDK and the CLI know the two new trigger fields `order` (whole number, optional) and `breakOnError` (true / false) on the trigger save request, the trigger read DTO and the trigger list row. +Not in scope: AI trigger methods in the SDKs (none exist today); the uncompiled `references/` folders; React-Redux (its hooks pass arguments straight to `@norbix.ai/ts`). + +## Decisions +- decision(sdks:types): typed SDKs (TypeScript, .NET, Go) get the fields in their generated hub / api types; their save methods already take the whole request, so no method changed. +- decision(sdks:untyped): Python, Dart, Kotlin and Swift send a plain map, so the fields already reached the gateway; they get docs with an example, and Python (the only one with trigger tests) a body test. +- decision(sdks:mcp): the MCP manifest is rebuilt from the released TypeScript SDK, so `norbix_describe_endpoint` shows the fields to an assistant. +- decision(cli:db): the CLI already saves schema triggers from a JSON file (`db trigger create --file`), which passes the fields through; it gets `--order` and `--[no-]break-on-error` flags that win over the file. `payments triggers` / `payments trigger` print the raw answer, so they show the fields with no change. +- decision(gateway:triggers): owner, 2026-10-08 — a pre-action code that throws does not stop the queue (kept as is; recorded in the gateway task file). + +## Plan +1. [done] docs(cli:tasks): this task file +2. [done] gen(sdk-ts:types): `order` / `breakOnError` + `IQueuedTrigger` in `hub2.dtos.ts` and `api2.dtos.ts`; test for all four saves — released 4.18.0 +3. [done] gen(sdk-net:types): `Hub.dtos.cs` / `Api.dtos.cs` regenerated; 8 body snapshots show the fields — released 3.16.0 +4. [done] gen(sdk-go:types): `norbix/{hub,api}/dtos/dtos.go` regenerated; save + decode tests — released 2.18.0 +5. [done] docs(sdk-python, sdk-dart, sdk-kotlin, sdk-swift): the fields on the trigger docs with an example; Python body test for all four saves +6. [done] release(sdks): seven pull requests merged (list below) +7. [done] gen(sdk-mcp:manifest): rebuilt from `@norbix.ai/ts` 4.18.0, describe test — released 0.5.0 +8. [done] feat(cli:db): `db trigger create --order --[no-]break-on-error`; `@norbix.ai/ts` ^4.18.0; docs/database.md +9. [done] release(cli): this file ships with the CLI pull request, merged last (after @norbix.ai/ts 4.18.0) + +## Shipped +| repo | pull request | release | +|---|---|---| +| sdk-ts | https://github.com/norbix-code/sdk-ts/pull/83 | v4.18.0 | +| sdk-net | https://github.com/norbix-code/sdk-net/pull/85 | v3.16.0 | +| sdk-go | https://github.com/norbix-code/sdk-go/pull/33 | v2.18.0 | +| sdk-python | https://github.com/norbix-code/sdk-python/pull/43 | none (docs + test) | +| sdk-dart | https://github.com/norbix-code/sdk-dart/pull/33 | none (docs) | +| sdk-kotlin | https://github.com/norbix-code/sdk-kotlin/pull/31 | none (docs) | +| sdk-swift | https://github.com/norbix-code/sdk-swift/pull/32 | none (docs) | +| mcp | https://github.com/norbix-code/mcp/pull/14 | v0.5.0 | +| cli | the pull request that carries this file | the release made when it merges | + +## Evidence +| repo | check | result | +|---|---|---| +| sdk-ts | `vitest run`, `tsc --noEmit`, lint, prettier | 927 / 927 (5 new), clean | +| sdk-net | `dotnet test Norbix.Sdk.sln` | 316 / 316 (8 snapshots now show both fields in the request bodies) | +| sdk-go | `go build ./... && go test ./...`, `go vet`, `gofmt -l` | green; 2 new tests (save sends order 0 + flag; typed read decodes both) | +| sdk-python | `uv run pytest`, `make typecheck` | green; 4 new (one per save method) | +| mcp | `npm test`, `tsc --noEmit` | 39 / 39 (1 new) | +| cli | `npm test` (after `npm run build`), `tsc --noEmit` | 862 / 862 (4 new + command-schema snapshot) | + +## Changes +| file (repo, branch) | what changed | step | +|------|--------------|------| +| sdk-ts `src/types/hub2.dtos.ts`, `src/types/api2.dtos.ts` (main via #83) | the two fields on `SaveTriggerRequest`, `TriggerDto`, `TriggerProjectionList`; `IQueuedTrigger` | 2 | +| sdk-ts `tests/hub/trigger-order.test.ts` (main via #83) | new test | 2 | +| sdk-net `src/Norbix.Sdk.Types/Generated/{Hub,Api}.dtos.cs` + 8 `tests/Norbix.Hub.Tests/test_results/*.verified.txt` (main via #85) | regenerated + snapshots | 3 | +| sdk-go `norbix/{hub,api}/dtos/dtos.go`, `norbix/hub/trigger_order_test.go` (main via #33) | regenerated + test | 4 | +| sdk-python `docs/hub/triggers.md`, `README.md`, `tests/hub/test_trigger_order.py` (main via #43) | docs + test | 5 | +| sdk-dart / sdk-kotlin / sdk-swift `README.md` (main via #33 / #31 / #32) | docs with an example | 5 | +| mcp `src/manifest/dtos.{hub,api}.json`, `package.json`, `tests/generic.test.ts` (main via #14) | manifest + bump + test | 7 | +| /Users/djovaisas/Projects/norbix/worktrees/cli/feat/trigger-order/src/commands/db/trigger/create.ts (feat/trigger-order) | `--order`, `--[no-]break-on-error` | 8 | +| /Users/djovaisas/Projects/norbix/worktrees/cli/feat/trigger-order/tests/db-routes.test.ts (feat/trigger-order) | 4 new tests; `tests/__snapshots__/schema.test.ts.snap` | 8 | +| /Users/djovaisas/Projects/norbix/worktrees/cli/feat/trigger-order/docs/database.md (feat/trigger-order) | the flags, the queue rule, an example | 8 | + +## Findings + +fix(sdk-ts:tooling): the TypeScript types could not be regenerated — the private `scripts/sync-types.mjs` (gitignored, only in the main checkout) writes its `@sdk-dto-patches` block outside the namespace, so the output does not compile (`tsc`: no exported member `UpdateUserNotificationsPreferences`, `GrantContactConsentRequest`, `UnsubscribeContactRequest`, …), and `scripts/generate-endpoints.mjs` fails at start (`Cannot access 'OPTIONAL_AUTH_ROUTES' before initialization`). The two fields were applied by hand in the generator's exact output shape (diff: 37 lines, only the trigger fields) — left open, known follow-up #60 + where: /Users/djovaisas/Projects/norbix/sdks/norbix-js/scripts/sync-types.mjs (not in git — gitignored) +```ts +// regenerated hub2.dtos.ts, end of file (sync-types --remote, 2026-10-08) — does not compile +} // <-- here: the namespace closes … +// @sdk-dto-patches (injected by sync-types.mjs) +export class UserApiKey { // <-- … and the patch classes land outside it +``` + +fix(sdk-go:types): every number in the Go DTOs is a plain `float64` with `omitempty`, so on a decoded `TriggerDto` an order of 0 and "no order" look the same (both `0`). Sending works (requests are maps, order 0 reaches the gateway — tested). Making optional numbers `*float64` is a generator-wide change in typegen `languages/go/generate.py` — left open + where: https://github.com/norbix-code/sdk-go/blob/main/norbix/hub/dtos/dtos.go +```go +// norbix-go norbix/hub/dtos/dtos.go (main) — generated + Order float64 `json:"order,omitempty"` // <-- here: 0 and "no order" are the same value + BreakOnError bool `json:"breakOnError,omitempty"` +``` + +chore(sdks:tooling): the regeneration tools of several SDKs are not in git — Python (`scripts/generate_endpoints.py`), Dart (`tool/generate_resources.py`), Kotlin (none), the TypeScript scripts above; typegen has generators for C# and Go only, and `tools/sync.py` named in its README does not exist — left open + where: /Users/djovaisas/Projects/norbix/sdks/typegen/README.md + +chore(sdks:ai-triggers): no SDK has AI trigger methods and no `AiTriggerRequest` type is emitted (only `SaveAiProjectTrigger` and `AiTriggerDto`); AI triggers are reachable through the generic calls only — left open + where: https://github.com/norbix-code/sdk-ts/blob/main/src/hub/ai.ts + +chore(git:sdks): main checkouts of typegen, cli, python, dart, kotlin, swift are on feature branches (nbx-doctor WARN); another session was shipping Swift and the CLI at the same time — not mine, untouched + where: /Users/djovaisas/Projects/norbix/sdks + +## Rejected / moved out +- decision(sdk-ts:tooling): fixing the private sync / endpoint scripts — moved out — tooling outside the task, not in git; recorded under Findings +- decision(cli:triggers): trigger save commands for membership / files / payments in the CLI — rejected — the CLI has only `db trigger create`; new commands are their own feature +- decision(sdk-react-redux): no change — hooks pass through to `@norbix.ai/ts` (peer `>=4.11.0`), so 4.18.0 carries the fields + +## Needs you +- (nothing) + +## Open questions +- (none)