@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/fesm/996.js CHANGED
@@ -1034,7 +1034,7 @@ async function insertChildren(node, child, parentId, env, state) {
1034
1034
  const cfg = c;
1035
1035
  const input = cfg.inputList !== undefined ? await env.ruleEngine.resolveExpression(cfg.inputList, evaluationContext(state, env)) : await mapValues(cfg.input, env, state);
1036
1036
  return {
1037
- output: await env.callFlow(cfg.flowSlug, input, state)
1037
+ output: await env.callFlow(cfg.flowSlug, input, state, cfg.endpoint || undefined)
1038
1038
  };
1039
1039
  }
1040
1040
  case 'http_request':
@@ -1128,7 +1128,8 @@ async function insertChildren(node, child, parentId, env, state) {
1128
1128
 
1129
1129
  const errorMessage = (error)=>error instanceof Error ? error.message : String(error);
1130
1130
  /**
1131
- * Walks a flow graph from its trigger: runs a node, follows the output port it chose, repeats.
1131
+ * Walks a flow graph from the Request step the call came in on (`state.startAt`; left out, the one
1132
+ * on the flow's own URL): runs a node, follows the output port it chose, repeats.
1132
1133
  * `foreach` runs its `each` branch once per item and then continues on `done`; inside a nested
1133
1134
  * loop the outer loop's scope stays reachable as `loop.parent`. A `respond` node
1134
1135
  * ends the whole run. Limits (steps, time) are checked before every node.
@@ -1147,7 +1148,7 @@ const errorMessage = (error)=>error instanceof Error ? error.message : String(er
1147
1148
  /** Runs to completion or throws (FlowNodeError wraps whatever a node threw). */ async walk(state) {
1148
1149
  const trigger = [
1149
1150
  ...this.nodes.values()
1150
- ].find((n)=>n.type === 'trigger');
1151
+ ].find((n)=>n.type === 'trigger' && (state.startAt ? n.id === state.startAt : !(0,flow_types/* .triggerPath */.um)(n)));
1151
1152
  if (!trigger) throw new flow_errors/* .FlowLimitError */.IM(message_keys/* .FLOW_ERROR_MESSAGES.NO_TRIGGER */.zr.NO_TRIGGER);
1152
1153
  await this.run(trigger.id, state);
1153
1154
  }
@@ -1741,7 +1742,7 @@ function conditionProblem(node, options) {
1741
1742
  return null;
1742
1743
  }
1743
1744
  /**
1744
- * Save-time check of a whole flow: structure (one trigger, valid ids/ports, no cycles),
1745
+ * Save-time check of a whole flow: structure (Request steps with distinct URLs, valid ids/ports, no cycles),
1745
1746
  * every node's configuration, every reference to another node, and the combination of
1746
1747
  * settings (run-as / auth / transaction). Pure: no I/O, so the same result is what the
1747
1748
  * designer shows and what the server enforces.
@@ -1791,9 +1792,7 @@ function conditionProblem(node, options) {
1791
1792
  }));
1792
1793
  }
1793
1794
  const triggers = nodes.filter((n)=>n.type === 'trigger');
1794
- if (triggers.length !== 1) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_COUNT */.qj.TRIGGER_COUNT, {
1795
- count: triggers.length
1796
- }));
1795
+ checkTriggers(triggers, err);
1797
1796
  for (const node of nodes){
1798
1797
  if (byId.get(node.id) === node) validateNodeConfig(node, flow, context, err, warnings);
1799
1798
  }
@@ -1841,7 +1840,9 @@ function conditionProblem(node, options) {
1841
1840
  }
1842
1841
  const cyclic = hasCycle(nodes, outgoing);
1843
1842
  if (cyclic) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.CYCLE */.qj.CYCLE));
1844
- const reachable = triggers.length === 1 ? reach(triggers[0].id, outgoing) : null;
1843
+ const reachable = triggers.length ? new Set(triggers.flatMap((trigger)=>[
1844
+ ...reach(trigger.id, outgoing)
1845
+ ])) : null;
1845
1846
  for (const node of nodes){
1846
1847
  if (reachable && !reachable.has(node.id)) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_UNREACHABLE */.qj.NODE_UNREACHABLE, {
1847
1848
  node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node)
@@ -1865,11 +1866,27 @@ function conditionProblem(node, options) {
1865
1866
  warnings
1866
1867
  };
1867
1868
  }
1869
+ /** At least one Request step; at most one on the flow's own URL, the others on distinct paths. */ function checkTriggers(triggers, err) {
1870
+ if (!triggers.length) return err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_MISSING */.qj.TRIGGER_MISSING));
1871
+ const main = triggers.filter((trigger)=>!(0,_flow_types_js__rspack_import_6/* .triggerPath */.um)(trigger));
1872
+ if (main.length > 1) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_MAIN_DUPLICATE */.qj.TRIGGER_MAIN_DUPLICATE, {
1873
+ count: main.length
1874
+ }));
1875
+ const paths = new Set();
1876
+ for (const trigger of triggers){
1877
+ const path = (0,_flow_types_js__rspack_import_6/* .triggerPath */.um)(trigger);
1878
+ if (!path) continue;
1879
+ if (paths.has(path)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_PATH_DUPLICATE */.qj.TRIGGER_PATH_DUPLICATE, {
1880
+ path
1881
+ }));
1882
+ paths.add(path);
1883
+ }
1884
+ }
1868
1885
  /**
1869
1886
  * What the runtime would always refuse, caught on save: an API-key flow called by a flow running
1870
1887
  * as the caller, a required input left out, a chain that comes back to this flow or nests deeper
1871
1888
  * than the limit. An inactive flow and an input the called flow does not declare are warnings.
1872
- */ function checkCalledFlow(slug, input, inputList, flow, known, at, err, warn) {
1889
+ */ function checkCalledFlow(slug, endpoint, input, inputList, flow, known, at, err, warn) {
1873
1890
  const target = known.get(slug);
1874
1891
  const vars = {
1875
1892
  ...at,
@@ -1881,12 +1898,19 @@ function conditionProblem(node, options) {
1881
1898
  if (flow.authMode === 'internal') warn.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_API_KEY */.qj.NODE_FLOW_API_KEY, vars));
1882
1899
  else if (flow.runAs === 'caller') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_API_KEY */.qj.NODE_FLOW_API_KEY, vars));
1883
1900
  }
1884
- if (target.bodyType === 'list') {
1901
+ const path = typeof endpoint === 'string' && endpoint ? endpoint : null;
1902
+ const entry = target.endpoints.find((e)=>e.path === path);
1903
+ if (!entry) {
1904
+ err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(path ? _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_ENDPOINT_UNKNOWN */.qj.NODE_FLOW_ENDPOINT_UNKNOWN : _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_ENDPOINT_REQUIRED */.qj.NODE_FLOW_ENDPOINT_REQUIRED, {
1905
+ ...vars,
1906
+ endpoint: path ?? ''
1907
+ }));
1908
+ } else if (entry.bodyType === 'list') {
1885
1909
  if (inputList === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_INPUT_LIST_REQUIRED */.qj.NODE_FLOW_INPUT_LIST_REQUIRED, vars));
1886
1910
  } else if (inputList !== undefined) {
1887
1911
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_INPUT_LIST_UNEXPECTED */.qj.NODE_FLOW_INPUT_LIST_UNEXPECTED, vars));
1888
1912
  } else {
1889
- checkCalledInputs(input, target, vars, err, warn);
1913
+ checkCalledInputs(input, entry.inputSchema, vars, err, warn);
1890
1914
  }
1891
1915
  const loop = callPathTo(slug, flow.slug, known, new Set());
1892
1916
  if (loop) return err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_LOOP */.qj.NODE_FLOW_LOOP, {
@@ -1901,9 +1925,8 @@ function conditionProblem(node, options) {
1901
1925
  max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH
1902
1926
  }));
1903
1927
  }
1904
- /** The named inputs sent to a flow whose body is an object: every required one given, the rest declared. */ function checkCalledInputs(input, target, vars, err, warn) {
1928
+ /** The named inputs sent to a flow whose body is an object: every required one given, the rest declared. */ function checkCalledInputs(input, schema, vars, err, warn) {
1905
1929
  const given = input && typeof input === 'object' && !Array.isArray(input) ? Object.keys(input) : [];
1906
- const schema = target.inputSchema ?? [];
1907
1930
  if (schema.length) {
1908
1931
  const declared = new Set(schema.map((field)=>field.name));
1909
1932
  for (const name of given)if (!declared.has(name)) warn.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_INPUT_UNDECLARED */.qj.NODE_FLOW_INPUT_UNDECLARED, {
@@ -2081,7 +2104,16 @@ function validateNodeConfig(node, flow, context, err, warn) {
2081
2104
  };
2082
2105
  switch(node.type){
2083
2106
  case 'trigger':
2084
- break;
2107
+ {
2108
+ if (c.path !== undefined && c.path !== '' && (typeof c.path !== 'string' || !_flow_types_js__rspack_import_6/* .FLOW_ENDPOINT_PATH_PATTERN.test */.YO.test(c.path))) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_PATH_INVALID */.qj.TRIGGER_PATH_INVALID, at));
2109
+ const path = (0,_flow_types_js__rspack_import_6/* .triggerPath */.um)(node);
2110
+ if (!path) break;
2111
+ if (c.bodyType !== undefined && !_flow_types_js__rspack_import_6/* .FLOW_BODY_TYPES.includes */.Y3.includes(c.bodyType)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.BODY_TYPE_UNKNOWN */.qj.BODY_TYPE_UNKNOWN, {
2112
+ type: String(c.bodyType)
2113
+ }));
2114
+ validateInputFields(Array.isArray(c.inputSchema) ? c.inputSchema : [], `${path}: `, 1, err);
2115
+ break;
2116
+ }
2085
2117
  case 'validate':
2086
2118
  validateChecks(c, at, rule, err);
2087
2119
  if (c.mode !== undefined && c.mode !== 'all' && c.mode !== 'first') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_MODE_INVALID */.qj.NODE_MODE_INVALID, at));
@@ -2216,7 +2248,8 @@ function validateNodeConfig(node, flow, context, err, warn) {
2216
2248
  ...at,
2217
2249
  flow: c.flowSlug
2218
2250
  }));
2219
- else if (context.knownFlows) checkCalledFlow(c.flowSlug, c.input, c.inputList, flow, context.knownFlows, at, err, warn);
2251
+ else if (context.knownFlows) checkCalledFlow(c.flowSlug, c.endpoint, c.input, c.inputList, flow, context.knownFlows, at, err, warn);
2252
+ if (c.endpoint !== undefined && c.endpoint !== '' && (typeof c.endpoint !== 'string' || !_flow_types_js__rspack_import_6/* .FLOW_ENDPOINT_PATH_PATTERN.test */.YO.test(c.endpoint))) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_PATH_INVALID */.qj.TRIGGER_PATH_INVALID, at));
2220
2253
  exprMap(c.input, KIND.input);
2221
2254
  if (c.inputList !== undefined) expr(c.inputList, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_INPUT_LIST */.bc.THE_INPUT_LIST));
2222
2255
  break;
@@ -2427,8 +2460,11 @@ function validateChecks(c, at, rule, err) {
2427
2460
  }));
2428
2461
  }
2429
2462
  }
2430
- /** Every `context.<node>` must name a node of this flow; `input.<name>` should be a declared input. */ function checkNodeReferences(nodes, byId, flow, err, warnings) {
2431
- const declaredInputs = new Set((flow.inputSchema ?? []).map((f)=>f.name));
2463
+ /** Every `context.<node>` must name a node of this flow; `input.<name>` should be an input some Request step declares. */ function checkNodeReferences(nodes, byId, flow, err, warnings) {
2464
+ const endpoints = (0,_flow_types_js__rspack_import_6/* .flowEndpoints */.Xr)(flow);
2465
+ const declaredInputs = new Set(endpoints.flatMap((endpoint)=>endpoint.inputSchema.map((f)=>f.name)));
2466
+ // A list body is read by index, and an object body without fields takes anything: neither has names to check.
2467
+ const namesKnown = endpoints.length > 0 && endpoints.every((endpoint)=>endpoint.bodyType !== 'list' && endpoint.inputSchema.length > 0);
2432
2468
  for (const node of nodes){
2433
2469
  let refs;
2434
2470
  try {
@@ -2452,7 +2488,7 @@ function validateChecks(c, at, rule, err) {
2452
2488
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.REF_SELF */.qj.REF_SELF, {
2453
2489
  node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node)
2454
2490
  }));
2455
- } else if (scope === 'input' && second && flow.bodyType !== 'list' && declaredInputs.size > 0 && !declaredInputs.has(second)) {
2491
+ } else if (scope === 'input' && second && namesKnown && !declaredInputs.has(second)) {
2456
2492
  warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.REF_INPUT_UNDECLARED */.qj.REF_INPUT_UNDECLARED, {
2457
2493
  node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node),
2458
2494
  input: second
@@ -2662,8 +2698,7 @@ function reach(start, outgoing) {
2662
2698
  nesting.set(id, max);
2663
2699
  return max;
2664
2700
  };
2665
- const start = nodes.find((n)=>n.type === 'trigger');
2666
- if (start && nestingFrom(start.id) > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_LOOP_DEPTH */.Or.MAX_LOOP_DEPTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.LOOP_TOO_DEEP */.qj.LOOP_TOO_DEEP, {
2701
+ if (nodes.some((n)=>n.type === 'trigger' && nestingFrom(n.id) > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_LOOP_DEPTH */.Or.MAX_LOOP_DEPTH)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.LOOP_TOO_DEEP */.qj.LOOP_TOO_DEEP, {
2667
2702
  max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_LOOP_DEPTH */.Or.MAX_LOOP_DEPTH
2668
2703
  }));
2669
2704
  }
@@ -3055,6 +3090,33 @@ const FLOW_LIMITS = {
3055
3090
  const FLOW_NODE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
3056
3091
  const FLOW_SLUG_PATTERN = /^[a-z][a-z0-9-]{2,62}$/;
3057
3092
  const FLOW_INPUT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,59}$/;
3093
+ const FLOW_ENDPOINT_PATH_PATTERN = /^[a-z][a-z0-9-]{0,62}$/;
3094
+ /** A Request step's path, or null when it answers on the flow's own URL. */ function triggerPath(node) {
3095
+ const path = node.config?.path;
3096
+ return typeof path === 'string' && path.trim() ? path.trim() : null;
3097
+ }
3098
+ /** Every Request step of a flow as a URL. */ function flowEndpoints(flow) {
3099
+ return (flow.nodes ?? []).filter((node)=>node?.type === 'trigger').map((node)=>{
3100
+ const path = triggerPath(node);
3101
+ if (!path) return {
3102
+ nodeId: node.id,
3103
+ path,
3104
+ bodyType: flow.bodyType ?? 'object',
3105
+ inputSchema: flow.inputSchema ?? []
3106
+ };
3107
+ const config = node.config;
3108
+ return {
3109
+ nodeId: node.id,
3110
+ path,
3111
+ bodyType: config.bodyType ?? 'object',
3112
+ inputSchema: Array.isArray(config.inputSchema) ? config.inputSchema : []
3113
+ };
3114
+ });
3115
+ }
3116
+ /** The URL a call came in on: `path` of a Request step, or null / empty for the flow's own URL. */ function findEndpoint(flow, path) {
3117
+ const wanted = path?.trim() || null;
3118
+ return flowEndpoints(flow).find((endpoint)=>endpoint.path === wanted);
3119
+ }
3058
3120
  /** Output ports a node of this type can have (a `respond` node ends the flow: none). */ function portsOf(node) {
3059
3121
  switch(node.type){
3060
3122
  case 'condition':
@@ -3097,7 +3159,10 @@ const FLOW_INPUT_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,59}$/;
3097
3159
  }
3098
3160
 
3099
3161
  __webpack_require__.d(__webpack_exports__, {
3100
- a5: () => (portsOf)
3162
+ Xr: () => (flowEndpoints),
3163
+ a5: () => (portsOf),
3164
+ um: () => (triggerPath),
3165
+ vS: () => (findEndpoint)
3101
3166
  }, {
3102
3167
  At: NON_CONTINUABLE_NODE_TYPES,
3103
3168
  Dn: RECORD_WRITE_TARGETS,
@@ -3105,6 +3170,7 @@ __webpack_require__.d(__webpack_exports__, {
3105
3170
  Q7: FLOW_INPUT_TYPES,
3106
3171
  Y3: FLOW_BODY_TYPES,
3107
3172
  YD: FLOW_SLUG_PATTERN,
3173
+ YO: FLOW_ENDPOINT_PATH_PATTERN,
3108
3174
  dA: nodeLabel,
3109
3175
  fO: FLOW_AUTH_MODES,
3110
3176
  fZ: FLOW_INPUT_NAME_PATTERN,
@@ -4100,8 +4166,7 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4100
4166
  {
4101
4167
  isActive: flow.isActive,
4102
4168
  authMode: flow.authMode,
4103
- bodyType: flow.bodyType,
4104
- inputSchema: flow.inputSchema ?? [],
4169
+ endpoints: (0,_flow_engine_flow_types_js__rspack_import_8/* .flowEndpoints */.Xr)(flow),
4105
4170
  calls: calledSlugs(flow.nodes)
4106
4171
  }
4107
4172
  ]))
@@ -4129,6 +4194,31 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4129
4194
  }
4130
4195
  });
4131
4196
  }
4197
+ /** A URL other flows' Call flow steps start at cannot be removed or renamed from under them. */ async assertEndpointsNotCalled(before, after) {
4198
+ const kept = new Set((0,_flow_engine_flow_types_js__rspack_import_8/* .flowEndpoints */.Xr)(after).map((e)=>e.path));
4199
+ const removed = (0,_flow_engine_flow_types_js__rspack_import_8/* .flowEndpoints */.Xr)(before).map((e)=>e.path).filter((path)=>!kept.has(path));
4200
+ if (!removed.length) return;
4201
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).find({
4202
+ select: {
4203
+ id: true,
4204
+ name: true,
4205
+ nodes: true
4206
+ }
4207
+ });
4208
+ for (const path of removed){
4209
+ const callers = flows.filter((flow)=>flow.id !== before.id && calledTargets(flow.nodes).some((t)=>t.slug === before.slug && t.endpoint === path)).map((flow)=>flow.name);
4210
+ if (!callers.length) continue;
4211
+ const url = `api-flows/${before.slug}${path ? `/${path}` : ''}`;
4212
+ throw new _nestjs_common__rspack_import_3.ConflictException({
4213
+ message: `The URL ${url} is called by: ${callers.join(', ')}. Change those flows first.`,
4214
+ messageKey: _config_message_keys_js__rspack_import_10/* .FLOW_MESSAGES.ENDPOINT_CALLED_BY_OTHER_FLOWS */.wu.ENDPOINT_CALLED_BY_OTHER_FLOWS,
4215
+ messageVariables: {
4216
+ url,
4217
+ flows: callers.join(', ')
4218
+ }
4219
+ });
4220
+ }
4221
+ }
4132
4222
  async assertSaveable(settings, user, selfId) {
4133
4223
  const { errors, warnings } = await this.validate(settings);
4134
4224
  if (errors.length) {
@@ -4204,6 +4294,7 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4204
4294
  if (merged.slug !== existing.slug) await this.assertNotCalled(existing.slug, [
4205
4295
  existing.id
4206
4296
  ]);
4297
+ else await this.assertEndpointsNotCalled(existing, merged);
4207
4298
  d.version = existing.version + 1;
4208
4299
  }
4209
4300
  }
@@ -4256,8 +4347,17 @@ FlowDefinitionService = _ts_decorate([
4256
4347
  typeof IPermissionActionRegistry === "undefined" ? Object : IPermissionActionRegistry
4257
4348
  ])
4258
4349
  ], FlowDefinitionService);
4350
+ /** What the `call_flow` nodes of a flow run: a slug, and the path of the Request step they start at (null: the flow's own URL). */ function calledTargets(nodes) {
4351
+ return (nodes ?? []).filter((node)=>node?.type === 'call_flow' && typeof node.config?.flowSlug === 'string').map((node)=>{
4352
+ const config = node.config;
4353
+ return {
4354
+ slug: config.flowSlug,
4355
+ endpoint: typeof config.endpoint === 'string' && config.endpoint ? config.endpoint : null
4356
+ };
4357
+ });
4358
+ }
4259
4359
  /** Slugs the `call_flow` nodes of a flow run. */ function calledSlugs(nodes) {
4260
- return (nodes ?? []).filter((node)=>node?.type === 'call_flow').map((node)=>node.config?.flowSlug).filter((slug)=>typeof slug === 'string');
4360
+ return calledTargets(nodes).map((target)=>target.slug);
4261
4361
  }
4262
4362
  function stripUndefined(value) {
4263
4363
  return Object.fromEntries(Object.entries(value).filter(([, v])=>v !== undefined));
@@ -4775,6 +4875,7 @@ class FlowExecutorService {
4775
4875
  timeoutMs: flowTimeout(definition.timeoutMs)
4776
4876
  };
4777
4877
  const state = {
4878
+ startAt: options.startAt,
4778
4879
  input: options.input,
4779
4880
  vars: {},
4780
4881
  context: {},
@@ -4898,10 +4999,10 @@ class FlowExecutorService {
4898
4999
  }
4899
5000
  /**
4900
5001
  * `call_flow`: runs another active flow as part of this run - same caller,
4901
- * transaction, test mode and step/HTTP/time budget. Returns what it answered: its `respond`
5002
+ * transaction, test mode and step/HTTP/time budget, from the Request step at `endpoint` (left out, its own URL). Returns what it answered: its `respond`
4902
5003
  * body, or the output of its last step. A 4xx/5xx answer or a rejection is passed on as the
4903
5004
  * caller's own; any other failure fails the calling node.
4904
- */ async callFlow(slug, input, parent, env) {
5005
+ */ async callFlow(slug, input, parent, env, endpoint) {
4905
5006
  if (parent.callStack.includes(slug)) {
4906
5007
  throw new flow_errors/* .FlowLimitError */.IM(message_keys/* .FLOW_ERROR_MESSAGES.CALLED_FLOW_RECURSION */.zr.CALLED_FLOW_RECURSION, {
4907
5008
  flow: slug,
@@ -4916,7 +5017,15 @@ class FlowExecutorService {
4916
5017
  });
4917
5018
  const flow = await this.findCallable(slug, parent.qr);
4918
5019
  await this.assertMayCall(flow, parent, env);
4919
- const checked = (0,flow_input_validator/* .validateFlowInput */.v)(flow.inputSchema ?? [], input, flow.bodyType);
5020
+ const settings = (0,flow_definition_service/* .toFlowSettings */.RK)(flow);
5021
+ const start = (0,flow_types/* .findEndpoint */.vS)(settings, endpoint);
5022
+ if (!start) {
5023
+ throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.CALLED_FLOW_ENDPOINT_NOT_FOUND */.zr.CALLED_FLOW_ENDPOINT_NOT_FOUND, {
5024
+ flow: slug,
5025
+ endpoint: endpoint ?? ''
5026
+ }, `Flow '${slug}' has no URL '${endpoint ?? ''}'.`);
5027
+ }
5028
+ const checked = (0,flow_input_validator/* .validateFlowInput */.v)(start.inputSchema, input, start.bodyType);
4920
5029
  if (checked.errors.length) {
4921
5030
  throw new flow_errors/* .FlowHttpError */.hk(400, 'Validation failed', checked.errors, {
4922
5031
  messageKey: message_keys/* .FLOW_MESSAGES.INPUT_INVALID */.wu.INPUT_INVALID,
@@ -4925,8 +5034,8 @@ class FlowExecutorService {
4925
5034
  }
4926
5035
  });
4927
5036
  }
4928
- const settings = (0,flow_definition_service/* .toFlowSettings */.RK)(flow);
4929
5037
  const child = {
5038
+ startAt: start.nodeId,
4930
5039
  input: checked.values,
4931
5040
  vars: {},
4932
5041
  context: {},
@@ -5022,7 +5131,7 @@ class FlowExecutorService {
5022
5131
  }
5023
5132
  return entityServices.get(entityCode);
5024
5133
  },
5025
- callFlow: (slug, input, state)=>this.callFlow(slug, input, state, env),
5134
+ callFlow: (slug, input, state, endpoint)=>this.callFlow(slug, input, state, env, endpoint),
5026
5135
  atomic: (state, work)=>this.atomic(state, work),
5027
5136
  commit: (state)=>this.commitProgress(state),
5028
5137
  http: flowHttpRequest,
@@ -5122,13 +5231,14 @@ FlowExecutorService = _ts_decorate([
5122
5231
  2300(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
5123
5232
  /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
5124
5233
  /* import */ var typeorm__rspack_import_1 = __webpack_require__(7158);
5125
- /* import */ var _config_message_keys_js__rspack_import_8 = __webpack_require__(5732);
5234
+ /* import */ var _config_message_keys_js__rspack_import_9 = __webpack_require__(5732);
5126
5235
  /* import */ var _entities_flow_definition_entity_js__rspack_import_2 = __webpack_require__(3770);
5127
5236
  /* import */ var _flow_engine_flow_input_validator_js__rspack_import_3 = __webpack_require__(6390);
5128
- /* import */ var _entity_builder_datasource_provider_js__rspack_import_4 = __webpack_require__(424);
5129
- /* import */ var _flow_definition_service_js__rspack_import_5 = __webpack_require__(5229);
5130
- /* import */ var _flow_execution_service_js__rspack_import_6 = __webpack_require__(7810);
5131
- /* import */ var _flow_executor_service_js__rspack_import_7 = __webpack_require__(4698);
5237
+ /* import */ var _flow_engine_flow_types_js__rspack_import_4 = __webpack_require__(6256);
5238
+ /* import */ var _entity_builder_datasource_provider_js__rspack_import_5 = __webpack_require__(424);
5239
+ /* import */ var _flow_definition_service_js__rspack_import_6 = __webpack_require__(5229);
5240
+ /* import */ var _flow_execution_service_js__rspack_import_7 = __webpack_require__(7810);
5241
+ /* import */ var _flow_executor_service_js__rspack_import_8 = __webpack_require__(4698);
5132
5242
  function _ts_decorate(decorators, target, key, desc) {
5133
5243
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5134
5244
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -5161,6 +5271,7 @@ function _ts_param(paramIndex, decorator) {
5161
5271
 
5162
5272
 
5163
5273
 
5274
+
5164
5275
  const HIDDEN_HEADERS = new Set([
5165
5276
  'authorization',
5166
5277
  'cookie',
@@ -5219,7 +5330,7 @@ const inputRejected = (errors)=>({
5219
5330
  body: {
5220
5331
  success: false,
5221
5332
  message: 'Validation failed',
5222
- messageKey: _config_message_keys_js__rspack_import_8/* .FLOW_MESSAGES.INPUT_INVALID */.wu.INPUT_INVALID,
5333
+ messageKey: _config_message_keys_js__rspack_import_9/* .FLOW_MESSAGES.INPUT_INVALID */.wu.INPUT_INVALID,
5223
5334
  errors
5224
5335
  },
5225
5336
  trace: [],
@@ -5246,19 +5357,20 @@ class FlowRuntimeService {
5246
5357
  }
5247
5358
  });
5248
5359
  }
5249
- /** The public entry: a caller hit `POST api-flows/:slug`. */ async handle(flow, body, caller, user) {
5360
+ /** The public entry: a caller hit `POST api-flows/:slug` or `POST api-flows/:slug/:endpoint`. */ async handle(flow, endpoint, body, caller, user) {
5250
5361
  const startedAt = new Date();
5251
- const checked = (0,_flow_engine_flow_input_validator_js__rspack_import_3/* .validateFlowInput */.v)(flow.inputSchema ?? [], body, flow.bodyType);
5362
+ const checked = (0,_flow_engine_flow_input_validator_js__rspack_import_3/* .validateFlowInput */.v)(endpoint.inputSchema, body, endpoint.bodyType);
5252
5363
  if (checked.errors.length) {
5253
5364
  const result = inputRejected(checked.errors);
5254
5365
  await this.logWebhook(flow, body, result, startedAt, user, caller);
5255
5366
  return result;
5256
5367
  }
5257
5368
  // A saved flow: saving it as `runAs: system` needed the system permission.
5258
- const result = await this.executor.run((0,_flow_definition_service_js__rspack_import_5/* .toFlowSettings */.RK)(flow), {
5369
+ const result = await this.executor.run((0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(flow), {
5259
5370
  mode: 'live',
5260
5371
  user,
5261
5372
  input: checked.values,
5373
+ startAt: endpoint.nodeId,
5262
5374
  request: requestInfo(caller),
5263
5375
  exposeDetails: false,
5264
5376
  systemAuthorized: true
@@ -5275,15 +5387,15 @@ class FlowRuntimeService {
5275
5387
  }
5276
5388
  if (dto.definition) {
5277
5389
  // The designer's current draft, even for a saved flow: `flow` above is kept only to attribute the run.
5278
- settings = (0,_flow_definition_service_js__rspack_import_5/* .toFlowSettings */.RK)(dto.definition);
5390
+ settings = (0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(dto.definition);
5279
5391
  permissionCode = dto.definition.permissionCode;
5280
5392
  } else if (flow) {
5281
- settings = (0,_flow_definition_service_js__rspack_import_5/* .toFlowSettings */.RK)(flow);
5393
+ settings = (0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(flow);
5282
5394
  permissionCode = flow.permissionCode;
5283
5395
  } else {
5284
5396
  throw new _nestjs_common__rspack_import_0.BadRequestException({
5285
5397
  message: 'Send a flowId or a definition to test.',
5286
- messageKey: _config_message_keys_js__rspack_import_8/* .FLOW_MESSAGES.NOTHING_TO_TEST */.wu.NOTHING_TO_TEST
5398
+ messageKey: _config_message_keys_js__rspack_import_9/* .FLOW_MESSAGES.NOTHING_TO_TEST */.wu.NOTHING_TO_TEST
5287
5399
  });
5288
5400
  }
5289
5401
  const mode = dto.mode ?? 'dry_run';
@@ -5300,21 +5412,32 @@ class FlowRuntimeService {
5300
5412
  if (validation.errors.length) {
5301
5413
  throw new _nestjs_common__rspack_import_0.BadRequestException({
5302
5414
  message: 'The flow has problems.',
5303
- messageKey: _config_message_keys_js__rspack_import_8/* .FLOW_MESSAGES.INVALID */.wu.INVALID,
5415
+ messageKey: _config_message_keys_js__rspack_import_9/* .FLOW_MESSAGES.INVALID */.wu.INVALID,
5304
5416
  errors: validation.errors.map((error)=>({
5305
5417
  field: 'flow',
5306
5418
  ...error
5307
5419
  }))
5308
5420
  });
5309
5421
  }
5422
+ const endpoint = (0,_flow_engine_flow_types_js__rspack_import_4/* .findEndpoint */.vS)(settings, dto.endpoint);
5423
+ if (!endpoint) {
5424
+ throw new _nestjs_common__rspack_import_0.BadRequestException({
5425
+ message: `This flow has no URL '${dto.endpoint ?? ''}'.`,
5426
+ messageKey: _config_message_keys_js__rspack_import_9/* .FLOW_MESSAGES.ENDPOINT_NOT_FOUND */.wu.ENDPOINT_NOT_FOUND,
5427
+ messageVariables: {
5428
+ endpoint: dto.endpoint ?? ''
5429
+ }
5430
+ });
5431
+ }
5310
5432
  const startedAt = new Date();
5311
5433
  const simulated = simulatedCaller(caller, dto.request);
5312
- const body = dto.input ?? (settings.bodyType === 'list' ? [] : {});
5313
- const checked = (0,_flow_engine_flow_input_validator_js__rspack_import_3/* .validateFlowInput */.v)(settings.inputSchema, body, settings.bodyType);
5434
+ const body = dto.input ?? (endpoint.bodyType === 'list' ? [] : {});
5435
+ const checked = (0,_flow_engine_flow_input_validator_js__rspack_import_3/* .validateFlowInput */.v)(endpoint.inputSchema, body, endpoint.bodyType);
5314
5436
  const result = checked.errors.length ? inputRejected(checked.errors) : await this.executor.run(settings, {
5315
5437
  mode,
5316
5438
  user,
5317
5439
  input: checked.values,
5440
+ startAt: endpoint.nodeId,
5318
5441
  request: requestInfo(simulated),
5319
5442
  exposeDetails: true,
5320
5443
  systemAuthorized: true
@@ -5355,16 +5478,16 @@ FlowRuntimeService = _ts_decorate([
5355
5478
  (0,_nestjs_common__rspack_import_0.Injectable)({
5356
5479
  scope: _nestjs_common__rspack_import_0.Scope.REQUEST
5357
5480
  }),
5358
- _ts_param(0, (0,_nestjs_common__rspack_import_0.Inject)(_entity_builder_datasource_provider_js__rspack_import_4/* .EntityBuilderDataSourceProvider */.D)),
5359
- _ts_param(1, (0,_nestjs_common__rspack_import_0.Inject)(_flow_executor_service_js__rspack_import_7/* .FlowExecutorService */.F)),
5360
- _ts_param(2, (0,_nestjs_common__rspack_import_0.Inject)(_flow_execution_service_js__rspack_import_6/* .FlowExecutionService */.A)),
5361
- _ts_param(3, (0,_nestjs_common__rspack_import_0.Inject)(_flow_definition_service_js__rspack_import_5/* .FlowDefinitionService */.Tk)),
5481
+ _ts_param(0, (0,_nestjs_common__rspack_import_0.Inject)(_entity_builder_datasource_provider_js__rspack_import_5/* .EntityBuilderDataSourceProvider */.D)),
5482
+ _ts_param(1, (0,_nestjs_common__rspack_import_0.Inject)(_flow_executor_service_js__rspack_import_8/* .FlowExecutorService */.F)),
5483
+ _ts_param(2, (0,_nestjs_common__rspack_import_0.Inject)(_flow_execution_service_js__rspack_import_7/* .FlowExecutionService */.A)),
5484
+ _ts_param(3, (0,_nestjs_common__rspack_import_0.Inject)(_flow_definition_service_js__rspack_import_6/* .FlowDefinitionService */.Tk)),
5362
5485
  _ts_metadata("design:type", Function),
5363
5486
  _ts_metadata("design:paramtypes", [
5364
- typeof _entity_builder_datasource_provider_js__rspack_import_4/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : _entity_builder_datasource_provider_js__rspack_import_4/* .EntityBuilderDataSourceProvider */.D,
5365
- typeof _flow_executor_service_js__rspack_import_7/* .FlowExecutorService */.F === "undefined" ? Object : _flow_executor_service_js__rspack_import_7/* .FlowExecutorService */.F,
5366
- typeof _flow_execution_service_js__rspack_import_6/* .FlowExecutionService */.A === "undefined" ? Object : _flow_execution_service_js__rspack_import_6/* .FlowExecutionService */.A,
5367
- typeof _flow_definition_service_js__rspack_import_5/* .FlowDefinitionService */.Tk === "undefined" ? Object : _flow_definition_service_js__rspack_import_5/* .FlowDefinitionService */.Tk
5487
+ typeof _entity_builder_datasource_provider_js__rspack_import_5/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : _entity_builder_datasource_provider_js__rspack_import_5/* .EntityBuilderDataSourceProvider */.D,
5488
+ typeof _flow_executor_service_js__rspack_import_8/* .FlowExecutorService */.F === "undefined" ? Object : _flow_executor_service_js__rspack_import_8/* .FlowExecutorService */.F,
5489
+ typeof _flow_execution_service_js__rspack_import_7/* .FlowExecutionService */.A === "undefined" ? Object : _flow_execution_service_js__rspack_import_7/* .FlowExecutionService */.A,
5490
+ typeof _flow_definition_service_js__rspack_import_6/* .FlowDefinitionService */.Tk === "undefined" ? Object : _flow_definition_service_js__rspack_import_6/* .FlowDefinitionService */.Tk
5368
5491
  ])
5369
5492
  ], FlowRuntimeService);
5370
5493
 
@@ -71,6 +71,8 @@ const DEFAULT_MAX_KEYS = 10_000;
71
71
  }
72
72
  }
73
73
 
74
+ // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow.types.ts
75
+ var flow_types = __webpack_require__(6256);
74
76
  ;// CONCATENATED MODULE: ./projects/nestjs-entity-builder/src/guards/flow-access.guard.ts
75
77
  function _ts_decorate(decorators, target, key, desc) {
76
78
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -106,6 +108,7 @@ function _ts_param(paramIndex, decorator) {
106
108
 
107
109
 
108
110
 
111
+
109
112
  const limiter = new FlowRateLimiter();
110
113
  class FlowAccessGuard {
111
114
  moduleRef;
@@ -123,11 +126,13 @@ class FlowAccessGuard {
123
126
  strict: false
124
127
  });
125
128
  const flow = slug ? await runtime.findActive(slug) : null;
126
- if (!flow) throw new common_.NotFoundException({
129
+ const endpoint = flow ? (0,flow_types/* .findEndpoint */.vS)(flow, request.params?.endpoint) : undefined;
130
+ if (!flow || !endpoint) throw new common_.NotFoundException({
127
131
  message: 'Not found',
128
132
  messageKey: message_keys/* .FLOW_MESSAGES.NOT_FOUND */.wu.NOT_FOUND
129
133
  });
130
134
  request.flowDefinition = flow;
135
+ request.flowEndpoint = endpoint;
131
136
  const ip = request.ip ?? request.socket?.remoteAddress ?? 'unknown';
132
137
  if (!limiter.hit(`${flow.id}|${ip}`, flow.rateLimitPerMinute)) {
133
138
  throw new common_.HttpException({