Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ webhook …) when records of a schema are inserted, updated or deleted.
|---|---|---|
| `norbix db triggers [--schema <id>] [--page-size <n>] [--after <cursor>]` | list triggers | `GET /schemas/triggers` |
| `norbix db trigger <id> [--schema <id>]` | show one trigger | `GET /schemas/triggers/{id}` |
| `norbix db trigger create --file <trigger.json> [--schema <id>] [--id <triggerId>]` | create (or, with `--id`, update) a trigger | `POST /schemas/triggers` |
| `norbix db trigger create --file <trigger.json> [--schema <id>] [--id <triggerId>] [--order <n>] [--[no-]break-on-error]` | create (or, with `--id`, update) a trigger | `POST /schemas/triggers` |
| `norbix db trigger enable <id>` | turn it on | `PATCH /schemas/triggers/{triggerId}/enable` |
| `norbix db trigger disable <id> [--yes]` | turn it off | `PATCH /schemas/triggers/{triggerId}/disable` |
| `norbix db trigger delete <id> [--yes]` | delete it | `DELETE /schemas/triggers/{triggerId}` |
Expand All @@ -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 <n>` 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 <id>` 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
```

Expand Down
101 changes: 101 additions & 0 deletions docs/tasks/trigger-order-sdks.md
Original file line number Diff line number Diff line change
@@ -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 <n> --[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)
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
18 changes: 17 additions & 1 deletion src/commands/db/trigger/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,34 @@ 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 <id> --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 = {
...BaseCommand.dryRunFlags,
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<unknown> {
Expand All @@ -39,6 +53,8 @@ and an action ({type, ...}) — the same shape \`db trigger <id> --json\` shows.
const trigger: Record<string, unknown> = {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`.
Expand Down
2 changes: 2 additions & 0 deletions tests/__snapshots__/schema.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions tests/db-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Loading