@flusys/nestjs-entity-builder 9.1.0-rc.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 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 per endpoint after the commit - slug `<code-with-dashes>-<endpoint-in-kebab-case>` (e.g. `ticket-get-by-ids`), `jwt` auth, runs as the caller (entity permissions apply). Single endpoints are trigger -> 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` per endpoint, `inputLabels` for `id` / `ids` / `search`, `noMatchMessage`) carries the text written into the flows in the creator's language - English for anything left out. Each flow is saved on its own after the commit: one that fails does not stop the rest or the permission set-up and 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`). They are ordinary flows 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` |
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,7 +24,8 @@ 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). Access is the flow's own auth mode; the answer is what its `respond` node says | per flow: public / API key / any user / a permission |
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 |
28
29
  | `entity-builder/flows/{insert,update,delete,get-all,get/:id,...}` | Flow CRUD - every save is validated as a whole | `...flow_definition.*` |
29
30
  | `entity-builder/flows/validate` | Check a draft without saving (errors block a save, warnings do not) | `...flow_definition.read` |
30
31
  | `entity-builder/flows/test-run` | Run a saved flow or an unsaved draft, with a per-node trace | `...flow_definition.test` |
@@ -79,6 +80,7 @@ A flow is an endpoint (`POST /api-flows/<slug>`) whose behaviour is a graph of s
79
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).
80
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.
81
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`).
82
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.
83
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.
84
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).
@@ -251,6 +251,7 @@ export declare const RULE_ENGINE_MESSAGES: {
251
251
  export declare const FLOW_MESSAGES: {
252
252
  readonly NOT_FOUND: "entity_builder.flow.not.found";
253
253
  readonly NOTHING_TO_TEST: "entity_builder.flow.nothing.to.test";
254
+ readonly ENDPOINT_NOT_FOUND: "entity_builder.flow.endpoint.not.found";
254
255
  readonly INVALID: "entity_builder.flow.invalid";
255
256
  readonly SLUG_TAKEN: "entity_builder.flow.slug.taken";
256
257
  readonly RATE_LIMITED: "entity_builder.flow.rate.limited";
@@ -263,6 +264,7 @@ export declare const FLOW_MESSAGES: {
263
264
  readonly API_KEY_EXPIRED: "entity_builder.flow.api.key.expired";
264
265
  readonly API_KEY_EXPIRY_IN_PAST: "entity_builder.flow.api.key.expiry.in.past";
265
266
  readonly CALLED_BY_OTHER_FLOWS: "entity_builder.flow.called.by.other.flows";
267
+ readonly ENDPOINT_CALLED_BY_OTHER_FLOWS: "entity_builder.flow.endpoint.called.by.other.flows";
266
268
  readonly EXECUTION_NOT_FOUND: "entity_builder.flow.execution.not.found";
267
269
  readonly EXECUTIONS_SUCCESS: "entity_builder.flow.executions.success";
268
270
  readonly EXECUTION_SUCCESS: "entity_builder.flow.execution.success";
@@ -309,6 +311,7 @@ export declare const FLOW_ERROR_MESSAGES: {
309
311
  readonly CODE_OUTPUT_INVALID: "entity_builder.flow.error.code.output.invalid";
310
312
  readonly CODE_UNAVAILABLE: "entity_builder.flow.error.code.unavailable";
311
313
  readonly CALLED_FLOW_NOT_FOUND: "entity_builder.flow.error.called.flow.not.found";
314
+ readonly CALLED_FLOW_ENDPOINT_NOT_FOUND: "entity_builder.flow.error.called.flow.endpoint.not.found";
312
315
  readonly CALLED_FLOW_NOT_ALLOWED: "entity_builder.flow.error.called.flow.not.allowed";
313
316
  readonly CALLED_FLOW_RECURSION: "entity_builder.flow.error.called.flow.recursion";
314
317
  readonly CALLED_FLOW_TOO_DEEP: "entity_builder.flow.error.called.flow.too.deep";
@@ -335,7 +338,10 @@ export declare const FLOW_VALIDATION_MESSAGES: {
335
338
  readonly NODE_ID_INVALID: "entity_builder.flow.validation.node.id.invalid";
336
339
  readonly NODE_ID_DUPLICATE: "entity_builder.flow.validation.node.id.duplicate";
337
340
  readonly NODE_TYPE_UNKNOWN: "entity_builder.flow.validation.node.type.unknown";
338
- readonly TRIGGER_COUNT: "entity_builder.flow.validation.trigger.count";
341
+ readonly TRIGGER_MISSING: "entity_builder.flow.validation.trigger.missing";
342
+ readonly TRIGGER_MAIN_DUPLICATE: "entity_builder.flow.validation.trigger.main.duplicate";
343
+ readonly TRIGGER_PATH_INVALID: "entity_builder.flow.validation.trigger.path.invalid";
344
+ readonly TRIGGER_PATH_DUPLICATE: "entity_builder.flow.validation.trigger.path.duplicate";
339
345
  readonly EDGE_ID_DUPLICATE: "entity_builder.flow.validation.edge.id.duplicate";
340
346
  readonly EDGE_DANGLING: "entity_builder.flow.validation.edge.dangling";
341
347
  readonly EDGE_SELF: "entity_builder.flow.validation.edge.self";
@@ -383,6 +389,8 @@ export declare const FLOW_VALIDATION_MESSAGES: {
383
389
  readonly NODE_CHILD_PARENT_FIELD_REQUIRED: "entity_builder.flow.validation.node.child.parent.field.required";
384
390
  readonly NODE_CHOOSE_FLOW: "entity_builder.flow.validation.node.choose.flow";
385
391
  readonly NODE_FLOW_UNKNOWN: "entity_builder.flow.validation.node.flow.unknown";
392
+ readonly NODE_FLOW_ENDPOINT_UNKNOWN: "entity_builder.flow.validation.node.flow.endpoint.unknown";
393
+ readonly NODE_FLOW_ENDPOINT_REQUIRED: "entity_builder.flow.validation.node.flow.endpoint.required";
386
394
  readonly NODE_CALLS_ITSELF: "entity_builder.flow.validation.node.calls.itself";
387
395
  readonly NODE_FLOW_INACTIVE: "entity_builder.flow.validation.node.flow.inactive";
388
396
  readonly NODE_FLOW_API_KEY: "entity_builder.flow.validation.node.flow.api.key";
@@ -1,16 +1,24 @@
1
1
  import { type FlowDefinition } from '../entities/flow-definition.entity.js';
2
+ import { type IFlowEndpoint } from '../flow-engine/flow.types.js';
2
3
  import { FlowRuntimeService } from '../services/flow-runtime.service.js';
4
+ type FlowRequest = {
5
+ flowDefinition: FlowDefinition;
6
+ flowEndpoint: IFlowEndpoint;
7
+ user?: never;
8
+ ip?: string;
9
+ headers: Record<string, unknown>;
10
+ query?: Record<string, unknown>;
11
+ path?: string;
12
+ };
3
13
  export declare class FlowRuntimeController {
4
14
  private readonly runtime;
5
15
  constructor(runtime: FlowRuntimeService);
6
- run(_slug: string, body: unknown, req: {
7
- flowDefinition: FlowDefinition;
8
- user?: never;
9
- ip?: string;
10
- headers: Record<string, unknown>;
11
- query?: Record<string, unknown>;
12
- path?: string;
13
- }, res: {
16
+ run(_slug: string, body: unknown, req: FlowRequest, res: {
14
17
  status(code: number): unknown;
15
18
  }): Promise<unknown>;
19
+ runEndpoint(_slug: string, _endpoint: string, body: unknown, req: FlowRequest, res: {
20
+ status(code: number): unknown;
21
+ }): Promise<unknown>;
22
+ private answer;
16
23
  }
24
+ export {};
@@ -41,6 +41,7 @@ export declare class TestFlowRequestDto {
41
41
  export declare class TestFlowDto {
42
42
  flowId?: string;
43
43
  definition?: FlowDraftDto;
44
+ endpoint?: string;
44
45
  input?: FlowInput;
45
46
  mode?: 'dry_run' | 'live';
46
47
  request?: TestFlowRequestDto;
package/fesm/21.js CHANGED
@@ -257,6 +257,7 @@ const RULE_ENGINE_MESSAGES = {
257
257
  const FLOW_MESSAGES = {
258
258
  NOT_FOUND: 'entity_builder.flow.not.found',
259
259
  NOTHING_TO_TEST: 'entity_builder.flow.nothing.to.test',
260
+ ENDPOINT_NOT_FOUND: 'entity_builder.flow.endpoint.not.found',
260
261
  INVALID: 'entity_builder.flow.invalid',
261
262
  SLUG_TAKEN: 'entity_builder.flow.slug.taken',
262
263
  RATE_LIMITED: 'entity_builder.flow.rate.limited',
@@ -269,6 +270,7 @@ const FLOW_MESSAGES = {
269
270
  API_KEY_EXPIRED: 'entity_builder.flow.api.key.expired',
270
271
  API_KEY_EXPIRY_IN_PAST: 'entity_builder.flow.api.key.expiry.in.past',
271
272
  CALLED_BY_OTHER_FLOWS: 'entity_builder.flow.called.by.other.flows',
273
+ ENDPOINT_CALLED_BY_OTHER_FLOWS: 'entity_builder.flow.endpoint.called.by.other.flows',
272
274
  EXECUTION_NOT_FOUND: 'entity_builder.flow.execution.not.found',
273
275
  EXECUTIONS_SUCCESS: 'entity_builder.flow.executions.success',
274
276
  EXECUTION_SUCCESS: 'entity_builder.flow.execution.success',
@@ -315,6 +317,7 @@ const FLOW_MESSAGES = {
315
317
  CODE_OUTPUT_INVALID: 'entity_builder.flow.error.code.output.invalid',
316
318
  CODE_UNAVAILABLE: 'entity_builder.flow.error.code.unavailable',
317
319
  CALLED_FLOW_NOT_FOUND: 'entity_builder.flow.error.called.flow.not.found',
320
+ CALLED_FLOW_ENDPOINT_NOT_FOUND: 'entity_builder.flow.error.called.flow.endpoint.not.found',
318
321
  CALLED_FLOW_NOT_ALLOWED: 'entity_builder.flow.error.called.flow.not.allowed',
319
322
  CALLED_FLOW_RECURSION: 'entity_builder.flow.error.called.flow.recursion',
320
323
  CALLED_FLOW_TOO_DEEP: 'entity_builder.flow.error.called.flow.too.deep',
@@ -341,7 +344,10 @@ const FLOW_MESSAGES = {
341
344
  NODE_ID_INVALID: 'entity_builder.flow.validation.node.id.invalid',
342
345
  NODE_ID_DUPLICATE: 'entity_builder.flow.validation.node.id.duplicate',
343
346
  NODE_TYPE_UNKNOWN: 'entity_builder.flow.validation.node.type.unknown',
344
- TRIGGER_COUNT: 'entity_builder.flow.validation.trigger.count',
347
+ TRIGGER_MISSING: 'entity_builder.flow.validation.trigger.missing',
348
+ TRIGGER_MAIN_DUPLICATE: 'entity_builder.flow.validation.trigger.main.duplicate',
349
+ TRIGGER_PATH_INVALID: 'entity_builder.flow.validation.trigger.path.invalid',
350
+ TRIGGER_PATH_DUPLICATE: 'entity_builder.flow.validation.trigger.path.duplicate',
345
351
  EDGE_ID_DUPLICATE: 'entity_builder.flow.validation.edge.id.duplicate',
346
352
  EDGE_DANGLING: 'entity_builder.flow.validation.edge.dangling',
347
353
  EDGE_SELF: 'entity_builder.flow.validation.edge.self',
@@ -389,6 +395,8 @@ const FLOW_MESSAGES = {
389
395
  NODE_CHILD_PARENT_FIELD_REQUIRED: 'entity_builder.flow.validation.node.child.parent.field.required',
390
396
  NODE_CHOOSE_FLOW: 'entity_builder.flow.validation.node.choose.flow',
391
397
  NODE_FLOW_UNKNOWN: 'entity_builder.flow.validation.node.flow.unknown',
398
+ NODE_FLOW_ENDPOINT_UNKNOWN: 'entity_builder.flow.validation.node.flow.endpoint.unknown',
399
+ NODE_FLOW_ENDPOINT_REQUIRED: 'entity_builder.flow.validation.node.flow.endpoint.required',
392
400
  NODE_CALLS_ITSELF: 'entity_builder.flow.validation.node.calls.itself',
393
401
  NODE_FLOW_INACTIVE: 'entity_builder.flow.validation.node.flow.inactive',
394
402
  NODE_FLOW_API_KEY: 'entity_builder.flow.validation.node.flow.api.key',
package/fesm/362.js CHANGED
@@ -125,7 +125,7 @@ _ts_decorate([
125
125
  ]) {
126
126
  }
127
127
  const FLOW_TEXT_MAX_LENGTH = 100;
128
- /** Name of each endpoint flow, in the creator's language: the flow is named `<entity label> - <this>`. */ class EntityFlowNamesDto {
128
+ /** Name of each endpoint's Request step in the entity's flow, in the creator's language. */ class EntityFlowNamesDto {
129
129
  insert;
130
130
  insertMany;
131
131
  getById;
@@ -227,7 +227,7 @@ _ts_decorate([
227
227
  (0,class_validator__rspack_import_3.MaxLength)(FLOW_TEXT_MAX_LENGTH),
228
228
  _ts_metadata("design:type", String)
229
229
  ], EntityFlowNamesDto.prototype, "delete", void 0);
230
- /** Labels of the inputs the endpoint flows add besides the entity fields. */ class EntityFlowInputLabelsDto {
230
+ /** Labels of the inputs the endpoints add besides the entity fields. */ class EntityFlowInputLabelsDto {
231
231
  id;
232
232
  ids;
233
233
  search;
@@ -260,8 +260,8 @@ _ts_decorate([
260
260
  _ts_metadata("design:type", String)
261
261
  ], EntityFlowInputLabelsDto.prototype, "search", void 0);
262
262
  /**
263
- * Text written into the endpoint flows, sent by the designer in the creator's language so the
264
- * saved flows read like ones the user wrote. Anything left out falls back to English.
263
+ * Text written into the entity's flow, sent by the designer in the creator's language so the
264
+ * saved flow reads like one the user wrote. Anything left out falls back to English.
265
265
  */ class EntityFlowTextsDto {
266
266
  names;
267
267
  inputLabels;
@@ -287,7 +287,7 @@ _ts_decorate([
287
287
  ], EntityFlowTextsDto.prototype, "inputLabels", void 0);
288
288
  _ts_decorate([
289
289
  (0,_nestjs_swagger__rspack_import_1.ApiPropertyOptional)({
290
- description: 'Message of the 404 answer of the get-by-filter flow',
290
+ description: 'Message of the 404 answer of the get-by-filter endpoint',
291
291
  example: 'No record matches the filter.'
292
292
  }),
293
293
  (0,class_validator__rspack_import_3.IsString)(),
@@ -324,7 +324,7 @@ _ts_decorate([
324
324
  (0,_nestjs_swagger__rspack_import_1.ApiPropertyOptional)({
325
325
  enum: _config_entity_builder_constants_js__rspack_import_5/* .ENTITY_FLOW_ENDPOINTS */.WT,
326
326
  isArray: true,
327
- description: 'Generic API controller endpoints to create as ready-made flows (POST api-flows/<code>-<endpoint>) once the entity exists',
327
+ description: 'Generic API controller endpoints to create, once the entity exists, as one ready-made flow with a Request step each (POST api-flows/<code-with-dashes>/<endpoint-in-kebab-case>)',
328
328
  example: [
329
329
  ..._config_entity_builder_constants_js__rspack_import_5/* .ENTITY_FLOW_ENDPOINTS */.WT
330
330
  ]
package/fesm/606.js CHANGED
@@ -472,7 +472,7 @@ flow_dto_ts_decorate([
472
472
  ], CreateFlowDefinitionDto.prototype, "name", void 0);
473
473
  flow_dto_ts_decorate([
474
474
  (0,swagger_.ApiProperty)({
475
- description: 'URL name: POST api-flows/<slug>',
475
+ description: 'URL name: POST api-flows/<slug> (and api-flows/<slug>/<path> for each Request step with a path)',
476
476
  example: 'create-order'
477
477
  }),
478
478
  (0,external_class_validator_.IsString)(),
@@ -699,6 +699,7 @@ flow_dto_ts_decorate([
699
699
  /** Runs a flow from the designer: a saved flow (flowId), an unsaved draft (definition), or both - flowId attributes/scopes the run while definition (the current draft, if sent) is what actually executes. */ class TestFlowDto {
700
700
  flowId;
701
701
  definition;
702
+ endpoint;
702
703
  input;
703
704
  mode;
704
705
  request;
@@ -721,7 +722,16 @@ flow_dto_ts_decorate([
721
722
  ], TestFlowDto.prototype, "definition", void 0);
722
723
  flow_dto_ts_decorate([
723
724
  (0,swagger_.ApiPropertyOptional)({
724
- description: 'The request body to test with: an object, or a list for a flow whose body is a list'
725
+ description: 'Path of the Request step to start at; left out, the one on the flow\'s own URL',
726
+ example: 'insert-many'
727
+ }),
728
+ (0,external_class_validator_.Matches)(flow_types/* .FLOW_ENDPOINT_PATH_PATTERN */.YO),
729
+ (0,external_class_validator_.IsOptional)(),
730
+ flow_dto_ts_metadata("design:type", String)
731
+ ], TestFlowDto.prototype, "endpoint", void 0);
732
+ flow_dto_ts_decorate([
733
+ (0,swagger_.ApiPropertyOptional)({
734
+ description: 'The request body to test with: an object, or a list for a Request step whose body is a list'
725
735
  }),
726
736
  (0,external_class_validator_.ValidateIf)((dto)=>!Array.isArray(dto.input)),
727
737
  (0,external_class_validator_.IsObject)(),
@@ -1398,8 +1408,14 @@ class FlowRuntimeController {
1398
1408
  constructor(runtime){
1399
1409
  this.runtime = runtime;
1400
1410
  }
1401
- async run(_slug, body, req, res) {
1402
- const result = await this.runtime.handle(req.flowDefinition, body, {
1411
+ run(_slug, body, req, res) {
1412
+ return this.answer(body, req, res);
1413
+ }
1414
+ runEndpoint(_slug, _endpoint, body, req, res) {
1415
+ return this.answer(body, req, res);
1416
+ }
1417
+ async answer(body, req, res) {
1418
+ const result = await this.runtime.handle(req.flowDefinition, req.flowEndpoint, body, {
1403
1419
  ip: req.ip ?? null,
1404
1420
  headers: req.headers,
1405
1421
  query: req.query ?? {},
@@ -1429,11 +1445,41 @@ flow_runtime_controller_ts_decorate([
1429
1445
  flow_runtime_controller_ts_metadata("design:paramtypes", [
1430
1446
  String,
1431
1447
  Object,
1432
- Object,
1448
+ typeof FlowRequest === "undefined" ? Object : FlowRequest,
1433
1449
  Object
1434
1450
  ]),
1435
- flow_runtime_controller_ts_metadata("design:returntype", Promise)
1451
+ flow_runtime_controller_ts_metadata("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1436
1452
  ], FlowRuntimeController.prototype, "run", null);
1453
+ flow_runtime_controller_ts_decorate([
1454
+ (0,common_.Post)(':slug/:endpoint'),
1455
+ (0,common_.HttpCode)(common_.HttpStatus.OK),
1456
+ (0,common_.UseGuards)(flow_access_guard/* .FlowAccessGuard */.C),
1457
+ (0,swagger_.ApiOperation)({
1458
+ summary: 'Call one of the URLs of a flow'
1459
+ }),
1460
+ (0,swagger_.ApiParam)({
1461
+ name: 'slug'
1462
+ }),
1463
+ (0,swagger_.ApiParam)({
1464
+ name: 'endpoint'
1465
+ }),
1466
+ flow_runtime_controller_ts_param(0, (0,common_.Param)('slug')),
1467
+ flow_runtime_controller_ts_param(1, (0,common_.Param)('endpoint')),
1468
+ flow_runtime_controller_ts_param(2, (0,common_.Body)()),
1469
+ flow_runtime_controller_ts_param(3, (0,common_.Req)()),
1470
+ flow_runtime_controller_ts_param(4, (0,common_.Res)({
1471
+ passthrough: true
1472
+ })),
1473
+ flow_runtime_controller_ts_metadata("design:type", Function),
1474
+ flow_runtime_controller_ts_metadata("design:paramtypes", [
1475
+ String,
1476
+ String,
1477
+ Object,
1478
+ typeof FlowRequest === "undefined" ? Object : FlowRequest,
1479
+ Object
1480
+ ]),
1481
+ flow_runtime_controller_ts_metadata("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1482
+ ], FlowRuntimeController.prototype, "runEndpoint", null);
1437
1483
  FlowRuntimeController = flow_runtime_controller_ts_decorate([
1438
1484
  (0,swagger_.ApiTags)('Flows (virtual APIs)'),
1439
1485
  (0,common_.Controller)('api-flows'),
@@ -1522,6 +1568,8 @@ const DEFAULT_MAX_KEYS = 10_000;
1522
1568
  }
1523
1569
  }
1524
1570
 
1571
+ // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow.types.ts
1572
+ var flow_types = __webpack_require__(6256);
1525
1573
  ;// CONCATENATED MODULE: ./projects/nestjs-entity-builder/src/guards/flow-access.guard.ts
1526
1574
  function _ts_decorate(decorators, target, key, desc) {
1527
1575
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -1557,6 +1605,7 @@ function _ts_param(paramIndex, decorator) {
1557
1605
 
1558
1606
 
1559
1607
 
1608
+
1560
1609
  const limiter = new FlowRateLimiter();
1561
1610
  class FlowAccessGuard {
1562
1611
  moduleRef;
@@ -1574,11 +1623,13 @@ class FlowAccessGuard {
1574
1623
  strict: false
1575
1624
  });
1576
1625
  const flow = slug ? await runtime.findActive(slug) : null;
1577
- if (!flow) throw new common_.NotFoundException({
1626
+ const endpoint = flow ? (0,flow_types/* .findEndpoint */.vS)(flow, request.params?.endpoint) : undefined;
1627
+ if (!flow || !endpoint) throw new common_.NotFoundException({
1578
1628
  message: 'Not found',
1579
1629
  messageKey: message_keys/* .FLOW_MESSAGES.NOT_FOUND */.wu.NOT_FOUND
1580
1630
  });
1581
1631
  request.flowDefinition = flow;
1632
+ request.flowEndpoint = endpoint;
1582
1633
  const ip = request.ip ?? request.socket?.remoteAddress ?? 'unknown';
1583
1634
  if (!limiter.hit(`${flow.id}|${ip}`, flow.rateLimitPerMinute)) {
1584
1635
  throw new common_.HttpException({