@flusys/nestjs-entity-builder 9.1.0 → 9.1.2

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.
Files changed (65) hide show
  1. package/README.md +59 -26
  2. package/config/entity-builder.constants.d.ts +2 -0
  3. package/config/message-keys.d.ts +96 -12
  4. package/controllers/definition-bundle.controller.d.ts +15 -0
  5. package/controllers/flow-definition.controller.d.ts +7 -1
  6. package/controllers/index.d.ts +1 -1
  7. package/dtos/definition-bundle.dto.d.ts +57 -0
  8. package/dtos/flow.dto.d.ts +12 -20
  9. package/dtos/index.d.ts +1 -0
  10. package/entities/flow-definition.entity.d.ts +3 -9
  11. package/entities/flow-execution.entity.d.ts +3 -0
  12. package/entities/flow-version.entity.d.ts +10 -0
  13. package/entities/index.d.ts +3 -3
  14. package/fesm/21.js +101 -16
  15. package/fesm/458.js +695 -0
  16. package/fesm/{996.js → 677.js} +1582 -1047
  17. package/fesm/794.js +135 -240
  18. package/fesm/{489.js → 857.js} +216 -289
  19. package/fesm/{719.js → 870.js} +1955 -220
  20. package/fesm/{362.js → 897.js} +566 -2
  21. package/fesm/{606.js → 946.js} +564 -290
  22. package/fesm/config/index.js +15 -2
  23. package/fesm/controllers/index.js +9 -9
  24. package/fesm/docs/index.js +7 -4
  25. package/fesm/dtos/index.js +451 -256
  26. package/fesm/entities/index.js +58 -3
  27. package/fesm/guards/index.js +101 -22
  28. package/fesm/index.js +444 -948
  29. package/fesm/interfaces/index.js +92 -4
  30. package/fesm/modules/index.js +10 -628
  31. package/fesm/rule-engine/index.js +12 -3
  32. package/fesm/services/index.js +97 -27
  33. package/flow-engine/flow-api-key.d.ts +3 -7
  34. package/flow-engine/flow-graph.validator.d.ts +4 -2
  35. package/flow-engine/flow-permission-logic.d.ts +2 -0
  36. package/flow-engine/flow-run.types.d.ts +17 -8
  37. package/flow-engine/flow.types.d.ts +107 -15
  38. package/guards/designer-writable.interceptor.d.ts +12 -0
  39. package/guards/index.d.ts +1 -0
  40. package/interfaces/definition-bundle.interface.d.ts +111 -0
  41. package/interfaces/entity-builder-module.interface.d.ts +6 -0
  42. package/interfaces/flow-definition.interface.d.ts +13 -9
  43. package/interfaces/generic-record.interface.d.ts +11 -1
  44. package/interfaces/index.d.ts +1 -0
  45. package/package.json +5 -6
  46. package/rule-engine/rule-engine.service.d.ts +3 -1
  47. package/rule-engine/rule-permission.d.ts +3 -7
  48. package/rule-engine/rule-regex.d.ts +0 -2
  49. package/rule-engine/rule-values.d.ts +4 -1
  50. package/rule-engine/rule.interface.d.ts +2 -3
  51. package/services/definition-bundle.diff.d.ts +62 -0
  52. package/services/definition-bundle.file.d.ts +7 -0
  53. package/services/definition-bundle.service.d.ts +32 -0
  54. package/services/entity-builder-config.service.d.ts +2 -0
  55. package/services/entity-flow-scaffold.d.ts +1 -1
  56. package/services/flow-definition.service.d.ts +25 -12
  57. package/services/flow-execution.service.d.ts +7 -0
  58. package/services/flow-executor.service.d.ts +8 -8
  59. package/services/generic-entity.service.d.ts +5 -1
  60. package/services/index.d.ts +3 -1
  61. package/services/schema-evolution.service.d.ts +3 -1
  62. package/services/schema-sync.service.d.ts +5 -1
  63. package/controllers/flow-api-key.controller.d.ts +0 -14
  64. package/entities/flow-api-key.entity.d.ts +0 -10
  65. package/services/flow-api-key.service.d.ts +0 -31
@@ -7,41 +7,11 @@ import * as __rspack_external_node_os_4f3c9d58 from "node:os";
7
7
  import * as __rspack_external_node_path_806ed179 from "node:path";
8
8
  import * as __rspack_external_node_url_3991086a from "node:url";
9
9
  import * as __rspack_external_node_worker_threads_28ca7740 from "node:worker_threads";
10
+ import * as __rspack_external__flusys_nestjs_shared_exceptions_3edf22e9 from "@flusys/nestjs-shared/exceptions";
10
11
  import * as __rspack_external_node_dns_c6249ebb from "node:dns";
11
- export const __rspack_esm_id = 996;
12
- export const __rspack_esm_ids = [996];
12
+ export const __rspack_esm_id = 677;
13
+ export const __rspack_esm_ids = [677];
13
14
  export const __webpack_modules__ = {
14
- 6576(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
15
- const ENTITY_BUILDER_MODULE_OPTIONS = 'ENTITY_BUILDER_MODULE_OPTIONS';
16
- /**
17
- * Identifier whitelist enforced on every EntityDefinition/FieldDefinition `code`.
18
- * Also the exact contract SchemaSyncService's DDL engine validates against
19
- * before generating a table/column name.
20
- */ const ENTITY_CODE_PATTERN = /^[a-z][a-z0-9_]{2,59}$/;
21
- /** Matches the `code` column length on both EntityDefinition and FieldDefinition. */ const ENTITY_CODE_MAX_LENGTH = 63;
22
- /** Defense-in-depth prefix applied to every dynamically created table name. */ const ENTITY_TABLE_PREFIX = 'eb_';
23
- /** The generic API controller's record endpoints - each can be created as a ready-made flow with the entity. */ const ENTITY_FLOW_ENDPOINTS = [
24
- 'insert',
25
- 'insertMany',
26
- 'getById',
27
- 'getByIds',
28
- 'getAll',
29
- 'getByFilter',
30
- 'update',
31
- 'updateMany',
32
- 'bulkUpsert',
33
- 'delete'
34
- ];
35
-
36
- __webpack_require__.d(__webpack_exports__, {
37
- }, {
38
- CP: ENTITY_BUILDER_MODULE_OPTIONS,
39
- WT: ENTITY_FLOW_ENDPOINTS,
40
- vs: ENTITY_CODE_PATTERN
41
- });
42
-
43
-
44
- },
45
15
  982(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
46
16
  /** Every entity the entity builder module publishes under */ const ENTITY_BUILDER_EVENT_ENTITIES = {
47
17
  ENTITY_DEFINITION: 'entity_definition',
@@ -59,6 +29,60 @@ __webpack_require__.d(__webpack_exports__, {
59
29
  });
60
30
 
61
31
 
32
+ },
33
+ 4816(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
34
+ /* import */ var node_crypto__rspack_import_0 = __webpack_require__(2561);
35
+ /* import */ var _flow_types_js__rspack_import_1 = __webpack_require__(6256);
36
+
37
+
38
+ const API_KEY_PREFIX_LENGTH = 11; // "fk_" + 8 hex
39
+ const hashApiKey = (key)=>(0,node_crypto__rspack_import_0.createHash)('sha256').update(key).digest('hex');
40
+ const apiKeyPrefix = (key)=>key.slice(0, API_KEY_PREFIX_LENGTH);
41
+ /** A key as the designer makes it: `fk_<8 hex>_<32 hex>`. */ function looksLikeApiKey(key) {
42
+ return typeof key === 'string' && /^fk_[0-9a-f]{8}_[0-9a-f]{32}$/.test(key);
43
+ }
44
+ /** Constant-time comparison of a presented key against a stored hash. */ function apiKeyMatches(presented, storedHash) {
45
+ const a = Buffer.from(hashApiKey(presented), 'hex');
46
+ const b = Buffer.from(storedHash, 'hex');
47
+ return a.length === b.length && (0,node_crypto__rspack_import_0.timingSafeEqual)(a, b);
48
+ }
49
+ /** Whether `presented` is the key a Request step stores. */ const apiKeyOpens = (stored, presented)=>!!stored && looksLikeApiKey(presented) && apiKeyMatches(presented, stored.hash);
50
+ /**
51
+ * What is saved of the Request steps' API keys: a plain key the designer made becomes its prefix and
52
+ * hash, so the key itself is never stored; a step without `api_key` access keeps no key at all.
53
+ */ function sealApiKeys(nodes) {
54
+ return nodes.map((node)=>{
55
+ if (node.type !== 'trigger' || node.config?.['apiKey'] === undefined) return node;
56
+ const { apiKey, ...config } = node.config;
57
+ if (config['authMode'] !== 'api_key') return {
58
+ ...node,
59
+ config
60
+ };
61
+ if (looksLikeApiKey(apiKey)) return {
62
+ ...node,
63
+ config: {
64
+ ...config,
65
+ apiKey: {
66
+ prefix: apiKeyPrefix(apiKey),
67
+ hash: hashApiKey(apiKey)
68
+ }
69
+ }
70
+ };
71
+ return (0,_flow_types_js__rspack_import_1/* .isStoredApiKey */.xA)(apiKey) ? node : {
72
+ ...node,
73
+ config
74
+ };
75
+ });
76
+ }
77
+
78
+ __webpack_require__.d(__webpack_exports__, {
79
+ Jb: () => (looksLikeApiKey),
80
+ ue: () => (sealApiKeys)
81
+ }, {
82
+ NT: apiKeyOpens
83
+ });
84
+
85
+
62
86
  },
63
87
  8333(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
64
88
 
@@ -80,10 +104,6 @@ var message_keys = __webpack_require__(5732);
80
104
  var flow_errors = __webpack_require__(8542);
81
105
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow.types.ts
82
106
  var flow_types = __webpack_require__(6256);
83
- // EXTERNAL MODULE: external "@flusys/nestjs-shared/constants"
84
- var constants_ = __webpack_require__(9140);
85
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/interfaces/generic-record.interface.ts
86
- var generic_record_interface = __webpack_require__(3372);
87
107
  ;// CONCATENATED MODULE: external "node:os"
88
108
 
89
109
  ;// CONCATENATED MODULE: external "node:path"
@@ -302,7 +322,7 @@ const timeoutError = (job)=>new flow_errors/* .FlowCodeError */.yL(message_keys/
302
322
  const unavailableError = (node, detail)=>new flow_errors/* .FlowCodeError */.yL(message_keys/* .FLOW_ERROR_MESSAGES.CODE_UNAVAILABLE */.zr.CODE_UNAVAILABLE, {
303
323
  node
304
324
  }, `The code sandbox could not run the code: ${detail}`);
305
- let CodeSandboxPool = class CodeSandboxPool {
325
+ let flow_code_sandbox_CodeSandboxPool = class CodeSandboxPool {
306
326
  idle = [];
307
327
  running = new Map();
308
328
  queue = [];
@@ -432,14 +452,12 @@ let pool;
432
452
  detail: error.message
433
453
  }, error.message);
434
454
  }
435
- pool ??= new CodeSandboxPool();
455
+ pool ??= new flow_code_sandbox_CodeSandboxPool();
436
456
  return pool.run(code, input, timeoutMs, node);
437
457
  }
438
458
 
439
459
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow-graph.validator.ts
440
460
  var flow_graph_validator = __webpack_require__(3186);
441
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/rule-engine/rule-permission.ts
442
- var rule_permission = __webpack_require__(5191);
443
461
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/rule-engine/rule-values.ts
444
462
  var rule_values = __webpack_require__(6622);
445
463
  ;// CONCATENATED MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow-node-handlers.ts
@@ -451,16 +469,7 @@ var rule_values = __webpack_require__(6622);
451
469
 
452
470
 
453
471
 
454
-
455
-
456
-
457
- const PERMISSION_ACTION = {
458
- insert: 'create',
459
- update: 'update',
460
- delete: 'delete',
461
- read: 'read'
462
- };
463
- /** With `env`, `lookup` expressions read through the flow (permission check, transaction). */ function evaluationContext(state, env) {
472
+ /** With `env`, `lookup` expressions read through the flow (its transaction). */ function evaluationContext(state, env) {
464
473
  const base = {
465
474
  input: state.input,
466
475
  vars: state.vars,
@@ -478,7 +487,6 @@ const PERMISSION_ACTION = {
478
487
  function lookupResolver(env, state) {
479
488
  return {
480
489
  find: async (entityCode, query)=>{
481
- await authorize(env, state, entityCode, 'read');
482
490
  const service = await env.entityService(entityCode, state.qr);
483
491
  const { data, total } = await service.getAll(undefined, {
484
492
  filter: query.filter,
@@ -522,24 +530,9 @@ async function mapValues(map, env, state) {
522
530
  });
523
531
  };
524
532
  for (const [key, item] of Object.entries(map ?? {})){
525
- if ((0,generic_record_interface/* .isRecordFilterCondition */.t1)(item)) {
526
- if (item.op === 'is_null' || item.op === 'not_null') {
527
- out[key] = {
528
- op: item.op
529
- };
530
- continue;
531
- }
532
- const value = item.value === undefined ? undefined : await env.ruleEngine.resolveExpression(item.value, ctx);
533
- if (value === undefined) absent(key);
534
- else out[key] = {
535
- op: item.op,
536
- value
537
- };
538
- } else {
539
- const value = await env.ruleEngine.resolveExpression(item, ctx);
540
- if (value === undefined) absent(key);
541
- else out[key] = (0,rule_values/* .equalsOperand */.$b)(value);
542
- }
533
+ const entry = await (0,rule_values/* .resolveFilterEntry */.vY)(item, (expression)=>env.ruleEngine.resolveExpression(expression, ctx));
534
+ if ((0,rule_values/* .isFilterValueMissing */.kp)(entry)) absent(key);
535
+ else out[key] = entry;
543
536
  }
544
537
  return out;
545
538
  }
@@ -556,7 +549,7 @@ async function runValidate(cfg, env, state) {
556
549
  passed: true
557
550
  }
558
551
  };
559
- const status = cfg.statusCode ?? (failed.every((check)=>(0,flow_graph_validator/* .usesPermissionFunction */.u)(check.rule)) ? 403 : 400);
552
+ const status = cfg.statusCode ?? (failed.every((check)=>(0,flow_graph_validator/* .usesPermissionFunction */.u9)(check.rule)) ? 403 : 400);
560
553
  const errors = failed.map((check)=>({
561
554
  field: check.field || check.id,
562
555
  message: check.message
@@ -569,21 +562,44 @@ async function runValidate(cfg, env, state) {
569
562
  }
570
563
  });
571
564
  }
572
- /** `{ allowed, missing }` - `missing` lists the codes the caller does not hold. */ async function runPermissionCheck(cfg, env, state) {
573
- const codes = (0,rule_permission/* .parsePermissionCodes */.PS)(cfg.codes);
574
- let branch;
575
- if (cfg.branch === null) branch = null;
576
- else if (cfg.branch !== undefined) branch = (0,rule_permission/* .toBranchArgument */.lH)([
577
- await env.ruleEngine.resolveExpression(cfg.branch, evaluationContext(state, env))
578
- ], 0);
579
- const permissions = state.permissions ?? env.ruleEngine.createPermissionResolver(state.user);
580
- const granted = state.user ? await permissions.granted(branch) : [];
581
- const missing = codes.filter((code)=>!(0,utils_.matchesPermission)(granted, code));
582
- const allowed = codes.length > 0 && (cfg.match === 'all' ? missing.length === 0 : missing.length < codes.length);
583
- const output = {
565
+ /** `{ allowed, missing }` - whether the user passes the AND / OR rule; `missing` lists the codes it names that the user does not hold. */ async function runPermissionCheck(cfg, env, state) {
566
+ const granted = await permissionCodes(cfg, env, state);
567
+ const missing = (0,utils_.permissionLogicCodes)(cfg.permissions).filter((code)=>!(0,utils_.matchesPermission)(granted, code));
568
+ const allowed = (0,utils_.evaluatePermissionLogic)(cfg.permissions, granted).passed;
569
+ return deniedResult(cfg, {
584
570
  allowed,
585
571
  missing
586
- };
572
+ }, allowed);
573
+ }
574
+ /** The codes the check's user holds in its company / branch; none without a user, for a set company that resolves to nothing, or outside the user's granted companies and branches. */ async function permissionCodes(cfg, env, state) {
575
+ const caller = state.user;
576
+ const asCaller = cfg.subject !== 'user';
577
+ const ctx = evaluationContext(state, env);
578
+ const resolveId = async (expression)=>expression === undefined ? null : oneId(await env.ruleEngine.resolveExpression(expression, ctx));
579
+ const userId = asCaller ? caller?.id ?? null : await resolveId(cfg.userId);
580
+ if (!userId) return [];
581
+ const currentCompanyId = caller?.companyId ?? null;
582
+ const companyId = cfg.companyId === undefined ? currentCompanyId : await resolveId(cfg.companyId);
583
+ if (cfg.companyId !== undefined && !companyId) return [];
584
+ const branchId = await resolveId(cfg.branchId);
585
+ if (asCaller && companyId === currentCompanyId) {
586
+ const permissions = state.permissions ?? env.ruleEngine.createPermissionResolver(caller);
587
+ if (cfg.branchId === undefined) return permissions.granted(undefined);
588
+ if (branchId === null || branchId === caller?.branchId) return permissions.granted(branchId);
589
+ }
590
+ if (companyId && !await isGrantedScope(userId, companyId, branchId, env)) return [];
591
+ return env.ruleEngine.permissionCodes({
592
+ userId,
593
+ companyId,
594
+ branchId
595
+ });
596
+ }
597
+ async function isGrantedScope(userId, companyId, branchId, env) {
598
+ const { companies, branches } = await env.companyAccess(userId);
599
+ if (!companies.some((company)=>company.id === companyId)) return false;
600
+ return branchId === null || branches.some((branch)=>branch.id === branchId && branch.companyId === companyId);
601
+ }
602
+ /** A denied check: `onDenied: branch` follows the `denied` port, otherwise the run is answered `statusCode` (403). */ function deniedResult(cfg, output, allowed) {
587
603
  if (cfg.onDenied === 'branch') return {
588
604
  output,
589
605
  port: allowed ? 'allowed' : 'denied'
@@ -599,6 +615,68 @@ async function runValidate(cfg, env, state) {
599
615
  output
600
616
  };
601
617
  }
618
+ /** One id, a list of ids, or nothing (undefined / null / empty text), each id once. */ function idList(value) {
619
+ const items = Array.isArray(value) ? value : [
620
+ value
621
+ ];
622
+ return [
623
+ ...new Set(items.filter((item)=>item !== undefined && item !== null && item !== '').map(String))
624
+ ];
625
+ }
626
+ /** One id from an expression's value; anything but non-empty text is none. */ function oneId(value) {
627
+ return typeof value === 'string' && value ? value : null;
628
+ }
629
+ /**
630
+ * `{ userId, currentCompanyId, currentBranchId, companies, branches }` - what the user (the signed-in
631
+ * caller, or the one `userId` names) may work in; none without a user or the company feature. With
632
+ * `companyIds` / `branchIds` it also answers `allowed` and `missing`: every id asked for must be within
633
+ * reach (`branchMatch`, among the branches of `branchCompany`), and a target that resolves to nothing
634
+ * is a denial, never a pass.
635
+ */ async function runCompanyBranchCheck(cfg, env, state) {
636
+ const ctx = evaluationContext(state, env);
637
+ const resolve = async (expression)=>expression === undefined ? undefined : env.ruleEngine.resolveExpression(expression, ctx);
638
+ const asCaller = cfg.subject !== 'user';
639
+ const userId = asCaller ? state.user?.id ?? null : oneId(await resolve(cfg.userId));
640
+ const { companies, branches } = userId ? await env.companyAccess(userId) : {
641
+ companies: [],
642
+ branches: []
643
+ };
644
+ const currentCompanyId = asCaller ? state.user?.companyId ?? null : null;
645
+ const currentBranchId = asCaller ? state.user?.branchId ?? null : null;
646
+ const output = {
647
+ userId,
648
+ currentCompanyId,
649
+ currentBranchId,
650
+ companies,
651
+ branches
652
+ };
653
+ if (cfg.companyIds === undefined && cfg.branchIds === undefined) return {
654
+ output
655
+ };
656
+ const wanted = async (expression)=>expression === undefined ? null : idList(await resolve(expression));
657
+ const wantedCompanies = await wanted(cfg.companyIds);
658
+ const wantedBranches = await wanted(cfg.branchIds);
659
+ const granted = branches.filter((branch)=>cfg.branchCompany !== 'current' || branch.companyId === currentCompanyId).map((branch)=>branch.id);
660
+ const reachable = await reachableBranches(cfg.branchMatch ?? 'direct', granted, wantedBranches, env);
661
+ const missing = {
662
+ companyIds: (wantedCompanies ?? []).filter((id)=>!companies.some((company)=>company.id === id)),
663
+ branchIds: (wantedBranches ?? []).filter((id)=>!reachable.has(id))
664
+ };
665
+ const nothingToCheck = wantedCompanies?.length === 0 || wantedBranches?.length === 0;
666
+ const allowed = !nothingToCheck && !missing.companyIds.length && !missing.branchIds.length;
667
+ return deniedResult(cfg, {
668
+ ...output,
669
+ allowed,
670
+ missing
671
+ }, allowed);
672
+ }
673
+ /** The branches a check passes: the granted ones (direct), and with `within` every branch under them too. */ async function reachableBranches(match, granted, wanted, env) {
674
+ if (match !== 'within' || !wanted?.length || !granted.length || !env.branchTree || wanted.every((id)=>granted.includes(id))) return new Set(granted);
675
+ return new Set([
676
+ ...granted,
677
+ ...await env.branchTree(granted)
678
+ ]);
679
+ }
602
680
  const USES_PERMISSIONS = /\b(permissions|hasPermission|hasAnyPermission|hasAllPermissions)\b/;
603
681
  /** The caller as a code step sees it; permission codes (current branch) are fetched only for code that reads them. */ async function codeUser(code, state) {
604
682
  const user = state.user;
@@ -641,16 +719,6 @@ function asId(value) {
641
719
  });
642
720
  return value;
643
721
  }
644
- /** Fails closed: the caller needs the entity permission; the system only when that was granted for this run; anything else is refused. */ async function authorize(env, state, entityCode, action) {
645
- if (state.runAs === 'caller') return env.assertPermission((0,constants_.dynamicEntityPermission)(entityCode, PERMISSION_ACTION[action]), state.user);
646
- if (state.runAs === 'system' && state.systemAuthorized === true) return;
647
- throw new flow_errors/* .FlowHttpError */.hk(403, 'This flow is not allowed to read or write records as the system.', undefined, {
648
- messageKey: message_keys/* .FLOW_ERROR_MESSAGES.RUN_AS_NOT_ALLOWED */.zr.RUN_AS_NOT_ALLOWED,
649
- messageVariables: {
650
- runAs: String(state.runAs)
651
- }
652
- });
653
- }
654
722
  function writeOptions(state) {
655
723
  return state.qr ? {
656
724
  queryRunner: state.qr,
@@ -727,15 +795,16 @@ async function updateRecord(entityCode, service, values, state) {
727
795
  }
728
796
  /** @returns false when the record does not exist and `skip` allows that. */ async function deleteRecord(entityCode, service, id, skipMissing, state) {
729
797
  countWrite(state);
798
+ let action;
730
799
  try {
731
- await service.delete(id, state.user, writeOptions(state));
800
+ action = await service.delete(id, state.user, writeOptions(state));
732
801
  } catch (error) {
733
802
  if (skipMissing && error instanceof common_.NotFoundException) return false;
734
803
  throw error;
735
804
  }
736
805
  if (state.qr) state.pendingEvents.push({
737
806
  entityCode,
738
- action: 'deleted',
807
+ action,
739
808
  row: {
740
809
  id
741
810
  }
@@ -780,8 +849,6 @@ async function insertChildren(node, child, parentId, env, state) {
780
849
  * A write of several records (many, or any children) is all-or-nothing.
781
850
  */ async function runInsert(node, cfg, env, state) {
782
851
  const children = cfg.children ?? [];
783
- await authorize(env, state, cfg.entityCode, 'insert');
784
- for (const child of children)await authorize(env, state, child.entityCode, 'insert');
785
852
  const service = await env.entityService(cfg.entityCode, state.qr);
786
853
  const insertOne = async ()=>{
787
854
  const saved = await insertRecord(cfg.entityCode, service, await mapFieldValues(cfg.fields, env, state), state);
@@ -835,8 +902,6 @@ async function insertChildren(node, child, parentId, env, state) {
835
902
  }
836
903
  /** one: the saved record (null when skipped). many / filter: `{ items, count, updated, created, skipped }`, all-or-nothing. */ async function runUpdate(node, cfg, env, state) {
837
904
  const target = writeTarget(cfg);
838
- await authorize(env, state, cfg.entityCode, 'update');
839
- if (target !== 'filter' && cfg.onNotFound === 'insert') await authorize(env, state, cfg.entityCode, 'insert');
840
905
  const service = await env.entityService(cfg.entityCode, state.qr);
841
906
  if (target === 'one') return {
842
907
  output: (await updateOrCreate(cfg, service, undefined, env, state)).row
@@ -867,7 +932,6 @@ async function insertChildren(node, child, parentId, env, state) {
867
932
  /** one: `{ id, deleted }`. many / filter: `{ ids, count, skipped }`, all-or-nothing; an id listed twice is deleted once. */ async function runDelete(node, cfg, env, state) {
868
933
  const target = writeTarget(cfg);
869
934
  const skipMissing = cfg.onNotFound === 'skip';
870
- await authorize(env, state, cfg.entityCode, 'delete');
871
935
  const service = await env.entityService(cfg.entityCode, state.qr);
872
936
  const idOf = async (item)=>asId(cfg.id === undefined ? itemId(item) : await env.ruleEngine.resolveExpression(cfg.id, evaluationContext(state, env)));
873
937
  if (target === 'one') {
@@ -897,6 +961,377 @@ async function insertChildren(node, child, parentId, env, state) {
897
961
  })
898
962
  };
899
963
  }
964
+ /** As strict as the email module's own check, and without the characters that would break an address header. */ const EMAIL_PATTERN = /^[^\s@,;<>"]+@[^\s@,;<>"]+\.[^\s@,;<>"]+$/;
965
+ const MAX_EMAIL_LENGTH = 254;
966
+ const shown = (value)=>String(typeof value === 'object' && value !== null ? JSON.stringify(value) : value).slice(0, 100);
967
+ /** A value as message text: nothing for null / undefined, JSON for an object. */ function asText(value) {
968
+ if (value === null || value === undefined) return '';
969
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
970
+ }
971
+ /** Reserves `count` sends of the run's budget before any goes out, so a step never sends only some of its messages because of the limit. */ function countMessages(state, count) {
972
+ if (state.messages + count > flow_types/* .FLOW_LIMITS.MAX_MESSAGES_PER_RUN */.Or.MAX_MESSAGES_PER_RUN) throw new flow_errors/* .FlowLimitError */.IM(message_keys/* .FLOW_ERROR_MESSAGES.TOO_MANY_MESSAGES */.zr.TOO_MANY_MESSAGES, {
973
+ max: flow_types/* .FLOW_LIMITS.MAX_MESSAGES_PER_RUN */.Or.MAX_MESSAGES_PER_RUN
974
+ });
975
+ state.messages += count;
976
+ }
977
+ function assertRecipientCount(node, count, max) {
978
+ if (count > max) throw new flow_errors/* .FlowLimitError */.IM(message_keys/* .FLOW_ERROR_MESSAGES.TOO_MANY_RECIPIENTS */.zr.TOO_MANY_RECIPIENTS, {
979
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
980
+ count,
981
+ max
982
+ });
983
+ }
984
+ /** Whatever the module threw, or its `success: false`, fails the step with the reason (a limit stays a limit). */ function sendFailure(key, node, error) {
985
+ if (error instanceof flow_errors/* .FlowKeyedError */.Hp || error instanceof flow_errors/* .FlowHttpError */.hk) return error;
986
+ const detail = error instanceof Error ? error.message : String(error);
987
+ return new flow_errors/* .FlowKeyedError */.Hp(key, {
988
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
989
+ detail
990
+ }, `"${(0,flow_types/* .nodeLabel */.dA)(node)}" could not send: ${detail}`);
991
+ }
992
+ /** Inside a transaction the message waits for the commit, so a run that rolls back sends nothing. */ async function deliver(node, state, send) {
993
+ if (state.qr) {
994
+ state.pendingEvents.push({
995
+ delivery: (0,flow_types/* .nodeLabel */.dA)(node),
996
+ send
997
+ });
998
+ return 'queued';
999
+ }
1000
+ await send();
1001
+ return 'sent';
1002
+ }
1003
+ /** One id or a list of them, each once; null / undefined / empty text is none. Anything but an id rejects with `key`. */ function idsOf(node, value, key, text) {
1004
+ const items = (Array.isArray(value) ? value : [
1005
+ value
1006
+ ]).filter((item)=>item !== undefined && item !== null && item !== '');
1007
+ for (const item of items){
1008
+ if (typeof item !== 'string' || !flow_types/* .FLOW_ID_PATTERN.test */.E2.test(item)) {
1009
+ throw new flow_errors/* .FlowHttpError */.hk(400, text(shown(item)), undefined, {
1010
+ messageKey: key,
1011
+ messageVariables: {
1012
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
1013
+ value: shown(item)
1014
+ }
1015
+ });
1016
+ }
1017
+ }
1018
+ return [
1019
+ ...new Set(items)
1020
+ ];
1021
+ }
1022
+ const notificationRecipients = (node, value)=>idsOf(node, value, message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_RECIPIENT_INVALID */.zr.NOTIFICATION_RECIPIENT_INVALID, (shownValue)=>`"${(0,flow_types/* .nodeLabel */.dA)(node)}" cannot notify "${shownValue}": it is not a user id.`);
1023
+ /** Addresses: one, a list, or comma / semicolon separated text; each once (case aside), in the order given. */ function emailAddresses(node, value) {
1024
+ const items = (Array.isArray(value) ? value : [
1025
+ value
1026
+ ]).filter((item)=>item !== undefined && item !== null);
1027
+ const seen = new Map();
1028
+ for (const item of items){
1029
+ const parts = typeof item === 'string' ? item.split(/[,;]/) : [
1030
+ item
1031
+ ];
1032
+ for (const part of parts){
1033
+ const address = typeof part === 'string' ? part.trim() : part;
1034
+ if (address === '') continue;
1035
+ if (typeof address !== 'string' || address.length > MAX_EMAIL_LENGTH || !EMAIL_PATTERN.test(address)) {
1036
+ throw new flow_errors/* .FlowHttpError */.hk(400, `"${shown(address)}" is not a valid email address.`, undefined, {
1037
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_ADDRESS_INVALID */.zr.EMAIL_ADDRESS_INVALID,
1038
+ messageVariables: {
1039
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
1040
+ address: shown(address)
1041
+ }
1042
+ });
1043
+ }
1044
+ if (!seen.has(address.toLowerCase())) seen.set(address.toLowerCase(), address);
1045
+ }
1046
+ }
1047
+ return [
1048
+ ...seen.values()
1049
+ ];
1050
+ }
1051
+ /** The company a notification shows under: the one given, else the caller's current one. */ function notificationCompany(node, value, given, state) {
1052
+ if (!given) return state.user?.companyId ?? null;
1053
+ if (value === undefined || value === null || value === '') return null;
1054
+ if (typeof value === 'string' && flow_types/* .FLOW_ID_PATTERN.test */.E2.test(value)) return value;
1055
+ throw new flow_errors/* .FlowHttpError */.hk(400, `"${(0,flow_types/* .nodeLabel */.dA)(node)}": "${shown(value)}" is not a company id.`, undefined, {
1056
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_COMPANY_INVALID */.zr.NOTIFICATION_COMPANY_INVALID,
1057
+ messageVariables: {
1058
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
1059
+ value: shown(value)
1060
+ }
1061
+ });
1062
+ }
1063
+ /** Sends `batches` (each already checked) as one step: the run's budget is taken for all of them first; a test run sends nothing. */ async function sendAll(node, state, batches, skipKey, recipients, send) {
1064
+ countMessages(state, batches.length);
1065
+ if (state.mode === 'dry_run') return {
1066
+ output: {
1067
+ skipped: true,
1068
+ reasonKey: skipKey,
1069
+ count: batches.length,
1070
+ recipients
1071
+ },
1072
+ skipped: true
1073
+ };
1074
+ let status = 'sent';
1075
+ for (const batch of batches)status = await deliver(node, state, ()=>send(batch));
1076
+ return status;
1077
+ }
1078
+ const nobody = (extra = {})=>({
1079
+ output: {
1080
+ skipped: true,
1081
+ reasonKey: message_keys/* .FLOW_SKIP_MESSAGES.NO_RECIPIENTS */.CB.NO_RECIPIENTS,
1082
+ recipients: 0,
1083
+ ...extra
1084
+ },
1085
+ skipped: true
1086
+ });
1087
+ /** Title, message, type, data and push of a notification, read in the current scope (the item's, in many mode). */ async function notificationContent(node, cfg, env, state) {
1088
+ const ctx = evaluationContext(state, env);
1089
+ const title = asText(await env.ruleEngine.resolveExpression(cfg.title, ctx)).trim().slice(0, flow_types/* .FLOW_LIMITS.MAX_NOTIFICATION_TITLE_LENGTH */.Or.MAX_NOTIFICATION_TITLE_LENGTH);
1090
+ if (!title) throw new flow_errors/* .FlowHttpError */.hk(400, `The title of "${(0,flow_types/* .nodeLabel */.dA)(node)}" is empty.`, undefined, {
1091
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_TITLE_EMPTY */.zr.NOTIFICATION_TITLE_EMPTY,
1092
+ messageVariables: {
1093
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1094
+ }
1095
+ });
1096
+ const message = cfg.message === undefined ? '' : asText(await env.ruleEngine.resolveExpression(cfg.message, ctx));
1097
+ const data = cfg.data && Object.keys(cfg.data).length ? await mapValues(cfg.data, env, state) : null;
1098
+ return {
1099
+ title,
1100
+ message: message || null,
1101
+ type: cfg.type ?? 'info',
1102
+ data,
1103
+ realtime: cfg.realtime !== false
1104
+ };
1105
+ }
1106
+ /** One notification to the users `userIds` names, or null when that is nobody. */ async function userNotification(node, cfg, env, state) {
1107
+ const ctx = evaluationContext(state, env);
1108
+ const userIds = notificationRecipients(node, cfg.userIds === undefined ? undefined : await env.ruleEngine.resolveExpression(cfg.userIds, ctx));
1109
+ if (!userIds.length) return null;
1110
+ assertRecipientCount(node, userIds.length, flow_types/* .FLOW_LIMITS.MAX_NOTIFICATION_RECIPIENTS */.Or.MAX_NOTIFICATION_RECIPIENTS);
1111
+ const content = await notificationContent(node, cfg, env, state);
1112
+ const companyId = notificationCompany(node, cfg.companyId === undefined ? undefined : await env.ruleEngine.resolveExpression(cfg.companyId, ctx), cfg.companyId !== undefined, state);
1113
+ return {
1114
+ ...content,
1115
+ userIds,
1116
+ companyId
1117
+ };
1118
+ }
1119
+ /** One notification per company that has members, each shown under its own company. */ async function companyNotifications(node, cfg, env, state) {
1120
+ const companyMembers = env.companyMembers;
1121
+ if (!companyMembers) {
1122
+ throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_COMPANY_UNAVAILABLE */.zr.NOTIFICATION_COMPANY_UNAVAILABLE, {
1123
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1124
+ }, `"${(0,flow_types/* .nodeLabel */.dA)(node)}" notifies companies, which needs the company feature.`);
1125
+ }
1126
+ const ctx = evaluationContext(state, env);
1127
+ const resolve = async (expression)=>expression === undefined ? undefined : env.ruleEngine.resolveExpression(expression, ctx);
1128
+ const companyIds = idsOf(node, await resolve(cfg.companyIds), message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_COMPANY_INVALID */.zr.NOTIFICATION_COMPANY_INVALID, (value)=>`"${(0,flow_types/* .nodeLabel */.dA)(node)}": "${value}" is not a company id.`);
1129
+ const branchIds = idsOf(node, await resolve(cfg.branchIds), message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_BRANCH_INVALID */.zr.NOTIFICATION_BRANCH_INVALID, (value)=>`"${(0,flow_types/* .nodeLabel */.dA)(node)}": "${value}" is not a branch id.`);
1130
+ if (!companyIds.length) return [];
1131
+ const content = await notificationContent(node, cfg, env, state);
1132
+ const batches = [];
1133
+ let total = 0;
1134
+ for (const companyId of companyIds){
1135
+ const userIds = await companyMembers(companyId, branchIds.length ? branchIds : undefined);
1136
+ total += userIds.length;
1137
+ assertRecipientCount(node, total, flow_types/* .FLOW_LIMITS.MAX_COMPANY_NOTIFICATION_RECIPIENTS */.Or.MAX_COMPANY_NOTIFICATION_RECIPIENTS);
1138
+ if (userIds.length) batches.push({
1139
+ ...content,
1140
+ userIds,
1141
+ companyId
1142
+ });
1143
+ }
1144
+ return batches;
1145
+ }
1146
+ /**
1147
+ * one: `{ status, recipients }`. many / company: `{ status, count, recipients }` (+ `skipped` items
1148
+ * with nobody to notify, in many mode). `status` is `sent`, or `queued` inside a transaction (sent once
1149
+ * it commits). Every message is checked before the first goes out; nobody at all skips the step, and
1150
+ * a test run checks everything but sends nothing.
1151
+ */ async function runSendNotification(node, cfg, env, state) {
1152
+ const notifications = env.notifications;
1153
+ if (!notifications) throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_UNAVAILABLE */.zr.NOTIFICATION_UNAVAILABLE, {
1154
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1155
+ }, `"${(0,flow_types/* .nodeLabel */.dA)(node)}" needs the notification module, which this app does not run.`);
1156
+ const target = cfg.target ?? 'one';
1157
+ let batches;
1158
+ let skippedItems = 0;
1159
+ if (target === 'company') {
1160
+ batches = await companyNotifications(node, cfg, env, state);
1161
+ } else if (target === 'many') {
1162
+ const items = await resolveList(node, cfg.items, flow_types/* .FLOW_LIMITS.MAX_MESSAGES_PER_RUN */.Or.MAX_MESSAGES_PER_RUN, env, state);
1163
+ const prepared = await eachItem(state, items, ()=>userNotification(node, cfg, env, state));
1164
+ batches = prepared.filter((batch)=>batch !== null);
1165
+ skippedItems = prepared.length - batches.length;
1166
+ } else {
1167
+ batches = [
1168
+ await userNotification(node, cfg, env, state)
1169
+ ].filter((batch)=>batch !== null);
1170
+ }
1171
+ const counts = target === 'one' ? {} : {
1172
+ count: batches.length,
1173
+ ...target === 'many' ? {
1174
+ skipped: skippedItems
1175
+ } : {}
1176
+ };
1177
+ if (!batches.length) return nobody(counts);
1178
+ const recipients = batches.reduce((sum, batch)=>sum + batch.userIds.length, 0);
1179
+ const sent = await sendAll(node, state, batches, message_keys/* .FLOW_SKIP_MESSAGES.NOTIFICATION */.CB.NOTIFICATION, recipients, async (batch)=>{
1180
+ try {
1181
+ await notifications.sendToMany(batch);
1182
+ } catch (error) {
1183
+ throw sendFailure(message_keys/* .FLOW_ERROR_MESSAGES.NOTIFICATION_FAILED */.zr.NOTIFICATION_FAILED, node, error);
1184
+ }
1185
+ });
1186
+ if (typeof sent !== 'string') return sent;
1187
+ return {
1188
+ output: {
1189
+ status: sent,
1190
+ recipients,
1191
+ ...counts
1192
+ }
1193
+ };
1194
+ }
1195
+ /** One email, read in the current scope (the item's, in many mode), or null when `to` has no address. */ async function prepareEmail(node, cfg, email, env, state) {
1196
+ const ctx = evaluationContext(state, env);
1197
+ const addresses = async (expression)=>expression === undefined ? [] : emailAddresses(node, await env.ruleEngine.resolveExpression(expression, ctx));
1198
+ const to = await addresses(cfg.to);
1199
+ if (!to.length) return null;
1200
+ // Someone already on `to` gets the email once.
1201
+ const listed = new Set(to.map((address)=>address.toLowerCase()));
1202
+ const notListed = (list)=>{
1203
+ const kept = list.filter((address)=>!listed.has(address.toLowerCase()));
1204
+ kept.forEach((address)=>listed.add(address.toLowerCase()));
1205
+ return kept;
1206
+ };
1207
+ const cc = notListed(await addresses(cfg.cc));
1208
+ const bcc = notListed(await addresses(cfg.bcc));
1209
+ const recipients = to.length + cc.length + bcc.length;
1210
+ assertRecipientCount(node, recipients, flow_types/* .FLOW_LIMITS.MAX_EMAIL_RECIPIENTS */.Or.MAX_EMAIL_RECIPIENTS);
1211
+ const replyTo = await addresses(cfg.replyTo);
1212
+ if (replyTo.length > 1) {
1213
+ throw new flow_errors/* .FlowHttpError */.hk(400, `"${replyTo.join(', ')}" is not a single email address.`, undefined, {
1214
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_ADDRESS_INVALID */.zr.EMAIL_ADDRESS_INVALID,
1215
+ messageVariables: {
1216
+ node: (0,flow_types/* .nodeLabel */.dA)(node),
1217
+ address: shown(replyTo.join(', '))
1218
+ }
1219
+ });
1220
+ }
1221
+ const base = {
1222
+ to,
1223
+ ...cc.length ? {
1224
+ cc
1225
+ } : {},
1226
+ ...bcc.length ? {
1227
+ bcc
1228
+ } : {},
1229
+ ...replyTo.length ? {
1230
+ replyTo: replyTo[0]
1231
+ } : {},
1232
+ ...cfg.emailConfigId ? {
1233
+ emailConfigId: cfg.emailConfigId
1234
+ } : {}
1235
+ };
1236
+ const user = state.user;
1237
+ if (cfg.mode === 'template') {
1238
+ const variables = await mapValues(cfg.variables, env, state);
1239
+ const templateSlug = (cfg.templateSlug ?? '').trim();
1240
+ return {
1241
+ recipients,
1242
+ send: ()=>email.sendTemplate({
1243
+ ...base,
1244
+ templateSlug,
1245
+ variables
1246
+ }, user)
1247
+ };
1248
+ }
1249
+ // A line break in a subject would start a new mail header.
1250
+ const subject = asText(cfg.subject === undefined ? '' : await env.ruleEngine.resolveExpression(cfg.subject, ctx)).replace(/[\r\n]+/g, ' ').trim().slice(0, flow_types/* .FLOW_LIMITS.MAX_EMAIL_SUBJECT_LENGTH */.Or.MAX_EMAIL_SUBJECT_LENGTH);
1251
+ if (!subject) throw new flow_errors/* .FlowHttpError */.hk(400, `The subject of "${(0,flow_types/* .nodeLabel */.dA)(node)}" is empty.`, undefined, {
1252
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_SUBJECT_EMPTY */.zr.EMAIL_SUBJECT_EMPTY,
1253
+ messageVariables: {
1254
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1255
+ }
1256
+ });
1257
+ const html = cfg.format === 'html';
1258
+ const body = cfg.body === undefined ? '' : asText(html ? await env.ruleEngine.resolveHtml(cfg.body, ctx) : await env.ruleEngine.resolveExpression(cfg.body, ctx));
1259
+ if (!body.trim()) throw new flow_errors/* .FlowHttpError */.hk(400, `The message of "${(0,flow_types/* .nodeLabel */.dA)(node)}" is empty.`, undefined, {
1260
+ messageKey: message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_BODY_EMPTY */.zr.EMAIL_BODY_EMPTY,
1261
+ messageVariables: {
1262
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1263
+ }
1264
+ });
1265
+ return {
1266
+ recipients,
1267
+ send: ()=>email.send({
1268
+ ...base,
1269
+ subject,
1270
+ ...html ? {
1271
+ html: body
1272
+ } : {
1273
+ text: body
1274
+ }
1275
+ }, user)
1276
+ };
1277
+ }
1278
+ /**
1279
+ * one: `{ status, recipients, messageId }`. many: `{ status, count, recipients, skipped, messageIds }`
1280
+ * (one id per email sent, in order). `status` is `sent`, or `queued` inside a transaction (sent once it
1281
+ * commits, no ids yet). Every email is checked before the first goes out; no address at all skips the
1282
+ * step, and a test run checks everything but sends nothing.
1283
+ */ async function runSendEmail(node, cfg, env, state) {
1284
+ const email = env.email;
1285
+ if (!email) throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_UNAVAILABLE */.zr.EMAIL_UNAVAILABLE, {
1286
+ node: (0,flow_types/* .nodeLabel */.dA)(node)
1287
+ }, `"${(0,flow_types/* .nodeLabel */.dA)(node)}" needs the email module, which this app does not run.`);
1288
+ const many = cfg.target === 'many';
1289
+ let emails;
1290
+ let skippedItems = 0;
1291
+ if (many) {
1292
+ const items = await resolveList(node, cfg.items, flow_types/* .FLOW_LIMITS.MAX_MESSAGES_PER_RUN */.Or.MAX_MESSAGES_PER_RUN, env, state);
1293
+ const prepared = await eachItem(state, items, ()=>prepareEmail(node, cfg, email, env, state));
1294
+ emails = prepared.filter((item)=>item !== null);
1295
+ skippedItems = prepared.length - emails.length;
1296
+ } else {
1297
+ emails = [
1298
+ await prepareEmail(node, cfg, email, env, state)
1299
+ ].filter((prepared)=>prepared !== null);
1300
+ }
1301
+ if (!emails.length) return nobody(many ? {
1302
+ count: 0,
1303
+ skipped: skippedItems
1304
+ } : {});
1305
+ const recipients = emails.reduce((sum, prepared)=>sum + prepared.recipients, 0);
1306
+ const messageIds = [];
1307
+ const sent = await sendAll(node, state, emails, message_keys/* .FLOW_SKIP_MESSAGES.EMAIL */.CB.EMAIL, recipients, async (prepared)=>{
1308
+ let result;
1309
+ try {
1310
+ result = await prepared.send();
1311
+ } catch (error) {
1312
+ throw sendFailure(message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_FAILED */.zr.EMAIL_FAILED, node, error);
1313
+ }
1314
+ if (!result?.success) throw sendFailure(message_keys/* .FLOW_ERROR_MESSAGES.EMAIL_FAILED */.zr.EMAIL_FAILED, node, result?.error || 'The email was not accepted.');
1315
+ messageIds.push(result.messageId ?? null);
1316
+ });
1317
+ if (typeof sent !== 'string') return sent;
1318
+ if (!many) return {
1319
+ output: {
1320
+ status: sent,
1321
+ recipients,
1322
+ messageId: messageIds[0] ?? null
1323
+ }
1324
+ };
1325
+ return {
1326
+ output: {
1327
+ status: sent,
1328
+ count: emails.length,
1329
+ recipients,
1330
+ skipped: skippedItems,
1331
+ messageIds
1332
+ }
1333
+ };
1334
+ }
900
1335
  /** Runs one node. Ports/branching/looping are the executor's job; this only does the node's own work. */ async function runNode(node, env, state) {
901
1336
  const ctx = evaluationContext(state, env);
902
1337
  const c = node.config;
@@ -911,6 +1346,8 @@ async function insertChildren(node, child, parentId, env, state) {
911
1346
  return runCode(node, c, state);
912
1347
  case 'permission_check':
913
1348
  return runPermissionCheck(c, env, state);
1349
+ case 'company_branch_check':
1350
+ return runCompanyBranchCheck(c, env, state);
914
1351
  case 'set':
915
1352
  {
916
1353
  const cfg = c;
@@ -967,7 +1404,6 @@ async function insertChildren(node, child, parentId, env, state) {
967
1404
  case 'entity_get':
968
1405
  {
969
1406
  const cfg = c;
970
- await authorize(env, state, cfg.entityCode, 'read');
971
1407
  const id = asId(await env.ruleEngine.resolveExpression(cfg.id, ctx));
972
1408
  const service = await env.entityService(cfg.entityCode, state.qr);
973
1409
  try {
@@ -984,7 +1420,6 @@ async function insertChildren(node, child, parentId, env, state) {
984
1420
  case 'entity_query':
985
1421
  {
986
1422
  const cfg = c;
987
- await authorize(env, state, cfg.entityCode, 'read');
988
1423
  const service = await env.entityService(cfg.entityCode, state.qr);
989
1424
  const filter = await mapFilter(node, cfg.filter, env, state, 'skip');
990
1425
  const search = cfg.search ? await env.ruleEngine.resolveExpression(cfg.search, ctx) : undefined;
@@ -1061,6 +1496,7 @@ async function insertChildren(node, child, parentId, env, state) {
1061
1496
  k,
1062
1497
  String(v ?? '')
1063
1498
  ]));
1499
+ if (cfg.forwardAuth === true && state.authorization && !Object.keys(headers).some((name)=>name.toLowerCase() === 'authorization')) headers['authorization'] = state.authorization;
1064
1500
  const body = cfg.body && Object.keys(cfg.body).length ? await mapValues(cfg.body, env, state) : undefined;
1065
1501
  const remaining = state.deadline - Date.now();
1066
1502
  if (remaining <= 0) throw new flow_errors/* .FlowLimitError */.IM(message_keys/* .FLOW_ERROR_MESSAGES.OUT_OF_TIME */.zr.OUT_OF_TIME);
@@ -1099,6 +1535,10 @@ async function insertChildren(node, child, parentId, env, state) {
1099
1535
  }
1100
1536
  };
1101
1537
  }
1538
+ case 'send_notification':
1539
+ return runSendNotification(node, c, env, state);
1540
+ case 'send_email':
1541
+ return runSendEmail(node, c, env, state);
1102
1542
  case 'respond':
1103
1543
  {
1104
1544
  const cfg = c;
@@ -1382,11 +1822,11 @@ function successResult(state, startedAt) {
1382
1822
  /* import */ var _config_entity_builder_constants_js__rspack_import_8 = __webpack_require__(6576);
1383
1823
  /* import */ var _config_message_keys_js__rspack_import_7 = __webpack_require__(5732);
1384
1824
  /* import */ var _interfaces_generic_record_interface_js__rspack_import_1 = __webpack_require__(3372);
1385
- /* import */ var _rule_engine_index_js__rspack_import_2 = __webpack_require__(5920);
1825
+ /* import */ var _rule_engine_index_js__rspack_import_2 = __webpack_require__(8552);
1386
1826
  /* import */ var _rule_engine_rule_ip_js__rspack_import_3 = __webpack_require__(809);
1387
1827
  /* import */ var _rule_engine_rule_permission_js__rspack_import_4 = __webpack_require__(5191);
1388
- /* import */ var _rule_engine_rule_regex_js__rspack_import_5 = __webpack_require__(6308);
1389
- /* import */ var _flow_types_js__rspack_import_6 = __webpack_require__(6256);
1828
+ /* import */ var _flow_types_js__rspack_import_5 = __webpack_require__(6256);
1829
+ /* import */ var _flow_api_key_js__rspack_import_6 = __webpack_require__(4816);
1390
1830
 
1391
1831
 
1392
1832
 
@@ -1530,7 +1970,14 @@ const DURATION_ARG = {
1530
1970
  case 'lookup':
1531
1971
  for (const item of Object.values(expression.filter ?? {})){
1532
1972
  const value = (0,_interfaces_generic_record_interface_js__rspack_import_1/* .isRecordFilterCondition */.t1)(item) ? item.value : item;
1533
- if (isExpression(value)) collectExpressionRefs(value, out);
1973
+ for (const operand of (0,_interfaces_generic_record_interface_js__rspack_import_1/* .isFilterRange */.iA)(value) ? [
1974
+ value.from,
1975
+ value.to
1976
+ ] : [
1977
+ value
1978
+ ]){
1979
+ if (isExpression(operand)) collectExpressionRefs(operand, out);
1980
+ }
1534
1981
  }
1535
1982
  break;
1536
1983
  }
@@ -1651,7 +2098,7 @@ function functionProblem(value, depth, options) {
1651
2098
  return null;
1652
2099
  }
1653
2100
  function patternProblem(pattern) {
1654
- switch((0,_rule_engine_rule_regex_js__rspack_import_5/* .rulePatternProblem */._V)(pattern)){
2101
+ switch((0,_flusys_nestjs_shared_utils__rspack_import_0.safeRegexProblem)(pattern)){
1655
2102
  case 'too_long':
1656
2103
  return (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SHAPE_MESSAGES.REGEX_TOO_LONG */.IP.REGEX_TOO_LONG, {
1657
2104
  max: _rule_engine_index_js__rspack_import_2/* .RULE_ENGINE_LIMITS.MAX_REGEX_PATTERN_LENGTH */.bw.MAX_REGEX_PATTERN_LENGTH
@@ -1694,11 +2141,16 @@ function lookupProblem(value, depth, options) {
1694
2141
  });
1695
2142
  return expressionProblem(item, depth + 1, options);
1696
2143
  }
1697
- if (item.op === 'is_null' || item.op === 'not_null') return null;
2144
+ const kind = _interfaces_generic_record_interface_js__rspack_import_1/* .FILTER_OP_VALUE_KIND */.w9[item.op];
2145
+ if (kind === 'none') return null;
1698
2146
  if (item.value === undefined) return (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SHAPE_MESSAGES.FILTER_VALUE_REQUIRED */.IP.FILTER_VALUE_REQUIRED, {
1699
2147
  op: item.op
1700
2148
  });
1701
- return expressionProblem(item.value, depth + 1, options);
2149
+ if (kind !== 'range') return expressionProblem(item.value, depth + 1, options);
2150
+ if (!(0,_interfaces_generic_record_interface_js__rspack_import_1/* .isFilterRange */.iA)(item.value)) return (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SHAPE_MESSAGES.FILTER_RANGE_INVALID */.IP.FILTER_RANGE_INVALID, {
2151
+ op: item.op
2152
+ });
2153
+ return expressionProblem(item.value.from, depth + 1, options) ?? expressionProblem(item.value.to, depth + 1, options);
1702
2154
  }
1703
2155
  function firstProblem(items, depth, options) {
1704
2156
  for (const item of items){
@@ -1744,39 +2196,29 @@ function conditionProblem(node, options) {
1744
2196
  /**
1745
2197
  * Save-time check of a whole flow: structure (Request steps with distinct URLs, valid ids/ports, no cycles),
1746
2198
  * every node's configuration, every reference to another node, and the combination of
1747
- * settings (run-as / auth / transaction). Pure: no I/O, so the same result is what the
2199
+ * settings (auth / transaction). Pure: no I/O, so the same result is what the
1748
2200
  * designer shows and what the server enforces.
1749
2201
  */ function validateFlow(flow, context = {}) {
1750
2202
  const errors = [];
1751
2203
  const warnings = [];
1752
2204
  const err = (message)=>errors.push(message);
1753
- if (!_flow_types_js__rspack_import_6/* .FLOW_SLUG_PATTERN.test */.YD.test(flow.slug ?? '')) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.SLUG_INVALID */.qj.SLUG_INVALID));
1754
- if (!_flow_types_js__rspack_import_6/* .FLOW_RUN_AS.includes */.gl.includes(flow.runAs)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.RUN_AS_INVALID */.qj.RUN_AS_INVALID, {
1755
- runAs: String(flow.runAs)
1756
- }));
1757
- if (!_flow_types_js__rspack_import_6/* .FLOW_AUTH_MODES.includes */.fO.includes(flow.authMode)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.AUTH_MODE_INVALID */.qj.AUTH_MODE_INVALID, {
1758
- authMode: String(flow.authMode)
1759
- }));
1760
- if (flow.runAs === 'caller' && flow.authMode !== 'jwt' && flow.authMode !== 'permission' && flow.authMode !== 'internal') {
1761
- err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.RUN_AS_CALLER_NEEDS_AUTH */.qj.RUN_AS_CALLER_NEEDS_AUTH));
1762
- }
1763
- validateInputSchema(flow, err);
2205
+ if (!_flow_types_js__rspack_import_5/* .FLOW_SLUG_PATTERN.test */.YD.test(flow.slug ?? '')) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.SLUG_INVALID */.qj.SLUG_INVALID));
1764
2206
  const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
1765
2207
  const edges = Array.isArray(flow.edges) ? flow.edges : [];
1766
2208
  if (!nodes.length) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NO_NODES */.qj.NO_NODES));
1767
- if (nodes.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TOO_MANY_NODES */.qj.TOO_MANY_NODES, {
1768
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES
2209
+ if (nodes.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TOO_MANY_NODES */.qj.TOO_MANY_NODES, {
2210
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES
1769
2211
  }));
1770
- if (edges.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TOO_MANY_EDGES */.qj.TOO_MANY_EDGES, {
1771
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES
2212
+ if (edges.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TOO_MANY_EDGES */.qj.TOO_MANY_EDGES, {
2213
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES
1772
2214
  }));
1773
- if (errors.length && (!nodes.length || nodes.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES || edges.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES)) return {
2215
+ if (errors.length && (!nodes.length || nodes.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_NODES */.Or.MAX_NODES || edges.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_EDGES */.Or.MAX_EDGES)) return {
1774
2216
  errors,
1775
2217
  warnings
1776
2218
  };
1777
2219
  const byId = new Map();
1778
2220
  for (const node of nodes){
1779
- if (!_flow_types_js__rspack_import_6/* .FLOW_NODE_ID_PATTERN.test */.z6.test(node?.id ?? '')) {
2221
+ if (!_flow_types_js__rspack_import_5/* .FLOW_NODE_ID_PATTERN.test */.z6.test(node?.id ?? '')) {
1780
2222
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_ID_INVALID */.qj.NODE_ID_INVALID, {
1781
2223
  id: node?.id
1782
2224
  }));
@@ -1786,7 +2228,7 @@ function conditionProblem(node, options) {
1786
2228
  id: node.id
1787
2229
  }));
1788
2230
  byId.set(node.id, node);
1789
- if (!_flow_types_js__rspack_import_6/* .FLOW_NODE_TYPES.includes */.q2.includes(node.type)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_TYPE_UNKNOWN */.qj.NODE_TYPE_UNKNOWN, {
2231
+ if (!_flow_types_js__rspack_import_5/* .FLOW_NODE_TYPES.includes */.q2.includes(node.type)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_TYPE_UNKNOWN */.qj.NODE_TYPE_UNKNOWN, {
1790
2232
  id: node.id,
1791
2233
  type: node.type
1792
2234
  }));
@@ -1816,7 +2258,7 @@ function conditionProblem(node, options) {
1816
2258
  id: edge.from
1817
2259
  }));
1818
2260
  if (to.type === 'trigger') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.EDGE_INTO_TRIGGER */.qj.EDGE_INTO_TRIGGER));
1819
- if (!(0,_flow_types_js__rspack_import_6/* .portsOf */.a5)(from).includes(edge.port)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.EDGE_NO_PORT */.qj.EDGE_NO_PORT, {
2261
+ if (!(0,_flow_types_js__rspack_import_5/* .portsOf */.a5)(from).includes(edge.port)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.EDGE_NO_PORT */.qj.EDGE_NO_PORT, {
1820
2262
  id: from.id,
1821
2263
  port: edge.port
1822
2264
  }));
@@ -1845,36 +2287,86 @@ function conditionProblem(node, options) {
1845
2287
  ])) : null;
1846
2288
  for (const node of nodes){
1847
2289
  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, {
1848
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node)
2290
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node)
1849
2291
  }));
1850
2292
  }
1851
2293
  checkNodeReferences(nodes, byId, flow, err, warnings);
1852
2294
  if (!cyclic && reachable) checkReferenceTiming(nodes, edges, byId, reachable, warnings);
1853
2295
  if (!cyclic) checkLoopDepth(nodes, outgoing, err);
1854
- if (flow.isTransactional && nodes.some((n)=>n.type === 'http_request')) {
1855
- warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRANSACTIONAL_HTTP */.qj.TRANSACTIONAL_HTTP));
1856
- }
1857
- checkTransactionUse(flow, nodes, outgoing, reachable, context, warnings);
1858
- if (flow.runAs === 'system' && flow.authMode === 'public' && nodes.some((n)=>n.type.startsWith('entity_') || collectLookupEntities(n.config).size > 0)) {
1859
- warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.PUBLIC_SYSTEM_ENTITY */.qj.PUBLIC_SYSTEM_ENTITY));
1860
- }
1861
- if ((flow.authMode === 'public' || flow.authMode === 'api_key') && nodes.some((n)=>n.type === 'permission_check' || usesPermissionFunction(n.config))) {
1862
- warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.PERMISSION_WITHOUT_USER */.qj.PERMISSION_WITHOUT_USER));
1863
- }
2296
+ // Transaction and access are a Request step's own: each is checked against the steps its runs reach.
2297
+ const runs = (0,_flow_types_js__rspack_import_5/* .flowEndpoints */.Xr)(flow).map((endpoint)=>({
2298
+ endpoint,
2299
+ label: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(byId.get(endpoint.nodeId) ?? {
2300
+ id: endpoint.nodeId,
2301
+ name: ''
2302
+ }),
2303
+ steps: [
2304
+ ...reach(endpoint.nodeId, outgoing)
2305
+ ].map((id)=>byId.get(id)).filter((node)=>!!node)
2306
+ }));
2307
+ checkEndpointRuns(runs, warnings);
2308
+ checkTransactionUse(flow, runs, outgoing, context, warnings);
1864
2309
  return {
1865
2310
  errors,
1866
2311
  warnings
1867
2312
  };
1868
2313
  }
2314
+ /** The entities a step reads or writes: its own, its child inserts' and those its lookups read. */ function entitiesOf(node) {
2315
+ const codes = collectLookupEntities(node.config);
2316
+ if (node.type.startsWith('entity_') && typeof node.config?.['entityCode'] === 'string') codes.add(node.config['entityCode']);
2317
+ const children = node.type === 'entity_insert' && Array.isArray(node.config?.['children']) ? node.config['children'] : [];
2318
+ for (const child of children)if (typeof child?.entityCode === 'string') codes.add(child.entityCode);
2319
+ return [
2320
+ ...codes
2321
+ ];
2322
+ }
2323
+ /** What depends on a Request step's access and transaction, for the steps its runs reach. A finding shared by several Request steps is reported once. */ function checkEndpointRuns(runs, warnings) {
2324
+ const reported = new Set();
2325
+ const warnOnce = (message)=>{
2326
+ const key = JSON.stringify(message);
2327
+ if (reported.has(key)) return;
2328
+ reported.add(key);
2329
+ warnings.push(message);
2330
+ };
2331
+ const keyless = {
2332
+ endpoints: [],
2333
+ entities: new Set()
2334
+ };
2335
+ for (const { endpoint, label, steps } of runs){
2336
+ if (endpoint.isTransactional) {
2337
+ if (steps.some((n)=>n.type === 'http_request')) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRANSACTIONAL_HTTP */.qj.TRANSACTIONAL_HTTP, {
2338
+ node: label
2339
+ }));
2340
+ // Inside the run's transaction a message is only sent once it commits: a failed send is logged, it cannot take the error path.
2341
+ for (const n of steps)if ((0,_flow_types_js__rspack_import_5/* .isMessageNode */.SE)(n.type) && n.onError === 'continue') warnOnce((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.MESSAGE_AFTER_COMMIT */.qj.MESSAGE_AFTER_COMMIT, {
2342
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(n)
2343
+ }));
2344
+ }
2345
+ if (endpoint.authMode === 'public' && steps.some((n)=>(0,_flow_types_js__rspack_import_5/* .isMessageNode */.SE)(n.type))) warnOnce((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.PUBLIC_MESSAGE */.qj.PUBLIC_MESSAGE));
2346
+ // A URL callers reach without signing in has no user: user checks deny them, and its record steps are open to anyone who can call it.
2347
+ if (!_flow_types_js__rspack_import_5/* .KEYLESS_AUTH_MODES.has */.d7.has(endpoint.authMode)) continue;
2348
+ if (steps.some((n)=>(n.type === 'permission_check' || n.type === 'company_branch_check') && n.config?.['subject'] !== 'user' || usesPermissionFunction(n.config))) warnOnce((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.PERMISSION_WITHOUT_USER */.qj.PERMISSION_WITHOUT_USER));
2349
+ const entities = steps.flatMap(entitiesOf);
2350
+ if (!entities.length) continue;
2351
+ keyless.endpoints.push(endpoint.path ? `/${endpoint.path}` : '/');
2352
+ for (const code of entities)keyless.entities.add(code);
2353
+ }
2354
+ if (keyless.endpoints.length) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.KEYLESS_ENTITY */.qj.KEYLESS_ENTITY, {
2355
+ endpoints: keyless.endpoints.join(', '),
2356
+ entities: [
2357
+ ...keyless.entities
2358
+ ].join(', ')
2359
+ }));
2360
+ }
1869
2361
  /** At least one Request step; at most one on the flow's own URL, the others on distinct paths. */ function checkTriggers(triggers, err) {
1870
2362
  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));
2363
+ const main = triggers.filter((trigger)=>!(0,_flow_types_js__rspack_import_5/* .triggerPath */.um)(trigger));
1872
2364
  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
2365
  count: main.length
1874
2366
  }));
1875
2367
  const paths = new Set();
1876
2368
  for (const trigger of triggers){
1877
- const path = (0,_flow_types_js__rspack_import_6/* .triggerPath */.um)(trigger);
2369
+ const path = (0,_flow_types_js__rspack_import_5/* .triggerPath */.um)(trigger);
1878
2370
  if (!path) continue;
1879
2371
  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
2372
  path
@@ -1883,23 +2375,24 @@ function conditionProblem(node, options) {
1883
2375
  }
1884
2376
  }
1885
2377
  /**
1886
- * What the runtime would always refuse, caught on save: an API-key flow called by a flow running
1887
- * as the caller, a required input left out, a chain that comes back to this flow or nests deeper
2378
+ * What the runtime would always refuse, caught on save: an API-key URL (a flow has no key to send),
2379
+ * a required input left out, a chain that comes back to this flow or nests deeper
1888
2380
  * than the limit. An inactive flow and an input the called flow does not declare are warnings.
1889
- */ function checkCalledFlow(slug, endpoint, input, inputList, flow, known, at, err, warn) {
2381
+ */ function checkCalledFlow(node, flow, known, err, warn) {
2382
+ const { flowSlug: slug, endpoint, input, inputList } = node.config;
2383
+ const at = {
2384
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node)
2385
+ };
1890
2386
  const target = known.get(slug);
1891
2387
  const vars = {
1892
2388
  ...at,
1893
2389
  flow: slug
1894
2390
  };
1895
2391
  if (!target.isActive) warn.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_INACTIVE */.qj.NODE_FLOW_INACTIVE, vars));
1896
- if (target.authMode === 'api_key') {
1897
- // A function runs as whoever calls it, so it fails only when a flow running as the caller does.
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));
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));
1900
- }
1901
2392
  const path = typeof endpoint === 'string' && endpoint ? endpoint : null;
1902
2393
  const entry = target.endpoints.find((e)=>e.path === path);
2394
+ // A flow has no key to send, so an API-key URL is never callable from one.
2395
+ if (entry?.authMode === 'api_key') 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));
1903
2396
  if (!entry) {
1904
2397
  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
2398
  ...vars,
@@ -1920,11 +2413,40 @@ function conditionProblem(node, options) {
1920
2413
  ...loop
1921
2414
  ].join(' > ')
1922
2415
  }));
1923
- if (1 + callDepth(slug, known, new Set()) > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_TOO_DEEP */.qj.NODE_FLOW_TOO_DEEP, {
2416
+ if (1 + callDepth(slug, known, new Set()) > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_TOO_DEEP */.qj.NODE_FLOW_TOO_DEEP, {
1924
2417
  ...at,
1925
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH
2418
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH
1926
2419
  }));
1927
2420
  }
2421
+ /**
2422
+ * What the Call flow steps of `caller` whose call chain reaches `slug` (that flow itself, or one
2423
+ * that calls it, however deep) would hit against the flows in `known`: the save errors of those
2424
+ * steps, and the flow they run being off (a warning on save, a failure at run).
2425
+ */ function callProblems(caller, slug, known) {
2426
+ const problems = [];
2427
+ for (const node of Array.isArray(caller.nodes) ? caller.nodes : []){
2428
+ const c = node?.config ?? {};
2429
+ if (node?.type !== 'call_flow' || typeof c.flowSlug !== 'string') continue;
2430
+ if (c.flowSlug !== slug && !callPathTo(c.flowSlug, slug, known, new Set())) continue;
2431
+ const at = {
2432
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node)
2433
+ };
2434
+ const target = known.get(c.flowSlug);
2435
+ if (!target) {
2436
+ problems.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_UNKNOWN */.qj.NODE_FLOW_UNKNOWN, {
2437
+ ...at,
2438
+ flow: c.flowSlug
2439
+ }));
2440
+ continue;
2441
+ }
2442
+ if (!target.isActive) problems.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_INACTIVE */.qj.NODE_FLOW_INACTIVE, {
2443
+ ...at,
2444
+ flow: c.flowSlug
2445
+ }));
2446
+ checkCalledFlow(node, caller, known, (m)=>problems.push(m), []);
2447
+ }
2448
+ return problems;
2449
+ }
1928
2450
  /** 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) {
1929
2451
  const given = input && typeof input === 'object' && !Array.isArray(input) ? Object.keys(input) : [];
1930
2452
  if (schema.length) {
@@ -1959,39 +2481,33 @@ function conditionProblem(node, options) {
1959
2481
  return null;
1960
2482
  }
1961
2483
  /** How many calls deep the longest chain starting inside `slug` goes, counted only as far as the limit; a chain that loops is cut where it repeats. */ function callDepth(slug, known, running) {
1962
- if (running.has(slug) || running.size > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH) return 0;
2484
+ if (running.has(slug) || running.size > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH) return 0;
1963
2485
  running.add(slug);
1964
2486
  let deepest = 0;
1965
2487
  for (const next of new Set(known.get(slug)?.calls ?? []))deepest = Math.max(deepest, 1 + callDepth(next, known, running));
1966
2488
  running.delete(slug);
1967
2489
  return deepest;
1968
2490
  }
1969
- function validateInputSchema(flow, err) {
1970
- if (flow.bodyType !== undefined && !_flow_types_js__rspack_import_6/* .FLOW_BODY_TYPES.includes */.Y3.includes(flow.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, {
1971
- type: String(flow.bodyType)
1972
- }));
1973
- validateInputFields(flow.inputSchema ?? [], '', 1, err);
1974
- }
1975
2491
  /** One level of the request body: the top-level fields, or the fields of an object / list item at `prefix`. */ function validateInputFields(fields, prefix, depth, err) {
1976
- if (fields.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_INPUT_FIELDS */.Or.MAX_INPUT_FIELDS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_TOO_MANY */.qj.INPUT_TOO_MANY, {
1977
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_INPUT_FIELDS */.Or.MAX_INPUT_FIELDS
2492
+ if (fields.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_INPUT_FIELDS */.Or.MAX_INPUT_FIELDS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_TOO_MANY */.qj.INPUT_TOO_MANY, {
2493
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_INPUT_FIELDS */.Or.MAX_INPUT_FIELDS
1978
2494
  }));
1979
2495
  const seen = new Set();
1980
2496
  for (const field of fields){
1981
2497
  const name = `${prefix}${field?.name ?? ''}`;
1982
- if (!_flow_types_js__rspack_import_6/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(field?.name ?? '')) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_NAME_INVALID */.qj.INPUT_NAME_INVALID, {
2498
+ if (!_flow_types_js__rspack_import_5/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(field?.name ?? '')) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_NAME_INVALID */.qj.INPUT_NAME_INVALID, {
1983
2499
  name
1984
2500
  }));
1985
2501
  else if (seen.has(field.name)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_NAME_DUPLICATE */.qj.INPUT_NAME_DUPLICATE, {
1986
2502
  name
1987
2503
  }));
1988
2504
  seen.add(field?.name);
1989
- if (!_flow_types_js__rspack_import_6/* .FLOW_INPUT_TYPES.includes */.Q7.includes(field?.type)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_TYPE_UNKNOWN */.qj.INPUT_TYPE_UNKNOWN, {
2505
+ if (!_flow_types_js__rspack_import_5/* .FLOW_INPUT_TYPES.includes */.Q7.includes(field?.type)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_TYPE_UNKNOWN */.qj.INPUT_TYPE_UNKNOWN, {
1990
2506
  name,
1991
2507
  type: field?.type
1992
2508
  }));
1993
2509
  const itemType = field?.type === 'array' ? field.itemType : undefined;
1994
- if (itemType !== undefined && (itemType === 'array' || !_flow_types_js__rspack_import_6/* .FLOW_INPUT_TYPES.includes */.Q7.includes(itemType))) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_ITEM_TYPE_UNKNOWN */.qj.INPUT_ITEM_TYPE_UNKNOWN, {
2510
+ if (itemType !== undefined && (itemType === 'array' || !_flow_types_js__rspack_import_5/* .FLOW_INPUT_TYPES.includes */.Q7.includes(itemType))) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_ITEM_TYPE_UNKNOWN */.qj.INPUT_ITEM_TYPE_UNKNOWN, {
1995
2511
  name,
1996
2512
  type: itemType
1997
2513
  }));
@@ -2000,10 +2516,10 @@ function validateInputSchema(flow, err) {
2000
2516
  }));
2001
2517
  const holdsFields = field?.type === 'object' || itemType === 'object';
2002
2518
  if (!holdsFields || !Array.isArray(field.fields) || !field.fields.length) continue;
2003
- if (depth >= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_INPUT_DEPTH */.Or.MAX_INPUT_DEPTH) {
2519
+ if (depth >= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_INPUT_DEPTH */.Or.MAX_INPUT_DEPTH) {
2004
2520
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.INPUT_TOO_DEEP */.qj.INPUT_TOO_DEEP, {
2005
2521
  name,
2006
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_INPUT_DEPTH */.Or.MAX_INPUT_DEPTH
2522
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_INPUT_DEPTH */.Or.MAX_INPUT_DEPTH
2007
2523
  }));
2008
2524
  continue;
2009
2525
  }
@@ -2012,7 +2528,7 @@ function validateInputSchema(flow, err) {
2012
2528
  }
2013
2529
  function validateNodeConfig(node, flow, context, err, warn) {
2014
2530
  const at = {
2015
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node)
2531
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node)
2016
2532
  };
2017
2533
  const c = node.config ?? {};
2018
2534
  const shape = {
@@ -2100,18 +2616,59 @@ function validateNodeConfig(node, flow, context, err, warn) {
2100
2616
  response: {
2101
2617
  list: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.RESPONSE_VALUES */.bc.RESPONSE_VALUES,
2102
2618
  item: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.RESPONSE_VALUE */.bc.RESPONSE_VALUE
2619
+ },
2620
+ data: {
2621
+ list: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.DATA_VALUES */.bc.DATA_VALUES,
2622
+ item: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.DATA_VALUE */.bc.DATA_VALUE
2623
+ },
2624
+ template: {
2625
+ list: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.TEMPLATE_VALUES */.bc.TEMPLATE_VALUES,
2626
+ item: _config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.TEMPLATE_VALUE */.bc.TEMPLATE_VALUE
2103
2627
  }
2104
2628
  };
2629
+ const optionalExpr = (value, what)=>{
2630
+ if (value !== undefined) expr(value, what);
2631
+ };
2632
+ /** An AND / OR rule of permission codes: one that names none needs a permission, a malformed one is `invalid`. */ const permissionRule = (value, invalid)=>{
2633
+ const problem = (0,_flusys_nestjs_shared_utils__rspack_import_0.permissionLogicProblem)(value);
2634
+ if (problem === 'empty') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_REQUIRED */.qj.NODE_PERMISSION_REQUIRED, at));
2635
+ else if (problem === 'too_many') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .RULE_ENGINE_MESSAGES.TOO_MANY_PERMISSION_CODES */.x3.TOO_MANY_PERMISSION_CODES, {
2636
+ max: _rule_engine_index_js__rspack_import_2/* .RULE_ENGINE_LIMITS.MAX_PERMISSION_CODES */.bw.MAX_PERMISSION_CODES
2637
+ }));
2638
+ else if (problem) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(invalid, at));
2639
+ };
2640
+ const serviceRunning = (service, unavailable)=>{
2641
+ if (context.messageServices && !context.messageServices[service]) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(unavailable, at));
2642
+ };
2105
2643
  switch(node.type){
2106
2644
  case 'trigger':
2107
2645
  {
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, {
2646
+ if (c.path !== undefined && c.path !== '' && (typeof c.path !== 'string' || !_flow_types_js__rspack_import_5/* .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));
2647
+ if (c.authMode !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_AUTH_MODES.includes */.fO.includes(c.authMode)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_AUTH_INVALID */.qj.TRIGGER_AUTH_INVALID, at));
2648
+ if (c.permissions !== undefined) permissionRule(c.permissions, _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_AUTH_INVALID */.qj.TRIGGER_AUTH_INVALID);
2649
+ if (c.isTransactional !== undefined && typeof c.isTransactional !== 'boolean') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_TRANSACTION_INVALID */.qj.TRIGGER_TRANSACTION_INVALID, at));
2650
+ if (c.timeoutMs !== undefined && !(Number.isInteger(c.timeoutMs) && c.timeoutMs >= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MIN_TIMEOUT_MS */.Or.MIN_TIMEOUT_MS && c.timeoutMs <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_TIMEOUT_MS */.Or.MAX_TIMEOUT_MS)) {
2651
+ err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_TIMEOUT_RANGE */.qj.TRIGGER_TIMEOUT_RANGE, {
2652
+ ...at,
2653
+ min: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MIN_TIMEOUT_MS */.Or.MIN_TIMEOUT_MS / 1000,
2654
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_TIMEOUT_MS */.Or.MAX_TIMEOUT_MS / 1000
2655
+ }));
2656
+ }
2657
+ if (c.rateLimitPerMinute !== undefined && !(Number.isInteger(c.rateLimitPerMinute) && c.rateLimitPerMinute >= 0 && c.rateLimitPerMinute <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_RATE_LIMIT_PER_MINUTE */.Or.MAX_RATE_LIMIT_PER_MINUTE)) {
2658
+ err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_RATE_LIMIT_RANGE */.qj.TRIGGER_RATE_LIMIT_RANGE, {
2659
+ ...at,
2660
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_RATE_LIMIT_PER_MINUTE */.Or.MAX_RATE_LIMIT_PER_MINUTE
2661
+ }));
2662
+ }
2663
+ // Without a key an API-key URL could never be called; any other access keeps none.
2664
+ if (c.authMode === 'api_key' && c.apiKey === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_API_KEY_REQUIRED */.qj.TRIGGER_API_KEY_REQUIRED, at));
2665
+ else if (c.apiKey !== undefined && !(0,_flow_api_key_js__rspack_import_6/* .looksLikeApiKey */.Jb)(c.apiKey) && !(0,_flow_types_js__rspack_import_5/* .isStoredApiKey */.xA)(c.apiKey)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.TRIGGER_API_KEY_INVALID */.qj.TRIGGER_API_KEY_INVALID, at));
2666
+ // Field errors are named after the URL (`<path>: <field>`) once a flow has several.
2667
+ const path = (0,_flow_types_js__rspack_import_5/* .triggerPath */.um)(node);
2668
+ if (c.bodyType !== undefined && !_flow_types_js__rspack_import_5/* .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
2669
  type: String(c.bodyType)
2113
2670
  }));
2114
- validateInputFields(Array.isArray(c.inputSchema) ? c.inputSchema : [], `${path}: `, 1, err);
2671
+ validateInputFields(Array.isArray(c.inputSchema) ? c.inputSchema : [], path ? `${path}: ` : '', 1, err);
2115
2672
  break;
2116
2673
  }
2117
2674
  case 'validate':
@@ -2123,16 +2680,39 @@ function validateNodeConfig(node, flow, context, err, warn) {
2123
2680
  break;
2124
2681
  case 'permission_check':
2125
2682
  {
2126
- try {
2127
- if (!(0,_rule_engine_rule_permission_js__rspack_import_4/* .parsePermissionCodes */.PS)(c.codes).length) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_REQUIRED */.qj.NODE_PERMISSION_REQUIRED, at));
2128
- } catch {
2129
- err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .RULE_ENGINE_MESSAGES.TOO_MANY_PERMISSION_CODES */.x3.TOO_MANY_PERMISSION_CODES, {
2130
- max: _rule_engine_index_js__rspack_import_2/* .RULE_ENGINE_LIMITS.MAX_PERMISSION_CODES */.bw.MAX_PERMISSION_CODES
2131
- }));
2683
+ if (c.permissions === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_REQUIRED */.qj.NODE_PERMISSION_REQUIRED, at));
2684
+ else permissionRule(c.permissions, _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID);
2685
+ if (c.subject !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_ACCESS_SUBJECTS.includes */.iM.includes(c.subject)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2686
+ if (c.subject === 'user') {
2687
+ if (c.userId === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_ACCESS_USER_REQUIRED */.qj.NODE_ACCESS_USER_REQUIRED, at));
2688
+ else expr(c.userId, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_USER */.bc.THE_USER));
2689
+ }
2690
+ if (c.companyId !== undefined) expr(c.companyId, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_COMPANY */.bc.THE_COMPANY));
2691
+ if (c.branchId !== undefined) expr(c.branchId, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_BRANCH */.bc.THE_BRANCH));
2692
+ if (c.onDenied !== undefined && c.onDenied !== 'reject' && c.onDenied !== 'branch') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2693
+ if (c.message !== undefined && typeof c.message !== 'string') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2694
+ if (c.statusCode !== undefined && !(Number.isInteger(c.statusCode) && c.statusCode >= 400 && c.statusCode <= 499)) {
2695
+ err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_STATUS_4XX */.qj.NODE_STATUS_4XX, at));
2696
+ }
2697
+ break;
2698
+ }
2699
+ case 'company_branch_check':
2700
+ {
2701
+ if (c.subject !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_ACCESS_SUBJECTS.includes */.iM.includes(c.subject)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2702
+ if (c.branchMatch !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_BRANCH_MATCHES.includes */.Et.includes(c.branchMatch)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2703
+ if (c.subject === 'user') {
2704
+ if (c.userId === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_ACCESS_USER_REQUIRED */.qj.NODE_ACCESS_USER_REQUIRED, at));
2705
+ else expr(c.userId, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_USER */.bc.THE_USER));
2706
+ }
2707
+ // Another user has no current company.
2708
+ if (c.branchCompany !== undefined && (!_flow_types_js__rspack_import_5/* .FLOW_BRANCH_COMPANIES.includes */.sk.includes(c.branchCompany) || c.branchCompany === 'current' && c.subject === 'user')) {
2709
+ err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2132
2710
  }
2133
- if (c.match !== undefined && c.match !== 'any' && c.match !== 'all') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2711
+ if (c.companyIds !== undefined) expr(c.companyIds, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_VALUE */.bc.THE_VALUE));
2712
+ if (c.branchIds !== undefined) expr(c.branchIds, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_VALUE */.bc.THE_VALUE));
2134
2713
  if (c.onDenied !== undefined && c.onDenied !== 'reject' && c.onDenied !== 'branch') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2135
- if (c.branch !== undefined && c.branch !== null) expr(c.branch, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_VALUE */.bc.THE_VALUE));
2714
+ // Without an id to check there is nothing to allow or deny.
2715
+ if (c.onDenied === 'branch' && c.companyIds === undefined && c.branchIds === undefined) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_ACCESS_TARGET_REQUIRED */.qj.NODE_ACCESS_TARGET_REQUIRED, at));
2136
2716
  if (c.message !== undefined && typeof c.message !== 'string') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_PERMISSION_OPTION_INVALID */.qj.NODE_PERMISSION_OPTION_INVALID, at));
2137
2717
  if (c.statusCode !== undefined && !(Number.isInteger(c.statusCode) && c.statusCode >= 400 && c.statusCode <= 499)) {
2138
2718
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_STATUS_4XX */.qj.NODE_STATUS_4XX, at));
@@ -2143,7 +2723,7 @@ function validateNodeConfig(node, flow, context, err, warn) {
2143
2723
  exprMap(c.values, KIND.variable);
2144
2724
  if (!c.values || !Object.keys(c.values).length) warn.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_SETS_NOTHING */.qj.NODE_SETS_NOTHING, at));
2145
2725
  for (const key of Object.keys(c.values ?? {})){
2146
- if (!_flow_types_js__rspack_import_6/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(key)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_VARIABLE_INVALID */.qj.NODE_VARIABLE_INVALID, {
2726
+ if (!_flow_types_js__rspack_import_5/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(key)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_VARIABLE_INVALID */.qj.NODE_VARIABLE_INVALID, {
2147
2727
  ...at,
2148
2728
  name: key
2149
2729
  }));
@@ -2174,10 +2754,10 @@ function validateNodeConfig(node, flow, context, err, warn) {
2174
2754
  case 'foreach':
2175
2755
  expr(c.items, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_LIST */.bc.THE_LIST));
2176
2756
  if (c.collect !== undefined) expr(c.collect, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_COLLECTED_VALUE */.bc.THE_COLLECTED_VALUE));
2177
- if (c.maxItems !== undefined && !(Number.isInteger(c.maxItems) && c.maxItems >= 1 && c.maxItems <= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_FOREACH_ITEMS */.Or.MAX_FOREACH_ITEMS)) {
2757
+ if (c.maxItems !== undefined && !(Number.isInteger(c.maxItems) && c.maxItems >= 1 && c.maxItems <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_FOREACH_ITEMS */.Or.MAX_FOREACH_ITEMS)) {
2178
2758
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_MAX_ITEMS_RANGE */.qj.NODE_MAX_ITEMS_RANGE, {
2179
2759
  ...at,
2180
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_FOREACH_ITEMS */.Or.MAX_FOREACH_ITEMS
2760
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_FOREACH_ITEMS */.Or.MAX_FOREACH_ITEMS
2181
2761
  }));
2182
2762
  }
2183
2763
  break;
@@ -2202,7 +2782,7 @@ function validateNodeConfig(node, flow, context, err, warn) {
2202
2782
  break;
2203
2783
  case 'entity_update':
2204
2784
  entity(c.entityCode);
2205
- writeTargetConfig(c, _flow_types_js__rspack_import_6/* .RECORD_WRITE_TARGETS */.Dn, [
2785
+ writeTargetConfig(c, _flow_types_js__rspack_import_5/* .RECORD_WRITE_TARGETS */.Dn, [
2206
2786
  'error',
2207
2787
  'skip',
2208
2788
  'insert'
@@ -2216,7 +2796,7 @@ function validateNodeConfig(node, flow, context, err, warn) {
2216
2796
  break;
2217
2797
  case 'entity_delete':
2218
2798
  entity(c.entityCode);
2219
- writeTargetConfig(c, _flow_types_js__rspack_import_6/* .RECORD_WRITE_TARGETS */.Dn, [
2799
+ writeTargetConfig(c, _flow_types_js__rspack_import_5/* .RECORD_WRITE_TARGETS */.Dn, [
2220
2800
  'error',
2221
2801
  'skip'
2222
2802
  ], {
@@ -2234,22 +2814,26 @@ function validateNodeConfig(node, flow, context, err, warn) {
2234
2814
  entity(c.entityCode);
2235
2815
  filterMap(c.filter, KIND.filter);
2236
2816
  if (c.search !== undefined) expr(c.search, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_SEARCH_TEXT */.bc.THE_SEARCH_TEXT));
2237
- if (c.limit !== undefined && !(Number.isInteger(c.limit) && c.limit >= 1 && c.limit <= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_QUERY_LIMIT */.Or.MAX_QUERY_LIMIT)) {
2817
+ if (c.sort !== undefined) {
2818
+ const sort = c.sort;
2819
+ if (typeof sort?.field !== 'string' || !COLUMN_CODE.test(sort.field) || sort.direction !== undefined && sort.direction !== 'ASC' && sort.direction !== 'DESC') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_SORT_INVALID */.qj.NODE_SORT_INVALID, at));
2820
+ }
2821
+ if (c.limit !== undefined && !(Number.isInteger(c.limit) && c.limit >= 1 && c.limit <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_QUERY_LIMIT */.Or.MAX_QUERY_LIMIT)) {
2238
2822
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_LIMIT_RANGE */.qj.NODE_LIMIT_RANGE, {
2239
2823
  ...at,
2240
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_QUERY_LIMIT */.Or.MAX_QUERY_LIMIT
2824
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_QUERY_LIMIT */.Or.MAX_QUERY_LIMIT
2241
2825
  }));
2242
2826
  }
2243
2827
  break;
2244
2828
  case 'call_flow':
2245
- if (typeof c.flowSlug !== 'string' || !_flow_types_js__rspack_import_6/* .FLOW_SLUG_PATTERN.test */.YD.test(c.flowSlug)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHOOSE_FLOW */.qj.NODE_CHOOSE_FLOW, at));
2829
+ if (typeof c.flowSlug !== 'string' || !_flow_types_js__rspack_import_5/* .FLOW_SLUG_PATTERN.test */.YD.test(c.flowSlug)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHOOSE_FLOW */.qj.NODE_CHOOSE_FLOW, at));
2246
2830
  else if (c.flowSlug === flow.slug) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CALLS_ITSELF */.qj.NODE_CALLS_ITSELF, at));
2247
2831
  else if (context.knownFlows && !context.knownFlows.has(c.flowSlug)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FLOW_UNKNOWN */.qj.NODE_FLOW_UNKNOWN, {
2248
2832
  ...at,
2249
2833
  flow: c.flowSlug
2250
2834
  }));
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));
2835
+ else if (context.knownFlows) checkCalledFlow(node, flow, context.knownFlows, err, warn);
2836
+ if (c.endpoint !== undefined && c.endpoint !== '' && (typeof c.endpoint !== 'string' || !_flow_types_js__rspack_import_5/* .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));
2253
2837
  exprMap(c.input, KIND.input);
2254
2838
  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));
2255
2839
  break;
@@ -2265,10 +2849,11 @@ function validateNodeConfig(node, flow, context, err, warn) {
2265
2849
  expr(c.url, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_URL */.bc.THE_URL));
2266
2850
  exprMap(c.headers, KIND.header);
2267
2851
  exprMap(c.body, KIND.body);
2268
- if (c.timeoutMs !== undefined && !(Number.isInteger(c.timeoutMs) && c.timeoutMs >= 100 && c.timeoutMs <= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_HTTP_TIMEOUT_MS */.Or.MAX_HTTP_TIMEOUT_MS)) {
2852
+ if (c.forwardAuth === true) warn.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FORWARDS_TOKEN */.qj.NODE_FORWARDS_TOKEN, at));
2853
+ if (c.timeoutMs !== undefined && !(Number.isInteger(c.timeoutMs) && c.timeoutMs >= 100 && c.timeoutMs <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_HTTP_TIMEOUT_MS */.Or.MAX_HTTP_TIMEOUT_MS)) {
2269
2854
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_TIMEOUT_RANGE */.qj.NODE_TIMEOUT_RANGE, {
2270
2855
  ...at,
2271
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_HTTP_TIMEOUT_MS */.Or.MAX_HTTP_TIMEOUT_MS
2856
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_HTTP_TIMEOUT_MS */.Or.MAX_HTTP_TIMEOUT_MS
2272
2857
  }));
2273
2858
  }
2274
2859
  break;
@@ -2277,7 +2862,50 @@ function validateNodeConfig(node, flow, context, err, warn) {
2277
2862
  if (typeof c.eventName !== 'string' || !/^[a-z][a-z0-9_.-]{1,59}$/.test(c.eventName)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EVENT_NAME_INVALID */.qj.NODE_EVENT_NAME_INVALID, at));
2278
2863
  exprMap(c.payload, KIND.payload);
2279
2864
  break;
2280
- case 'respond':
2865
+ case 'send_notification':
2866
+ {
2867
+ serviceRunning('notification', _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_NOTIFICATION_UNAVAILABLE */.qj.NODE_NOTIFICATION_UNAVAILABLE);
2868
+ const target = c.target ?? 'one';
2869
+ if (!_flow_types_js__rspack_import_5/* .FLOW_NOTIFICATION_TARGETS.includes */.as.includes(target)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_MESSAGE_TARGET_INVALID */.qj.NODE_MESSAGE_TARGET_INVALID, at));
2870
+ if (target === 'many') expr(c.items, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_LIST */.bc.THE_LIST));
2871
+ if (target === 'company') {
2872
+ if (context.companyMembers === false) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_NOTIFICATION_COMPANY_UNAVAILABLE */.qj.NODE_NOTIFICATION_COMPANY_UNAVAILABLE, at));
2873
+ expr(c.companyIds, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_COMPANIES */.bc.THE_COMPANIES));
2874
+ optionalExpr(c.branchIds, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_BRANCHES */.bc.THE_BRANCHES));
2875
+ } else {
2876
+ expr(c.userIds, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_RECIPIENTS */.bc.THE_RECIPIENTS));
2877
+ optionalExpr(c.companyId, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_COMPANY */.bc.THE_COMPANY));
2878
+ }
2879
+ expr(c.title, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_TITLE */.bc.THE_TITLE));
2880
+ optionalExpr(c.message, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_MESSAGE */.bc.THE_MESSAGE));
2881
+ exprMap(c.data, KIND.data);
2882
+ if (c.type !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_NOTIFICATION_TYPES.includes */.LG.includes(c.type)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_NOTIFICATION_OPTION_INVALID */.qj.NODE_NOTIFICATION_OPTION_INVALID, at));
2883
+ if (c.realtime !== undefined && typeof c.realtime !== 'boolean') err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_NOTIFICATION_OPTION_INVALID */.qj.NODE_NOTIFICATION_OPTION_INVALID, at));
2884
+ break;
2885
+ }
2886
+ case 'send_email':
2887
+ {
2888
+ serviceRunning('email', _config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EMAIL_UNAVAILABLE */.qj.NODE_EMAIL_UNAVAILABLE);
2889
+ if (!_flow_types_js__rspack_import_5/* .FLOW_EMAIL_TARGETS.includes */.B9.includes(c.target ?? 'one')) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_MESSAGE_TARGET_INVALID */.qj.NODE_MESSAGE_TARGET_INVALID, at));
2890
+ if (c.target === 'many') expr(c.items, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_LIST */.bc.THE_LIST));
2891
+ expr(c.to, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_RECIPIENTS */.bc.THE_RECIPIENTS));
2892
+ optionalExpr(c.cc, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_CC */.bc.THE_CC));
2893
+ optionalExpr(c.bcc, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_BCC */.bc.THE_BCC));
2894
+ optionalExpr(c.replyTo, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_REPLY_TO */.bc.THE_REPLY_TO));
2895
+ if (c.mode !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_EMAIL_MODES.includes */.v1.includes(c.mode)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EMAIL_OPTION_INVALID */.qj.NODE_EMAIL_OPTION_INVALID, at));
2896
+ if (c.format !== undefined && !_flow_types_js__rspack_import_5/* .FLOW_EMAIL_FORMATS.includes */.FU.includes(c.format)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EMAIL_OPTION_INVALID */.qj.NODE_EMAIL_OPTION_INVALID, at));
2897
+ if (c.emailConfigId !== undefined && c.emailConfigId !== '' && (typeof c.emailConfigId !== 'string' || !_flow_types_js__rspack_import_5/* .FLOW_ID_PATTERN.test */.E2.test(c.emailConfigId))) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EMAIL_OPTION_INVALID */.qj.NODE_EMAIL_OPTION_INVALID, at));
2898
+ if (c.mode === 'template') {
2899
+ const slug = typeof c.templateSlug === 'string' ? c.templateSlug.trim() : '';
2900
+ if (!slug || slug.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_EMAIL_TEMPLATE_SLUG_LENGTH */.Or.MAX_EMAIL_TEMPLATE_SLUG_LENGTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_EMAIL_TEMPLATE_REQUIRED */.qj.NODE_EMAIL_TEMPLATE_REQUIRED, at));
2901
+ exprMap(c.variables, KIND.template);
2902
+ } else {
2903
+ expr(c.subject, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_SUBJECT */.bc.THE_SUBJECT));
2904
+ expr(c.body, (0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_SUBJECT_MESSAGES.THE_BODY */.bc.THE_BODY));
2905
+ }
2906
+ break;
2907
+ }
2908
+ case 'respond':
2281
2909
  {
2282
2910
  const status = c.statusCode;
2283
2911
  if (!(Number.isInteger(status) && status >= 200 && status <= 599)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_STATUS_2XX_5XX */.qj.NODE_STATUS_2XX_5XX, at));
@@ -2290,15 +2918,15 @@ function validateNodeConfig(node, flow, context, err, warn) {
2290
2918
  break;
2291
2919
  case 'code':
2292
2920
  if (typeof c.code !== 'string' || !c.code.trim()) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CODE_REQUIRED */.qj.NODE_CODE_REQUIRED, at));
2293
- else if (c.code.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CODE_LENGTH */.Or.MAX_CODE_LENGTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CODE_TOO_LONG */.qj.NODE_CODE_TOO_LONG, {
2921
+ else if (c.code.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CODE_LENGTH */.Or.MAX_CODE_LENGTH) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CODE_TOO_LONG */.qj.NODE_CODE_TOO_LONG, {
2294
2922
  ...at,
2295
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CODE_LENGTH */.Or.MAX_CODE_LENGTH
2923
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CODE_LENGTH */.Or.MAX_CODE_LENGTH
2296
2924
  }));
2297
- if (c.timeoutMs !== undefined && !(Number.isInteger(c.timeoutMs) && c.timeoutMs >= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MIN_CODE_TIMEOUT_MS */.Or.MIN_CODE_TIMEOUT_MS && c.timeoutMs <= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CODE_TIMEOUT_MS */.Or.MAX_CODE_TIMEOUT_MS)) {
2925
+ if (c.timeoutMs !== undefined && !(Number.isInteger(c.timeoutMs) && c.timeoutMs >= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MIN_CODE_TIMEOUT_MS */.Or.MIN_CODE_TIMEOUT_MS && c.timeoutMs <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CODE_TIMEOUT_MS */.Or.MAX_CODE_TIMEOUT_MS)) {
2298
2926
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CODE_TIMEOUT_RANGE */.qj.NODE_CODE_TIMEOUT_RANGE, {
2299
2927
  ...at,
2300
- min: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MIN_CODE_TIMEOUT_MS */.Or.MIN_CODE_TIMEOUT_MS,
2301
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CODE_TIMEOUT_MS */.Or.MAX_CODE_TIMEOUT_MS
2928
+ min: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MIN_CODE_TIMEOUT_MS */.Or.MIN_CODE_TIMEOUT_MS,
2929
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CODE_TIMEOUT_MS */.Or.MAX_CODE_TIMEOUT_MS
2302
2930
  }));
2303
2931
  }
2304
2932
  break;
@@ -2317,10 +2945,10 @@ function validateNodeConfig(node, flow, context, err, warn) {
2317
2945
  if (target === 'filter') {
2318
2946
  filterMap(c.filter);
2319
2947
  if (!c.filter || typeof c.filter !== 'object' || !Object.keys(c.filter).length) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_FILTER_REQUIRED */.qj.NODE_FILTER_REQUIRED, at));
2320
- if (c.limit !== undefined && !(Number.isInteger(c.limit) && c.limit >= 1 && c.limit <= _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_WRITE_ITEMS */.Or.MAX_WRITE_ITEMS)) {
2948
+ if (c.limit !== undefined && !(Number.isInteger(c.limit) && c.limit >= 1 && c.limit <= _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_WRITE_ITEMS */.Or.MAX_WRITE_ITEMS)) {
2321
2949
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_LIMIT_RANGE */.qj.NODE_LIMIT_RANGE, {
2322
2950
  ...at,
2323
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_WRITE_ITEMS */.Or.MAX_WRITE_ITEMS
2951
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_WRITE_ITEMS */.Or.MAX_WRITE_ITEMS
2324
2952
  }));
2325
2953
  }
2326
2954
  }
@@ -2328,9 +2956,9 @@ function validateNodeConfig(node, flow, context, err, warn) {
2328
2956
  function childInserts(value, { at, entity, expr, err }) {
2329
2957
  if (value === undefined) return;
2330
2958
  if (!Array.isArray(value)) return err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHILDREN_INVALID */.qj.NODE_CHILDREN_INVALID, at));
2331
- if (value.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CHILD_INSERTS */.Or.MAX_CHILD_INSERTS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHILDREN_TOO_MANY */.qj.NODE_CHILDREN_TOO_MANY, {
2959
+ if (value.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CHILD_INSERTS */.Or.MAX_CHILD_INSERTS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHILDREN_TOO_MANY */.qj.NODE_CHILDREN_TOO_MANY, {
2332
2960
  ...at,
2333
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_CHILD_INSERTS */.Or.MAX_CHILD_INSERTS
2961
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_CHILD_INSERTS */.Or.MAX_CHILD_INSERTS
2334
2962
  }));
2335
2963
  const names = new Set();
2336
2964
  for (const child of value){
@@ -2339,7 +2967,7 @@ function childInserts(value, { at, entity, expr, err }) {
2339
2967
  continue;
2340
2968
  }
2341
2969
  const name = String(child.as ?? '');
2342
- if (!_flow_types_js__rspack_import_6/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(name)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHILD_NAME_INVALID */.qj.NODE_CHILD_NAME_INVALID, {
2970
+ if (!_flow_types_js__rspack_import_5/* .FLOW_INPUT_NAME_PATTERN.test */.fZ.test(name)) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHILD_NAME_INVALID */.qj.NODE_CHILD_NAME_INVALID, {
2343
2971
  ...at,
2344
2972
  name
2345
2973
  }));
@@ -2373,7 +3001,8 @@ function childInserts(value, { at, entity, expr, err }) {
2373
3001
  /**
2374
3002
  * How many loop levels each part of a node's config runs inside, beyond the loops around the node:
2375
3003
  * a foreach's `collect` runs in its own loop; a many / filter write's per-record expressions in one
2376
- * of the node's own; child records one deeper than their parent record.
3004
+ * of the node's own; child records one deeper than their parent record; a many message step's values
3005
+ * (all but its list) one per item.
2377
3006
  */ function configScopes(node) {
2378
3007
  const config = node.config ?? {};
2379
3008
  if (node.type === 'foreach') {
@@ -2391,6 +3020,21 @@ function childInserts(value, { at, entity, expr, err }) {
2391
3020
  }
2392
3021
  ];
2393
3022
  }
3023
+ if ((0,_flow_types_js__rspack_import_5/* .isMessageNode */.SE)(node.type) && config['target'] === 'many') {
3024
+ const { items, ...perItem } = config;
3025
+ return [
3026
+ {
3027
+ value: items,
3028
+ extra: 0,
3029
+ ownLoop: false
3030
+ },
3031
+ {
3032
+ value: perItem,
3033
+ extra: 1,
3034
+ ownLoop: false
3035
+ }
3036
+ ];
3037
+ }
2394
3038
  if (!WRITE_NODE_TYPES.has(node.type)) return [
2395
3039
  {
2396
3040
  value: config,
@@ -2430,9 +3074,9 @@ function childInserts(value, { at, entity, expr, err }) {
2430
3074
  }
2431
3075
  function validateChecks(c, at, rule, err) {
2432
3076
  if (!Array.isArray(c.checks) || !c.checks.length) return err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_CHECK_REQUIRED */.qj.NODE_CHECK_REQUIRED, at));
2433
- if (c.checks.length > _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_VALIDATE_CHECKS */.Or.MAX_VALIDATE_CHECKS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_TOO_MANY_CHECKS */.qj.NODE_TOO_MANY_CHECKS, {
3077
+ if (c.checks.length > _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_VALIDATE_CHECKS */.Or.MAX_VALIDATE_CHECKS) err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.NODE_TOO_MANY_CHECKS */.qj.NODE_TOO_MANY_CHECKS, {
2434
3078
  ...at,
2435
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_VALIDATE_CHECKS */.Or.MAX_VALIDATE_CHECKS
3079
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_VALIDATE_CHECKS */.Or.MAX_VALIDATE_CHECKS
2436
3080
  }));
2437
3081
  const ids = new Set();
2438
3082
  for (const check of c.checks){
@@ -2461,7 +3105,7 @@ function validateChecks(c, at, rule, err) {
2461
3105
  }
2462
3106
  }
2463
3107
  /** 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);
3108
+ const endpoints = (0,_flow_types_js__rspack_import_5/* .flowEndpoints */.Xr)(flow);
2465
3109
  const declaredInputs = new Set(endpoints.flatMap((endpoint)=>endpoint.inputSchema.map((f)=>f.name)));
2466
3110
  // A list body is read by index, and an object body without fields takes anything: neither has names to check.
2467
3111
  const namesKnown = endpoints.length > 0 && endpoints.every((endpoint)=>endpoint.bodyType !== 'list' && endpoint.inputSchema.length > 0);
@@ -2476,21 +3120,21 @@ function validateChecks(c, at, rule, err) {
2476
3120
  const [scope, second] = ref.split('.');
2477
3121
  if (!REF_SCOPES.has(scope)) {
2478
3122
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.REF_BAD_SCOPE */.qj.REF_BAD_SCOPE, {
2479
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node),
3123
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node),
2480
3124
  ref
2481
3125
  }));
2482
3126
  } else if (scope === 'context' && second && !byId.has(second)) {
2483
3127
  err((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.REF_NODE_MISSING */.qj.REF_NODE_MISSING, {
2484
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node),
3128
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node),
2485
3129
  target: second
2486
3130
  }));
2487
3131
  } else if (scope === 'context' && second === node.id) {
2488
3132
  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, {
2489
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node)
3133
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node)
2490
3134
  }));
2491
3135
  } else if (scope === 'input' && second && namesKnown && !declaredInputs.has(second)) {
2492
3136
  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, {
2493
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(node),
3137
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(node),
2494
3138
  input: second
2495
3139
  }));
2496
3140
  }
@@ -2505,15 +3149,28 @@ function refsOf(value, refs = [], depth = 0) {
2505
3149
  return refs;
2506
3150
  }
2507
3151
  /**
2508
- * Writes that do not share a transaction, and Save progress steps that would not commit: in a flow
2509
- * without the transaction setting, inside a loop (once per item), or in a flow other flows call
2510
- * (inside its caller's transaction it is skipped).
2511
- */ function checkTransactionUse(flow, nodes, outgoing, reachable, context, warnings) {
2512
- const live = nodes.filter((n)=>!reachable || reachable.has(n.id));
2513
- const writes = live.filter((n)=>WRITE_NODE_TYPES.has(n.type)).length;
2514
- if (!flow.isTransactional && writes >= 2) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.WRITES_NOT_TRANSACTIONAL */.qj.WRITES_NOT_TRANSACTIONAL, {
2515
- count: writes
2516
- }));
3152
+ * Writes that do not share a transaction, and Save progress steps that would not commit: reached
3153
+ * from a Request step without the transaction setting, inside a loop (once per item), or in a flow
3154
+ * other flows call (inside its caller's transaction it is skipped).
3155
+ */ function checkTransactionUse(flow, runs, outgoing, context, warnings) {
3156
+ for (const { endpoint, label, steps } of runs){
3157
+ if (endpoint.isTransactional) continue;
3158
+ const writes = steps.filter((n)=>WRITE_NODE_TYPES.has(n.type)).length;
3159
+ if (writes >= 2) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.WRITES_NOT_TRANSACTIONAL */.qj.WRITES_NOT_TRANSACTIONAL, {
3160
+ count: writes,
3161
+ node: label
3162
+ }));
3163
+ for (const commit of steps)if (commit.type === 'commit') warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.COMMIT_NOT_TRANSACTIONAL */.qj.COMMIT_NOT_TRANSACTIONAL, {
3164
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(commit),
3165
+ endpoint: label
3166
+ }));
3167
+ }
3168
+ const live = [
3169
+ ...new Map(runs.flatMap((run)=>run.steps).map((n)=>[
3170
+ n.id,
3171
+ n
3172
+ ])).values()
3173
+ ];
2517
3174
  const commits = live.filter((n)=>n.type === 'commit');
2518
3175
  if (!commits.length) return;
2519
3176
  const loops = live.filter((n)=>n.type === 'foreach').map((n)=>({
@@ -2525,13 +3182,12 @@ function refsOf(value, refs = [], depth = 0) {
2525
3182
  ].some((f)=>f.calls.includes(flow.slug));
2526
3183
  for (const commit of commits){
2527
3184
  const at = {
2528
- node: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(commit)
3185
+ node: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(commit)
2529
3186
  };
2530
- if (!flow.isTransactional) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.COMMIT_NOT_TRANSACTIONAL */.qj.COMMIT_NOT_TRANSACTIONAL, at));
2531
3187
  const loop = loops.find((l)=>l.body.has(commit.id));
2532
3188
  if (loop) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.COMMIT_IN_LOOP */.qj.COMMIT_IN_LOOP, {
2533
3189
  ...at,
2534
- loop: (0,_flow_types_js__rspack_import_6/* .nodeLabel */.dA)(loop.node)
3190
+ loop: (0,_flow_types_js__rspack_import_5/* .nodeLabel */.dA)(loop.node)
2535
3191
  }));
2536
3192
  if (called) warnings.push((0,_flusys_nestjs_shared_utils__rspack_import_0.messageRef)(_config_message_keys_js__rspack_import_7/* .FLOW_VALIDATION_MESSAGES.COMMIT_IN_CALLED_FLOW */.qj.COMMIT_IN_CALLED_FLOW, at));
2537
3193
  }
@@ -2698,14 +3354,15 @@ function reach(start, outgoing) {
2698
3354
  nesting.set(id, max);
2699
3355
  return max;
2700
3356
  };
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, {
2702
- max: _flow_types_js__rspack_import_6/* .FLOW_LIMITS.MAX_LOOP_DEPTH */.Or.MAX_LOOP_DEPTH
3357
+ if (nodes.some((n)=>n.type === 'trigger' && nestingFrom(n.id) > _flow_types_js__rspack_import_5/* .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, {
3358
+ max: _flow_types_js__rspack_import_5/* .FLOW_LIMITS.MAX_LOOP_DEPTH */.Or.MAX_LOOP_DEPTH
2703
3359
  }));
2704
3360
  }
2705
3361
 
2706
3362
  __webpack_require__.d(__webpack_exports__, {
2707
- n: () => (validateFlow),
2708
- u: () => (usesPermissionFunction)
3363
+ nC: () => (validateFlow),
3364
+ r7: () => (callProblems),
3365
+ u9: () => (usesPermissionFunction)
2709
3366
  });
2710
3367
 
2711
3368
 
@@ -2908,6 +3565,19 @@ __webpack_require__.d(__webpack_exports__, {
2908
3565
  });
2909
3566
 
2910
3567
 
3568
+ },
3569
+ 8220(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3570
+ const permissionAction = (code)=>({
3571
+ type: 'action',
3572
+ actionId: code
3573
+ });
3574
+
3575
+ __webpack_require__.d(__webpack_exports__, {
3576
+ }, {
3577
+ s: permissionAction
3578
+ });
3579
+
3580
+
2911
3581
  },
2912
3582
  8542(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2913
3583
  /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
@@ -2989,203 +3659,14 @@ __webpack_require__.d(__webpack_exports__, {
2989
3659
  });
2990
3660
 
2991
3661
 
2992
- },
2993
- 6256(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
2994
- const FLOW_NODE_TYPES = [
2995
- 'trigger',
2996
- 'validate',
2997
- 'permission_check',
2998
- 'set',
2999
- 'condition',
3000
- 'switch',
3001
- 'foreach',
3002
- 'entity_insert',
3003
- 'entity_update',
3004
- 'entity_delete',
3005
- 'entity_get',
3006
- 'entity_query',
3007
- 'commit',
3008
- 'call_flow',
3009
- 'http_request',
3010
- 'publish_event',
3011
- 'respond',
3012
- 'code'
3013
- ];
3014
- /** A failure of these always ends the run: checks can never be carried past, and Save progress cannot run in a savepoint. */ const NON_CONTINUABLE_NODE_TYPES = new Set([
3015
- 'validate',
3016
- 'permission_check',
3017
- 'commit'
3018
- ]);
3019
- const FLOW_AUTH_MODES = [
3020
- 'public',
3021
- 'api_key',
3022
- 'jwt',
3023
- 'permission',
3024
- 'internal'
3025
- ];
3026
- const FLOW_RUN_AS = [
3027
- 'caller',
3028
- 'system'
3029
- ];
3030
- /** object: the body is one object and the fields are its keys (`input.<name>`). list: the body is a list and the fields describe each item (`input.0.<name>`). */ const FLOW_BODY_TYPES = [
3031
- 'object',
3032
- 'list'
3033
- ];
3034
- const FLOW_INPUT_TYPES = [
3035
- 'string',
3036
- 'text',
3037
- 'number',
3038
- 'integer',
3039
- 'boolean',
3040
- 'date',
3041
- 'datetime',
3042
- 'email',
3043
- 'uuid',
3044
- 'object',
3045
- 'array',
3046
- 'choice'
3047
- ];
3048
- /** How a step is named in messages: its name, or its id when it was left unnamed. */ const nodeLabel = (node)=>node.name || node.id;
3049
- /**
3050
- * Which records a write node touches. one (default): a single record. many: one per item of `items`,
3051
- * whose per-record expressions read the item as `loop.item` (a loop around the node becomes
3052
- * `loop.parent`). filter: every record matching `filter`, read as `loop.item`.
3053
- */ const RECORD_WRITE_TARGETS = [
3054
- 'one',
3055
- 'many',
3056
- 'filter'
3057
- ];
3058
- const FLOW_LIMITS = {
3059
- MAX_NODES: 100,
3060
- MAX_EDGES: 300,
3061
- /** Room for a full foreach (MAX_FOREACH_ITEMS) with a few steps per item. */ MAX_STEPS_PER_RUN: 5000,
3062
- MAX_FOREACH_ITEMS: 1000,
3063
- MAX_LOOP_DEPTH: 3,
3064
- MAX_CALL_DEPTH: 5,
3065
- MAX_HTTP_CALLS: 10,
3066
- MAX_HTTP_TIMEOUT_MS: 15_000,
3067
- MAX_HTTP_RESPONSE_BYTES: 1_000_000,
3068
- MAX_QUERY_LIMIT: 200,
3069
- MAX_WRITE_ITEMS: 1000,
3070
- DEFAULT_WRITE_FILTER_LIMIT: 100,
3071
- MAX_CHILD_INSERTS: 10,
3072
- MAX_WRITES_PER_RUN: 5000,
3073
- DEFAULT_TIMEOUT_MS: 30_000,
3074
- MIN_TIMEOUT_MS: 1000,
3075
- MAX_TIMEOUT_MS: 120_000,
3076
- TRACE_PREVIEW_CHARS: 2000,
3077
- MAX_INPUT_FIELDS: 50,
3078
- /** How deep object/list-of-object fields may nest inside the request body. */ MAX_INPUT_DEPTH: 4,
3079
- MAX_VALIDATE_CHECKS: 50,
3080
- MAX_CODE_LENGTH: 20_000,
3081
- DEFAULT_CODE_TIMEOUT_MS: 1000,
3082
- MIN_CODE_TIMEOUT_MS: 10,
3083
- MAX_CODE_TIMEOUT_MS: 5000,
3084
- CODE_MEMORY_LIMIT_BYTES: 32 * 1024 * 1024,
3085
- CODE_STACK_SIZE_BYTES: 512 * 1024,
3086
- MAX_CODE_OUTPUT_BYTES: 1_000_000,
3087
- MAX_CODE_LOG_LINES: 50,
3088
- MAX_CODE_LOG_LINE_CHARS: 500
3089
- };
3090
- const FLOW_NODE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,39}$/;
3091
- const FLOW_SLUG_PATTERN = /^[a-z][a-z0-9-]{2,62}$/;
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
- }
3120
- /** Output ports a node of this type can have (a `respond` node ends the flow: none). */ function portsOf(node) {
3121
- switch(node.type){
3122
- case 'condition':
3123
- return [
3124
- 'true',
3125
- 'false'
3126
- ];
3127
- case 'switch':
3128
- return [
3129
- ...(node.config.cases ?? []).map((c)=>c.id),
3130
- 'default'
3131
- ];
3132
- case 'foreach':
3133
- return [
3134
- 'each',
3135
- 'done'
3136
- ];
3137
- case 'permission_check':
3138
- return node.config.onDenied === 'branch' ? [
3139
- 'allowed',
3140
- 'denied'
3141
- ] : [
3142
- 'out'
3143
- ];
3144
- case 'respond':
3145
- return [];
3146
- case 'trigger':
3147
- case 'validate':
3148
- case 'set':
3149
- case 'commit':
3150
- return [
3151
- 'out'
3152
- ];
3153
- default:
3154
- return [
3155
- 'out',
3156
- 'error'
3157
- ];
3158
- }
3159
- }
3160
-
3161
- __webpack_require__.d(__webpack_exports__, {
3162
- Xr: () => (flowEndpoints),
3163
- a5: () => (portsOf),
3164
- um: () => (triggerPath),
3165
- vS: () => (findEndpoint)
3166
- }, {
3167
- At: NON_CONTINUABLE_NODE_TYPES,
3168
- Dn: RECORD_WRITE_TARGETS,
3169
- Or: FLOW_LIMITS,
3170
- Q7: FLOW_INPUT_TYPES,
3171
- Y3: FLOW_BODY_TYPES,
3172
- YD: FLOW_SLUG_PATTERN,
3173
- YO: FLOW_ENDPOINT_PATH_PATTERN,
3174
- dA: nodeLabel,
3175
- fO: FLOW_AUTH_MODES,
3176
- fZ: FLOW_INPUT_NAME_PATTERN,
3177
- gl: FLOW_RUN_AS,
3178
- q2: FLOW_NODE_TYPES,
3179
- z6: FLOW_NODE_ID_PATTERN
3180
- });
3181
-
3182
-
3183
3662
  },
3184
3663
  2678(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3185
3664
  const SENSITIVE_KEY = /pass(word)?|secret|token|authorization|api[-_]?key|cookie|credential|private[-_]?key/i;
3186
3665
  /** Copy of `value` with the content of sensitive-looking keys replaced, safe to store in a log. */ function redactSensitive(value, depth = 0) {
3187
3666
  if (depth > 8) return '[deep]';
3188
3667
  if (Array.isArray(value)) return value.slice(0, 100).map((v)=>redactSensitive(v, depth + 1));
3668
+ // Dates (and anything else with its own JSON form) have no own keys: walked as objects they would log as `{}`.
3669
+ if (value && typeof value.toJSON === 'function') return redactSensitive(value.toJSON(), depth + 1);
3189
3670
  if (value && typeof value === 'object') {
3190
3671
  const out = {};
3191
3672
  for (const [key, item] of Object.entries(value)){
@@ -3214,6 +3695,24 @@ __webpack_require__.d(__webpack_exports__, {
3214
3695
  });
3215
3696
 
3216
3697
 
3698
+ },
3699
+ 9609(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3700
+ /** What a version holds and a draft edits: everything about a flow but its identity. */ const FLOW_VERSIONED_FIELDS = [
3701
+ 'name',
3702
+ 'slug',
3703
+ 'description',
3704
+ 'isActive',
3705
+ 'retainExecutions',
3706
+ 'nodes',
3707
+ 'edges'
3708
+ ];
3709
+
3710
+ __webpack_require__.d(__webpack_exports__, {
3711
+ }, {
3712
+ v: FLOW_VERSIONED_FIELDS
3713
+ });
3714
+
3715
+
3217
3716
  },
3218
3717
  8323(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3219
3718
  /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);
@@ -3415,6 +3914,7 @@ function _ts_param(paramIndex, decorator) {
3415
3914
  }
3416
3915
 
3417
3916
 
3917
+ const VARIABLE_ENV_PREFIX = 'ENTITY_BUILDER_VAR_';
3418
3918
  class EntityBuilderConfigService {
3419
3919
  options;
3420
3920
  constructor(options){
@@ -3435,6 +3935,20 @@ class EntityBuilderConfigService {
3435
3935
  getConfig() {
3436
3936
  return this.options.config;
3437
3937
  }
3938
+ /** `designer.readOnly`, else the `ENTITY_BUILDER_READ_ONLY=true` env variable (production). */ isDesignerReadOnly() {
3939
+ return this.getConfig()?.designer?.readOnly ?? process.env['ENTITY_BUILDER_READ_ONLY'] === 'true';
3940
+ }
3941
+ /** The values behind `${var:NAME}` in promoted flows: every non-empty `ENTITY_BUILDER_VAR_<NAME>` env variable, overridden by `bundles.variables`; names a placeholder cannot carry are left out. */ getBundleVariables() {
3942
+ const fromEnv = Object.entries(process.env).filter((entry)=>entry[0].startsWith(VARIABLE_ENV_PREFIX) && !!entry[1]).map(([key, value])=>[
3943
+ key.slice(VARIABLE_ENV_PREFIX.length),
3944
+ value
3945
+ ]);
3946
+ const variables = {
3947
+ ...Object.fromEntries(fromEnv),
3948
+ ...this.getConfig()?.bundles?.variables
3949
+ };
3950
+ return Object.fromEntries(Object.entries(variables).filter((entry)=>_config_entity_builder_constants_js__rspack_import_1/* .BUNDLE_VARIABLE_NAME_PATTERN.test */.JI.test(entry[0]) && typeof entry[1] === 'string'));
3951
+ }
3438
3952
  }
3439
3953
  EntityBuilderConfigService = _ts_decorate([
3440
3954
  (0,_nestjs_common__rspack_import_0.Injectable)(),
@@ -3840,54 +4354,24 @@ __webpack_require__.d(__webpack_exports__, {
3840
4354
 
3841
4355
 
3842
4356
  },
3843
- 8888(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
3844
-
3845
- // EXPORTS
3846
- __webpack_require__.d(__webpack_exports__, {
3847
- T: () => (/* binding */ FlowApiKeyService)
3848
- });
3849
-
3850
- // EXTERNAL MODULE: external "@nestjs/common"
3851
- var common_ = __webpack_require__(6157);
3852
- // EXTERNAL MODULE: external "typeorm"
3853
- var external_typeorm_ = __webpack_require__(7158);
3854
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/config/message-keys.ts
3855
- var message_keys = __webpack_require__(5732);
3856
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/entities/flow-api-key.entity.ts
3857
- var flow_api_key_entity = __webpack_require__(201);
3858
- // EXTERNAL MODULE: external "node:crypto"
3859
- var external_node_crypto_ = __webpack_require__(2561);
3860
- ;// CONCATENATED MODULE: ./projects/nestjs-entity-builder/src/flow-engine/flow-api-key.ts
3861
-
3862
- const API_KEY_PREFIX_LENGTH = 11; // "fk_" + 8 hex
3863
- /** A new key: `fk_<8 hex>_<32 hex>`. Shown once; only its hash is stored. */ function generateApiKey() {
3864
- const key = `fk_${(0,external_node_crypto_.randomBytes)(4).toString('hex')}_${(0,external_node_crypto_.randomBytes)(16).toString('hex')}`;
3865
- return {
3866
- key,
3867
- prefix: apiKeyPrefix(key),
3868
- hash: hashApiKey(key)
3869
- };
3870
- }
3871
- const hashApiKey = (key)=>(0,external_node_crypto_.createHash)('sha256').update(key).digest('hex');
3872
- const apiKeyPrefix = (key)=>key.slice(0, API_KEY_PREFIX_LENGTH);
3873
- function looksLikeApiKey(key) {
3874
- return typeof key === 'string' && /^fk_[0-9a-f]{8}_[0-9a-f]{32}$/.test(key);
3875
- }
3876
- /** Constant-time comparison of a presented key against a stored hash. */ function apiKeyMatches(presented, storedHash) {
3877
- const a = Buffer.from(hashApiKey(presented), 'hex');
3878
- const b = Buffer.from(storedHash, 'hex');
3879
- return a.length === b.length && (0,external_node_crypto_.timingSafeEqual)(a, b);
3880
- }
3881
-
3882
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/entities/flow-definition.entity.ts
3883
- var flow_definition_entity = __webpack_require__(3770);
3884
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/rule-engine/rule-values.ts
3885
- var rule_values = __webpack_require__(6622);
3886
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/services/entity-builder-datasource.provider.ts
3887
- var entity_builder_datasource_provider = __webpack_require__(424);
3888
- // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/services/flow-definition.service.ts
3889
- var flow_definition_service = __webpack_require__(5229);
3890
- ;// CONCATENATED MODULE: ./projects/nestjs-entity-builder/src/services/flow-api-key.service.ts
4357
+ 5229(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4358
+ /* import */ var _flusys_nestjs_shared__rspack_import_0 = __webpack_require__(3422);
4359
+ /* import */ var _flusys_nestjs_shared_classes__rspack_import_1 = __webpack_require__(6937);
4360
+ /* import */ var _flusys_nestjs_shared_interfaces__rspack_import_2 = __webpack_require__(5103);
4361
+ /* import */ var _flusys_nestjs_shared_modules__rspack_import_3 = __webpack_require__(1236);
4362
+ /* import */ var _nestjs_common__rspack_import_4 = __webpack_require__(6157);
4363
+ /* import */ var typeorm__rspack_import_5 = __webpack_require__(7158);
4364
+ /* import */ var _config_event_actions_js__rspack_import_16 = __webpack_require__(982);
4365
+ /* import */ var _config_message_keys_js__rspack_import_14 = __webpack_require__(5732);
4366
+ /* import */ var _entities_entity_definition_entity_js__rspack_import_6 = __webpack_require__(3189);
4367
+ /* import */ var _entities_flow_definition_entity_js__rspack_import_7 = __webpack_require__(3770);
4368
+ /* import */ var _entities_flow_version_entity_js__rspack_import_8 = __webpack_require__(2399);
4369
+ /* import */ var _flow_engine_flow_api_key_js__rspack_import_9 = __webpack_require__(4816);
4370
+ /* import */ var _flow_engine_flow_permission_logic_js__rspack_import_13 = __webpack_require__(8220);
4371
+ /* import */ var _flow_engine_flow_graph_validator_js__rspack_import_10 = __webpack_require__(3186);
4372
+ /* import */ var _flow_engine_flow_types_js__rspack_import_11 = __webpack_require__(6256);
4373
+ /* import */ var _interfaces_flow_definition_interface_js__rspack_import_15 = __webpack_require__(9609);
4374
+ /* import */ var _entity_builder_datasource_provider_js__rspack_import_12 = __webpack_require__(424);
3891
4375
  function _ts_decorate(decorators, target, key, desc) {
3892
4376
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3893
4377
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -3920,194 +4404,6 @@ function _ts_param(paramIndex, decorator) {
3920
4404
 
3921
4405
 
3922
4406
 
3923
- const TOUCH_INTERVAL_MS = 60_000;
3924
- class FlowApiKeyService {
3925
- provider;
3926
- constructor(provider){
3927
- this.provider = provider;
3928
- }
3929
- async repo() {
3930
- return this.provider.getRepository(flow_api_key_entity/* .FlowApiKey */.S);
3931
- }
3932
- async assertFlowExists(flowId) {
3933
- const flow = await (await this.provider.getRepository(flow_definition_entity/* .FlowDefinition */.G)).findOne({
3934
- where: {
3935
- id: flowId
3936
- },
3937
- select: {
3938
- id: true
3939
- }
3940
- });
3941
- if (!flow) throw (0,flow_definition_service/* .flowNotFound */.YW)();
3942
- }
3943
- async findKey(id) {
3944
- const key = await (await this.repo()).findOne({
3945
- where: {
3946
- id
3947
- }
3948
- });
3949
- if (!key) throw new common_.NotFoundException({
3950
- message: 'API key not found.',
3951
- messageKey: message_keys/* .FLOW_MESSAGES.API_KEY_NOT_FOUND */.wu.API_KEY_NOT_FOUND
3952
- });
3953
- await this.assertFlowExists(key.flowId);
3954
- return key;
3955
- }
3956
- /** The plain key is returned here and never again. */ async create(flowId, name, expiresAt, user) {
3957
- const expiry = expiresAt ? (0,rule_values/* .parseDateText */.hz)(expiresAt) : null;
3958
- if (expiry !== null && !(expiry > Date.now())) {
3959
- throw new common_.BadRequestException({
3960
- message: 'The expiry date of an API key must be in the future.',
3961
- messageKey: message_keys/* .FLOW_MESSAGES.API_KEY_EXPIRY_IN_PAST */.wu.API_KEY_EXPIRY_IN_PAST
3962
- });
3963
- }
3964
- await this.assertFlowExists(flowId);
3965
- const { key, prefix, hash } = generateApiKey();
3966
- const repo = await this.repo();
3967
- const saved = await repo.save(repo.create({
3968
- flowId,
3969
- name,
3970
- keyPrefix: prefix,
3971
- keyHash: hash,
3972
- isActive: true,
3973
- expiresAt: expiry === null ? null : new Date(expiry),
3974
- lastUsedAt: null,
3975
- createdById: user?.id ?? null
3976
- }));
3977
- return {
3978
- key,
3979
- info: this.info(saved)
3980
- };
3981
- }
3982
- async list(flowId) {
3983
- await this.assertFlowExists(flowId);
3984
- const rows = await (await this.repo()).find({
3985
- where: {
3986
- flowId
3987
- },
3988
- order: {
3989
- createdAt: 'DESC'
3990
- }
3991
- });
3992
- return rows.map((r)=>this.info(r));
3993
- }
3994
- async revoke(id) {
3995
- const key = await this.findKey(id);
3996
- await (await this.repo()).update({
3997
- id: key.id
3998
- }, {
3999
- isActive: false
4000
- });
4001
- }
4002
- async remove(id) {
4003
- const key = await this.findKey(id);
4004
- await (await this.repo()).softDelete({
4005
- id: key.id
4006
- });
4007
- }
4008
- /**
4009
- * Checks `presented` against the unrevoked keys of this flow. Only the flow access guard calls
4010
- * this, and only while the flow's authMode is `api_key`; revoking works in any authMode.
4011
- */ async verify(flowId, presented) {
4012
- if (!looksLikeApiKey(presented)) return 'invalid';
4013
- const repo = await this.repo();
4014
- const candidates = await repo.find({
4015
- where: {
4016
- flowId,
4017
- keyPrefix: apiKeyPrefix(presented),
4018
- isActive: true,
4019
- deletedAt: (0,external_typeorm_.IsNull)()
4020
- },
4021
- select: {
4022
- id: true,
4023
- keyHash: true,
4024
- expiresAt: true,
4025
- lastUsedAt: true
4026
- }
4027
- });
4028
- const match = candidates.find((c)=>apiKeyMatches(presented, c.keyHash));
4029
- if (!match) return 'invalid';
4030
- if (isExpired(match.expiresAt)) return 'expired';
4031
- if (!match.lastUsedAt || Date.now() - match.lastUsedAt.getTime() > TOUCH_INTERVAL_MS) {
4032
- await repo.update({
4033
- id: match.id
4034
- }, {
4035
- lastUsedAt: new Date()
4036
- });
4037
- }
4038
- return 'valid';
4039
- }
4040
- info(row) {
4041
- const status = !row.isActive ? 'revoked' : isExpired(row.expiresAt) ? 'expired' : 'active';
4042
- return {
4043
- id: row.id,
4044
- flowId: row.flowId,
4045
- name: row.name,
4046
- keyPrefix: row.keyPrefix,
4047
- isActive: row.isActive,
4048
- status,
4049
- expiresAt: row.expiresAt,
4050
- lastUsedAt: row.lastUsedAt,
4051
- createdAt: row.createdAt
4052
- };
4053
- }
4054
- }
4055
- FlowApiKeyService = _ts_decorate([
4056
- (0,common_.Injectable)({
4057
- scope: common_.Scope.REQUEST
4058
- }),
4059
- _ts_param(0, (0,common_.Inject)(entity_builder_datasource_provider/* .EntityBuilderDataSourceProvider */.D)),
4060
- _ts_metadata("design:type", Function),
4061
- _ts_metadata("design:paramtypes", [
4062
- typeof entity_builder_datasource_provider/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : entity_builder_datasource_provider/* .EntityBuilderDataSourceProvider */.D
4063
- ])
4064
- ], FlowApiKeyService);
4065
- function isExpired(expiresAt) {
4066
- return !!expiresAt && expiresAt.getTime() <= Date.now();
4067
- }
4068
-
4069
-
4070
- },
4071
- 5229(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4072
- /* import */ var _flusys_nestjs_shared__rspack_import_0 = __webpack_require__(3422);
4073
- /* import */ var _flusys_nestjs_shared_classes__rspack_import_1 = __webpack_require__(6937);
4074
- /* import */ var _flusys_nestjs_shared_modules__rspack_import_2 = __webpack_require__(1236);
4075
- /* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(6157);
4076
- /* import */ var typeorm__rspack_import_4 = __webpack_require__(7158);
4077
- /* import */ var _config_event_actions_js__rspack_import_11 = __webpack_require__(982);
4078
- /* import */ var _config_message_keys_js__rspack_import_10 = __webpack_require__(5732);
4079
- /* import */ var _entities_entity_definition_entity_js__rspack_import_5 = __webpack_require__(3189);
4080
- /* import */ var _entities_flow_definition_entity_js__rspack_import_6 = __webpack_require__(3770);
4081
- /* import */ var _flow_engine_flow_graph_validator_js__rspack_import_7 = __webpack_require__(3186);
4082
- /* import */ var _flow_engine_flow_types_js__rspack_import_8 = __webpack_require__(6256);
4083
- /* import */ var _entity_builder_datasource_provider_js__rspack_import_9 = __webpack_require__(424);
4084
- function _ts_decorate(decorators, target, key, desc) {
4085
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4086
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
4087
- r = Reflect.decorate(decorators, target, key, desc);
4088
- } else {
4089
- for(var i = decorators.length - 1; i >= 0; i--){
4090
- if (d = decorators[i]) {
4091
- r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
4092
- }
4093
- }
4094
- }
4095
- return c > 3 && r && Object.defineProperty(target, key, r), r;
4096
- }
4097
- function _ts_metadata(metadataKey, metadataValue) {
4098
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
4099
- return Reflect.metadata(metadataKey, metadataValue);
4100
- }
4101
- }
4102
- function _ts_param(paramIndex, decorator) {
4103
- return function(target, key) {
4104
- decorator(target, key, paramIndex);
4105
- };
4106
- }
4107
-
4108
-
4109
-
4110
-
4111
4407
 
4112
4408
 
4113
4409
 
@@ -4117,21 +4413,28 @@ function _ts_param(paramIndex, decorator) {
4117
4413
 
4118
4414
 
4119
4415
  const defaultFlowPermissionCode = (slug)=>`entity_builder.flow.${slug}.execute`;
4120
- const flowNotFound = ()=>new _nestjs_common__rspack_import_3.NotFoundException({
4416
+ /** The rule a `permission` URL asks callers to pass: its own, or the flow's execute permission. */ const flowPermissions = (endpoint, slug)=>endpoint.permissions ?? (0,_flow_engine_flow_permission_logic_js__rspack_import_13/* .permissionAction */.s)(defaultFlowPermissionCode(slug));
4417
+ const flowNotFound = ()=>new _nestjs_common__rspack_import_4.NotFoundException({
4121
4418
  message: 'Flow not found.',
4122
- messageKey: _config_message_keys_js__rspack_import_10/* .FLOW_MESSAGES.NOT_FOUND */.wu.NOT_FOUND
4419
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.NOT_FOUND */.wu.NOT_FOUND
4420
+ });
4421
+ /** A flow that has been published at least once: its columns are what callers run. */ const isPublished = (flow)=>flow.version > 0;
4422
+ /** The versioned part of a flow (or of a draft), in a fixed key order so two snapshots compare as text. */ function snapshotOf(source) {
4423
+ return Object.fromEntries(_interfaces_flow_definition_interface_js__rspack_import_15/* .FLOW_VERSIONED_FIELDS.filter */.v.filter((key)=>source[key] !== undefined).map((key)=>[
4424
+ key,
4425
+ source[key]
4426
+ ]));
4427
+ }
4428
+ const sameSnapshot = (a, b)=>JSON.stringify(a) === JSON.stringify(b);
4429
+ /** What the designer edits: the saved draft, or the published flow when there is none. */ const workingCopy = (flow)=>({
4430
+ ...snapshotOf(flow),
4431
+ ...flow.draft
4123
4432
  });
4124
- /** Fills the defaults a flow definition gets when a field is left out. */ function toFlowSettings(dto) {
4433
+ /** What running a flow needs; everything about a run is on its Request steps. */ function toFlowSettings(dto) {
4125
4434
  return {
4126
4435
  slug: dto.slug,
4127
- authMode: dto.authMode ?? 'jwt',
4128
- runAs: dto.runAs ?? 'caller',
4129
- isTransactional: dto.isTransactional ?? false,
4130
- bodyType: dto.bodyType ?? 'object',
4131
- inputSchema: dto.inputSchema ?? [],
4132
4436
  nodes: dto.nodes ?? [],
4133
- edges: dto.edges ?? [],
4134
- timeoutMs: dto.timeoutMs ?? _flow_engine_flow_types_js__rspack_import_8/* .FLOW_LIMITS.DEFAULT_TIMEOUT_MS */.Or.DEFAULT_TIMEOUT_MS
4437
+ edges: dto.edges ?? []
4135
4438
  };
4136
4439
  }
4137
4440
  class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import_1.ApiService {
@@ -4140,11 +4443,14 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4140
4443
  provider;
4141
4444
  permissionCache;
4142
4445
  actionRegistry;
4143
- constructor(cacheManager, utilsService, provider, permissionCache, actionRegistry){
4144
- super(_config_event_actions_js__rspack_import_11/* .ENTITY_BUILDER_EVENT_ENTITIES.FLOW_DEFINITION */.P.FLOW_DEFINITION, cacheManager, utilsService, FlowDefinitionService.name, true, _config_event_actions_js__rspack_import_11/* .ENTITY_BUILDER_EVENT_MODULE */.S, provider, _entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G), this.cacheManager = cacheManager, this.utilsService = utilsService, this.provider = provider, this.permissionCache = permissionCache, this.actionRegistry = actionRegistry;
4446
+ notifications;
4447
+ email;
4448
+ companyAccess;
4449
+ constructor(cacheManager, utilsService, provider, permissionCache, actionRegistry, notifications, email, companyAccess){
4450
+ super(_config_event_actions_js__rspack_import_16/* .ENTITY_BUILDER_EVENT_ENTITIES.FLOW_DEFINITION */.P.FLOW_DEFINITION, cacheManager, utilsService, FlowDefinitionService.name, true, _config_event_actions_js__rspack_import_16/* .ENTITY_BUILDER_EVENT_MODULE */.S, provider, _entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G), this.cacheManager = cacheManager, this.utilsService = utilsService, this.provider = provider, this.permissionCache = permissionCache, this.actionRegistry = actionRegistry, this.notifications = notifications, this.email = email, this.companyAccess = companyAccess;
4145
4451
  }
4146
4452
  async findOrFail(id) {
4147
- const flow = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).findOne({
4453
+ const flow = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).findOne({
4148
4454
  where: {
4149
4455
  id
4150
4456
  }
@@ -4152,28 +4458,28 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4152
4458
  if (!flow) throw flowNotFound();
4153
4459
  return flow;
4154
4460
  }
4155
- /** Validation with the real entity and flow lists. Returns errors and warnings without throwing. */ async validate(settings) {
4156
- const entities = await (await this.provider.getRepository(_entities_entity_definition_entity_js__rspack_import_5/* .EntityDefinition */.f)).find({
4461
+ /** Validation with the real entity and flow lists and the modules this app runs. Returns errors and warnings without throwing. */ async validate(settings) {
4462
+ const entities = await (await this.provider.getRepository(_entities_entity_definition_entity_js__rspack_import_6/* .EntityDefinition */.f)).find({
4157
4463
  select: {
4158
4464
  code: true
4159
4465
  }
4160
4466
  });
4161
- const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).find();
4162
- return (0,_flow_engine_flow_graph_validator_js__rspack_import_7/* .validateFlow */.n)(settings, {
4467
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).find();
4468
+ return (0,_flow_engine_flow_graph_validator_js__rspack_import_10/* .validateFlow */.nC)(settings, {
4163
4469
  knownEntityCodes: new Set(entities.map((e)=>e.code)),
4164
4470
  knownFlows: new Map(flows.map((flow)=>[
4165
4471
  flow.slug,
4166
- {
4167
- isActive: flow.isActive,
4168
- authMode: flow.authMode,
4169
- endpoints: (0,_flow_engine_flow_types_js__rspack_import_8/* .flowEndpoints */.Xr)(flow),
4170
- calls: calledSlugs(flow.nodes)
4171
- }
4172
- ]))
4472
+ callableOf(flow)
4473
+ ])),
4474
+ messageServices: {
4475
+ notification: !!this.notifications,
4476
+ email: !!this.email
4477
+ },
4478
+ companyMembers: !!this.companyAccess
4173
4479
  });
4174
4480
  }
4175
4481
  /** Names of the other flows whose `call_flow` nodes run `slug`. */ async callersOf(slug, excludeIds) {
4176
- const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).find({
4482
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).find({
4177
4483
  select: {
4178
4484
  id: true,
4179
4485
  name: true,
@@ -4185,20 +4491,83 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4185
4491
  async assertNotCalled(slug, excludeIds) {
4186
4492
  const callers = await this.callersOf(slug, excludeIds);
4187
4493
  if (!callers.length) return;
4188
- throw new _nestjs_common__rspack_import_3.ConflictException({
4494
+ throw new _nestjs_common__rspack_import_4.ConflictException({
4189
4495
  message: `The flow '${slug}' is called by: ${callers.join(', ')}. Change those flows first.`,
4190
- messageKey: _config_message_keys_js__rspack_import_10/* .FLOW_MESSAGES.CALLED_BY_OTHER_FLOWS */.wu.CALLED_BY_OTHER_FLOWS,
4496
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.CALLED_BY_OTHER_FLOWS */.wu.CALLED_BY_OTHER_FLOWS,
4191
4497
  messageVariables: {
4192
4498
  slug,
4193
4499
  flows: callers.join(', ')
4194
4500
  }
4195
4501
  });
4196
4502
  }
4503
+ /**
4504
+ * Other flows' Call flow steps keep working: the URL name they call is not renamed, the paths they
4505
+ * start at are not dropped, and nothing else they rely on changes under them - access, body,
4506
+ * required inputs, the flow staying on, the call chain staying loop-free and within depth.
4507
+ */ async assertCallersKept(before, after, live) {
4508
+ if (after.slug !== before.slug) return this.assertNotCalled(before.slug, [
4509
+ before.id
4510
+ ]);
4511
+ await this.assertEndpointsNotCalled(before, after);
4512
+ await this.assertCallsStillValid(before, {
4513
+ ...after,
4514
+ ...live
4515
+ });
4516
+ }
4517
+ /**
4518
+ * Checks each running flow that calls this one, directly or through other flows, against the flows
4519
+ * as they are and as this change leaves them, and refuses the change when it adds a problem. A
4520
+ * caller already broken for another reason does not block it; only what this change breaks does.
4521
+ */ async assertCallsStillValid(before, after) {
4522
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).find();
4523
+ const running = flows.filter((flow)=>flow.id !== before.id && flow.isActive && isPublished(flow) && calledSlugs(flow.nodes).length);
4524
+ if (!running.length) return;
4525
+ const now = new Map(flows.map((flow)=>[
4526
+ flow.slug,
4527
+ callableOf(flow)
4528
+ ]));
4529
+ const next = new Map(now).set(before.slug, callableOf(after));
4530
+ const broken = [];
4531
+ for (const caller of running){
4532
+ const settings = toFlowSettings(caller);
4533
+ // Counted, not a set: two steps with the same label can share a problem, and only the new one is added.
4534
+ const existing = new Map();
4535
+ for (const problem of (0,_flow_engine_flow_graph_validator_js__rspack_import_10/* .callProblems */.r7)(settings, before.slug, now)){
4536
+ const key = JSON.stringify(problem);
4537
+ existing.set(key, (existing.get(key) ?? 0) + 1);
4538
+ }
4539
+ const added = (0,_flow_engine_flow_graph_validator_js__rspack_import_10/* .callProblems */.r7)(settings, before.slug, next).filter((problem)=>{
4540
+ const key = JSON.stringify(problem);
4541
+ const left = existing.get(key) ?? 0;
4542
+ existing.set(key, left - 1);
4543
+ return left <= 0;
4544
+ });
4545
+ if (added.length) broken.push({
4546
+ flow: caller.name,
4547
+ problems: added
4548
+ });
4549
+ }
4550
+ if (!broken.length) return;
4551
+ const names = broken.map((b)=>b.flow).join(', ');
4552
+ throw new _nestjs_common__rspack_import_4.ConflictException({
4553
+ message: `This change breaks flows that call '${before.slug}': ${names}. Change those flows first.`,
4554
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.CALLERS_BROKEN */.wu.CALLERS_BROKEN,
4555
+ messageVariables: {
4556
+ slug: before.slug,
4557
+ flows: names
4558
+ },
4559
+ errors: broken.flatMap((b)=>b.problems.map((problem)=>({
4560
+ field: 'flow',
4561
+ flow: b.flow,
4562
+ ...problem
4563
+ })))
4564
+ });
4565
+ }
4197
4566
  /** 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));
4567
+ const kept = new Set((0,_flow_engine_flow_types_js__rspack_import_11/* .flowEndpoints */.Xr)(after).map((e)=>e.path));
4568
+ const removed = (0,_flow_engine_flow_types_js__rspack_import_11/* .flowEndpoints */.Xr)(before).map((e)=>e.path).filter((path)=>!kept.has(path));
4200
4569
  if (!removed.length) return;
4201
- const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).find({
4570
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).find({
4202
4571
  select: {
4203
4572
  id: true,
4204
4573
  name: true,
@@ -4209,9 +4578,9 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4209
4578
  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
4579
  if (!callers.length) continue;
4211
4580
  const url = `api-flows/${before.slug}${path ? `/${path}` : ''}`;
4212
- throw new _nestjs_common__rspack_import_3.ConflictException({
4581
+ throw new _nestjs_common__rspack_import_4.ConflictException({
4213
4582
  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,
4583
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.ENDPOINT_CALLED_BY_OTHER_FLOWS */.wu.ENDPOINT_CALLED_BY_OTHER_FLOWS,
4215
4584
  messageVariables: {
4216
4585
  url,
4217
4586
  flows: callers.join(', ')
@@ -4222,9 +4591,9 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4222
4591
  async assertSaveable(settings, user, selfId) {
4223
4592
  const { errors, warnings } = await this.validate(settings);
4224
4593
  if (errors.length) {
4225
- throw new _nestjs_common__rspack_import_3.BadRequestException({
4594
+ throw new _nestjs_common__rspack_import_4.BadRequestException({
4226
4595
  message: 'The flow has problems.',
4227
- messageKey: _config_message_keys_js__rspack_import_10/* .FLOW_MESSAGES.INVALID */.wu.INVALID,
4596
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.INVALID */.wu.INVALID,
4228
4597
  errors: errors.map((error)=>({
4229
4598
  field: 'flow',
4230
4599
  ...error
@@ -4232,16 +4601,16 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4232
4601
  warnings
4233
4602
  });
4234
4603
  }
4235
- const clash = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).findOne({
4604
+ const clash = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).findOne({
4236
4605
  where: {
4237
4606
  slug: settings.slug
4238
4607
  },
4239
4608
  withDeleted: true
4240
4609
  });
4241
4610
  if (clash && clash.id !== selfId) {
4242
- throw new _nestjs_common__rspack_import_3.ConflictException({
4611
+ throw new _nestjs_common__rspack_import_4.ConflictException({
4243
4612
  message: `The URL name '${settings.slug}' is already used by another flow.`,
4244
- messageKey: _config_message_keys_js__rspack_import_10/* .FLOW_MESSAGES.SLUG_TAKEN */.wu.SLUG_TAKEN,
4613
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.SLUG_TAKEN */.wu.SLUG_TAKEN,
4245
4614
  messageVariables: {
4246
4615
  slug: settings.slug
4247
4616
  }
@@ -4250,7 +4619,6 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4250
4619
  await this.assertMayAuthor(settings, user);
4251
4620
  }
4252
4621
  /** The extra permissions a flow needs from whoever writes it: saving it, or test-running it as an unsaved draft. */ async assertMayAuthor(settings, user) {
4253
- if (settings.runAs === 'system') await this.assertHasPermission(user, _flusys_nestjs_shared__rspack_import_0.FLOW_DEFINITION_PERMISSIONS.SYSTEM);
4254
4622
  if (settings.nodes.some((node)=>node?.type === 'code')) await this.assertHasPermission(user, _flusys_nestjs_shared__rspack_import_0.FLOW_DEFINITION_PERMISSIONS.CODE);
4255
4623
  }
4256
4624
  async assertMayCreate(user) {
@@ -4258,13 +4626,13 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4258
4626
  }
4259
4627
  /**
4260
4628
  * A test run needs what running the flow for real would: the author permissions for its
4261
- * run-as and code nodes (a saved flow as much as a draft). A live run also needs what makes it
4262
- * real - a draft the permission to save it, a permission flow its own execute permission.
4263
- */ async assertMayTest(settings, user, run) {
4629
+ * code nodes (a saved flow as much as a draft). A live run also needs what makes it
4630
+ * real - a draft the permission to save it, a permission URL its own execute permission.
4631
+ */ async assertMayTest(settings, endpoint, user, run) {
4264
4632
  await this.assertMayAuthor(settings, user);
4265
4633
  if (run.mode !== 'live') return;
4266
4634
  if (run.draft) await this.assertHasPermission(user, run.saved ? _flusys_nestjs_shared__rspack_import_0.FLOW_DEFINITION_PERMISSIONS.UPDATE : _flusys_nestjs_shared__rspack_import_0.FLOW_DEFINITION_PERMISSIONS.CREATE);
4267
- if (settings.authMode === 'permission') await this.assertHasPermission(user, settings.permissionCode || defaultFlowPermissionCode(settings.slug));
4635
+ if (endpoint?.authMode === 'permission') await this.permissionCache.assertPermissionLogic(user, flowPermissions(endpoint, settings.slug));
4268
4636
  }
4269
4637
  assertHasPermission(user, permission) {
4270
4638
  return this.permissionCache.assertPermission(user, permission);
@@ -4274,37 +4642,171 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4274
4642
  dto
4275
4643
  ]){
4276
4644
  await this.assertSaveable(toFlowSettings(d), user);
4277
- // JSON columns have no database default, so leaving the optional settings out must not mean NULL.
4278
- d.inputSchema ??= [];
4279
- d.bodyType ??= 'object';
4280
- d.authMode ??= 'jwt';
4281
- d.runAs ??= 'caller';
4645
+ d.nodes = (0,_flow_engine_flow_api_key_js__rspack_import_9/* .sealApiKeys */.ue)(d.nodes);
4646
+ d.version = 0;
4282
4647
  }
4283
4648
  }
4284
- async beforeUpdateOperation(dto, user, _qr) {
4649
+ /**
4650
+ * Checked against what the save produces: the draft of a published flow, the flow itself before its
4651
+ * first publish. An unpublished flow changes in place, so its callers are protected now, not at publish.
4652
+ */ async beforeUpdateOperation(dto, user, _qr) {
4285
4653
  for (const d of Array.isArray(dto) ? dto : [
4286
4654
  dto
4287
4655
  ]){
4288
4656
  const existing = await this.findOrFail(d.id);
4289
- const merged = toFlowSettings({
4290
- ...existing,
4657
+ const settings = toFlowSettings({
4658
+ ...workingCopy(existing),
4291
4659
  ...stripUndefined(d)
4292
4660
  });
4293
- await this.assertSaveable(merged, user, existing.id);
4294
- if (merged.slug !== existing.slug) await this.assertNotCalled(existing.slug, [
4295
- existing.id
4296
- ]);
4297
- else await this.assertEndpointsNotCalled(existing, merged);
4298
- d.version = existing.version + 1;
4661
+ await this.assertSaveable(settings, user, existing.id);
4662
+ if (!isPublished(existing)) await this.assertCallersKept(existing, settings, {
4663
+ isActive: d.isActive ?? existing.isActive,
4664
+ version: existing.version
4665
+ });
4666
+ if (d.nodes) d.nodes = (0,_flow_engine_flow_api_key_js__rspack_import_9/* .sealApiKeys */.ue)(d.nodes);
4667
+ }
4668
+ }
4669
+ /** A published flow keeps running as published: the edits go to its draft (none when they match what is live). */ async convertSingleDtoToEntity(dto, user) {
4670
+ const id = dto.id;
4671
+ if (!id) return super.convertSingleDtoToEntity(dto, user);
4672
+ const existing = await this.findOrFail(id);
4673
+ if (!isPublished(existing)) return super.convertSingleDtoToEntity(dto, user);
4674
+ const draft = snapshotOf({
4675
+ ...workingCopy(existing),
4676
+ ...stripUndefined(dto)
4677
+ });
4678
+ existing.draft = sameSnapshot(draft, snapshotOf(existing)) ? null : draft;
4679
+ return existing;
4680
+ }
4681
+ /**
4682
+ * Makes the draft (or, before the first publish, the flow as saved) what callers run, as the next
4683
+ * version. It is checked like a save, plus what only going live can break: other flows calling
4684
+ * this one by a URL name or path it no longer has.
4685
+ */ async publish(id, note, user) {
4686
+ const existing = await this.findOrFail(id);
4687
+ const published = isPublished(existing);
4688
+ if (published && !existing.draft) throw new _nestjs_common__rspack_import_4.BadRequestException({
4689
+ message: 'There are no changes to publish.',
4690
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.NOTHING_TO_PUBLISH */.wu.NOTHING_TO_PUBLISH
4691
+ });
4692
+ const next = workingCopy(existing);
4693
+ const settings = toFlowSettings(next);
4694
+ await this.assertSaveable(settings, user, existing.id);
4695
+ await this.assertCallersKept(existing, settings, {
4696
+ isActive: next.isActive ?? existing.isActive,
4697
+ version: existing.version + 1
4698
+ });
4699
+ const qr = await this.provider.getQueryRunner();
4700
+ try {
4701
+ await qr.connect();
4702
+ await qr.startTransaction();
4703
+ const versions = qr.manager.getRepository(_entities_flow_version_entity_js__rspack_import_8/* .FlowVersion */.p);
4704
+ Object.assign(existing, next, {
4705
+ draft: null,
4706
+ version: existing.version + 1,
4707
+ updatedById: user?.id ?? null
4708
+ });
4709
+ await qr.manager.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G).save(existing);
4710
+ await versions.save(versions.create({
4711
+ flowId: existing.id,
4712
+ version: existing.version,
4713
+ definition: next,
4714
+ note: note?.trim() || null,
4715
+ publishedAt: new Date(),
4716
+ publishedById: user?.id ?? null
4717
+ }));
4718
+ await qr.commitTransaction();
4719
+ } catch (error) {
4720
+ await qr.rollbackTransaction().catch(()=>undefined);
4721
+ throw error;
4722
+ } finally{
4723
+ await qr.release().catch(()=>undefined);
4299
4724
  }
4725
+ await this.clearCaches(existing);
4726
+ await this.provisionPermissions([
4727
+ existing
4728
+ ]);
4729
+ return existing;
4730
+ }
4731
+ /** Newest first, without the definitions (`getVersion` returns one in full). */ async listVersions(flowId) {
4732
+ await this.findOrFail(flowId);
4733
+ return (await this.provider.getRepository(_entities_flow_version_entity_js__rspack_import_8/* .FlowVersion */.p)).find({
4734
+ where: {
4735
+ flowId
4736
+ },
4737
+ order: {
4738
+ version: 'DESC'
4739
+ },
4740
+ select: {
4741
+ id: true,
4742
+ flowId: true,
4743
+ version: true,
4744
+ note: true,
4745
+ publishedAt: true,
4746
+ publishedById: true
4747
+ }
4748
+ });
4749
+ }
4750
+ async getVersion(flowId, version) {
4751
+ const row = await (await this.provider.getRepository(_entities_flow_version_entity_js__rspack_import_8/* .FlowVersion */.p)).findOne({
4752
+ where: {
4753
+ flowId,
4754
+ version
4755
+ }
4756
+ });
4757
+ if (!row) throw new _nestjs_common__rspack_import_4.NotFoundException({
4758
+ message: `Version ${version} of this flow was not found.`,
4759
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.VERSION_NOT_FOUND */.wu.VERSION_NOT_FOUND,
4760
+ messageVariables: {
4761
+ version
4762
+ }
4763
+ });
4764
+ return row;
4765
+ }
4766
+ /**
4767
+ * Puts an old version back into the draft; callers keep the published flow until it is published
4768
+ * again. Not checked like a save (the entities it names may have changed since), but the author
4769
+ * still needs the permissions its code steps call for.
4770
+ */ async restoreVersion(flowId, version, user) {
4771
+ const flow = await this.findOrFail(flowId);
4772
+ const snapshot = snapshotOf((await this.getVersion(flowId, version)).definition);
4773
+ await this.assertMayAuthor(toFlowSettings(snapshot), user);
4774
+ flow.draft = sameSnapshot(snapshot, snapshotOf(flow)) ? null : snapshot;
4775
+ return this.saveDraft(flow, user);
4776
+ }
4777
+ async discardDraft(flowId, user) {
4778
+ const flow = await this.findOrFail(flowId);
4779
+ if (!flow.draft) throw new _nestjs_common__rspack_import_4.BadRequestException({
4780
+ message: 'This flow has no unpublished changes.',
4781
+ messageKey: _config_message_keys_js__rspack_import_14/* .FLOW_MESSAGES.NO_DRAFT */.wu.NO_DRAFT
4782
+ });
4783
+ flow.draft = null;
4784
+ return this.saveDraft(flow, user);
4785
+ }
4786
+ async saveDraft(flow, user) {
4787
+ await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).update(flow.id, {
4788
+ draft: flow.draft,
4789
+ updatedById: user?.id ?? null
4790
+ });
4791
+ await this.clearCaches(flow);
4792
+ return flow;
4793
+ }
4794
+ async clearCaches(flow) {
4795
+ if (!this.isCacheable) return;
4796
+ await Promise.all([
4797
+ this.clearCacheForAll(),
4798
+ this.clearCacheForId([
4799
+ flow
4800
+ ])
4801
+ ]);
4300
4802
  }
4301
4803
  async beforeDeleteOperation(dto, user, _qr) {
4302
4804
  const ids = Array.isArray(dto.id) ? dto.id : [
4303
4805
  dto.id
4304
4806
  ];
4305
- const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_6/* .FlowDefinition */.G)).find({
4807
+ const flows = await (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_7/* .FlowDefinition */.G)).find({
4306
4808
  where: {
4307
- id: (0,typeorm__rspack_import_4.In)(ids)
4809
+ id: (0,typeorm__rspack_import_5.In)(ids)
4308
4810
  },
4309
4811
  withDeleted: true
4310
4812
  });
@@ -4312,39 +4814,50 @@ class FlowDefinitionService extends _flusys_nestjs_shared_classes__rspack_import
4312
4814
  if (dto.type === 'restore') return;
4313
4815
  for (const flow of flows)await this.assertNotCalled(flow.slug, ids);
4314
4816
  }
4315
- async afterInsertOperation(entities) {
4316
- await this.provisionPermissions(entities);
4317
- }
4318
- async afterUpdateOperation(entities) {
4319
- await this.provisionPermissions(entities);
4320
- }
4321
- async provisionPermissions(flows) {
4322
- const needing = flows.filter((f)=>f.authMode === 'permission');
4323
- if (!needing.length) return;
4817
+ /** The flow's execute permission, registered so it can be granted, when a published URL asks for it (a URL with its own rule names existing permissions). */ async provisionPermissions(flows) {
4324
4818
  if (!this.actionRegistry) return;
4325
- await this.actionRegistry.registerActions(needing.map((f)=>({
4326
- code: f.permissionCode || defaultFlowPermissionCode(f.slug),
4327
- name: `Run ${f.name}`
4819
+ const actions = new Map();
4820
+ for (const flow of flows){
4821
+ for (const endpoint of (0,_flow_engine_flow_types_js__rspack_import_11/* .flowEndpoints */.Xr)(flow)){
4822
+ if (endpoint.authMode !== 'permission' || endpoint.permissions) continue;
4823
+ const code = defaultFlowPermissionCode(flow.slug);
4824
+ if (!actions.has(code)) actions.set(code, endpoint.path ? `Run ${flow.name} (${endpoint.path})` : `Run ${flow.name}`);
4825
+ }
4826
+ }
4827
+ if (actions.size) await this.actionRegistry.registerActions([
4828
+ ...actions
4829
+ ].map(([code, name])=>({
4830
+ code,
4831
+ name
4328
4832
  })));
4329
4833
  }
4330
4834
  }
4331
4835
  FlowDefinitionService = _ts_decorate([
4332
- (0,_nestjs_common__rspack_import_3.Injectable)({
4333
- scope: _nestjs_common__rspack_import_3.Scope.REQUEST
4836
+ (0,_nestjs_common__rspack_import_4.Injectable)({
4837
+ scope: _nestjs_common__rspack_import_4.Scope.REQUEST
4334
4838
  }),
4335
- _ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
4336
- _ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_2.UtilsService)),
4337
- _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_entity_builder_datasource_provider_js__rspack_import_9/* .EntityBuilderDataSourceProvider */.D)),
4338
- _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_2.SharedPermissionCacheService)),
4339
- _ts_param(4, (0,_nestjs_common__rspack_import_3.Optional)()),
4340
- _ts_param(4, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared__rspack_import_0.PERMISSION_ACTION_REGISTRY)),
4839
+ _ts_param(0, (0,_nestjs_common__rspack_import_4.Inject)('CACHE_INSTANCE')),
4840
+ _ts_param(1, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_modules__rspack_import_3.UtilsService)),
4841
+ _ts_param(2, (0,_nestjs_common__rspack_import_4.Inject)(_entity_builder_datasource_provider_js__rspack_import_12/* .EntityBuilderDataSourceProvider */.D)),
4842
+ _ts_param(3, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService)),
4843
+ _ts_param(4, (0,_nestjs_common__rspack_import_4.Optional)()),
4844
+ _ts_param(4, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared__rspack_import_0.PERMISSION_ACTION_REGISTRY)),
4845
+ _ts_param(5, (0,_nestjs_common__rspack_import_4.Optional)()),
4846
+ _ts_param(5, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_interfaces__rspack_import_2.NOTIFICATION_ADAPTER)),
4847
+ _ts_param(6, (0,_nestjs_common__rspack_import_4.Optional)()),
4848
+ _ts_param(6, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_interfaces__rspack_import_2.EMAIL_ADAPTER)),
4849
+ _ts_param(7, (0,_nestjs_common__rspack_import_4.Optional)()),
4850
+ _ts_param(7, (0,_nestjs_common__rspack_import_4.Inject)(_flusys_nestjs_shared_interfaces__rspack_import_2.COMPANY_ACCESS_RESOLVER)),
4341
4851
  _ts_metadata("design:type", Function),
4342
4852
  _ts_metadata("design:paramtypes", [
4343
4853
  typeof _flusys_nestjs_shared_classes__rspack_import_1.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_1.HybridCache,
4344
- typeof _flusys_nestjs_shared_modules__rspack_import_2.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_2.UtilsService,
4345
- typeof _entity_builder_datasource_provider_js__rspack_import_9/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : _entity_builder_datasource_provider_js__rspack_import_9/* .EntityBuilderDataSourceProvider */.D,
4346
- typeof _flusys_nestjs_shared_modules__rspack_import_2.SharedPermissionCacheService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_2.SharedPermissionCacheService,
4347
- typeof IPermissionActionRegistry === "undefined" ? Object : IPermissionActionRegistry
4854
+ typeof _flusys_nestjs_shared_modules__rspack_import_3.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.UtilsService,
4855
+ typeof _entity_builder_datasource_provider_js__rspack_import_12/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : _entity_builder_datasource_provider_js__rspack_import_12/* .EntityBuilderDataSourceProvider */.D,
4856
+ typeof _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_3.SharedPermissionCacheService,
4857
+ typeof IPermissionActionRegistry === "undefined" ? Object : IPermissionActionRegistry,
4858
+ typeof INotificationAdapter === "undefined" ? Object : INotificationAdapter,
4859
+ typeof IEmailAdapter === "undefined" ? Object : IEmailAdapter,
4860
+ typeof ICompanyAccessResolver === "undefined" ? Object : ICompanyAccessResolver
4348
4861
  ])
4349
4862
  ], FlowDefinitionService);
4350
4863
  /** 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) {
@@ -4356,6 +4869,13 @@ FlowDefinitionService = _ts_decorate([
4356
4869
  };
4357
4870
  });
4358
4871
  }
4872
+ /** What a Call flow step checks about a flow: it runs only once published and switched on. */ function callableOf(flow) {
4873
+ return {
4874
+ isActive: flow.isActive && isPublished(flow),
4875
+ endpoints: (0,_flow_engine_flow_types_js__rspack_import_11/* .flowEndpoints */.Xr)(flow),
4876
+ calls: calledSlugs(flow.nodes)
4877
+ };
4878
+ }
4359
4879
  /** Slugs the `call_flow` nodes of a flow run. */ function calledSlugs(nodes) {
4360
4880
  return calledTargets(nodes).map((target)=>target.slug);
4361
4881
  }
@@ -4365,10 +4885,14 @@ function stripUndefined(value) {
4365
4885
 
4366
4886
  __webpack_require__.d(__webpack_exports__, {
4367
4887
  RK: () => (toFlowSettings),
4368
- Tk: () => (FlowDefinitionService)
4888
+ Tk: () => (FlowDefinitionService),
4889
+ eY: () => (snapshotOf)
4369
4890
  }, {
4370
4891
  Er: defaultFlowPermissionCode,
4371
- YW: flowNotFound
4892
+ HF: isPublished,
4893
+ YW: flowNotFound,
4894
+ j4: flowPermissions,
4895
+ lw: workingCopy
4372
4896
  });
4373
4897
 
4374
4898
 
@@ -4428,8 +4952,11 @@ class FlowExecutionService {
4428
4952
  await repo.save(repo.create({
4429
4953
  flowId: entry.flowId,
4430
4954
  flowSlug: entry.flowSlug,
4955
+ flowVersion: entry.flowVersion,
4431
4956
  status: entry.result.status,
4432
4957
  trigger: entry.trigger,
4958
+ endpointNodeId: entry.endpoint?.nodeId ?? null,
4959
+ endpointPath: entry.endpoint?.path ?? null,
4433
4960
  mode: entry.mode,
4434
4961
  statusCode: entry.result.statusCode,
4435
4962
  input: inputText.length > INPUT_LIMIT_CHARS ? {
@@ -4480,6 +5007,8 @@ class FlowExecutionService {
4480
5007
  const where = {};
4481
5008
  if (query.flowId) where.flowId = query.flowId;
4482
5009
  if (query.status) where.status = query.status;
5010
+ if (query.trigger) where.trigger = query.trigger;
5011
+ if (query.endpointNodeId) where.endpointNodeId = query.endpointNodeId;
4483
5012
  const [rows, total] = await repo.findAndCount({
4484
5013
  where,
4485
5014
  order: {
@@ -4492,8 +5021,11 @@ class FlowExecutionService {
4492
5021
  id: true,
4493
5022
  flowId: true,
4494
5023
  flowSlug: true,
5024
+ flowVersion: true,
4495
5025
  status: true,
4496
5026
  trigger: true,
5027
+ endpointNodeId: true,
5028
+ endpointPath: true,
4497
5029
  mode: true,
4498
5030
  statusCode: true,
4499
5031
  durationMs: true,
@@ -4538,7 +5070,7 @@ __webpack_require__.d(__webpack_exports__, {
4538
5070
 
4539
5071
 
4540
5072
  },
4541
- 4698(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
5073
+ 3908(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
4542
5074
 
4543
5075
  // EXPORTS
4544
5076
  __webpack_require__.d(__webpack_exports__, {
@@ -4548,10 +5080,14 @@ __webpack_require__.d(__webpack_exports__, {
4548
5080
 
4549
5081
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/classes"
4550
5082
  var classes_ = __webpack_require__(6937);
4551
- // EXTERNAL MODULE: external "@flusys/nestjs-shared/exceptions"
4552
- var exceptions_ = __webpack_require__(2417);
5083
+ ;// CONCATENATED MODULE: external "@flusys/nestjs-shared/exceptions"
5084
+
5085
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/interfaces"
5086
+ var interfaces_ = __webpack_require__(5103);
4553
5087
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/modules"
4554
5088
  var modules_ = __webpack_require__(1236);
5089
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
5090
+ var utils_ = __webpack_require__(9216);
4555
5091
  // EXTERNAL MODULE: external "@nestjs/common"
4556
5092
  var common_ = __webpack_require__(6157);
4557
5093
  // EXTERNAL MODULE: external "@nestjs/core"
@@ -4790,7 +5326,7 @@ const BLOCKED_REQUEST_HEADERS = new Set([
4790
5326
  }
4791
5327
 
4792
5328
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/rule-engine/index.ts + 2 modules
4793
- var rule_engine = __webpack_require__(5920);
5329
+ var rule_engine = __webpack_require__(8552);
4794
5330
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/services/entity-builder-config.service.ts
4795
5331
  var entity_builder_config_service = __webpack_require__(4281);
4796
5332
  // EXTERNAL MODULE: ./projects/nestjs-entity-builder/src/services/entity-builder-datasource.provider.ts
@@ -4843,6 +5379,8 @@ function _ts_param(paramIndex, decorator) {
4843
5379
 
4844
5380
 
4845
5381
 
5382
+
5383
+
4846
5384
 
4847
5385
 
4848
5386
 
@@ -4853,27 +5391,32 @@ const publishFlowEvent = (eventName, payload, user)=>(0,classes_.publishDomainEv
4853
5391
  metadata: payload,
4854
5392
  user
4855
5393
  });
4856
- /** The run's time budget, whatever a definition asks for. */ const flowTimeout = (timeoutMs)=>typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) ? Math.min(Math.max(timeoutMs, flow_types/* .FLOW_LIMITS.MIN_TIMEOUT_MS */.Or.MIN_TIMEOUT_MS), flow_types/* .FLOW_LIMITS.MAX_TIMEOUT_MS */.Or.MAX_TIMEOUT_MS) : flow_types/* .FLOW_LIMITS.DEFAULT_TIMEOUT_MS */.Or.DEFAULT_TIMEOUT_MS;
5394
+ /** The run's time budget, whatever a Request step asks for. */ const flowTimeout = (timeoutMs)=>typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) ? Math.min(Math.max(timeoutMs, flow_types/* .FLOW_LIMITS.MIN_TIMEOUT_MS */.Or.MIN_TIMEOUT_MS), flow_types/* .FLOW_LIMITS.MAX_TIMEOUT_MS */.Or.MAX_TIMEOUT_MS) : flow_types/* .FLOW_LIMITS.DEFAULT_TIMEOUT_MS */.Or.DEFAULT_TIMEOUT_MS;
4857
5395
  class FlowExecutorService {
4858
5396
  provider;
4859
5397
  ruleEngine;
4860
5398
  moduleRef;
4861
5399
  permissionCache;
4862
5400
  config;
5401
+ companyAccess;
5402
+ notifications;
5403
+ email;
5404
+ branchHierarchy;
4863
5405
  logger = new common_.Logger(FlowExecutorService.name);
4864
- constructor(provider, ruleEngine, moduleRef, permissionCache, config){
5406
+ constructor(provider, ruleEngine, moduleRef, permissionCache, config, companyAccess, notifications, email, branchHierarchy){
4865
5407
  this.provider = provider;
4866
5408
  this.ruleEngine = ruleEngine;
4867
5409
  this.moduleRef = moduleRef;
4868
5410
  this.permissionCache = permissionCache;
4869
5411
  this.config = config;
5412
+ this.companyAccess = companyAccess;
5413
+ this.notifications = notifications;
5414
+ this.email = email;
5415
+ this.branchHierarchy = branchHierarchy;
4870
5416
  }
4871
- async run(definition, options) {
5417
+ async run(flow, options) {
4872
5418
  const startedAt = Date.now();
4873
- const flow = {
4874
- ...definition,
4875
- timeoutMs: flowTimeout(definition.timeoutMs)
4876
- };
5419
+ const start = (0,flow_types/* .startEndpoint */.IE)(flow, options.startAt);
4877
5420
  const state = {
4878
5421
  startAt: options.startAt,
4879
5422
  input: options.input,
@@ -4881,14 +5424,14 @@ class FlowExecutorService {
4881
5424
  context: {},
4882
5425
  request: options.request,
4883
5426
  user: options.user,
5427
+ authorization: options.authorization,
4884
5428
  permissions: this.ruleEngine.createPermissionResolver(options.user),
4885
5429
  mode: options.mode,
4886
- runAs: flow.runAs,
4887
- systemAuthorized: flow.runAs === 'system' && options.systemAuthorized === true,
4888
5430
  steps: 0,
4889
5431
  httpCalls: 0,
5432
+ messages: 0,
4890
5433
  writes: 0,
4891
- deadline: startedAt + flow.timeoutMs,
5434
+ deadline: startedAt + flowTimeout(start?.timeoutMs),
4892
5435
  callStack: [
4893
5436
  flow.slug
4894
5437
  ],
@@ -4896,15 +5439,7 @@ class FlowExecutorService {
4896
5439
  trace: []
4897
5440
  };
4898
5441
  try {
4899
- if (!flow_types/* .FLOW_RUN_AS.includes */.gl.includes(flow.runAs)) {
4900
- throw new flow_errors/* .FlowHttpError */.hk(403, 'This flow is not allowed to run.', undefined, {
4901
- messageKey: message_keys/* .FLOW_ERROR_MESSAGES.RUN_AS_NOT_ALLOWED */.zr.RUN_AS_NOT_ALLOWED,
4902
- messageVariables: {
4903
- runAs: String(flow.runAs)
4904
- }
4905
- });
4906
- }
4907
- await this.execute(flow, state, this.createEnv());
5442
+ await this.execute(flow, start?.isTransactional === true, state, this.createEnv());
4908
5443
  return {
4909
5444
  ...(0,flow_engine/* .successResult */.JK)(state, startedAt),
4910
5445
  ...state.savedUpTo ? {
@@ -4934,10 +5469,10 @@ class FlowExecutorService {
4934
5469
  }
4935
5470
  }
4936
5471
  /**
4937
- * Walks `flow`. A flow called from inside a transaction joins it; otherwise a transactional
4938
- * flow (or any test run) gets its own, and its held-back events are published after the commit.
4939
- */ async execute(flow, state, env) {
4940
- if (state.qr || !(flow.isTransactional || state.mode === 'dry_run')) {
5472
+ * Walks `flow`. A flow called from inside a transaction joins it; otherwise a run whose Request step
5473
+ * is transactional (or any test run) gets its own, and its held-back events are published after the commit.
5474
+ */ async execute(flow, transactional, state, env) {
5475
+ if (state.qr || !(transactional || state.mode === 'dry_run')) {
4941
5476
  await new flow_engine/* .FlowEngine */.$I(flow, env).walk(state);
4942
5477
  return;
4943
5478
  }
@@ -5016,7 +5551,6 @@ class FlowExecutorService {
5016
5551
  max: flow_types/* .FLOW_LIMITS.MAX_CALL_DEPTH */.Or.MAX_CALL_DEPTH
5017
5552
  });
5018
5553
  const flow = await this.findCallable(slug, parent.qr);
5019
- await this.assertMayCall(flow, parent, env);
5020
5554
  const settings = (0,flow_definition_service/* .toFlowSettings */.RK)(flow);
5021
5555
  const start = (0,flow_types/* .findEndpoint */.vS)(settings, endpoint);
5022
5556
  if (!start) {
@@ -5025,6 +5559,7 @@ class FlowExecutorService {
5025
5559
  endpoint: endpoint ?? ''
5026
5560
  }, `Flow '${slug}' has no URL '${endpoint ?? ''}'.`);
5027
5561
  }
5562
+ await this.assertMayCall(flow, start, parent, env);
5028
5563
  const checked = (0,flow_input_validator/* .validateFlowInput */.v)(start.inputSchema, input, start.bodyType);
5029
5564
  if (checked.errors.length) {
5030
5565
  throw new flow_errors/* .FlowHttpError */.hk(400, 'Validation failed', checked.errors, {
@@ -5041,15 +5576,14 @@ class FlowExecutorService {
5041
5576
  context: {},
5042
5577
  request: parent.request,
5043
5578
  user: parent.user,
5579
+ authorization: parent.authorization,
5044
5580
  permissions: parent.permissions,
5045
5581
  mode: parent.mode,
5046
- runAs: flow.authMode === 'internal' ? parent.runAs : settings.runAs,
5047
- // A function inherits the caller's grant; any other called flow is a saved flow whose own run-as was checked when it was saved.
5048
- systemAuthorized: flow.authMode === 'internal' ? parent.systemAuthorized === true : settings.runAs === 'system',
5049
5582
  steps: parent.steps,
5050
5583
  httpCalls: parent.httpCalls,
5584
+ messages: parent.messages,
5051
5585
  writes: parent.writes,
5052
- deadline: Math.min(parent.deadline, Date.now() + flowTimeout(settings.timeoutMs)),
5586
+ deadline: Math.min(parent.deadline, Date.now() + flowTimeout(start.timeoutMs)),
5053
5587
  qr: parent.qr,
5054
5588
  callStack: [
5055
5589
  ...parent.callStack,
@@ -5064,13 +5598,14 @@ class FlowExecutorService {
5064
5598
  if (child.pendingEvents === parent.pendingEvents) parent.pendingEvents.length = queued;
5065
5599
  };
5066
5600
  try {
5067
- await this.execute(settings, child, env);
5601
+ await this.execute(settings, start.isTransactional, child, env);
5068
5602
  } catch (error) {
5069
5603
  dropQueued();
5070
5604
  throw calledFlowFailure(flow, error);
5071
5605
  } finally{
5072
5606
  parent.steps = child.steps;
5073
5607
  parent.httpCalls = child.httpCalls;
5608
+ parent.messages = child.messages;
5074
5609
  parent.writes = child.writes;
5075
5610
  }
5076
5611
  if (child.response) {
@@ -5089,7 +5624,8 @@ class FlowExecutorService {
5089
5624
  where: {
5090
5625
  slug,
5091
5626
  isActive: true,
5092
- deletedAt: (0,external_typeorm_.IsNull)()
5627
+ deletedAt: (0,external_typeorm_.IsNull)(),
5628
+ version: (0,external_typeorm_.MoreThan)(0)
5093
5629
  }
5094
5630
  });
5095
5631
  if (!flow) throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.CALLED_FLOW_NOT_FOUND */.zr.CALLED_FLOW_NOT_FOUND, {
@@ -5098,21 +5634,23 @@ class FlowExecutorService {
5098
5634
  return flow;
5099
5635
  }
5100
5636
  /**
5101
- * Any flow may call a function, and a flow running as the system may call any flow. One running
5102
- * as the caller may call only what that caller could call directly: public and signed-in flows,
5103
- * and permission flows when the caller holds the permission - never an API-key flow.
5104
- */ async assertMayCall(flow, parent, env) {
5105
- if (parent.runAs === 'system' && parent.systemAuthorized === true || flow.authMode === 'public' || flow.authMode === 'internal') return;
5106
- if (flow.authMode === 'api_key' || !parent.user) {
5637
+ * The called URL's own access, checked against the run's user: any flow may call a function or a
5638
+ * public URL; a signed-in one needs a user, a permission one the user holding it. An API-key URL
5639
+ * is never called from a flow, which has no key to send.
5640
+ */ async assertMayCall(flow, endpoint, parent, env) {
5641
+ const mode = endpoint.authMode;
5642
+ if (mode === 'public' || mode === 'internal') return;
5643
+ if (mode === 'api_key' || !parent.user) {
5107
5644
  throw new flow_errors/* .FlowKeyedError */.Hp(message_keys/* .FLOW_ERROR_MESSAGES.CALLED_FLOW_NOT_ALLOWED */.zr.CALLED_FLOW_NOT_ALLOWED, {
5108
5645
  flow: flow.slug,
5109
- authMode: flow.authMode
5110
- }, `Flow '${flow.slug}' (${flow.authMode}) cannot be called by this caller.`);
5646
+ authMode: mode
5647
+ }, `Flow '${flow.slug}' (${mode}) cannot be called by this caller.`);
5111
5648
  }
5112
- if (flow.authMode === 'permission') await env.assertPermission(flow.permissionCode || (0,flow_definition_service/* .defaultFlowPermissionCode */.Er)(flow.slug), parent.user);
5649
+ if (mode === 'permission') await env.assertPermissions((0,flow_definition_service/* .flowPermissions */.j4)(endpoint, flow.slug), parent.user);
5113
5650
  }
5114
5651
  createEnv() {
5115
5652
  const entityServices = new Map();
5653
+ /** One lookup per user for the whole run, however many steps ask. */ const access = new Map();
5116
5654
  let permissions;
5117
5655
  const env = {
5118
5656
  ruleEngine: this.ruleEngine,
@@ -5135,29 +5673,48 @@ class FlowExecutorService {
5135
5673
  atomic: (state, work)=>this.atomic(state, work),
5136
5674
  commit: (state)=>this.commitProgress(state),
5137
5675
  http: flowHttpRequest,
5676
+ notifications: this.notifications,
5677
+ email: this.email,
5678
+ ...this.companyAccess ? {
5679
+ companyMembers: (companyId, branchIds)=>this.companyAccess.getCompanyMembers(companyId, branchIds)
5680
+ } : {},
5681
+ ...this.branchHierarchy ? {
5682
+ branchTree: (branchIds)=>this.branchHierarchy.getDescendantIds(branchIds, {
5683
+ includeSelf: true
5684
+ })
5685
+ } : {},
5686
+ companyAccess: (userId)=>{
5687
+ if (!access.has(userId)) access.set(userId, this.companyAccess ? this.companyAccess.getUserAccess(userId) : Promise.resolve({
5688
+ companies: [],
5689
+ branches: []
5690
+ }));
5691
+ return access.get(userId);
5692
+ },
5138
5693
  publish: (eventName, payload, user)=>publishFlowEvent(eventName, payload, user),
5139
- assertPermission: async (code, user)=>{
5694
+ assertPermissions: async (logic, user)=>{
5140
5695
  if (!user) throw new common_.UnauthorizedException({
5141
5696
  message: 'Authentication required',
5142
5697
  messageKey: constants_.ERROR_MESSAGES.UNAUTHORIZED
5143
5698
  });
5144
- if (!this.permissionCache.isCacheConfigured()) throw new exceptions_.PermissionSystemUnavailableException();
5699
+ if (!this.permissionCache.isCacheConfigured()) throw new __rspack_external__flusys_nestjs_shared_exceptions_3edf22e9.PermissionSystemUnavailableException();
5145
5700
  permissions ??= this.permissionCache.getPermissions({
5146
5701
  userId: user.id,
5147
5702
  companyId: user.companyId,
5148
5703
  branchId: user.branchId
5149
5704
  });
5150
- const granted = await permissions;
5151
- if (!this.permissionCache.matchPermission(granted, code)) throw new exceptions_.InsufficientPermissionsException([
5152
- code
5153
- ], 'AND');
5705
+ const result = (0,utils_.evaluatePermissionLogic)(logic, await permissions);
5706
+ if (!result.passed) throw new __rspack_external__flusys_nestjs_shared_exceptions_3edf22e9.InsufficientPermissionsException(result.missingPermissions, result.operator);
5154
5707
  }
5155
5708
  };
5156
5709
  return env;
5157
5710
  }
5158
- /** After commit, so listeners see committed data. A failing publish never turns a completed run into an error. */ async publishPendingEvents(events, user) {
5711
+ /** After commit, so listeners see committed data and messages go out only for what was saved. A failing publish or send never turns a completed run into an error. */ async publishPendingEvents(events, user) {
5159
5712
  for (const event of events){
5160
5713
  try {
5714
+ if ('delivery' in event) {
5715
+ await event.send();
5716
+ continue;
5717
+ }
5161
5718
  if ('eventName' in event) {
5162
5719
  await publishFlowEvent(event.eventName, event.payload, user);
5163
5720
  continue;
@@ -5176,6 +5733,10 @@ class FlowExecutorService {
5176
5733
  user
5177
5734
  });
5178
5735
  } catch (error) {
5736
+ if ('delivery' in event) {
5737
+ this.logger.error(`Could not send the message of "${event.delivery}" after the commit: ${error.message}`);
5738
+ continue;
5739
+ }
5179
5740
  const name = 'eventName' in event ? event.eventName : `${event.entityCode}.${event.action}`;
5180
5741
  this.logger.error(`Could not publish ${name}: ${error.message}`);
5181
5742
  }
@@ -5189,13 +5750,25 @@ FlowExecutorService = _ts_decorate([
5189
5750
  _ts_param(2, (0,common_.Inject)(core_.ModuleRef)),
5190
5751
  _ts_param(3, (0,common_.Inject)(modules_.SharedPermissionCacheService)),
5191
5752
  _ts_param(4, (0,common_.Inject)(entity_builder_config_service/* .EntityBuilderConfigService */.E)),
5753
+ _ts_param(5, (0,common_.Optional)()),
5754
+ _ts_param(5, (0,common_.Inject)(interfaces_.COMPANY_ACCESS_RESOLVER)),
5755
+ _ts_param(6, (0,common_.Optional)()),
5756
+ _ts_param(6, (0,common_.Inject)(interfaces_.NOTIFICATION_ADAPTER)),
5757
+ _ts_param(7, (0,common_.Optional)()),
5758
+ _ts_param(7, (0,common_.Inject)(interfaces_.EMAIL_ADAPTER)),
5759
+ _ts_param(8, (0,common_.Optional)()),
5760
+ _ts_param(8, (0,common_.Inject)(interfaces_.BRANCH_HIERARCHY_RESOLVER)),
5192
5761
  _ts_metadata("design:type", Function),
5193
5762
  _ts_metadata("design:paramtypes", [
5194
5763
  typeof entity_builder_datasource_provider/* .EntityBuilderDataSourceProvider */.D === "undefined" ? Object : entity_builder_datasource_provider/* .EntityBuilderDataSourceProvider */.D,
5195
5764
  typeof rule_engine/* .RuleEngineService */.iI === "undefined" ? Object : rule_engine/* .RuleEngineService */.iI,
5196
5765
  typeof core_.ModuleRef === "undefined" ? Object : core_.ModuleRef,
5197
5766
  typeof modules_.SharedPermissionCacheService === "undefined" ? Object : modules_.SharedPermissionCacheService,
5198
- typeof entity_builder_config_service/* .EntityBuilderConfigService */.E === "undefined" ? Object : entity_builder_config_service/* .EntityBuilderConfigService */.E
5767
+ typeof entity_builder_config_service/* .EntityBuilderConfigService */.E === "undefined" ? Object : entity_builder_config_service/* .EntityBuilderConfigService */.E,
5768
+ typeof ICompanyAccessResolver === "undefined" ? Object : ICompanyAccessResolver,
5769
+ typeof INotificationAdapter === "undefined" ? Object : INotificationAdapter,
5770
+ typeof IEmailAdapter === "undefined" ? Object : IEmailAdapter,
5771
+ typeof IBranchHierarchyResolver === "undefined" ? Object : IBranchHierarchyResolver
5199
5772
  ])
5200
5773
  ], FlowExecutorService);
5201
5774
  /** A rejection or a limit inside the called flow is passed on as it is; anything else fails the calling node with the reason. */ function calledFlowFailure(flow, error) {
@@ -5238,7 +5811,7 @@ FlowExecutorService = _ts_decorate([
5238
5811
  /* import */ var _entity_builder_datasource_provider_js__rspack_import_5 = __webpack_require__(424);
5239
5812
  /* import */ var _flow_definition_service_js__rspack_import_6 = __webpack_require__(5229);
5240
5813
  /* 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);
5814
+ /* import */ var _flow_executor_service_js__rspack_import_8 = __webpack_require__(3908);
5242
5815
  function _ts_decorate(decorators, target, key, desc) {
5243
5816
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5244
5817
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -5308,6 +5881,11 @@ const MAX_QUERY_VALUE_LENGTH = 2000;
5308
5881
  receivedAt: new Date().toISOString()
5309
5882
  };
5310
5883
  }
5884
+ /** The caller's own `Authorization` header, kept for Request steps that forward it: only for a signed-in (verified) caller. */ function callerAuthorization(caller, user) {
5885
+ if (!user) return undefined;
5886
+ const value = Object.entries(caller.headers).find(([key])=>key.toLowerCase() === 'authorization')?.[1];
5887
+ return typeof value === 'string' && value ? value : undefined;
5888
+ }
5311
5889
  /** A test run's request: the designer's own, with the headers / query / IP the author chose to simulate on top. */ function simulatedCaller(caller, simulated) {
5312
5890
  if (!simulated) return caller;
5313
5891
  const headers = {
@@ -5347,13 +5925,13 @@ class FlowRuntimeService {
5347
5925
  this.executions = executions;
5348
5926
  this.definitions = definitions;
5349
5927
  }
5350
- /** A function flow has no endpoint: it looks the same as an unknown one. */ async findActive(slug) {
5928
+ /** A flow never published is not live yet: it looks the same as an unknown one. */ async findActive(slug) {
5351
5929
  return (await this.provider.getRepository(_entities_flow_definition_entity_js__rspack_import_2/* .FlowDefinition */.G)).findOne({
5352
5930
  where: {
5353
5931
  slug,
5354
5932
  isActive: true,
5355
5933
  deletedAt: (0,typeorm__rspack_import_1.IsNull)(),
5356
- authMode: (0,typeorm__rspack_import_1.Not)('internal')
5934
+ version: (0,typeorm__rspack_import_1.MoreThan)(0)
5357
5935
  }
5358
5936
  });
5359
5937
  }
@@ -5362,25 +5940,23 @@ class FlowRuntimeService {
5362
5940
  const checked = (0,_flow_engine_flow_input_validator_js__rspack_import_3/* .validateFlowInput */.v)(endpoint.inputSchema, body, endpoint.bodyType);
5363
5941
  if (checked.errors.length) {
5364
5942
  const result = inputRejected(checked.errors);
5365
- await this.logWebhook(flow, body, result, startedAt, user, caller);
5943
+ await this.logWebhook(flow, endpoint, body, result, startedAt, user, caller);
5366
5944
  return result;
5367
5945
  }
5368
- // A saved flow: saving it as `runAs: system` needed the system permission.
5369
5946
  const result = await this.executor.run((0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(flow), {
5370
5947
  mode: 'live',
5371
5948
  user,
5372
5949
  input: checked.values,
5373
5950
  startAt: endpoint.nodeId,
5374
5951
  request: requestInfo(caller),
5375
- exposeDetails: false,
5376
- systemAuthorized: true
5952
+ authorization: callerAuthorization(caller, user),
5953
+ exposeDetails: false
5377
5954
  });
5378
- await this.logWebhook(flow, checked.values, result, startedAt, user, caller);
5955
+ await this.logWebhook(flow, endpoint, checked.values, result, startedAt, user, caller);
5379
5956
  return result;
5380
5957
  }
5381
5958
  /** The designer's "Test run": a saved flow or an unsaved draft. */ async testRun(dto, user, caller) {
5382
5959
  let settings;
5383
- let permissionCode;
5384
5960
  let flow = null;
5385
5961
  if (dto.flowId) {
5386
5962
  flow = await this.definitions.findOrFail(dto.flowId);
@@ -5388,10 +5964,8 @@ class FlowRuntimeService {
5388
5964
  if (dto.definition) {
5389
5965
  // The designer's current draft, even for a saved flow: `flow` above is kept only to attribute the run.
5390
5966
  settings = (0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(dto.definition);
5391
- permissionCode = dto.definition.permissionCode;
5392
5967
  } else if (flow) {
5393
5968
  settings = (0,_flow_definition_service_js__rspack_import_6/* .toFlowSettings */.RK)(flow);
5394
- permissionCode = flow.permissionCode;
5395
5969
  } else {
5396
5970
  throw new _nestjs_common__rspack_import_0.BadRequestException({
5397
5971
  message: 'Send a flowId or a definition to test.',
@@ -5399,15 +5973,22 @@ class FlowRuntimeService {
5399
5973
  });
5400
5974
  }
5401
5975
  const mode = dto.mode ?? 'dry_run';
5402
- // Checked before anything runs; passing it is what lets a `runAs: system` test touch entities.
5403
- await this.definitions.assertMayTest({
5404
- ...settings,
5405
- permissionCode
5406
- }, user, {
5976
+ const endpoint = (0,_flow_engine_flow_types_js__rspack_import_4/* .findEndpoint */.vS)(settings, dto.endpoint);
5977
+ // Checked before anything runs.
5978
+ await this.definitions.assertMayTest(settings, endpoint, user, {
5407
5979
  mode,
5408
5980
  draft: !!dto.definition,
5409
5981
  saved: !!flow
5410
5982
  });
5983
+ if (!endpoint) {
5984
+ throw new _nestjs_common__rspack_import_0.BadRequestException({
5985
+ message: `This flow has no URL '${dto.endpoint ?? ''}'.`,
5986
+ messageKey: _config_message_keys_js__rspack_import_9/* .FLOW_MESSAGES.ENDPOINT_NOT_FOUND */.wu.ENDPOINT_NOT_FOUND,
5987
+ messageVariables: {
5988
+ endpoint: dto.endpoint ?? ''
5989
+ }
5990
+ });
5991
+ }
5411
5992
  const validation = await this.definitions.validate(settings);
5412
5993
  if (validation.errors.length) {
5413
5994
  throw new _nestjs_common__rspack_import_0.BadRequestException({
@@ -5419,16 +6000,6 @@ class FlowRuntimeService {
5419
6000
  }))
5420
6001
  });
5421
6002
  }
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
- }
5432
6003
  const startedAt = new Date();
5433
6004
  const simulated = simulatedCaller(caller, dto.request);
5434
6005
  const body = dto.input ?? (endpoint.bodyType === 'list' ? [] : {});
@@ -5439,13 +6010,15 @@ class FlowRuntimeService {
5439
6010
  input: checked.values,
5440
6011
  startAt: endpoint.nodeId,
5441
6012
  request: requestInfo(simulated),
5442
- exposeDetails: true,
5443
- systemAuthorized: true
6013
+ authorization: callerAuthorization(caller, user),
6014
+ exposeDetails: true
5444
6015
  });
5445
6016
  await this.executions.record({
5446
6017
  flowId: flow?.id ?? null,
5447
6018
  flowSlug: settings.slug,
6019
+ flowVersion: dto.definition ? null : flow?.version ?? null,
5448
6020
  trigger: 'test',
6021
+ endpoint,
5449
6022
  mode,
5450
6023
  input: body,
5451
6024
  result,
@@ -5459,11 +6032,13 @@ class FlowRuntimeService {
5459
6032
  warnings: validation.warnings
5460
6033
  };
5461
6034
  }
5462
- async logWebhook(flow, input, result, startedAt, user, caller) {
6035
+ async logWebhook(flow, endpoint, input, result, startedAt, user, caller) {
5463
6036
  await this.executions.record({
5464
6037
  flowId: flow.id,
5465
6038
  flowSlug: flow.slug,
6039
+ flowVersion: flow.version,
5466
6040
  trigger: 'webhook',
6041
+ endpoint,
5467
6042
  mode: 'live',
5468
6043
  input,
5469
6044
  result,
@@ -5607,9 +6182,9 @@ __webpack_require__.d(__webpack_exports__, {
5607
6182
  /* import */ var _flusys_nestjs_shared_utils__rspack_import_1 = __webpack_require__(9216);
5608
6183
  /* import */ var _nestjs_common__rspack_import_2 = __webpack_require__(6157);
5609
6184
  /* import */ var _config_event_actions_js__rspack_import_10 = __webpack_require__(982);
5610
- /* import */ var _config_message_keys_js__rspack_import_7 = __webpack_require__(5732);
6185
+ /* import */ var _config_message_keys_js__rspack_import_8 = __webpack_require__(5732);
5611
6186
  /* import */ var _enums_entity_status_enum_js__rspack_import_9 = __webpack_require__(1313);
5612
- /* import */ var _enums_field_type_enum_js__rspack_import_8 = __webpack_require__(9614);
6187
+ /* import */ var _enums_field_type_enum_js__rspack_import_7 = __webpack_require__(9614);
5613
6188
  /* import */ var _interfaces_generic_record_interface_js__rspack_import_3 = __webpack_require__(3372);
5614
6189
  /* import */ var _database_error_mapper_js__rspack_import_4 = __webpack_require__(8323);
5615
6190
  /* import */ var _field_value_validator_js__rspack_import_5 = __webpack_require__(3525);
@@ -5641,6 +6216,18 @@ const FILTER_COMPARISON_SQL = {
5641
6216
  lt: '<',
5642
6217
  lte: '<='
5643
6218
  };
6219
+ /** The LIKE pattern of each text-match operator, built from the already escaped text. */ const TEXT_PATTERN = {
6220
+ contains: (t)=>`%${t}%`,
6221
+ not_contains: (t)=>`%${t}%`,
6222
+ starts_with: (t)=>`${t}%`,
6223
+ ends_with: (t)=>`%${t}`
6224
+ };
6225
+ const TEXT_FIELD_TYPES = new Set([
6226
+ _enums_field_type_enum_js__rspack_import_7/* .FieldType.TEXT */.P.TEXT,
6227
+ _enums_field_type_enum_js__rspack_import_7/* .FieldType.LONG_TEXT */.P.LONG_TEXT,
6228
+ _enums_field_type_enum_js__rspack_import_7/* .FieldType.EMAIL */.P.EMAIL,
6229
+ _enums_field_type_enum_js__rspack_import_7/* .FieldType.SINGLE_SELECT */.P.SINGLE_SELECT
6230
+ ]);
5644
6231
  /**
5645
6232
  * Generic CRUD for one dynamically-defined entity, mirroring ApiService's shape
5646
6233
  * (transactional writes, lifecycle event publishing) but
@@ -5692,7 +6279,7 @@ const FILTER_COMPARISON_SQL = {
5692
6279
  if (errors.length === 0) return;
5693
6280
  throw new _nestjs_common__rspack_import_2.BadRequestException({
5694
6281
  message: 'Validation failed',
5695
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.VALIDATION_FAILED */.j4.VALIDATION_FAILED,
6282
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.VALIDATION_FAILED */.j4.VALIDATION_FAILED,
5696
6283
  errors
5697
6284
  });
5698
6285
  }
@@ -5701,14 +6288,14 @@ const FILTER_COMPARISON_SQL = {
5701
6288
  const errors = [];
5702
6289
  for (const field of this.fields){
5703
6290
  const value = values[field.code];
5704
- if (field.fieldType !== _enums_field_type_enum_js__rspack_import_8/* .FieldType.RELATION */.P.RELATION || !field.relationTargetEntityId || typeof value !== 'string') continue;
6291
+ if (field.fieldType !== _enums_field_type_enum_js__rspack_import_7/* .FieldType.RELATION */.P.RELATION || !field.relationTargetEntityId || typeof value !== 'string') continue;
5705
6292
  const related = await this.resolveRelated(field.relationTargetEntityId, queryRunner);
5706
6293
  const found = await related.exists(value, {
5707
6294
  queryRunner
5708
6295
  });
5709
6296
  if (!found) errors.push({
5710
6297
  field: field.code,
5711
- ...(0,_flusys_nestjs_shared_utils__rspack_import_1.messageRef)(_config_message_keys_js__rspack_import_7/* .FIELD_VALIDATION_MESSAGES.RELATED_NOT_FOUND */.KE.RELATED_NOT_FOUND, {
6298
+ ...(0,_flusys_nestjs_shared_utils__rspack_import_1.messageRef)(_config_message_keys_js__rspack_import_8/* .FIELD_VALIDATION_MESSAGES.RELATED_NOT_FOUND */.KE.RELATED_NOT_FOUND, {
5712
6299
  label: field.label
5713
6300
  })
5714
6301
  });
@@ -5718,7 +6305,7 @@ const FILTER_COMPARISON_SQL = {
5718
6305
  notFound(id) {
5719
6306
  return new _nestjs_common__rspack_import_2.NotFoundException({
5720
6307
  message: `Record '${id}' not found on entity '${this.entityCode}'.`,
5721
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.NOT_FOUND */.j4.NOT_FOUND,
6308
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.NOT_FOUND */.j4.NOT_FOUND,
5722
6309
  messageVariables: {
5723
6310
  id,
5724
6311
  entity: this.entityCode
@@ -5746,7 +6333,7 @@ const FILTER_COMPARISON_SQL = {
5746
6333
  if (this.definition.status === _enums_entity_status_enum_js__rspack_import_9/* .EntityStatus.DEPRECATED */.c.DEPRECATED) {
5747
6334
  throw new _nestjs_common__rspack_import_2.ForbiddenException({
5748
6335
  message: `Entity '${this.definition.code}' has been deprecated and no longer accepts writes.`,
5749
- messageKey: _config_message_keys_js__rspack_import_7/* .ENTITY_DEFINITION_MESSAGES.DEPRECATED */.pL.DEPRECATED,
6336
+ messageKey: _config_message_keys_js__rspack_import_8/* .ENTITY_DEFINITION_MESSAGES.DEPRECATED */.pL.DEPRECATED,
5750
6337
  messageVariables: {
5751
6338
  code: this.definition.code
5752
6339
  }
@@ -5774,7 +6361,7 @@ const FILTER_COMPARISON_SQL = {
5774
6361
  if (SYSTEM_COLUMNS.has(key)) {
5775
6362
  throw new _nestjs_common__rspack_import_2.BadRequestException({
5776
6363
  message: `'${key}' is a system column and cannot be set directly.`,
5777
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.SYSTEM_COLUMN_READONLY */.j4.SYSTEM_COLUMN_READONLY,
6364
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.SYSTEM_COLUMN_READONLY */.j4.SYSTEM_COLUMN_READONLY,
5778
6365
  messageVariables: {
5779
6366
  key
5780
6367
  }
@@ -5783,7 +6370,7 @@ const FILTER_COMPARISON_SQL = {
5783
6370
  if (!this.activeFieldCodes.has(key)) {
5784
6371
  throw new _nestjs_common__rspack_import_2.BadRequestException({
5785
6372
  message: `'${key}' is not a field on entity '${this.entityCode}'.`,
5786
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.UNKNOWN_FIELD */.j4.UNKNOWN_FIELD,
6373
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.UNKNOWN_FIELD */.j4.UNKNOWN_FIELD,
5787
6374
  messageVariables: {
5788
6375
  key,
5789
6376
  entity: this.entityCode
@@ -5856,7 +6443,7 @@ const FILTER_COMPARISON_SQL = {
5856
6443
  if (!dto.id) {
5857
6444
  throw new _nestjs_common__rspack_import_2.BadRequestException({
5858
6445
  message: 'id is required to update a record.',
5859
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.ID_REQUIRED */.j4.ID_REQUIRED
6446
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.ID_REQUIRED */.j4.ID_REQUIRED
5860
6447
  });
5861
6448
  }
5862
6449
  const { id: _id, ...rest } = dto;
@@ -5865,6 +6452,7 @@ const FILTER_COMPARISON_SQL = {
5865
6452
  /** Soft-deletes the record, or deletes it for good when the entity has soft delete turned off. */ async delete(id, user, options = {}) {
5866
6453
  this.ensureActive();
5867
6454
  const repository = await this.ensureRepository();
6455
+ const soft = this.definition.isSoftDeleteEnabled;
5868
6456
  await this.withTransaction(options, async (queryRunner)=>{
5869
6457
  const existing = await queryRunner.manager.findOne(repository.target, {
5870
6458
  where: {
@@ -5872,7 +6460,6 @@ const FILTER_COMPARISON_SQL = {
5872
6460
  }
5873
6461
  });
5874
6462
  if (!existing) throw this.notFound(id);
5875
- const soft = this.definition.isSoftDeleteEnabled;
5876
6463
  if (soft) {
5877
6464
  await queryRunner.manager.softDelete(repository.target, [
5878
6465
  id
@@ -5898,6 +6485,7 @@ const FILTER_COMPARISON_SQL = {
5898
6485
  ]
5899
6486
  };
5900
6487
  }, user);
6488
+ return soft ? 'deleted' : 'purged';
5901
6489
  }
5902
6490
  async findById(id, options = {}) {
5903
6491
  const repository = await this.repositoryFor(options.queryRunner);
@@ -5944,55 +6532,177 @@ const FILTER_COMPARISON_SQL = {
5944
6532
  * getFilterQuery/getSortQuery (raw client-supplied keys concatenated into SQL) is
5945
6533
  * structurally impossible here since an unknown key is rejected outright. Operators map
5946
6534
  * to a fixed SQL table and every value is a bound parameter. */ applyFilter(query, filter) {
6535
+ const postgres = query.connection.options.type === 'postgres';
5947
6536
  Object.entries(filter).forEach(([key, value], index)=>{
5948
6537
  this.assertKnownColumn(key);
5949
- const column = `${this.entityCode}.${key}`;
5950
- const param = `filter_${index}`;
5951
- if (!(0,_interfaces_generic_record_interface_js__rspack_import_3/* .isRecordFilterCondition */.t1)(value)) {
5952
- if (value !== null && typeof value === 'object') {
5953
- const op = value.op;
5954
- if (typeof op === 'string' && !('type' in value) && !_interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_OPS.includes */.ox.includes(op)) throw this.unknownOperator(key, op);
5955
- throw this.invalidFilterValue(key, 'eq');
5956
- }
5957
- query.andWhere(`${column} = :${param}`, {
5958
- [param]: value
5959
- });
5960
- return;
6538
+ if (!(0,_interfaces_generic_record_interface_js__rspack_import_3/* .isRecordFilterCondition */.t1)(value) && value !== null && typeof value === 'object') {
6539
+ const op = value.op;
6540
+ if (typeof op === 'string' && !('type' in value) && !_interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_OPS.includes */.ox.includes(op)) throw this.unknownOperator(key, op);
5961
6541
  }
5962
- const { op, value: operand } = value;
5963
- if (op === 'is_null' || op === 'not_null') {
5964
- query.andWhere(`${column} ${op === 'is_null' ? 'IS NULL' : 'IS NOT NULL'}`);
5965
- return;
5966
- }
5967
- if (op === 'in') {
5968
- if (!Array.isArray(operand) || !operand.every(isFilterScalar)) throw this.invalidFilterValue(key, op);
5969
- if (operand.length > _interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES) {
5970
- throw new _nestjs_common__rspack_import_2.BadRequestException({
5971
- message: `The '${key}' filter lists more than ${_interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES} values.`,
5972
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.FILTER_TOO_MANY_VALUES */.j4.FILTER_TOO_MANY_VALUES,
5973
- messageVariables: {
5974
- key,
5975
- max: _interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES
6542
+ const { op, value: operand } = (0,_interfaces_generic_record_interface_js__rspack_import_3/* .isRecordFilterCondition */.t1)(value) ? value : {
6543
+ op: 'eq',
6544
+ value
6545
+ };
6546
+ this.assertOperatorFits(key, op);
6547
+ const [sql, params] = this.conditionSql(`${this.entityCode}.${key}`, key, op, operand, `filter_${index}`, postgres);
6548
+ query.andWhere(sql, params);
6549
+ });
6550
+ return query;
6551
+ }
6552
+ /** One filter as SQL: the column is a checked field code and every value is a bound parameter. */ conditionSql(column, key, op, operand, param, postgres) {
6553
+ const multiSelect = this.fieldTypeOf(key) === _enums_field_type_enum_js__rspack_import_7/* .FieldType.MULTI_SELECT */.P.MULTI_SELECT;
6554
+ switch(_interfaces_generic_record_interface_js__rspack_import_3/* .FILTER_OP_VALUE_KIND */.w9[op]){
6555
+ case 'none':
6556
+ if (op === 'is_null') return [
6557
+ `${column} IS NULL`,
6558
+ {}
6559
+ ];
6560
+ if (op === 'not_null') return [
6561
+ `${column} IS NOT NULL`,
6562
+ {}
6563
+ ];
6564
+ if (!multiSelect) return [
6565
+ op === 'is_empty' ? `(${column} IS NULL OR ${column} = '')` : `(${column} IS NOT NULL AND ${column} <> '')`,
6566
+ {}
6567
+ ];
6568
+ if (postgres) return [
6569
+ op === 'is_empty' ? `(${column} IS NULL OR ${column} = '[]'::jsonb)` : `(${column} IS NOT NULL AND ${column} <> '[]'::jsonb)`,
6570
+ {}
6571
+ ];
6572
+ return [
6573
+ op === 'is_empty' ? `(${column} IS NULL OR JSON_LENGTH(${column}) = 0)` : `JSON_LENGTH(${column}) > 0`,
6574
+ {}
6575
+ ];
6576
+ case 'list':
6577
+ {
6578
+ const values = this.listOperand(key, op, operand);
6579
+ if (op === 'in') return values.length ? [
6580
+ `${column} IN (:...${param})`,
6581
+ {
6582
+ [param]: values
5976
6583
  }
5977
- });
6584
+ ] : [
6585
+ '1 = 0',
6586
+ {}
6587
+ ];
6588
+ if (op === 'not_in') return values.length ? [
6589
+ `${column} NOT IN (:...${param})`,
6590
+ {
6591
+ [param]: values
6592
+ }
6593
+ ] : [
6594
+ '1 = 1',
6595
+ {}
6596
+ ];
6597
+ const holds = (list, name)=>[
6598
+ postgres ? `${column} @> CAST(:${name} AS jsonb)` : `JSON_CONTAINS(${column}, CAST(:${name} AS JSON))`,
6599
+ {
6600
+ [name]: JSON.stringify(list)
6601
+ }
6602
+ ];
6603
+ if (op === 'has_all') return values.length ? holds(values, param) : [
6604
+ '1 = 1',
6605
+ {}
6606
+ ];
6607
+ if (!values.length) return [
6608
+ op === 'has_any' ? '1 = 0' : '1 = 1',
6609
+ {}
6610
+ ];
6611
+ const each = values.map((v, i)=>holds([
6612
+ v
6613
+ ], `${param}_${i}`));
6614
+ const any = `(${each.map(([sql])=>sql).join(' OR ')})`;
6615
+ return [
6616
+ op === 'has_any' ? any : `NOT ${any}`,
6617
+ Object.assign({}, ...each.map(([, params])=>params))
6618
+ ];
6619
+ }
6620
+ case 'range':
6621
+ {
6622
+ if (!Array.isArray(operand) || operand.length !== 2 || !operand.every(isFilterScalar)) throw this.invalidFilterValue(key, op);
6623
+ const [from, to] = [
6624
+ `${param}_from`,
6625
+ `${param}_to`
6626
+ ];
6627
+ return [
6628
+ `${column} ${op === 'between' ? 'BETWEEN' : 'NOT BETWEEN'} :${from} AND :${to}`,
6629
+ {
6630
+ [from]: operand[0],
6631
+ [to]: operand[1]
6632
+ }
6633
+ ];
6634
+ }
6635
+ case 'single':
6636
+ {
6637
+ if (operand === null && (op === 'eq' || op === 'ne')) return [
6638
+ `${column} IS ${op === 'eq' ? '' : 'NOT '}NULL`,
6639
+ {}
6640
+ ];
6641
+ if (operand === null || operand === undefined || !isFilterScalar(operand)) throw this.invalidFilterValue(key, op);
6642
+ if (op === 'ieq') return [
6643
+ `LOWER(${column}) = LOWER(:${param})`,
6644
+ {
6645
+ [param]: String(operand)
6646
+ }
6647
+ ];
6648
+ const pattern = TEXT_PATTERN[op];
6649
+ if (!pattern) return [
6650
+ `${column} ${FILTER_COMPARISON_SQL[op]} :${param}`,
6651
+ {
6652
+ [param]: operand
6653
+ }
6654
+ ];
6655
+ const text = String(operand);
6656
+ if (!text.trim()) throw this.invalidFilterValue(key, op);
6657
+ const like = `${op === 'not_contains' ? 'NOT ' : ''}${postgres ? 'ILIKE' : 'LIKE'}`;
6658
+ return [
6659
+ `${column} ${like} :${param}`,
6660
+ {
6661
+ [param]: pattern(text.replace(/[\\%_]/g, '\\$&'))
6662
+ }
6663
+ ];
6664
+ }
6665
+ }
6666
+ }
6667
+ /** A list of scalars; one scalar (e.g. a single value read from input) counts as a one-item list. */ listOperand(key, op, operand) {
6668
+ const list = isFilterScalar(operand) ? [
6669
+ operand
6670
+ ] : operand;
6671
+ if (!Array.isArray(list) || !list.every(isFilterScalar)) throw this.invalidFilterValue(key, op);
6672
+ if (list.length > _interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES) {
6673
+ throw new _nestjs_common__rspack_import_2.BadRequestException({
6674
+ message: `The '${key}' filter lists more than ${_interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES} values.`,
6675
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.FILTER_TOO_MANY_VALUES */.j4.FILTER_TOO_MANY_VALUES,
6676
+ messageVariables: {
6677
+ key,
6678
+ max: _interfaces_generic_record_interface_js__rspack_import_3/* .RECORD_FILTER_LIMITS.MAX_IN_VALUES */.M5.MAX_IN_VALUES
5978
6679
  }
5979
- if (!operand.length) query.andWhere('1 = 0');
5980
- else query.andWhere(`${column} IN (:...${param})`, {
5981
- [param]: operand
5982
- });
5983
- return;
5984
- }
5985
- if (operand === null || operand === undefined || !isFilterScalar(operand)) throw this.invalidFilterValue(key, op);
5986
- query.andWhere(`${column} ${FILTER_COMPARISON_SQL[op]} :${param}`, {
5987
- [param]: operand
5988
6680
  });
5989
- });
5990
- return query;
6681
+ }
6682
+ return list;
6683
+ }
6684
+ fieldTypeOf(key) {
6685
+ return this.fields.find((f)=>f.code === key && !f.deprecatedAt && !f.isSystemField)?.fieldType;
6686
+ }
6687
+ /** Text operators need a text field, multi-select operators a multi-select one, and nothing but null / blank checks reads a list or JSON field. */ assertOperatorFits(key, op) {
6688
+ const type = this.fieldTypeOf(key);
6689
+ const textField = type !== undefined && TEXT_FIELD_TYPES.has(type);
6690
+ const fits = _interfaces_generic_record_interface_js__rspack_import_3/* .TEXT_FILTER_OPS.includes */.wk.includes(op) ? textField : _interfaces_generic_record_interface_js__rspack_import_3/* .LIST_FIELD_FILTER_OPS.includes */.yV.includes(op) ? type === _enums_field_type_enum_js__rspack_import_7/* .FieldType.MULTI_SELECT */.P.MULTI_SELECT : _interfaces_generic_record_interface_js__rspack_import_3/* .EMPTY_FILTER_OPS.includes */.Oh.includes(op) ? textField || type === _enums_field_type_enum_js__rspack_import_7/* .FieldType.MULTI_SELECT */.P.MULTI_SELECT : op === 'is_null' || op === 'not_null' || type !== _enums_field_type_enum_js__rspack_import_7/* .FieldType.MULTI_SELECT */.P.MULTI_SELECT && type !== _enums_field_type_enum_js__rspack_import_7/* .FieldType.JSON */.P.JSON;
6691
+ if (!fits) {
6692
+ throw new _nestjs_common__rspack_import_2.BadRequestException({
6693
+ message: `The '${op}' filter cannot be used on '${key}'.`,
6694
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.FILTER_OPERATOR_NOT_FOR_FIELD */.j4.FILTER_OPERATOR_NOT_FOR_FIELD,
6695
+ messageVariables: {
6696
+ key,
6697
+ op
6698
+ }
6699
+ });
6700
+ }
5991
6701
  }
5992
6702
  unknownOperator(key, op) {
5993
6703
  return new _nestjs_common__rspack_import_2.BadRequestException({
5994
6704
  message: `'${op}' is not a filter operator.`,
5995
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.FILTER_OPERATOR_UNKNOWN */.j4.FILTER_OPERATOR_UNKNOWN,
6705
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.FILTER_OPERATOR_UNKNOWN */.j4.FILTER_OPERATOR_UNKNOWN,
5996
6706
  messageVariables: {
5997
6707
  key,
5998
6708
  op,
@@ -6002,8 +6712,8 @@ const FILTER_COMPARISON_SQL = {
6002
6712
  }
6003
6713
  invalidFilterValue(key, op) {
6004
6714
  return new _nestjs_common__rspack_import_2.BadRequestException({
6005
- message: op === 'in' ? `The '${key}' filter needs a list of values.` : `The '${key}' filter needs a single value.`,
6006
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.FILTER_VALUE_INVALID */.j4.FILTER_VALUE_INVALID,
6715
+ message: `The '${key}' filter has an invalid value for '${op}'.`,
6716
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.FILTER_VALUE_INVALID */.j4.FILTER_VALUE_INVALID,
6007
6717
  messageVariables: {
6008
6718
  key,
6009
6719
  op
@@ -6032,7 +6742,7 @@ const FILTER_COMPARISON_SQL = {
6032
6742
  if (key !== 'id' && !this.presentSystemColumns.has(key) && !this.activeFieldCodes.has(key)) {
6033
6743
  throw new _nestjs_common__rspack_import_2.BadRequestException({
6034
6744
  message: `'${key}' is not a field on entity '${this.entityCode}'.`,
6035
- messageKey: _config_message_keys_js__rspack_import_7/* .GENERIC_RECORD_MESSAGES.UNKNOWN_FIELD */.j4.UNKNOWN_FIELD,
6745
+ messageKey: _config_message_keys_js__rspack_import_8/* .GENERIC_RECORD_MESSAGES.UNKNOWN_FIELD */.j4.UNKNOWN_FIELD,
6036
6746
  messageVariables: {
6037
6747
  key,
6038
6748
  entity: this.entityCode
@@ -6100,181 +6810,6 @@ __webpack_require__.d(__webpack_exports__, {
6100
6810
  });
6101
6811
 
6102
6812
 
6103
- },
6104
- 5313(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
6105
- /* import */ var node_crypto__rspack_import_0 = __webpack_require__(2561);
6106
- /* import */ var typeorm__rspack_import_1 = __webpack_require__(7158);
6107
- /* import */ var _enums_field_type_enum_js__rspack_import_2 = __webpack_require__(9614);
6108
-
6109
-
6110
-
6111
- /** Columns every dynamic table may carry (the `Identity` contract). Everything else is a designed field. */ const SYSTEM_COLUMN_NAMES = [
6112
- 'id',
6113
- 'created_at',
6114
- 'updated_at',
6115
- 'deleted_at',
6116
- 'created_by_id',
6117
- 'updated_by_id',
6118
- 'deleted_by_id'
6119
- ];
6120
- /** Field types stored as unbounded text or JSON: an index or unique constraint on them fails (MySQL) or breaks on large values (PostgreSQL). */ const NON_INDEXABLE_FIELD_TYPES = [
6121
- _enums_field_type_enum_js__rspack_import_2/* .FieldType.LONG_TEXT */.P.LONG_TEXT,
6122
- _enums_field_type_enum_js__rspack_import_2/* .FieldType.JSON */.P.JSON,
6123
- _enums_field_type_enum_js__rspack_import_2/* .FieldType.MULTI_SELECT */.P.MULTI_SELECT
6124
- ];
6125
- const isIndexableFieldType = (fieldType)=>!NON_INDEXABLE_FIELD_TYPES.includes(fieldType);
6126
- const uuidColumn = (name, driver, options = {})=>new typeorm__rspack_import_1.TableColumn({
6127
- name,
6128
- ...driver === 'postgres' ? {
6129
- type: 'uuid'
6130
- } : {
6131
- type: 'char',
6132
- length: '36'
6133
- },
6134
- ...options
6135
- });
6136
- function buildSystemColumns(flags, driver) {
6137
- const columns = [
6138
- uuidColumn('id', driver, {
6139
- isPrimary: true,
6140
- isGenerated: true,
6141
- generationStrategy: 'uuid'
6142
- }),
6143
- new typeorm__rspack_import_1.TableColumn({
6144
- name: 'created_at',
6145
- type: 'timestamp',
6146
- isNullable: false,
6147
- default: 'CURRENT_TIMESTAMP'
6148
- }),
6149
- new typeorm__rspack_import_1.TableColumn({
6150
- name: 'updated_at',
6151
- type: 'timestamp',
6152
- isNullable: false,
6153
- default: 'CURRENT_TIMESTAMP'
6154
- })
6155
- ];
6156
- if (flags.isSoftDeleteEnabled) columns.push(softDeleteColumn());
6157
- if (flags.isAuditEnabled) columns.push(...auditColumns(driver));
6158
- return columns;
6159
- }
6160
- const softDeleteColumn = ()=>new typeorm__rspack_import_1.TableColumn({
6161
- name: 'deleted_at',
6162
- type: 'timestamp',
6163
- isNullable: true
6164
- });
6165
- const auditColumns = (driver)=>[
6166
- uuidColumn('created_by_id', driver, {
6167
- isNullable: true
6168
- }),
6169
- uuidColumn('updated_by_id', driver, {
6170
- isNullable: true
6171
- }),
6172
- uuidColumn('deleted_by_id', driver, {
6173
- isNullable: true
6174
- })
6175
- ];
6176
- const sameColumns = (columnNames, expected)=>columnNames.length === expected.length && expected.every((c)=>columnNames.includes(c));
6177
- const NAME_HASH_LENGTH = 8;
6178
- /**
6179
- * Deterministic index/constraint name within the driver's identifier limit. The hash of
6180
- * `table.column` keeps names distinct when the readable part is cut short, and when two
6181
- * table/column pairs would read the same (`eb_a_b` + `c` vs `eb_a` + `b_c`).
6182
- */ function indexName(prefix, table, column, maxLength) {
6183
- const hash = (0,node_crypto__rspack_import_0.createHash)('sha256').update(`${table}.${column}`).digest('hex').slice(0, NAME_HASH_LENGTH);
6184
- const room = maxLength - prefix.length - hash.length - 2;
6185
- return `${prefix}_${`${table}_${column}`.slice(0, room)}_${hash}`;
6186
- }
6187
- const plainIndex = (table, column, driver, adapter)=>new typeorm__rspack_import_1.TableIndex({
6188
- name: indexName('idx', table, column, adapter.maxIdentifierLength(driver)),
6189
- columnNames: [
6190
- column
6191
- ]
6192
- });
6193
- const tableUnique = (table, column, driver, adapter)=>new typeorm__rspack_import_1.TableUnique({
6194
- name: indexName('uq', table, column, adapter.maxIdentifierLength(driver)),
6195
- columnNames: [
6196
- column
6197
- ]
6198
- });
6199
- /** MySQL has no unique constraints (TypeORM refuses them), only unique indexes. */ async function addUnique(qr, table, column, driver, adapter) {
6200
- const name = indexName('uq', table.name, column, adapter.maxIdentifierLength(driver));
6201
- if (driver === 'mysql') {
6202
- await qr.createIndex(table, new typeorm__rspack_import_1.TableIndex({
6203
- name,
6204
- columnNames: [
6205
- column
6206
- ],
6207
- isUnique: true
6208
- }));
6209
- return;
6210
- }
6211
- await qr.createUniqueConstraint(table, new typeorm__rspack_import_1.TableUnique({
6212
- name,
6213
- columnNames: [
6214
- column
6215
- ]
6216
- }));
6217
- }
6218
- /** The stored column type, with a `uuid` stored before MySQL support became char(36) resolved to what MySQL accepts. */ function columnType(field, driver, adapter) {
6219
- const type = field.dbType || adapter.mapFieldType(field.fieldType, driver);
6220
- return driver === 'mysql' && type === 'uuid' ? 'char' : type;
6221
- }
6222
- /**
6223
- * Column for a field. `enforceNotNull` controls whether the DB column is NOT NULL: true when
6224
- * the table is brand new (no rows can violate it); false when adding to a table that already
6225
- * has rows (the NOT NULL is applied afterwards, once the column has been backfilled).
6226
- */ function buildFieldColumn(field, driver, adapter, options) {
6227
- const sizing = adapter.getColumnSizing(field.fieldType, driver);
6228
- return new typeorm__rspack_import_1.TableColumn({
6229
- name: field.code,
6230
- type: columnType(field, driver, adapter),
6231
- length: sizing.length,
6232
- precision: sizing.precision ?? undefined,
6233
- scale: sizing.scale,
6234
- isNullable: options.enforceNotNull ? field.nullable : true,
6235
- isUnique: options.withUnique ? field.unique : false
6236
- });
6237
- }
6238
- /** The SQL type text for a field type on a driver, e.g. `varchar(255)`, `numeric(18,4)`, `char(36)`. */ function sqlTypeFor(fieldType, driver, adapter) {
6239
- const base = adapter.mapFieldType(fieldType, driver);
6240
- const sizing = adapter.getColumnSizing(fieldType, driver);
6241
- if (sizing.length) return `${base}(${sizing.length})`;
6242
- if (sizing.precision !== undefined) return `${base}(${sizing.precision},${sizing.scale ?? 0})`;
6243
- return base;
6244
- }
6245
- /** Coarse type family, so `character varying` (pg) and `varchar` (mysql) compare equal. */ function typeFamily(type) {
6246
- const t = type.toLowerCase();
6247
- if (t === 'uuid') return 'uuid';
6248
- if (t.includes('json')) return 'json';
6249
- if (t.includes('timestamp') || t === 'datetime') return 'timestamp';
6250
- if (t === 'date') return 'date';
6251
- if (t.includes('bool') || t === 'tinyint') return 'boolean';
6252
- if (t.includes('int') && !t.includes('point')) return 'integer';
6253
- if (t.includes('numeric') || t.includes('decimal')) return 'numeric';
6254
- if (t.includes('char') || t === 'varchar') return 'varchar';
6255
- if (t.includes('text')) return 'text';
6256
- return t;
6257
- }
6258
-
6259
- __webpack_require__.d(__webpack_exports__, {
6260
- $6: () => (addUnique),
6261
- Rm: () => (buildSystemColumns),
6262
- cg: () => (sqlTypeFor),
6263
- fu: () => (buildFieldColumn),
6264
- jp: () => (typeFamily),
6265
- mo: () => (indexName)
6266
- }, {
6267
- G7: sameColumns,
6268
- HK: auditColumns,
6269
- OG: softDeleteColumn,
6270
- d4: isIndexableFieldType,
6271
- gW: NON_INDEXABLE_FIELD_TYPES,
6272
- mq: SYSTEM_COLUMN_NAMES,
6273
- uy: tableUnique,
6274
- v7: plainIndex
6275
- });
6276
-
6277
-
6278
6813
  },
6279
6814
  7259(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
6280
6815
  /* import */ var _nestjs_common__rspack_import_0 = __webpack_require__(6157);