@flusys/nestjs-entity-builder 9.0.1 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -28
- package/config/message-keys.d.ts +9 -62
- package/controllers/flow-api-key.controller.d.ts +14 -0
- package/controllers/flow-definition.controller.d.ts +1 -7
- package/controllers/index.d.ts +1 -0
- package/dtos/flow.dto.d.ts +20 -10
- package/entities/flow-api-key.entity.d.ts +10 -0
- package/entities/flow-definition.entity.d.ts +9 -3
- package/entities/flow-execution.entity.d.ts +0 -1
- package/entities/index.d.ts +3 -3
- package/fesm/21.js +11 -64
- package/fesm/{582.js → 489.js} +11 -20
- package/fesm/{913.js → 606.js} +295 -209
- package/fesm/719.js +186 -162
- package/fesm/794.js +187 -116
- package/fesm/{677.js → 996.js} +616 -1490
- package/fesm/controllers/index.js +6 -5
- package/fesm/docs/index.js +2 -2
- package/fesm/entities/index.js +2 -2
- package/fesm/guards/index.js +19 -12
- package/fesm/index.js +638 -50
- package/fesm/modules/index.js +625 -7
- package/fesm/rule-engine/index.js +2 -2
- package/fesm/services/index.js +25 -54
- package/flow-engine/flow-api-key.d.ts +7 -3
- package/flow-engine/flow-graph.validator.d.ts +2 -4
- package/flow-engine/flow-run.types.d.ts +4 -14
- package/flow-engine/flow.types.d.ts +9 -91
- package/interfaces/flow-definition.interface.d.ts +9 -13
- package/package.json +5 -5
- package/rule-engine/rule-engine.service.d.ts +0 -1
- package/services/entity-flow-scaffold.d.ts +1 -1
- package/services/flow-api-key.service.d.ts +31 -0
- package/services/flow-definition.service.d.ts +12 -24
- package/services/flow-execution.service.d.ts +0 -1
- package/services/flow-executor.service.d.ts +7 -7
- package/services/generic-entity.service.d.ts +1 -1
- package/services/index.d.ts +1 -0
- package/entities/flow-version.entity.d.ts +0 -10
- package/fesm/458.js +0 -649
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Works on PostgreSQL and MySQL (via `SchemaDialectAdapterService`). Import `Entit
|
|
|
15
15
|
|
|
16
16
|
| Path | Purpose | Permission |
|
|
17
17
|
| --- | --- | --- |
|
|
18
|
-
| `entity-builder/entity-definitions/create-entity` | Create entity + table (+ initial fields) in one DDL transaction. `flowEndpoints` (any of the generic API controller's endpoints: `insert`, `insertMany`, `getById`, `getByIds`, `getAll`, `getByFilter`, `update`, `updateMany`, `bulkUpsert`, `delete`) also saves one ready-made flow for the entity after the commit - slug `<code-with-dashes>` (e.g. `customer-ticket`), named after the entity,
|
|
18
|
+
| `entity-builder/entity-definitions/create-entity` | Create entity + table (+ initial fields) in one DDL transaction. `flowEndpoints` (any of the generic API controller's endpoints: `insert`, `insertMany`, `getById`, `getByIds`, `getAll`, `getByFilter`, `update`, `updateMany`, `bulkUpsert`, `delete`) also saves one ready-made flow for the entity after the commit - slug `<code-with-dashes>` (e.g. `customer-ticket`), named after the entity, `jwt` auth, runs as the caller (entity permissions apply), transactional when an endpoint writes - with one Request step per endpoint on its own path, `POST api-flows/<code-with-dashes>/<endpoint-in-kebab-case>` (e.g. `customer-ticket/get-by-ids`, `ENTITY_FLOW_PATHS`), each with its own body; there is no Request step on the flow's own URL. Each endpoint's steps are ids `<endpoint>_<step>` (e.g. `getById_record`) laid out one below the other. Single endpoints are Request -> entity step -> respond with its result; `getAll` / `getByFilter` take an optional equality filter per scalar field (`getByFilter` answers the first match or 404); `getByIds` takes `ids` (`in` filter); `insertMany` / `updateMany` / `bulkUpsert` have a `list` body type - the request body is the list of records itself (`[{ ... }, { ... }]`), each item checked against the entity fields (insert: the insert inputs; update many: `id` required, the rest optional; bulk upsert: all optional) - loop over `input` (up to 200) in one transaction and answer the saved records in order (`bulkUpsert` updates an item with an `id`, inserts one without). `flowTexts` (`names` of each endpoint's Request step, `inputLabels` for `id` / `ids` / `search`, `noMatchMessage`) carries the text written into the flow in the creator's language - English for anything left out. The flow is saved after the commit: when it fails the entity stays and the failure comes back in the response's `warnings` (`entity_builder.schema.warning.endpoint.flow.failed`, with the cause as a nested message ref), next to a failed permission set-up (`...follow.up.failed`). It is an ordinary flow the user can change or delete later. Needs `flow_definition.create` too; a slug already in use is a plan blocker | `entity_builder.entity_definition.create` |
|
|
19
19
|
| `entity-builder/entity-definitions/plan-create-entity` | Dry run of the above: the real `CREATE TABLE` SQL, nothing is created | `...entity_definition.create` |
|
|
20
20
|
| `entity-builder/entity-definitions/plan-change` | Dry run of any change below: exact SQL + reverse SQL, data checks, blockers, warnings, affected flows. Plans count records (never show their values), so planning needs the permission that applies the change | `...entity_definition.update` (`...delete` for `drop_entity` / `purge_field`) |
|
|
21
21
|
| `entity-builder/entity-definitions/apply-change` | Apply `add_field`, `update_field`, `deprecate_field`, `restore_field`, `update_entity`, `repair`. Answers the plan of what ran (`downSql` = the undo statements of what ran) plus `result` (the change's summary, e.g. repair's `fixed`) | `...entity_definition.update` |
|
|
@@ -24,14 +24,12 @@ Works on PostgreSQL and MySQL (via `SchemaDialectAdapterService`). Import `Entit
|
|
|
24
24
|
| `entity-builder/entity-definitions/schema-history` | Every schema change with its SQL, failed attempts included | `...entity_definition.read` |
|
|
25
25
|
| `entity-builder/entity-definitions/{get-all,get/:id,get-by-ids,get-by-filter}` | Metadata read (writes only through the endpoints above) | `...entity_definition.read` |
|
|
26
26
|
| `entity-builder/field-definitions/{get-all,get/:id,get-by-ids,get-by-filter}` | Field metadata read | `...field_definition.read` |
|
|
27
|
-
| `api-flows/:slug` | **Call a flow** (a virtual API) from its Request step on the flow's own URL. Access is
|
|
28
|
-
| `api-flows/:slug/:endpoint` | **Call one of the flow's other URLs**: runs from the Request step whose `path` is `endpoint`, checked against that step's own body
|
|
29
|
-
| `entity-builder/flows/{insert,update,delete,get-all,get/:id,...}` | Flow CRUD - every save is validated as a whole
|
|
30
|
-
| `entity-builder/flows/publish` | `{ id, note? }`: makes the draft live as the next version | `...flow_definition.update` |
|
|
31
|
-
| `entity-builder/flows/{versions,version}` | `{ flowId }`: published versions, newest first; `{ flowId, version }`: one in full | `...flow_definition.read` |
|
|
32
|
-
| `entity-builder/flows/{restore-version,discard-draft}` | Put an old version back into the draft / drop the draft | `...flow_definition.update` |
|
|
27
|
+
| `api-flows/:slug` | **Call a flow** (a virtual API) from its Request step on the flow's own URL. Access is the flow's own auth mode; the answer is what its `respond` node says |
|
|
28
|
+
| `api-flows/:slug/:endpoint` | **Call one of the flow's other URLs**: runs from the Request step whose `path` is `endpoint`, checked against that step's own body. Same auth, rate limit and run-as as the flow's own URL | per flow: public / API key / any user / a permission |
|
|
29
|
+
| `entity-builder/flows/{insert,update,delete,get-all,get/:id,...}` | Flow CRUD - every save is validated as a whole | `...flow_definition.*` |
|
|
33
30
|
| `entity-builder/flows/validate` | Check a draft without saving (errors block a save, warnings do not) | `...flow_definition.read` |
|
|
34
31
|
| `entity-builder/flows/test-run` | Run a saved flow or an unsaved draft, with a per-node trace | `...flow_definition.test` |
|
|
32
|
+
| `entity-builder/flow-api-keys/{create,list,revoke,delete}` | API keys of a flow (the key is shown once). `create` takes `{ flowId, name, expiresAt? }` (ISO, in the future; no zone = UTC); every key has `status` `active` / `revoked` / `expired`. Revoke and delete work whatever the flow's auth mode is | `...flow_definition.update/read` |
|
|
35
33
|
| `entity-builder/flow-executions/{list,get}` | Run history | `...flow_definition.read` |
|
|
36
34
|
|
|
37
35
|
`entity_builder.entity.*` is a wildcard grant covering every dynamic entity (seeded by `seed:admin`); it covers only dynamic entities, not the static schema-admin permissions (`entity_builder.entity_definition.*`, `entity_builder.field_definition.*`, `entity_builder.flow_definition.*`).
|
|
@@ -40,7 +38,7 @@ Works on PostgreSQL and MySQL (via `SchemaDialectAdapterService`). Import `Entit
|
|
|
40
38
|
|
|
41
39
|
- **Every structural change is a plan first.** `plan-change` runs the same code as `apply-change` in a dry run (TypeORM SQL-memory mode), so the SQL you preview is the SQL that runs. Data is checked before anything changes: converting `TEXT` to `INTEGER` is refused while any value is not a whole number (with examples), a unique constraint is refused while duplicates exist, and a required field on a table with records needs a default/backfill value. Unsupported conversions are refused with the reason. MySQL has no `USING` clause, so on MySQL single select <-> multi select and text -> boolean are refused too (add a new field, copy the data, deprecate the old one).
|
|
42
40
|
- Supported: add field (optionally required/unique/indexed, with backfill), change type, required/optional, unique, index, default, validation rules, select options, label; **rename a field** (column renamed, flows that use it are rewritten, including `{{ ... }}` template placeholders); deprecate / restore; **permanently delete** a deprecated field; entity label/description/status/soft-delete/audit; **drop entity**; **repair** drift (missing columns, indexes, unique constraints, NOT NULL).
|
|
43
|
-
- Every change value is type-checked (`FieldChangesDto` / `EntityChangesDto`): `nullable: "false"`, `null` for a flag, an unknown field type or a too long label is a 400 `entity_builder.schema.invalid.change.value`. `update_field` can set `relationTargetEntityId` (RELATION fields only; the target must exist and is locked for the change; converting to RELATION needs one, `...blocker.relation.target.required`).
|
|
41
|
+
- Every change value is type-checked (`FieldChangesDto` / `EntityChangesDto`): `nullable: "false"`, `null` for a flag, an unknown field type or a too long label is a 400 `entity_builder.schema.invalid.change.value`. `update_field` can set `relationTargetEntityId` (RELATION fields only; the target must exist and is locked for the change; converting to RELATION needs one, `...blocker.relation.target.required`).
|
|
44
42
|
- **Optimistic concurrency**: send `expectedUpdatedAt` (the `updatedAt` you loaded - of the field for field actions, of the entity otherwise; select `updatedAt` when reading) with `plan-change` / `apply-change`; a newer one answers 409 `entity_builder.schema.stale.definition`. Every run re-reads the definitions inside its own transaction (under the entity lock when applying) and only trusts those; a field renamed or deprecated meanwhile blocks with `...blocker.definition.changed`.
|
|
45
43
|
- `LONG_TEXT`, `JSON` and `MULTI_SELECT` fields cannot be unique or indexed (`entity_builder.schema.not.indexable`). Index and unique names are `idx_` / `uq_` + table + column cut to the driver limit (63 PostgreSQL, 64 MySQL) + a hash of table and column, so they never collide.
|
|
46
44
|
- Dropping an entity is blocked while relation fields of other entities point at it (`...blocker.drop.relation.in.use`, listing `"entity.field"`).
|
|
@@ -49,9 +47,9 @@ Works on PostgreSQL and MySQL (via `SchemaDialectAdapterService`). Import `Entit
|
|
|
49
47
|
- Changes made outside the entity builder (a column dropped by hand) show up in `inspect` and are fixed by `repair`; extra columns are reported and never dropped automatically.
|
|
50
48
|
- The entity/field metadata `update` endpoints are gone on purpose: every edit goes through `apply-change` so the table, the metadata and the schema cache cannot disagree.
|
|
51
49
|
- Identifiers must match `^[a-z][a-z0-9_]{2,59}$`, not be an SQL reserved word, and the table (`eb_<code>`) must not already exist. Every DDL change takes a per-entity lock (`pg_advisory_xact_lock` / `GET_LOCK`, names over 64 characters hashed) - plus the lock of any relation target it points at - and is written to `eb_schema_change_log`.
|
|
52
|
-
- Every filter/sort key of a flow's **Find records** step and of a `lookup` is checked against the entity's real fields; unknown keys are rejected (never interpolated into SQL). A filter value is a plain value (`=`) or `{ op, value? }` with `op` one of `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in` (a list of at most 100 scalars; an empty list matches nothing), `is_null`, `not_null`. Operators map to a fixed SQL table and every value is a bound parameter.
|
|
50
|
+
- Every filter/sort key of a flow's **Find records** step and of a `lookup` is checked against the entity's real fields; unknown keys are rejected (never interpolated into SQL). A filter value is a plain value (`=`) or `{ op, value? }` with `op` one of `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in` (a list of at most 100 scalars; an empty list matches nothing), `is_null`, `not_null`. Operators map to a fixed SQL table and every value is a bound parameter.
|
|
53
51
|
- **Search** (a Find records step's search text) matches every searchable field by its text form, case-insensitively on both drivers (`CAST ... AS TEXT ILIKE` / `CAST ... AS CHAR LIKE`), so non-text fields can be searchable; `%` and `_` in the search text are literal.
|
|
54
|
-
- Each entity gets the IAM actions `entity_builder.entity.<code>.<create|read|update|delete>` (only when IAM is installed); flows running as the caller check them on every entity step. DECIMAL fields are returned as numbers. `RELATION`/`FILE` are plain uuid columns (`char(36)` on MySQL, which has no uuid type) with no foreign-key constraint.
|
|
52
|
+
- Each entity gets the IAM actions `entity_builder.entity.<code>.<create|read|update|delete>` (only when IAM is installed); flows running as the caller check them on every entity step. DECIMAL fields are returned as numbers. `RELATION`/`FILE` are plain uuid columns (`char(36)` on MySQL, which has no uuid type) with no foreign-key constraint.
|
|
55
53
|
- **Tenants never share schemas**: every tenant DataSource gets its own copy of the entities array (`getEntityBuilderEntities()` returns a copy), and runtime schemas are registered per DataSource.
|
|
56
54
|
- `forRootAsync` takes `useFactory`, `useClass`, `useExisting` or a static `config`; with none of them it throws at configuration time. The root entry also exports the flow types, errors and `validateFlow`, and the schema helpers (`schema-columns`, `identifier-rules`, `field-type-conversion`).
|
|
57
55
|
|
|
@@ -71,34 +69,30 @@ Conditions and expressions are declarative JSON (`RuleGroup` / `RuleExpression`)
|
|
|
71
69
|
|
|
72
70
|
## Flows (virtual APIs)
|
|
73
71
|
|
|
74
|
-
A flow is an endpoint (`POST /api-flows/<slug>`) whose behaviour is a graph of steps, so a backend can be built without writing backend code. Steps: **Request** (trigger), **Validate** (checks that must pass), **Check permission**, **
|
|
72
|
+
A flow is an endpoint (`POST /api-flows/<slug>`) whose behaviour is a graph of steps, so a backend can be built without writing backend code. Steps: **Request** (trigger), **Validate** (checks that must pass), **Check permission**, **Code** (sandboxed JavaScript), **Set variables**, **If**, **Switch**, **For each**, **Create / Update / Delete / Get / Find record**, **Save progress**, **Call flow**, **HTTP request**, **Publish event**, **Respond**. Each step's result is `context.<stepId>`; the request body is `input.*`; also `vars.*`, `loop.item` / `loop.index` (inside a loop within a loop the outer loop is `loop.parent.item` / `loop.parent.index`, and so on up), `request.*`, `user.*`. `request` holds `ip`, `method`, `path`, `query` (text values, first of a repeated one, at most 50), `headers` (lower-case names, credential headers removed; read dashed names with `HEADER()`), and ready-made `userAgent`, `origin`, `referer`, `contentType`, `language` (first `accept-language` tag) and `receivedAt`. Text can use `{{ scope.path }}` placeholders (the `template` expression). Steps connect by output ports (`out`, `true`/`false`, `each`/`done`, switch cases, `error`); a step marked "carry on if it fails" stores `context.<id>.error` and follows its `error` port. Inside a transaction (transactional flows and every dry run) such a step runs in a savepoint, so its failure does not abort the flow's transaction.
|
|
75
73
|
|
|
76
74
|
- **Loops hand data on** by `collect` (evaluated after each pass, reading `loop.*` and the body steps; the values become `context.<loop>.results` next to `count` - collect a Set/Code step to hand on several values), by variables (`vars` is flow-wide and survives passes; initialize before the loop), and by body step results (after the loop they hold the last pass).
|
|
77
75
|
- **Reference timing** (validation warnings): a step's result exists for the steps on a path after it; once a loop is done (`done` branch) its whole body's results exist too (their last pass). The validator warns when a step reads `context.<id>` of a step that has not run yet when it runs (later on the path, on another branch, or later in the same loop body), `loop.*` outside a loop's `each` branch (a foreach's own `collect` counts as inside), `loop.parent` beyond the loops around the step, or `context.<loop>.results` inside that loop (only `count` exists until it is done). Unreachable steps are not checked (they already warn).
|
|
78
76
|
|
|
79
77
|
- **Validate**: `{ checks: [{ id, rule, message, field? }], mode: 'all' | 'first', statusCode? }`. `all` (default) evaluates every check and rejects with every failure; `first` stops at the first. The answer is `statusCode` (default 400, or 403 when every failed check is a permission check) with `errors: [{ field, message }]` (`field` defaults to the check id); one failure uses its message as the top-level message, several use `flow.checks.failed`. Example (attendance check-out): check 1 `left` = lookup `attendance` with `employee_id = input.employeeId`, `out_time is_null`, `in_time gte START_OF_DAY(NOW())`, mode `exists`, `is_true`; check 2 `left` = `DATE_DIFF(NOW(), <same lookup, mode first, field in_time>, 'hour')` `greater_or_equal` 5.
|
|
80
|
-
- **Check permission** (`permission_check`): `{ codes, match: 'any' | 'all', onDenied: 'reject' | 'branch', statusCode?, message? }`. `reject` (default) answers `statusCode` (403) with `message` (or `flow.permission.denied`) and stops, otherwise follows `out`; `branch` follows `allowed` / `denied` and never rejects.
|
|
81
|
-
- **Company & Branch check** (`company_branch_check`, company feature): `{ companyIds?, branchIds?, onDenied?, statusCode?, message? }` - what the signed-in user may work in. It needs no permission codes: it gets exactly what company select offers the user - the active companies granted to them and, inside those, the active branches granted to them (a grant on a branch does not reach the branches below it). Output: `{ userId, companies, branches }` - `companies` as `{ id, name }` (by name), `branches` as `{ id, companyId, name }` (per company, by serial). `companyIds` / `branchIds` (expressions: one id or a list, typically the record being changed) turn it into a guard: each id must be within reach (`allowed`, `missing: { companyIds, branchIds }`); a target that resolves to nothing is a denial. Then `onDenied` rejects (default, `statusCode` 403) or follows `allowed` / `denied` (which needs a target). No user means nothing is reachable. The data comes from `COMPANY_ACCESS_RESOLVER` (nestjs-shared), provided by nestjs-auth when the company feature is on (the grants `UserPermissionService` lists, kept to active companies and branches, looked up once per run); without it nothing is reachable. The designer offers the step only with the company feature.
|
|
78
|
+
- **Check permission** (`permission_check`): `{ codes, match: 'any' | 'all', branch?, onDenied: 'reject' | 'branch', statusCode?, message? }`. `reject` (default) answers `statusCode` (403) with `message` (or `flow.permission.denied`) and stops, otherwise follows `out`; `branch` follows `allowed` / `denied` and never rejects. `branch` left out = the caller's current branch, `null` = company-wide grants, an expression = that branch id. Output `{ allowed, missing }`. A caller who is not signed in is denied (and the validator warns in `public` / `api_key` flows).
|
|
82
79
|
- **Code**: `{ code, timeoutMs? }` runs `code` as the body of a synchronous function in a QuickJS WebAssembly sandbox (`quickjs-emscripten`) on a worker thread (a small pool, at most 4), so a busy code step never blocks the server's event loop; the host also kills a worker that overruns its deadline. It reads a deep-frozen JSON copy of `{ input, vars, context, loop, user: { id, email, name, companyId, branchId, permissions? } }` as the global `ctx` (no request headers; `permissions`, the caller's codes in the current branch, is fetched only when the code mentions permissions). `hasPermission(code)`, `hasAnyPermission(...codes)` and `hasAllPermissions(...codes)` are plain JavaScript inside the sandbox over `ctx.user.permissions` with the server's wildcards and `return`s a JSON value, which becomes `context.<id>`. Nothing of the host is reachable (no `require`, `process`, network, filesystem, timers or database); `console.log` is captured into the trace (`trace[].logs`, 50 lines of 500 characters, sensitive-looking keys masked). Every run gets a fresh runtime with a 32 MB memory limit, a 512 KB stack and an interrupt deadline of `timeoutMs` (default 1000, 10-5000) clamped to the time left in the flow; the output may be at most 1 MB. A throw, time-out or memory overflow is a node error (`flow.error.code.*`), so `onError: continue` and the `error` port work. Code nodes also run in dry runs. Saving a flow that contains a code node needs `entity_builder.flow_definition.code`.
|
|
83
80
|
- **Create / Update / Delete record** write several records in one step through `target`: `one` (default, left out) writes one record; `many` writes one per item of `items` (up to `FLOW_LIMITS.MAX_WRITE_ITEMS` = 1000), and the step's per-record values (`fields`, `id`) read the item as `loop.item` / `loop.index` with a loop around the step as `loop.parent`; `filter` (update / delete) writes every record matching `filter` (same shape as Find), read as `loop.item` (e.g. `stock = loop.item.stock - 1`). A filter that resolves to nothing is refused (`flow.error.write.filter.empty`) instead of changing every record, and more matches than `limit` (1-1000, default 100) fail the step (`flow.error.too.many.matches`) instead of changing some. In `many` mode `id` defaults to the item's own id (the item itself when it is text, else `item.id`). `onNotFound`: `error` (default), `skip`, or for update `insert` (upsert: a missing record, or in `many` mode an item with an empty id, is created with the same field values - needs the entity's `create` permission too). Create takes `children` (up to 10): `{ as, entityCode, parentField, items, fields }` saves child records under every saved record, `parentField` filled with the parent's id; `items` is read in the parent's scope (`loop.item.lines` in `many` mode, `input.lines` otherwise) and child `fields` read the child item as `loop.item` and the parent's scope as `loop.parent`. Results: create one = the record plus one array per `as`; create many = `{ items, count }`; update one = the record (`null` when skipped); update many / filter = `{ items, count, updated, created, skipped }`; delete one = `{ id, deleted }`; delete many / filter = `{ ids, count, skipped }` (an id listed twice is deleted once). A create never takes an `id` field (`generic_record.system.column.readonly`), so it cannot overwrite an existing record; an upsert's created record ignores a mapped `id`. A step that writes several records (`many`, `filter`, or any `children`) is **all-or-nothing on its own**: inside a transactional run it uses the run's transaction, otherwise it opens a transaction for just that step and publishes its record events after that commit. Every record written counts against `FLOW_LIMITS.MAX_WRITES_PER_RUN` (5000, shared with called flows).
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **Who can call it**: set on each Request step - there is no flow-level access. `config.authMode`: `jwt` (any signed-in user; the default, stored as no setting), `permission` (`config.permissionCode`, or `entity_builder.flow.<slug>.execute` when empty; provisioned as an IAM action on publish), `api_key` (the step's own key, sent as `x-api-key`: the designer makes it (`fk_<8 hex>_<32 hex>`) and sends it in `config.apiKey` once; every save replaces it with `{ prefix, hash }` (SHA-256, `sealApiKeys()`), so the key itself is never stored and cannot be shown again. The guard compares in constant time against the published step's hash; a wrong or missing key answers 401 `flow.api.key.invalid`, and one step's key never opens another. A step without a key cannot be saved (`trigger.api.key.required`, or `.invalid` for a malformed one); a new key replaces the old one once the flow is published; a step that leaves API-key access drops its key), `public`, or `internal` - **other flows only**: the step has no URL (it answers 404), only other flows' Call flow steps start there, and that run takes the calling flow's user and run-as. A flow whose every Request step is `internal` is a **function** (`isFunctionFlow()`). A URL reached without signing in (`public`, `api_key`) runs with no `user`: permission and branch checks deny, and a flow running as the caller cannot touch entities there (validator warning `keyless.endpoint.as.caller`). Unknown, inactive and never-published flows all answer 404. Each Request step's per-caller-IP rate limit (`config.rateLimitPerMinute`, default 60, 0 = unlimited; counted per step) runs **before** credentials are checked (in memory per server instance, fixed one-minute windows, at most 10,000 tracked callers - the oldest window is dropped beyond that).
|
|
81
|
+
- **Save progress** (`commit`, no settings, port `out`): commits everything the run has written so far, publishes the events held back until then, and starts a new transaction for the rest (the statement timeout is set again). A later failure rolls back only what came after it; the run's result then carries `savedUpTo: { nodeId, name, at }` (the last one that committed) and a failed answer's body `savedUpTo: '<step name>'`, so a caller knows the first part is already saved (make a retry check for it). It commits only in the transaction the run opened itself - it is skipped (trace `skipped`, `reasonKey` `flow.skip.commit.*`) in a test run (nothing is ever saved there), in a flow without the transaction setting (every step already saves on its own) and in a flow called inside its caller's transaction (only the caller may commit that one; a called flow that opened its own transaction commits normally). It cannot be set to carry on if it fails (a save error). Save-time warnings: a Save progress step in a flow without the transaction setting, inside a loop (it saves once per item - fine for batch imports) or in a flow other flows call; and, for any flow, two or more write steps without the transaction setting.
|
|
82
|
+
- **Call flow** (`call_flow`): `{ flowSlug, input }` runs another active flow as part of this run and puts its answer in `context.<id>`: its `respond` body, or the output of its last step. The called flow gets `input` checked against its own input schema (a mismatch rejects with 400), the same caller, request and test mode, and shares the run's step, HTTP-call and time budget. It joins the caller's transaction when there is one (so a dry run rolls back its writes too, and its events wait for the caller's commit; a joined call that fails or answers 4xx/5xx drops the events it queued, so a caller that continues past it with `onError: continue` never announces writes the savepoint rolled back); otherwise a transactional called flow commits on its own. A called flow that answers 4xx/5xx, or rejects (validate, permission), passes that answer on as the caller's; any other failure fails the step with `flow.error.called.flow.failed[.at.node]`, so `onError: continue` and the `error` port work. Access: any flow may call a function (`internal`), which then runs as its caller's run does (`runAs` inherited, its own ignored); a flow running as the system may call any flow; one running as the caller may call only what that caller could call directly - `public` and `jwt` flows, and `permission` flows when the caller holds the permission, never an `api_key` flow. A chain that comes back to a running flow and nesting deeper than 5 levels stop the run. On save the step must name an existing flow the author can see (not the flow itself), and what the runtime would always refuse is refused then too: an `api_key` flow from a flow running as the caller (only a warning from a function, which runs as whoever calls it), a required input (without a default) left unmapped, a chain of calls that comes back to this flow, and a chain nested deeper than `FLOW_LIMITS.MAX_CALL_DEPTH`. Calling an inactive flow, and mapping an input the called flow does not declare (it is dropped), are warnings; a flow another flow calls cannot be deleted or have its URL name changed until those callers are changed.
|
|
83
|
+
- **Several URLs in one flow**: a flow can hold more than one **Request** step. The one without a `path` answers on `POST /api-flows/<slug>` and takes the flow's `bodyType` / `inputSchema`; each other one sets `config.path` (1-63 lowercase letters, digits and dashes, starting with a letter; `ITriggerConfig`) and answers on `POST /api-flows/<slug>/<path>` with its own `config.bodyType` / `config.inputSchema` - e.g. a `customer-ticker` flow with `insert` (an object) and `insert-many` (a list) sharing the steps after them. Saving needs at least one Request step (`trigger.missing`), at most one without a path (`trigger.main.duplicate`) and distinct, valid paths (`trigger.path.duplicate` / `.invalid`); a path's fields are checked like the flow's, with errors named `<path>: <field>`. A flow whose every Request step has a path answers 404 on its own URL. Auth mode, permission, rate limit, run-as, transaction and timeout stay per flow. `flowEndpoints()` / `findEndpoint()` (`flow.types.ts`) turn the Request steps into `IFlowEndpoint`s; the run starts at `IFlowRunState.startAt`. The test run takes `endpoint` (a path; left out, the flow's own URL - an unknown one is refused with `flow.endpoint.not.found`), and a Call flow step takes `config.endpoint` to start the called flow at one of its paths, checked on save against that path's body (`node.flow.endpoint.unknown`, or `.required` when the called flow has no own URL) and at run time (`error.called.flow.endpoint.not.found`).
|
|
84
|
+
- **Body type** (`bodyType`, column `body_type`, default `object`): `object` - the body is one object and the input fields are its keys (`input.<name>`); `list` - the body itself is a list, like `insert-many` (`POST api-flows/<slug>` with `[{...}, {...}]`), the input fields describe each item, every item is checked the same way (errors name the index: `[0].sku`; a body that is not a list is refused with `flow.input.body.not.list`), and the flow reads the list as `input` (a For each over `input`, or `input.0.<name>`). With no fields declared a list body is passed through as-is. A Call flow step sends a list-bodied flow one expression, `inputList`, instead of named `input`s; saving checks the step matches the called flow's body type (`node.flow.input.list.required` / `.unexpected`). The test run accepts a list `input` too.
|
|
85
|
+
- **Input**: the flow declares the request fields (type, required, default, min/max, choices). The body is checked and converted with the same validator entity records use; undeclared keys are dropped; every problem is returned at once (400). An `object` field may declare its own `fields`; an `array` field may declare an `itemType` (any type but `array`) that every item must have - min/max/length/options then apply to each item - and a list of objects (`itemType: 'object'`) declares the `fields` of each item. Nested values are checked the same way (undeclared keys inside them are dropped too), errors name their path (`address.city`, `items[0].qty`), and nesting goes at most `FLOW_LIMITS.MAX_INPUT_DEPTH` (4) levels. Without `fields` / `itemType` the value is only checked to be an object / a list. Generated entity flows declare `ids` as a list of ids; their bulk flows use a `list` body whose items are the entity fields.
|
|
86
|
+
- **Who can call it**: `public`, `api_key` (`x-api-key`, only a SHA-256 hash stored, constant-time compare; a key past its `expiresAt` answers 401 `flow.api.key.expired`; keys are only checked while the auth mode is `api_key`, so they stop working when it changes), `jwt` (any signed-in user), `permission` (`entity_builder.flow.<slug>.execute` by default, provisioned as an IAM action) or `internal` - a **function**: no endpoint of its own, run only by other flows' Call flow steps, for logic several flows repeat. Unknown, inactive and `internal` flows all answer 404 on `POST /api-flows/<slug>`. A per-caller-IP rate limit runs **before** credentials are checked (in memory per server instance, fixed one-minute windows, at most 10,000 tracked callers - the oldest window is dropped beyond that).
|
|
91
87
|
- **Find record** filters accept the `{ op, value }` operators (the value side is an expression). A filter entry is an operator only when it is authored as exactly `{ op, value? }` with a known `op` (any other key, or an unknown `op`, and it is not a condition - so an expression such as `{ type: 'arithmetic', op: 'add', ... }` is a plain value). A value resolved at run time (from input, a variable, a step result) is only ever an operand: an object or list is compared for equality and refused as not one value, never read as an operator, so `{ "op": "not_null" }` sent as input cannot widen a Find, lookup, update or delete. On a Find an entry that resolves to nothing is left out; on an update / delete by filter it fails the step (`flow.error.filter.value.missing`) instead of widening the write.
|
|
92
88
|
- **Checks cannot be carried past**: a failed Validate or Check permission step (like Save progress) always ends the run, whatever its `onError` says.
|
|
93
|
-
- **Runs as
|
|
94
|
-
- **Transactions
|
|
95
|
-
- **Test runs** need what running the flow for real would, for a saved flow as much as a draft and in both modes: `flow_definition.system` for run-as system, `flow_definition.code` for a code node. A `live` test run also needs what makes it real: for a draft the permission to save it (`flow_definition.create`, or `update` when it has a `flowId`), for a `permission`
|
|
96
|
-
- **Test runs**: `dry_run` (default) executes inside a transaction that is always rolled back and skips HTTP
|
|
97
|
-
- **Safety limits**: at most 100 nodes, 5000 steps per run (room for a full 1000-item loop with a few steps per item), 1000 items per loop, 10 HTTP calls,
|
|
98
|
-
- **Forwarding the caller's token**: an HTTP step with `forwardAuth: true` sends the signed-in caller's own `Authorization` header (unless its `headers` set one), so the next API sees the same user. The token lives only in the run state (`IFlowRunState.authorization`, passed on to called flows) - never in `request.headers`, expressions, code steps, traces or logs - and exists only when the caller was verified (signed-in URLs; a test run forwards the tester's own). The validator warns on every such step, since the token goes to that URL.
|
|
89
|
+
- **Runs as**: `caller` (every entity step and lookup re-checks `dynamicEntityPermission(entity, action)` = `entity_builder.entity.<entity>.<action>` for the caller) or `system` (no checks - so saving such a flow needs `entity_builder.flow_definition.system`, and a public system flow is flagged in the warnings). It fails closed: any other value is refused on save and at run time, and a system run touches entities only when it was granted the system (a saved flow, or a test run whose tester holds `flow_definition.system`) - otherwise every entity step and lookup answers 403 `flow.error.run.as.not.allowed`.
|
|
90
|
+
- **Transactions**: a transactional flow's entity writes commit or roll back together; entity events and **Publish event** steps are published only after the commit, in order, and never for a run that rolled back - nor for the rows of a step whose savepoint (`onError: continue`) rolled back. Metadata a transactional run needs (entity and field definitions, called flows) is read on the run's own transaction, so a run never waits on a second pooled connection. HTTP calls cannot be rolled back, so a transactional flow with an HTTP step is flagged in the warnings. In a non-transactional flow each step commits on its own, except that a multi-record write step is always all-or-nothing (see above); make the flow transactional when several steps must succeed or fail together, and add **Save progress** steps where the work so far must be kept even if a later step fails.
|
|
91
|
+
- **Test runs** need what running the flow for real would, for a saved flow as much as a draft and in both modes: `flow_definition.system` for run-as system, `flow_definition.code` for a code node. A `live` test run also needs what makes it real: for a draft the permission to save it (`flow_definition.create`, or `update` when it has a `flowId`), for a `permission` flow its own execute permission. The draft is validated like a saved flow (DTO: run-as, auth mode, time-out, rate limit and retention bounds; then `validateFlow`); the server-owned fields of a loaded flow (`id`, `version`, timestamps) are ignored. Test runs of unsaved drafts are logged without a flow id and pruned together, keeping the newest 200.
|
|
92
|
+
- **Test runs**: `dry_run` (default) executes inside a transaction that is always rolled back and skips HTTP and events; `live` does everything. Test runs return the real error text; live callers get a safe summary. `request: { headers?, query?, ip? }` simulates what a caller would send, on top of the designer's own request, so header / query / IP rules can be tried (credential headers are still removed).
|
|
93
|
+
- **Safety limits**: at most 100 nodes, 5000 steps per run (room for a full 1000-item loop with a few steps per item, and for the scaffolded 200-item bulk flows), 1000 items per loop, 10 HTTP calls, 120 s total; loops cannot nest deeper than 3; a flow cannot loop back on itself (use For each). Steps are data walked by a fixed engine; the only author-written code that runs is a code node's, inside the QuickJS sandbox described above.
|
|
99
94
|
- **HTTP steps** never follow redirects, cap the response at 1 MB, and refuse loopback / private / link-local (cloud metadata) addresses **at connect time** (DNS-rebinding safe), including every IPv6 form that embeds or tunnels to one (IPv4-mapped `::ffff:`, IPv4-translated `::ffff:0:`, IPv4-compatible, NAT64 `64:ff9b::/96` and all of the local-use `64:ff9b:1::/48`, 6to4 `2002:`, and all of Teredo `2001::/32`). The timeout is one deadline for the whole request, response body included. For local development only, set `flows.allowPrivateHttp` in the module config or `ENTITY_BUILDER_FLOW_ALLOW_PRIVATE_HTTP=true`.
|
|
100
|
-
- **Runs are logged** (`eb_flow_execution`) with the redacted input, a trace of every step
|
|
101
|
-
- **Versions**: the flow's columns are the published flow - what callers run. A new flow is created unpublished (`version` 0: callers get 404, Call flow steps refuse it, the validator treats it as inactive) and is edited in place until its first **publish** - so saving it is checked like a publish against other flows calling a URL name or path it drops. After that, saving (`update`) keeps the edits in `draft` (dropped again when they match what is live) and callers keep running the published columns. `publish` validates the draft like a save, plus what only going live can break (other flows calling a URL name or path it drops), copies it over the columns as `version + 1`, clears the draft and records the version in `eb_flow_version` (`FlowVersion`: the full snapshot, `note`, `publishedAt`, `publishedById`); a version that went live before versions were kept is recorded first, with `publishedAt` null. `restore-version` copies any version into the draft (it needs the permissions its run-as and code steps call for, but is not validated, since the entities it names may have changed); `discard-draft` drops the draft. Everything but the flow's id is versioned (`FLOW_VERSIONED_FIELDS`), `isActive` included. Entity schema changes rewrite field references in the published flow, its draft, its kept versions and in deleted flows (so a restored version or flow keeps working), without a new version.
|
|
95
|
+
- **Runs are logged** (`eb_flow_execution`) with the redacted input, a trace of every step and the output; the newest N (default 200) are kept.
|
|
102
96
|
- **Schema changes are flow-aware**: renaming an entity field rewrites the flows that use it; dropping an entity or field lists (and for a drop, blocks on) the flows that use it. Tracked: the entity step's `fields` / `filter` / `sort`, a create step's `children[]` (`entityCode`, `fields` keys, `parentField`), lookups, and output paths holding records - `context.<id>.<field>` (get / create / update one), `context.<id>.items.<n>.<field>` (Find, many / filter writes), `context.<id>[.items.<n>].<as>.<n>.<field>` (saved children). References reached through a loop over query results (`loop.item.<field>`) are not tracked.
|
|
103
97
|
- Secrets typed into an HTTP header are stored in the flow definition (there is no credential vault yet); flows are readable only with `flow_definition.read`.
|
|
104
98
|
|
package/config/message-keys.d.ts
CHANGED
|
@@ -256,9 +256,15 @@ export declare const FLOW_MESSAGES: {
|
|
|
256
256
|
readonly SLUG_TAKEN: "entity_builder.flow.slug.taken";
|
|
257
257
|
readonly RATE_LIMITED: "entity_builder.flow.rate.limited";
|
|
258
258
|
readonly API_KEY_INVALID: "entity_builder.flow.api.key.invalid";
|
|
259
|
+
readonly API_KEY_NOT_FOUND: "entity_builder.flow.api.key.not.found";
|
|
260
|
+
readonly API_KEY_CREATE_SUCCESS: "entity_builder.flow.api.key.create.success";
|
|
261
|
+
readonly API_KEY_LIST_SUCCESS: "entity_builder.flow.api.key.list.success";
|
|
262
|
+
readonly API_KEY_REVOKE_SUCCESS: "entity_builder.flow.api.key.revoke.success";
|
|
263
|
+
readonly API_KEY_DELETE_SUCCESS: "entity_builder.flow.api.key.delete.success";
|
|
264
|
+
readonly API_KEY_EXPIRED: "entity_builder.flow.api.key.expired";
|
|
265
|
+
readonly API_KEY_EXPIRY_IN_PAST: "entity_builder.flow.api.key.expiry.in.past";
|
|
259
266
|
readonly CALLED_BY_OTHER_FLOWS: "entity_builder.flow.called.by.other.flows";
|
|
260
267
|
readonly ENDPOINT_CALLED_BY_OTHER_FLOWS: "entity_builder.flow.endpoint.called.by.other.flows";
|
|
261
|
-
readonly CALLERS_BROKEN: "entity_builder.flow.callers.broken";
|
|
262
268
|
readonly EXECUTION_NOT_FOUND: "entity_builder.flow.execution.not.found";
|
|
263
269
|
readonly EXECUTIONS_SUCCESS: "entity_builder.flow.executions.success";
|
|
264
270
|
readonly EXECUTION_SUCCESS: "entity_builder.flow.execution.success";
|
|
@@ -276,14 +282,6 @@ export declare const FLOW_MESSAGES: {
|
|
|
276
282
|
readonly INPUT_BODY_ITEM_NOT_OBJECT: "entity_builder.flow.input.body.item.not.object";
|
|
277
283
|
readonly CHECKS_FAILED: "entity_builder.flow.checks.failed";
|
|
278
284
|
readonly PERMISSION_DENIED: "entity_builder.flow.permission.denied";
|
|
279
|
-
readonly PUBLISH_SUCCESS: "entity_builder.flow.publish.success";
|
|
280
|
-
readonly NOTHING_TO_PUBLISH: "entity_builder.flow.nothing.to.publish";
|
|
281
|
-
readonly VERSIONS_SUCCESS: "entity_builder.flow.versions.success";
|
|
282
|
-
readonly VERSION_SUCCESS: "entity_builder.flow.version.success";
|
|
283
|
-
readonly VERSION_NOT_FOUND: "entity_builder.flow.version.not.found";
|
|
284
|
-
readonly VERSION_RESTORED: "entity_builder.flow.version.restored";
|
|
285
|
-
readonly DRAFT_DISCARDED: "entity_builder.flow.draft.discarded";
|
|
286
|
-
readonly NO_DRAFT: "entity_builder.flow.no.draft";
|
|
287
285
|
};
|
|
288
286
|
export declare const FLOW_ERROR_MESSAGES: {
|
|
289
287
|
readonly NO_TRIGGER: "entity_builder.flow.error.no.trigger";
|
|
@@ -321,35 +319,19 @@ export declare const FLOW_ERROR_MESSAGES: {
|
|
|
321
319
|
readonly CALLED_FLOW_FAILED_AT_NODE: "entity_builder.flow.error.called.flow.failed.at.node";
|
|
322
320
|
readonly RUN_AS_NOT_ALLOWED: "entity_builder.flow.error.run.as.not.allowed";
|
|
323
321
|
readonly FILTER_VALUE_MISSING: "entity_builder.flow.error.filter.value.missing";
|
|
324
|
-
readonly TOO_MANY_MESSAGES: "entity_builder.flow.error.too.many.messages";
|
|
325
|
-
readonly TOO_MANY_RECIPIENTS: "entity_builder.flow.error.too.many.recipients";
|
|
326
|
-
readonly NOTIFICATION_UNAVAILABLE: "entity_builder.flow.error.notification.unavailable";
|
|
327
|
-
readonly NOTIFICATION_RECIPIENT_INVALID: "entity_builder.flow.error.notification.recipient.invalid";
|
|
328
|
-
readonly NOTIFICATION_COMPANY_INVALID: "entity_builder.flow.error.notification.company.invalid";
|
|
329
|
-
readonly NOTIFICATION_BRANCH_INVALID: "entity_builder.flow.error.notification.branch.invalid";
|
|
330
|
-
readonly NOTIFICATION_COMPANY_UNAVAILABLE: "entity_builder.flow.error.notification.company.unavailable";
|
|
331
|
-
readonly NOTIFICATION_COMPANY_DENIED: "entity_builder.flow.error.notification.company.denied";
|
|
332
|
-
readonly NOTIFICATION_TITLE_EMPTY: "entity_builder.flow.error.notification.title.empty";
|
|
333
|
-
readonly NOTIFICATION_FAILED: "entity_builder.flow.error.notification.failed";
|
|
334
|
-
readonly EMAIL_UNAVAILABLE: "entity_builder.flow.error.email.unavailable";
|
|
335
|
-
readonly EMAIL_ADDRESS_INVALID: "entity_builder.flow.error.email.address.invalid";
|
|
336
|
-
readonly EMAIL_SUBJECT_EMPTY: "entity_builder.flow.error.email.subject.empty";
|
|
337
|
-
readonly EMAIL_BODY_EMPTY: "entity_builder.flow.error.email.body.empty";
|
|
338
|
-
readonly EMAIL_FAILED: "entity_builder.flow.error.email.failed";
|
|
339
322
|
};
|
|
340
323
|
export declare const FLOW_SKIP_MESSAGES: {
|
|
341
324
|
readonly HTTP: "entity_builder.flow.skip.http";
|
|
342
325
|
readonly EVENTS: "entity_builder.flow.skip.events";
|
|
343
|
-
readonly NOTIFICATION: "entity_builder.flow.skip.notification";
|
|
344
|
-
readonly EMAIL: "entity_builder.flow.skip.email";
|
|
345
|
-
readonly NO_RECIPIENTS: "entity_builder.flow.skip.no.recipients";
|
|
346
326
|
readonly COMMIT_DRY_RUN: "entity_builder.flow.skip.commit.dry.run";
|
|
347
327
|
readonly COMMIT_NO_TRANSACTION: "entity_builder.flow.skip.commit.no.transaction";
|
|
348
328
|
readonly COMMIT_JOINED: "entity_builder.flow.skip.commit.joined";
|
|
349
329
|
};
|
|
350
330
|
export declare const FLOW_VALIDATION_MESSAGES: {
|
|
351
331
|
readonly SLUG_INVALID: "entity_builder.flow.validation.slug.invalid";
|
|
332
|
+
readonly RUN_AS_CALLER_NEEDS_AUTH: "entity_builder.flow.validation.run.as.caller.needs.auth";
|
|
352
333
|
readonly RUN_AS_INVALID: "entity_builder.flow.validation.run.as.invalid";
|
|
334
|
+
readonly AUTH_MODE_INVALID: "entity_builder.flow.validation.auth.mode.invalid";
|
|
353
335
|
readonly NO_NODES: "entity_builder.flow.validation.no.nodes";
|
|
354
336
|
readonly TOO_MANY_NODES: "entity_builder.flow.validation.too.many.nodes";
|
|
355
337
|
readonly TOO_MANY_EDGES: "entity_builder.flow.validation.too.many.edges";
|
|
@@ -376,15 +358,6 @@ export declare const FLOW_VALIDATION_MESSAGES: {
|
|
|
376
358
|
readonly COMMIT_IN_CALLED_FLOW: "entity_builder.flow.validation.commit.in.called.flow";
|
|
377
359
|
readonly PUBLIC_SYSTEM_ENTITY: "entity_builder.flow.validation.public.system.entity";
|
|
378
360
|
readonly PERMISSION_WITHOUT_USER: "entity_builder.flow.validation.permission.without.user";
|
|
379
|
-
readonly KEYLESS_ENDPOINT_AS_CALLER: "entity_builder.flow.validation.keyless.endpoint.as.caller";
|
|
380
|
-
readonly TRIGGER_AUTH_INVALID: "entity_builder.flow.validation.trigger.auth.invalid";
|
|
381
|
-
readonly TRIGGER_TRANSACTION_INVALID: "entity_builder.flow.validation.trigger.transaction.invalid";
|
|
382
|
-
readonly TRIGGER_TIMEOUT_RANGE: "entity_builder.flow.validation.trigger.timeout.range";
|
|
383
|
-
readonly TRIGGER_RATE_LIMIT_RANGE: "entity_builder.flow.validation.trigger.rate.limit.range";
|
|
384
|
-
readonly TRIGGER_API_KEY_REQUIRED: "entity_builder.flow.validation.trigger.api.key.required";
|
|
385
|
-
readonly TRIGGER_API_KEY_INVALID: "entity_builder.flow.validation.trigger.api.key.invalid";
|
|
386
|
-
readonly NODE_FORWARDS_TOKEN: "entity_builder.flow.validation.node.forwards.token";
|
|
387
|
-
readonly NODE_ACCESS_TARGET_REQUIRED: "entity_builder.flow.validation.node.access.target.required";
|
|
388
361
|
readonly INPUT_TOO_MANY: "entity_builder.flow.validation.input.too.many";
|
|
389
362
|
readonly INPUT_NAME_INVALID: "entity_builder.flow.validation.input.name.invalid";
|
|
390
363
|
readonly INPUT_NAME_DUPLICATE: "entity_builder.flow.validation.input.name.duplicate";
|
|
@@ -406,7 +379,6 @@ export declare const FLOW_VALIDATION_MESSAGES: {
|
|
|
406
379
|
readonly NODE_CASE_DUPLICATE: "entity_builder.flow.validation.node.case.duplicate";
|
|
407
380
|
readonly NODE_MAX_ITEMS_RANGE: "entity_builder.flow.validation.node.max.items.range";
|
|
408
381
|
readonly NODE_LIMIT_RANGE: "entity_builder.flow.validation.node.limit.range";
|
|
409
|
-
readonly NODE_SORT_INVALID: "entity_builder.flow.validation.node.sort.invalid";
|
|
410
382
|
readonly NODE_WRITE_TARGET_INVALID: "entity_builder.flow.validation.node.write.target.invalid";
|
|
411
383
|
readonly NODE_NOT_FOUND_INVALID: "entity_builder.flow.validation.node.not.found.invalid";
|
|
412
384
|
readonly NODE_FILTER_REQUIRED: "entity_builder.flow.validation.node.filter.required";
|
|
@@ -431,16 +403,6 @@ export declare const FLOW_VALIDATION_MESSAGES: {
|
|
|
431
403
|
readonly NODE_CHOOSE_METHOD: "entity_builder.flow.validation.node.choose.method";
|
|
432
404
|
readonly NODE_TIMEOUT_RANGE: "entity_builder.flow.validation.node.timeout.range";
|
|
433
405
|
readonly NODE_EVENT_NAME_INVALID: "entity_builder.flow.validation.node.event.name.invalid";
|
|
434
|
-
readonly NODE_NOTIFICATION_UNAVAILABLE: "entity_builder.flow.validation.node.notification.unavailable";
|
|
435
|
-
readonly NODE_NOTIFICATION_OPTION_INVALID: "entity_builder.flow.validation.node.notification.option.invalid";
|
|
436
|
-
readonly NODE_NOTIFICATION_COMPANY_UNAVAILABLE: "entity_builder.flow.validation.node.notification.company.unavailable";
|
|
437
|
-
readonly NODE_MESSAGE_TARGET_INVALID: "entity_builder.flow.validation.node.message.target.invalid";
|
|
438
|
-
readonly COMPANY_NOTIFICATION_WITHOUT_USER: "entity_builder.flow.validation.company.notification.without.user";
|
|
439
|
-
readonly NODE_EMAIL_UNAVAILABLE: "entity_builder.flow.validation.node.email.unavailable";
|
|
440
|
-
readonly NODE_EMAIL_OPTION_INVALID: "entity_builder.flow.validation.node.email.option.invalid";
|
|
441
|
-
readonly NODE_EMAIL_TEMPLATE_REQUIRED: "entity_builder.flow.validation.node.email.template.required";
|
|
442
|
-
readonly MESSAGE_AFTER_COMMIT: "entity_builder.flow.validation.message.after.commit";
|
|
443
|
-
readonly PUBLIC_MESSAGE: "entity_builder.flow.validation.public.message";
|
|
444
406
|
readonly NODE_STATUS_2XX_5XX: "entity_builder.flow.validation.node.status.2xx.5xx";
|
|
445
407
|
readonly REF_BAD_SCOPE: "entity_builder.flow.validation.ref.bad.scope";
|
|
446
408
|
readonly REF_NODE_MISSING: "entity_builder.flow.validation.ref.node.missing";
|
|
@@ -508,21 +470,6 @@ export declare const FLOW_SUBJECT_MESSAGES: {
|
|
|
508
470
|
readonly THE_URL: "entity_builder.flow.subject.the.url";
|
|
509
471
|
readonly THE_INPUT_LIST: "entity_builder.flow.subject.the.input.list";
|
|
510
472
|
readonly THE_RESPONSE: "entity_builder.flow.subject.the.response";
|
|
511
|
-
readonly THE_RECIPIENTS: "entity_builder.flow.subject.the.recipients";
|
|
512
|
-
readonly THE_TITLE: "entity_builder.flow.subject.the.title";
|
|
513
|
-
readonly THE_MESSAGE: "entity_builder.flow.subject.the.message";
|
|
514
|
-
readonly THE_COMPANY: "entity_builder.flow.subject.the.company";
|
|
515
|
-
readonly THE_COMPANIES: "entity_builder.flow.subject.the.companies";
|
|
516
|
-
readonly THE_BRANCHES: "entity_builder.flow.subject.the.branches";
|
|
517
|
-
readonly THE_CC: "entity_builder.flow.subject.the.cc";
|
|
518
|
-
readonly THE_BCC: "entity_builder.flow.subject.the.bcc";
|
|
519
|
-
readonly THE_REPLY_TO: "entity_builder.flow.subject.the.reply.to";
|
|
520
|
-
readonly THE_SUBJECT: "entity_builder.flow.subject.the.subject";
|
|
521
|
-
readonly THE_BODY: "entity_builder.flow.subject.the.body";
|
|
522
|
-
readonly DATA_VALUES: "entity_builder.flow.subject.data.values";
|
|
523
|
-
readonly DATA_VALUE: "entity_builder.flow.subject.data.value";
|
|
524
|
-
readonly TEMPLATE_VALUES: "entity_builder.flow.subject.template.values";
|
|
525
|
-
readonly TEMPLATE_VALUE: "entity_builder.flow.subject.template.value";
|
|
526
473
|
readonly VARIABLES: "entity_builder.flow.subject.variables";
|
|
527
474
|
readonly VARIABLE: "entity_builder.flow.subject.variable";
|
|
528
475
|
readonly FIELDS: "entity_builder.flow.subject.fields";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ILoggedUserInfo, SingleResponseDto } from '@flusys/nestjs-shared';
|
|
2
|
+
import { CreateFlowApiKeyDto, FlowIdDto, IdBodyDto } from '../dtos/flow.dto.js';
|
|
3
|
+
import { FlowApiKeyService, type IFlowApiKeyInfo } from '../services/flow-api-key.service.js';
|
|
4
|
+
export declare class FlowApiKeyController {
|
|
5
|
+
private readonly keys;
|
|
6
|
+
constructor(keys: FlowApiKeyService);
|
|
7
|
+
create(dto: CreateFlowApiKeyDto, user: ILoggedUserInfo): Promise<SingleResponseDto<{
|
|
8
|
+
key: string;
|
|
9
|
+
info: IFlowApiKeyInfo;
|
|
10
|
+
}>>;
|
|
11
|
+
list(dto: FlowIdDto): Promise<SingleResponseDto<IFlowApiKeyInfo[]>>;
|
|
12
|
+
revoke(dto: IdBodyDto): Promise<SingleResponseDto<null>>;
|
|
13
|
+
remove(dto: IdBodyDto): Promise<SingleResponseDto<null>>;
|
|
14
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ILoggedUserInfo, SingleResponseDto } from '@flusys/nestjs-shared';
|
|
2
|
-
import { CreateFlowDefinitionDto, FlowDefinitionResponseDto,
|
|
3
|
-
import { type IFlowDefinition, type IFlowVersion } from '../interfaces/flow-definition.interface.js';
|
|
2
|
+
import { CreateFlowDefinitionDto, FlowDefinitionResponseDto, TestFlowDto, UpdateFlowDefinitionDto, ValidateFlowDto } from '../dtos/flow.dto.js';
|
|
4
3
|
import { type IFlowValidationResult } from '../flow-engine/flow-graph.validator.js';
|
|
5
4
|
import { FlowDefinitionService } from '../services/flow-definition.service.js';
|
|
6
5
|
import { FlowRuntimeService } from '../services/flow-runtime.service.js';
|
|
@@ -24,11 +23,6 @@ export declare class FlowDefinitionController extends FlowDefinitionController_b
|
|
|
24
23
|
private readonly runtime;
|
|
25
24
|
constructor(flowDefinitionService: FlowDefinitionService, runtime: FlowRuntimeService);
|
|
26
25
|
validate(dto: ValidateFlowDto): Promise<SingleResponseDto<IFlowValidationResult>>;
|
|
27
|
-
publish(dto: PublishFlowDto, user: ILoggedUserInfo): Promise<SingleResponseDto<IFlowDefinition>>;
|
|
28
|
-
versions(dto: FlowIdDto): Promise<SingleResponseDto<IFlowVersion[]>>;
|
|
29
|
-
version(dto: FlowVersionRefDto): Promise<SingleResponseDto<IFlowVersion>>;
|
|
30
|
-
restoreVersion(dto: FlowVersionRefDto, user: ILoggedUserInfo): Promise<SingleResponseDto<IFlowDefinition>>;
|
|
31
|
-
discardDraft(dto: FlowIdDto, user: ILoggedUserInfo): Promise<SingleResponseDto<IFlowDefinition>>;
|
|
32
26
|
testRun(dto: TestFlowDto, user: ILoggedUserInfo, req: {
|
|
33
27
|
ip?: string;
|
|
34
28
|
headers: Record<string, unknown>;
|
package/controllers/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './entity-definition.controller.js';
|
|
2
2
|
export * from './field-definition.controller.js';
|
|
3
3
|
export * from './flow-definition.controller.js';
|
|
4
|
+
export * from './flow-api-key.controller.js';
|
|
4
5
|
export * from './flow-execution.controller.js';
|
|
5
6
|
export * from './flow-runtime.controller.js';
|
package/dtos/flow.dto.d.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { IdentityResponseDto } from '@flusys/nestjs-shared/dtos';
|
|
2
|
-
import { type
|
|
3
|
-
import { type FlowInput, type IFlowEdge, type IFlowNode } from '../flow-engine/flow.types.js';
|
|
2
|
+
import { type FlowAuthMode, type FlowBodyType, type FlowInput, type FlowRunAs, type IFlowEdge, type IFlowInputField, type IFlowNode } from '../flow-engine/flow.types.js';
|
|
4
3
|
export declare class CreateFlowDefinitionDto {
|
|
5
4
|
name: string;
|
|
6
5
|
slug: string;
|
|
7
6
|
description?: string;
|
|
8
7
|
isActive?: boolean;
|
|
8
|
+
authMode?: FlowAuthMode;
|
|
9
|
+
permissionCode?: string;
|
|
10
|
+
runAs?: FlowRunAs;
|
|
11
|
+
isTransactional?: boolean;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
rateLimitPerMinute?: number;
|
|
9
14
|
retainExecutions?: number;
|
|
15
|
+
bodyType?: FlowBodyType;
|
|
16
|
+
inputSchema?: IFlowInputField[];
|
|
10
17
|
nodes: IFlowNode[];
|
|
11
18
|
edges: IFlowEdge[];
|
|
12
19
|
}
|
|
@@ -19,7 +26,6 @@ export declare class FlowDraftDto extends FlowDraftDto_base {
|
|
|
19
26
|
name?: string;
|
|
20
27
|
id?: string;
|
|
21
28
|
version?: unknown;
|
|
22
|
-
draft?: unknown;
|
|
23
29
|
createdAt?: unknown;
|
|
24
30
|
updatedAt?: unknown;
|
|
25
31
|
deletedAt?: unknown;
|
|
@@ -46,12 +52,9 @@ export declare class ValidateFlowDto {
|
|
|
46
52
|
export declare class FlowIdDto {
|
|
47
53
|
flowId: string;
|
|
48
54
|
}
|
|
49
|
-
export declare class
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
export declare class FlowVersionRefDto extends FlowIdDto {
|
|
54
|
-
version: number;
|
|
55
|
+
export declare class CreateFlowApiKeyDto extends FlowIdDto {
|
|
56
|
+
name: string;
|
|
57
|
+
expiresAt?: string;
|
|
55
58
|
}
|
|
56
59
|
export declare class IdBodyDto {
|
|
57
60
|
id: string;
|
|
@@ -67,10 +70,17 @@ export declare class FlowDefinitionResponseDto extends IdentityResponseDto {
|
|
|
67
70
|
slug: string;
|
|
68
71
|
description: string | null;
|
|
69
72
|
isActive: boolean;
|
|
73
|
+
authMode: FlowAuthMode;
|
|
74
|
+
permissionCode: string | null;
|
|
75
|
+
runAs: FlowRunAs;
|
|
76
|
+
isTransactional: boolean;
|
|
77
|
+
timeoutMs: number;
|
|
78
|
+
rateLimitPerMinute: number;
|
|
70
79
|
retainExecutions: number;
|
|
80
|
+
bodyType: FlowBodyType;
|
|
81
|
+
inputSchema: IFlowInputField[];
|
|
71
82
|
nodes: IFlowNode[];
|
|
72
83
|
edges: IFlowEdge[];
|
|
73
84
|
version: number;
|
|
74
|
-
draft: IFlowSnapshot | null;
|
|
75
85
|
}
|
|
76
86
|
export {};
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { Identity } from '@flusys/nestjs-shared';
|
|
2
|
-
import type { IFlowEdge, IFlowNode } from '../flow-engine/flow.types.js';
|
|
3
|
-
import type { IFlowSnapshot } from '../interfaces/flow-definition.interface.js';
|
|
2
|
+
import type { FlowAuthMode, FlowBodyType, FlowRunAs, IFlowEdge, IFlowInputField, IFlowNode } from '../flow-engine/flow.types.js';
|
|
4
3
|
export declare class FlowDefinition extends Identity {
|
|
5
4
|
name: string;
|
|
6
5
|
slug: string;
|
|
7
6
|
description: string | null;
|
|
8
7
|
isActive: boolean;
|
|
8
|
+
authMode: FlowAuthMode;
|
|
9
|
+
permissionCode: string | null;
|
|
10
|
+
runAs: FlowRunAs;
|
|
11
|
+
isTransactional: boolean;
|
|
12
|
+
timeoutMs: number;
|
|
13
|
+
rateLimitPerMinute: number;
|
|
9
14
|
retainExecutions: number;
|
|
15
|
+
bodyType: FlowBodyType;
|
|
16
|
+
inputSchema: IFlowInputField[];
|
|
10
17
|
nodes: IFlowNode[];
|
|
11
18
|
edges: IFlowEdge[];
|
|
12
19
|
version: number;
|
|
13
|
-
draft: IFlowSnapshot | null;
|
|
14
20
|
}
|
package/entities/index.d.ts
CHANGED
|
@@ -2,13 +2,13 @@ export * from './entity-definition.entity.js';
|
|
|
2
2
|
export * from './field-definition.entity.js';
|
|
3
3
|
export * from './schema-change-log.entity.js';
|
|
4
4
|
export * from './flow-definition.entity.js';
|
|
5
|
+
export * from './flow-api-key.entity.js';
|
|
5
6
|
export * from './flow-execution.entity.js';
|
|
6
|
-
export * from './flow-version.entity.js';
|
|
7
7
|
import { EntityDefinition } from './entity-definition.entity.js';
|
|
8
8
|
import { FieldDefinition } from './field-definition.entity.js';
|
|
9
9
|
import { SchemaChangeLog } from './schema-change-log.entity.js';
|
|
10
10
|
import { FlowDefinition } from './flow-definition.entity.js';
|
|
11
|
+
import { FlowApiKey } from './flow-api-key.entity.js';
|
|
11
12
|
import { FlowExecution } from './flow-execution.entity.js';
|
|
12
|
-
|
|
13
|
-
export declare const EntityBuilderEntities: (typeof EntityDefinition | typeof FieldDefinition | typeof SchemaChangeLog | typeof FlowDefinition | typeof FlowExecution | typeof FlowVersion)[];
|
|
13
|
+
export declare const EntityBuilderEntities: (typeof EntityDefinition | typeof FieldDefinition | typeof SchemaChangeLog | typeof FlowDefinition | typeof FlowApiKey | typeof FlowExecution)[];
|
|
14
14
|
export declare function getEntityBuilderEntities(): typeof EntityBuilderEntities;
|