@rytass/bpm-core-nestjs-module 0.20.1 → 0.21.1

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 (92) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +109 -5
  3. package/package.json +2 -2
  4. package/src/lib/attachment/attachment.service.js +2 -10
  5. package/src/lib/attachment/attachment.service.js.map +1 -1
  6. package/src/lib/attachment/file-upload-value-ids.d.ts +8 -0
  7. package/src/lib/attachment/file-upload-value-ids.js +15 -0
  8. package/src/lib/attachment/file-upload-value-ids.js.map +1 -0
  9. package/src/lib/bpm/bpm-root-options.d.ts +18 -0
  10. package/src/lib/bpm/bpm-root-options.js.map +1 -1
  11. package/src/lib/bpm/bpm-root.module.d.ts +11 -0
  12. package/src/lib/bpm/bpm-root.module.js +9 -4
  13. package/src/lib/bpm/bpm-root.module.js.map +1 -1
  14. package/src/lib/form/form-data-issue.d.ts +34 -0
  15. package/src/lib/form/form-data-issue.js +59 -0
  16. package/src/lib/form/form-data-issue.js.map +1 -0
  17. package/src/lib/form/form-member-value.validator.d.ts +2 -2
  18. package/src/lib/form/form-member-value.validator.js +2 -3
  19. package/src/lib/form/form-member-value.validator.js.map +1 -1
  20. package/src/lib/form/form-org-unit-value.validator.d.ts +2 -2
  21. package/src/lib/form/form-org-unit-value.validator.js +4 -1
  22. package/src/lib/form/form-org-unit-value.validator.js.map +1 -1
  23. package/src/lib/form/form-schema.validator.js +40 -0
  24. package/src/lib/form/form-schema.validator.js.map +1 -1
  25. package/src/lib/form-data-source/form-data-source.module.d.ts +8 -0
  26. package/src/lib/form-data-source/form-data-source.module.js +1 -1
  27. package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
  28. package/src/lib/form-data-source/form-data-source.service.js +1 -1
  29. package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
  30. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.d.ts +29 -0
  31. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.js +44 -0
  32. package/src/lib/migrations/0000000031000-approval-instance-form-data-gin.js.map +1 -0
  33. package/src/lib/migrations/index.d.ts +1 -0
  34. package/src/lib/migrations/index.js +3 -0
  35. package/src/lib/migrations/index.js.map +1 -1
  36. package/src/lib/notification/notification.service.js +6 -0
  37. package/src/lib/notification/notification.service.js.map +1 -1
  38. package/src/lib/organization/organization-time-zone.d.ts +4 -2
  39. package/src/lib/organization/organization-time-zone.js +3 -6
  40. package/src/lib/organization/organization-time-zone.js.map +1 -1
  41. package/src/lib/template/filterable-field-guard.d.ts +49 -0
  42. package/src/lib/template/filterable-field-guard.js +141 -0
  43. package/src/lib/template/filterable-field-guard.js.map +1 -0
  44. package/src/lib/template/filterable-field-issue.object.d.ts +21 -0
  45. package/src/lib/template/filterable-field-issue.object.js +65 -0
  46. package/src/lib/template/filterable-field-issue.object.js.map +1 -0
  47. package/src/lib/template/filterable-field.queries.d.ts +20 -0
  48. package/src/lib/template/filterable-field.queries.js +80 -0
  49. package/src/lib/template/filterable-field.queries.js.map +1 -0
  50. package/src/lib/template/index.d.ts +1 -0
  51. package/src/lib/template/index.js +1 -0
  52. package/src/lib/template/index.js.map +1 -1
  53. package/src/lib/template/template.module.js +2 -0
  54. package/src/lib/template/template.module.js.map +1 -1
  55. package/src/lib/template/template.service.d.ts +39 -0
  56. package/src/lib/template/template.service.js +218 -49
  57. package/src/lib/template/template.service.js.map +1 -1
  58. package/src/lib/template/workflow-definition.validator.js +84 -2
  59. package/src/lib/template/workflow-definition.validator.js.map +1 -1
  60. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.d.ts +49 -0
  61. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js +501 -0
  62. package/src/lib/workflow-engine/approval-instance-field-filter.compiler.js.map +1 -0
  63. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.d.ts +15 -0
  64. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js +20 -0
  65. package/src/lib/workflow-engine/approval-instance-filterable-field.policy.js.map +1 -0
  66. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.d.ts +15 -0
  67. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js +55 -0
  68. package/src/lib/workflow-engine/dto/approval-instance-field-filter.input.js.map +1 -0
  69. package/src/lib/workflow-engine/dto/approval-instance-list-args.d.ts +9 -0
  70. package/src/lib/workflow-engine/dto/approval-instance-list-args.js +24 -0
  71. package/src/lib/workflow-engine/dto/approval-instance-list-args.js.map +1 -0
  72. package/src/lib/workflow-engine/dto/approval-instance-sort.input.d.ts +10 -0
  73. package/src/lib/workflow-engine/dto/approval-instance-sort.input.js +32 -0
  74. package/src/lib/workflow-engine/dto/approval-instance-sort.input.js.map +1 -0
  75. package/src/lib/workflow-engine/index.d.ts +1 -0
  76. package/src/lib/workflow-engine/index.js +1 -0
  77. package/src/lib/workflow-engine/index.js.map +1 -1
  78. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.d.ts +108 -0
  79. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.js +9 -0
  80. package/src/lib/workflow-engine/initiator-custom-fields-resolver.token.js.map +1 -0
  81. package/src/lib/workflow-engine/workflow-engine.enums.d.ts +13 -0
  82. package/src/lib/workflow-engine/workflow-engine.enums.js +22 -1
  83. package/src/lib/workflow-engine/workflow-engine.enums.js.map +1 -1
  84. package/src/lib/workflow-engine/workflow-engine.module.d.ts +2 -0
  85. package/src/lib/workflow-engine/workflow-engine.module.js +9 -3
  86. package/src/lib/workflow-engine/workflow-engine.module.js.map +1 -1
  87. package/src/lib/workflow-engine/workflow-engine.queries.d.ts +6 -4
  88. package/src/lib/workflow-engine/workflow-engine.queries.js +35 -12
  89. package/src/lib/workflow-engine/workflow-engine.queries.js.map +1 -1
  90. package/src/lib/workflow-engine/workflow-engine.service.d.ts +166 -18
  91. package/src/lib/workflow-engine/workflow-engine.service.js +918 -139
  92. package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filterable-field.queries.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/template/filterable-field.queries.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@ export * from './dto/approval-template.input';
6
6
  export * from './dto/compose-approval-template.input';
7
7
  export * from './dto/expression-designer.dto';
8
8
  export * from './expression-designer.service';
9
+ export * from './filterable-field-issue.object';
9
10
  export * from './template-observer.token';
10
11
  export * from './template.enums';
11
12
  export * from './template.module';
@@ -9,6 +9,7 @@ tslib_1.__exportStar(require("./dto/approval-template.input"), exports);
9
9
  tslib_1.__exportStar(require("./dto/compose-approval-template.input"), exports);
10
10
  tslib_1.__exportStar(require("./dto/expression-designer.dto"), exports);
11
11
  tslib_1.__exportStar(require("./expression-designer.service"), exports);
12
+ tslib_1.__exportStar(require("./filterable-field-issue.object"), exports);
12
13
  tslib_1.__exportStar(require("./template-observer.token"), exports);
13
14
  tslib_1.__exportStar(require("./template.enums"), exports);
14
15
  tslib_1.__exportStar(require("./template.module"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/template/index.ts"],"names":[],"mappings":";;;AAAA,8EAAoD;AACpD,6EAAmD;AACnD,qEAA2C;AAC3C,6EAAmD;AACnD,wEAA8C;AAC9C,gFAAsD;AACtD,wEAA8C;AAC9C,wEAA8C;AAC9C,oEAA0C;AAC1C,2DAAiC;AACjC,4DAAkC;AAClC,6DAAmC;AACnC,0EAAgD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/template/index.ts"],"names":[],"mappings":";;;AAAA,8EAAoD;AACpD,6EAAmD;AACnD,qEAA2C;AAC3C,6EAAmD;AACnD,wEAA8C;AAC9C,gFAAsD;AACtD,wEAA8C;AAC9C,wEAA8C;AAC9C,0EAAgD;AAChD,oEAA0C;AAC1C,2DAAiC;AACjC,4DAAkC;AAClC,6DAAmC;AACnC,0EAAgD"}
@@ -13,6 +13,7 @@ const approval_template_version_entity_1 = require("./approval-template-version.
13
13
  const compose_template_mutations_1 = require("./compose-template.mutations");
14
14
  const expression_designer_queries_1 = require("./expression-designer.queries");
15
15
  const expression_designer_service_1 = require("./expression-designer.service");
16
+ const filterable_field_queries_1 = require("./filterable-field.queries");
16
17
  const template_mutations_1 = require("./template.mutations");
17
18
  const template_queries_1 = require("./template.queries");
18
19
  const template_service_1 = require("./template.service");
@@ -35,6 +36,7 @@ exports.TemplateModule = TemplateModule = tslib_1.__decorate([
35
36
  compose_template_mutations_1.ComposeTemplateMutations,
36
37
  expression_designer_queries_1.ExpressionDesignerQueries,
37
38
  expression_designer_service_1.ExpressionDesignerService,
39
+ filterable_field_queries_1.FilterableFieldQueries,
38
40
  template_mutations_1.TemplateMutations,
39
41
  template_queries_1.TemplateQueries,
40
42
  template_service_1.TemplateService,
@@ -1 +1 @@
1
- {"version":3,"file":"template.module.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/template/template.module.ts"],"names":[],"mappings":";;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,oEAAgE;AAChE,2FAAqF;AACrF,qDAAiD;AACjD,2FAAqF;AACrF,yEAAoE;AACpE,yFAAmF;AACnF,6EAAwE;AACxE,+EAA0E;AAC1E,+EAA0E;AAC1E,6DAAyD;AACzD,yDAAqD;AACrD,yDAAqD;AAuB9C,IAAM,cAAc,GAApB,MAAM,cAAc;CAAG,CAAA;AAAjB,wCAAc;yBAAd,cAAc;IArB1B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,kCAAe;YACf,wBAAU;YACV,uBAAa,CAAC,UAAU,CAAC;gBACvB,kEAA8B;gBAC9B,iDAAsB;gBACtB,gEAA6B;gBAC7B,4DAA2B;aAC5B,CAAC;SACH;QACD,SAAS,EAAE;YACT,qDAAwB;YACxB,uDAAyB;YACzB,uDAAyB;YACzB,sCAAiB;YACjB,kCAAe;YACf,kCAAe;SAChB;QACD,OAAO,EAAE,CAAC,kCAAe,CAAC;KAC3B,CAAC;GACW,cAAc,CAAG"}
1
+ {"version":3,"file":"template.module.js","sourceRoot":"","sources":["../../../../../../libs/bpm-core/src/lib/template/template.module.ts"],"names":[],"mappings":";;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,oEAAgE;AAChE,2FAAqF;AACrF,qDAAiD;AACjD,2FAAqF;AACrF,yEAAoE;AACpE,yFAAmF;AACnF,6EAAwE;AACxE,+EAA0E;AAC1E,+EAA0E;AAC1E,yEAAoE;AACpE,6DAAyD;AACzD,yDAAqD;AACrD,yDAAqD;AAwB9C,IAAM,cAAc,GAApB,MAAM,cAAc;CAAG,CAAA;AAAjB,wCAAc;yBAAd,cAAc;IAtB1B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,kCAAe;YACf,wBAAU;YACV,uBAAa,CAAC,UAAU,CAAC;gBACvB,kEAA8B;gBAC9B,iDAAsB;gBACtB,gEAA6B;gBAC7B,4DAA2B;aAC5B,CAAC;SACH;QACD,SAAS,EAAE;YACT,qDAAwB;YACxB,uDAAyB;YACzB,uDAAyB;YACzB,iDAAsB;YACtB,sCAAiB;YACjB,kCAAe;YACf,kCAAe;SAChB;QACD,OAAO,EAAE,CAAC,kCAAe,CAAC;KAC3B,CAAC;GACW,cAAc,CAAG"}
@@ -73,7 +73,19 @@ export declare class TemplateService {
73
73
  listApprovalTemplateVersions(templateId: string): Promise<readonly ApprovalTemplateVersionEntity[]>;
74
74
  getApprovalTemplateVersion(id: string): Promise<ApprovalTemplateVersionEntity>;
75
75
  updateApprovalTemplateDraft(input: UpdateApprovalTemplateDraftInput, manager?: EntityManager): Promise<ApprovalTemplateVersionEntity>;
76
+ /**
77
+ * Runs `run` in the caller's transaction when one is passed, otherwise in a
78
+ * new one, so a template lock taken inside it is held until the write
79
+ * commits.
80
+ */
81
+ private runInTemplateTransaction;
76
82
  forkApprovalTemplate(templateId: string, manager?: EntityManager): Promise<ApprovalTemplateVersionEntity>;
83
+ /**
84
+ * Forks under the template row lock (ADR 25 §15): the draft check, the
85
+ * source read and the insert see the template as no concurrent publish or
86
+ * fork can change it until this commits.
87
+ */
88
+ private forkApprovalTemplateLocked;
77
89
  publishApprovalTemplateVersion(versionId: string, publishedByMemberId?: string, manager?: EntityManager): Promise<ApprovalTemplateVersionEntity>;
78
90
  /**
79
91
  * The publish itself, without the observer notification. Split out so
@@ -81,6 +93,11 @@ export declare class TemplateService {
81
93
  * and still report a single `COMPOSED` event rather than two.
82
94
  */
83
95
  private publishVersion;
96
+ /**
97
+ * Reads one template row, optionally locked. Locks are only taken inside a
98
+ * transaction; the caller passes the transaction's manager.
99
+ */
100
+ private lockTemplateRow;
84
101
  private publishApprovalTemplateVersionWithManager;
85
102
  rollbackApprovalTemplateVersion(versionId: string, rolledBackByMemberId?: string): Promise<ApprovalTemplateVersionEntity>;
86
103
  /**
@@ -90,6 +107,10 @@ export declare class TemplateService {
90
107
  */
91
108
  private notifyTemplateObserver;
92
109
  private readHostObserver;
110
+ /**
111
+ * Returns the bound form schema so the publish can run the filterable-field
112
+ * check against it without loading the form version a second time.
113
+ */
93
114
  private validatePublishableVersion;
94
115
  /** The cel-legacy publish lint, unchanged (docs/24 §1). */
95
116
  private lintCelLegacyExpressions;
@@ -121,9 +142,27 @@ export declare class TemplateService {
121
142
  private templateVersions;
122
143
  private templateCategories;
123
144
  private formVersions;
145
+ /**
146
+ * The form schema of the template's current version: the one whose
147
+ * filterable fields the stored instances already satisfy. Null when the
148
+ * template has no published version yet. Read under the caller's template
149
+ * row lock, so no write can change what it guarantees before the publish
150
+ * commits.
151
+ */
152
+ private readInForceFormSchema;
153
+ private readFormSchemaOfVersion;
124
154
  private getTemplateOrThrow;
125
155
  private getTemplateCategoryOrThrow;
126
156
  private setApprovalTemplateActive;
157
+ /**
158
+ * Locks the template row FOR UPDATE, the lock a publish, compose or rollback
159
+ * takes (ADR 25 §15), then reads the row with its category. A template edit
160
+ * saves the whole entity it read, so reading it unlocked would let a publish
161
+ * that commits in between be overwritten with the pre-publish
162
+ * `currentVersionId`; under the lock the read is the latest committed row
163
+ * and a publish waits for this edit instead.
164
+ */
165
+ private readLockedTemplateOrThrow;
127
166
  private setApprovalTemplateCategoryActive;
128
167
  private getTemplateVersionOrThrow;
129
168
  private readNextVersionNumber;
@@ -22,6 +22,7 @@ const form_enums_1 = require("../form/form.enums");
22
22
  const form_service_1 = require("../form/form.service");
23
23
  const workflow_webhook_validator_1 = require("../workflow-webhook/workflow-webhook.validator");
24
24
  const workflow_webhook_service_1 = require("../workflow-webhook/workflow-webhook.service");
25
+ const filterable_field_guard_1 = require("./filterable-field-guard");
25
26
  const approval_template_category_entity_1 = require("./approval-template-category.entity");
26
27
  const approval_template_entity_1 = require("./approval-template.entity");
27
28
  const approval_template_version_entity_1 = require("./approval-template-version.entity");
@@ -108,6 +109,14 @@ let TemplateService = TemplateService_1 = class TemplateService {
108
109
  */
109
110
  async composeApprovalTemplateWithForm(input, currentMemberId) {
110
111
  const { event, result } = await this.templateRepository.manager.transaction(async (manager) => {
112
+ // ADR 25 §15: an existing template's row is locked before anything
113
+ // else, so this compose takes the template lock before it forks or
114
+ // writes the draft row, the same order a standalone publish uses.
115
+ // Taking it later (after the draft write, inside publishVersion) let a
116
+ // concurrent publish of the same draft deadlock against this one.
117
+ if (input.templateId) {
118
+ await this.readLockedTemplateOrThrow(manager, input.templateId);
119
+ }
111
120
  const formDefinitionVersion = await this.resolveComposedFormVersion(manager, input, currentMemberId);
112
121
  const templateDraftVersion = await this.resolveComposedTemplateDraftVersion(manager, input, currentMemberId);
113
122
  let templateVersion = await this.updateApprovalTemplateDraft({
@@ -218,27 +227,30 @@ let TemplateService = TemplateService_1 = class TemplateService {
218
227
  });
219
228
  }
220
229
  async updateApprovalTemplate(input) {
221
- const existing = await this.getTemplateOrThrow(input.id);
222
230
  const category = input.categoryId === undefined
223
231
  ? undefined
224
232
  : await this.validateOptionalTemplateCategory(input.categoryId);
225
- const next = this.templateRepository.merge(existing, {
226
- category: input.category !== undefined
227
- ? input.category
228
- : category !== undefined
229
- ? (category?.name ?? null)
230
- : existing.category,
231
- description: input.description ?? existing.description,
232
- name: input.name ?? existing.name,
233
+ await this.templateRepository.manager.transaction(async (manager) => {
234
+ const existing = await this.readLockedTemplateOrThrow(manager, input.id);
235
+ const templates = manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity);
236
+ const next = templates.merge(existing, {
237
+ category: input.category !== undefined
238
+ ? input.category
239
+ : category !== undefined
240
+ ? (category?.name ?? null)
241
+ : existing.category,
242
+ description: input.description ?? existing.description,
243
+ name: input.name ?? existing.name,
244
+ });
245
+ // Assigned outside `merge` because the relation, not the `categoryId`
246
+ // scalar, owns `category_id`. Leaving `categoryDetail` untouched when the
247
+ // caller omitted `categoryId` keeps the loaded relation, which persists
248
+ // the current category unchanged.
249
+ if (category !== undefined) {
250
+ next.categoryDetail = category;
251
+ }
252
+ await templates.save(next);
233
253
  });
234
- // Assigned outside `merge` because the relation, not the `categoryId`
235
- // scalar, owns `category_id`. Leaving `categoryDetail` untouched when the
236
- // caller omitted `categoryId` keeps the loaded relation, which persists
237
- // the current category unchanged.
238
- if (category !== undefined) {
239
- next.categoryDetail = category;
240
- }
241
- await this.templateRepository.save(next);
242
254
  // Re-read rather than returning the saved entity: `categoryId` is not
243
255
  // written by `save()` any more, so the in-memory copy would still carry
244
256
  // the previous category alongside the new `categoryDetail` — the same
@@ -364,25 +376,51 @@ let TemplateService = TemplateService_1 = class TemplateService {
364
376
  return this.getTemplateVersionOrThrow(id);
365
377
  }
366
378
  async updateApprovalTemplateDraft(input, manager) {
367
- const versionRepository = this.templateVersions(manager);
368
- const existing = await this.getTemplateVersionOrThrow(input.versionId, manager);
369
- if (existing.status !== template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT) {
370
- throw new common_1.ConflictException('Only draft approval template versions can be updated');
371
- }
372
- await this.validateOptionalFormDefinitionVersion(input.formDefinitionVersionId, manager);
373
- const workflowDefinition = this.parseWorkflowDefinitionOrThrow(input.workflowDefinitionJson);
374
- const next = versionRepository.merge(existing, {
375
- formDefinitionVersionId: input.formDefinitionVersionId,
376
- initiatorPolicyCel: input.initiatorPolicyCel,
377
- notificationConfig: parseOptionalJsonObject(input.notificationConfigJson),
378
- slaDefaults: parseOptionalJsonObject(input.slaDefaultsJson),
379
- workflowDefinition,
379
+ return this.runInTemplateTransaction(manager, async (txManager) => {
380
+ const versionRepository = this.templateVersions(txManager);
381
+ // Read once to learn the template, lock its row (ADR 25 §15), then read
382
+ // the draft again: a publish that committed before the lock has turned
383
+ // it PUBLISHED, and saving the copy read before would write the old
384
+ // DRAFT status and empty publish fields back over it.
385
+ const unlocked = await this.getTemplateVersionOrThrow(input.versionId, txManager);
386
+ await this.readLockedTemplateOrThrow(txManager, unlocked.templateId);
387
+ const existing = await this.getTemplateVersionOrThrow(input.versionId, txManager);
388
+ if (existing.status !== template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT) {
389
+ throw new common_1.ConflictException('Only draft approval template versions can be updated');
390
+ }
391
+ await this.validateOptionalFormDefinitionVersion(input.formDefinitionVersionId, txManager);
392
+ const workflowDefinition = this.parseWorkflowDefinitionOrThrow(input.workflowDefinitionJson);
393
+ const next = versionRepository.merge(existing, {
394
+ formDefinitionVersionId: input.formDefinitionVersionId,
395
+ initiatorPolicyCel: input.initiatorPolicyCel,
396
+ notificationConfig: parseOptionalJsonObject(input.notificationConfigJson),
397
+ slaDefaults: parseOptionalJsonObject(input.slaDefaultsJson),
398
+ workflowDefinition,
399
+ });
400
+ return versionRepository.save(next);
380
401
  });
381
- return versionRepository.save(next);
402
+ }
403
+ /**
404
+ * Runs `run` in the caller's transaction when one is passed, otherwise in a
405
+ * new one, so a template lock taken inside it is held until the write
406
+ * commits.
407
+ */
408
+ runInTemplateTransaction(manager, run) {
409
+ return manager
410
+ ? run(manager)
411
+ : this.templateRepository.manager.transaction(run);
382
412
  }
383
413
  async forkApprovalTemplate(templateId, manager) {
414
+ return this.runInTemplateTransaction(manager, (txManager) => this.forkApprovalTemplateLocked(templateId, txManager));
415
+ }
416
+ /**
417
+ * Forks under the template row lock (ADR 25 §15): the draft check, the
418
+ * source read and the insert see the template as no concurrent publish or
419
+ * fork can change it until this commits.
420
+ */
421
+ async forkApprovalTemplateLocked(templateId, manager) {
384
422
  const versionRepository = this.templateVersions(manager);
385
- const template = await this.getTemplateOrThrow(templateId, manager);
423
+ const template = await this.readLockedTemplateOrThrow(manager, templateId);
386
424
  const existingDraft = await versionRepository.findOne({
387
425
  where: {
388
426
  status: template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT,
@@ -433,18 +471,45 @@ let TemplateService = TemplateService_1 = class TemplateService {
433
471
  if (version.status !== template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT) {
434
472
  throw new common_1.ConflictException('Only draft approval template versions can be published');
435
473
  }
474
+ // Early refusals only: they run outside the lock and are repeated below.
436
475
  await this.validatePublishableVersion(version, manager);
437
- const run = (txManager) => this.publishApprovalTemplateVersionWithManager(txManager, version, publishedByMemberId);
476
+ // ADR 25 §5.4 and §15: the template row is locked before the enable check
477
+ // and before the draft is read again, so a concurrent form-data write
478
+ // (which takes FOR SHARE on the same row) cannot slip between the check
479
+ // and the commit, and the draft is judged as it is under the lock.
480
+ const run = async (txManager) => {
481
+ const lockedTemplate = await this.lockTemplateRow(txManager, version.templateId, 'pessimistic_write');
482
+ if (!lockedTemplate) {
483
+ throw new common_1.NotFoundException(`Approval template ${version.templateId} was not found`);
484
+ }
485
+ const lockedVersion = await this.getTemplateVersionOrThrow(versionId, txManager);
486
+ if (lockedVersion.status !== template_enums_1.ApprovalTemplateVersionStatusEnum.DRAFT) {
487
+ throw new common_1.ConflictException('Only draft approval template versions can be published');
488
+ }
489
+ const formSchema = await this.validatePublishableVersion(lockedVersion, txManager);
490
+ return this.publishApprovalTemplateVersionWithManager(txManager, lockedVersion, publishedByMemberId, formSchema);
491
+ };
438
492
  return manager
439
493
  ? run(manager)
440
494
  : this.templateRepository.manager.transaction(run);
441
495
  }
442
- async publishApprovalTemplateVersionWithManager(manager, version, publishedByMemberId) {
496
+ /**
497
+ * Reads one template row, optionally locked. Locks are only taken inside a
498
+ * transaction; the caller passes the transaction's manager.
499
+ */
500
+ async lockTemplateRow(manager, templateId, mode) {
501
+ return manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity).findOne({
502
+ lock: { mode },
503
+ where: { deletedAt: (0, typeorm_2.IsNull)(), id: templateId },
504
+ });
505
+ }
506
+ async publishApprovalTemplateVersionWithManager(manager, version, publishedByMemberId, formSchema) {
443
507
  const templateRepository = manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity);
444
508
  const versionRepository = manager.getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity);
445
- const template = await templateRepository.findOne({
446
- where: { deletedAt: (0, typeorm_2.IsNull)(), id: version.templateId },
447
- });
509
+ // Already locked by the caller (`publishVersion`); the lock is taken again
510
+ // here so this method is safe on its own, and the lock is re-entrant
511
+ // within one transaction.
512
+ const template = await this.lockTemplateRow(manager, version.templateId, 'pessimistic_write');
448
513
  if (!template) {
449
514
  throw new common_1.NotFoundException(`Approval template ${version.templateId} was not found`);
450
515
  }
@@ -454,6 +519,11 @@ let TemplateService = TemplateService_1 = class TemplateService {
454
519
  if (!template.isActive) {
455
520
  throw new common_1.ConflictException('Approval template is deactivated');
456
521
  }
522
+ // ADR 25 §5.4: a field that becomes filterable must not hold values the
523
+ // template's existing instances cannot satisfy. Checked here, inside the
524
+ // publish transaction, so the instances it reads are the ones it archives.
525
+ // Only what this publish makes filterable is scanned (ADR 25 §15).
526
+ await (0, filterable_field_guard_1.assertTemplateFilterableFieldsEnabled)(manager, template.id, formSchema, () => this.readInForceFormSchema(manager, template));
457
527
  const previousVersionId = template.currentVersionId;
458
528
  if (template.currentVersionId) {
459
529
  await versionRepository.update({ id: template.currentVersionId }, {
@@ -486,6 +556,8 @@ let TemplateService = TemplateService_1 = class TemplateService {
486
556
  // and is not linted again, since a rule tightened since could refuse it
487
557
  // (ADR 22 §5, docs/24 §1). The one check is that every function it calls
488
558
  // is still registered; without it every case would fail on evaluation.
559
+ // The filterable-field check (ADR 25 §5.4) is the exception: it reads the
560
+ // instances stored now, so it runs again inside the transaction below.
489
561
  const unregisteredFunctionErrors = this.lintRegisteredFunctions(target.workflowDefinition, target.initiatorPolicyCel);
490
562
  if (unregisteredFunctionErrors.length) {
491
563
  throw new common_1.BadRequestException(unregisteredFunctionErrors.join('; '));
@@ -493,9 +565,9 @@ let TemplateService = TemplateService_1 = class TemplateService {
493
565
  const rolledBack = await this.templateRepository.manager.transaction(async (manager) => {
494
566
  const templateRepository = manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity);
495
567
  const versionRepository = manager.getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity);
496
- const template = await templateRepository.findOne({
497
- where: { deletedAt: (0, typeorm_2.IsNull)(), id: target.templateId },
498
- });
568
+ // ADR 25 §5.4 and §15: lock the template row before the enable check,
569
+ // the same lock a publish takes (see `publishVersion`).
570
+ const template = await this.lockTemplateRow(manager, target.templateId, 'pessimistic_write');
499
571
  if (!template) {
500
572
  throw new common_1.NotFoundException(`Approval template ${target.templateId} was not found`);
501
573
  }
@@ -504,6 +576,9 @@ let TemplateService = TemplateService_1 = class TemplateService {
504
576
  if (!template.isActive) {
505
577
  throw new common_1.ConflictException('Approval template is deactivated');
506
578
  }
579
+ // The target becomes the current version again, so its filterable
580
+ // fields are checked against the instances stored now (ADR 25 §5.4).
581
+ await (0, filterable_field_guard_1.assertTemplateFilterableFieldsEnabled)(manager, template.id, await this.readFormSchemaOfVersion(manager, target.formDefinitionVersionId), () => this.readInForceFormSchema(manager, template));
507
582
  const previousVersionId = template.currentVersionId;
508
583
  if (template.currentVersionId &&
509
584
  template.currentVersionId !== target.id) {
@@ -561,6 +636,10 @@ let TemplateService = TemplateService_1 = class TemplateService {
561
636
  return null;
562
637
  }
563
638
  }
639
+ /**
640
+ * Returns the bound form schema so the publish can run the filterable-field
641
+ * check against it without loading the form version a second time.
642
+ */
564
643
  async validatePublishableVersion(version, manager) {
565
644
  if (!version.formDefinitionVersionId) {
566
645
  throw new common_1.BadRequestException('Approval template version must bind a form definition version before publishing');
@@ -599,6 +678,9 @@ let TemplateService = TemplateService_1 = class TemplateService {
599
678
  const nodeActorSourceErrors = workflowResult.valid
600
679
  ? lintWorkflowNodeActorSources(version.workflowDefinition)
601
680
  : [];
681
+ const excludeNodeActorsErrors = workflowResult.valid
682
+ ? lintWorkflowExcludeNodeActors(version.workflowDefinition)
683
+ : [];
602
684
  const approverFilledFieldErrors = (0, bpm_core_shared_1.readFormFieldAccessIssues)(version.workflowDefinition, formVersion.schema).map((issue) => issue.message);
603
685
  const approverFilledReadErrors = lintApproverFilledFieldReads(version.workflowDefinition, fieldReader);
604
686
  const webhookErrors = await this.lintWorkflowWebhooks(version.workflowDefinition, formVersion.schema);
@@ -610,6 +692,7 @@ let TemplateService = TemplateService_1 = class TemplateService {
610
692
  ...memberReferenceErrors,
611
693
  ...orgUnitReferenceErrors,
612
694
  ...nodeActorSourceErrors,
695
+ ...excludeNodeActorsErrors,
613
696
  ...approverFilledFieldErrors,
614
697
  ...approverFilledReadErrors,
615
698
  ...webhookErrors,
@@ -617,6 +700,7 @@ let TemplateService = TemplateService_1 = class TemplateService {
617
700
  if (errors.length) {
618
701
  throw new common_1.BadRequestException(errors.join('; '));
619
702
  }
703
+ return formVersion.schema;
620
704
  }
621
705
  /** The cel-legacy publish lint, unchanged (docs/24 §1). */
622
706
  lintCelLegacyExpressions(version) {
@@ -748,6 +832,31 @@ let TemplateService = TemplateService_1 = class TemplateService {
748
832
  ? manager.getRepository(form_definition_version_entity_1.FormDefinitionVersionEntity)
749
833
  : this.formDefinitionVersionRepository;
750
834
  }
835
+ /**
836
+ * The form schema of the template's current version: the one whose
837
+ * filterable fields the stored instances already satisfy. Null when the
838
+ * template has no published version yet. Read under the caller's template
839
+ * row lock, so no write can change what it guarantees before the publish
840
+ * commits.
841
+ */
842
+ async readInForceFormSchema(manager, template) {
843
+ if (!template.currentVersionId) {
844
+ return null;
845
+ }
846
+ const current = await manager
847
+ .getRepository(approval_template_version_entity_1.ApprovalTemplateVersionEntity)
848
+ .findOne({ where: { id: template.currentVersionId } });
849
+ return this.readFormSchemaOfVersion(manager, current?.formDefinitionVersionId ?? null);
850
+ }
851
+ async readFormSchemaOfVersion(manager, formDefinitionVersionId) {
852
+ if (!formDefinitionVersionId) {
853
+ return null;
854
+ }
855
+ const formVersion = await manager
856
+ .getRepository(form_definition_version_entity_1.FormDefinitionVersionEntity)
857
+ .findOne({ where: { id: formDefinitionVersionId } });
858
+ return formVersion?.schema ?? null;
859
+ }
751
860
  async getTemplateOrThrow(id, manager) {
752
861
  const entity = await this.templates(manager).findOne({
753
862
  relations: { categoryDetail: true },
@@ -768,10 +877,28 @@ let TemplateService = TemplateService_1 = class TemplateService {
768
877
  return entity;
769
878
  }
770
879
  async setApprovalTemplateActive(id, isActive) {
771
- const template = await this.getTemplateOrThrow(id);
772
- return this.templateRepository.save(this.templateRepository.merge(template, {
773
- isActive,
774
- }));
880
+ return this.templateRepository.manager.transaction(async (manager) => {
881
+ const template = await this.readLockedTemplateOrThrow(manager, id);
882
+ const templates = manager.getRepository(approval_template_entity_1.ApprovalTemplateEntity);
883
+ return templates.save(templates.merge(template, { isActive }));
884
+ });
885
+ }
886
+ /**
887
+ * Locks the template row FOR UPDATE, the lock a publish, compose or rollback
888
+ * takes (ADR 25 §15), then reads the row with its category. A template edit
889
+ * saves the whole entity it read, so reading it unlocked would let a publish
890
+ * that commits in between be overwritten with the pre-publish
891
+ * `currentVersionId`; under the lock the read is the latest committed row
892
+ * and a publish waits for this edit instead.
893
+ */
894
+ async readLockedTemplateOrThrow(manager, id) {
895
+ // The lock is taken without the category join: Postgres refuses FOR
896
+ // UPDATE on the nullable side of an outer join.
897
+ const locked = await this.lockTemplateRow(manager, id, 'pessimistic_write');
898
+ if (!locked) {
899
+ throw new common_1.NotFoundException(`Approval template ${id} was not found`);
900
+ }
901
+ return this.getTemplateOrThrow(id, manager);
775
902
  }
776
903
  async setApprovalTemplateCategoryActive(id, isActive) {
777
904
  const category = await this.getTemplateCategoryOrThrow(id);
@@ -937,12 +1064,15 @@ function lintWorkflowTableReferences(definition, initiatorPolicyCel, schema, fie
937
1064
  * approver resolver reads one member id there. A path that names no
938
1065
  * member field of the bound form version resolves to nothing at runtime, which
939
1066
  * throws while the task is created and can strand the case — so it is caught
940
- * here instead. The same throw happens when the field is merely empty, and on a
1067
+ * here instead. The same throw happens when the field is merely empty (a
1068
+ * manager chain with a `FORM_MEMBER` start is the exception: an empty field
1069
+ * sends it to its fallback, and it throws only when it has none), and on a
941
1070
  * node past the first one it lands inside the previous approver's decision,
942
1071
  * which then can never commit. So the field must also be one the submit
943
1072
  * validation guarantees a value for: required, with no `visibleWhen` that can
944
1073
  * hide it and not a store-only `hidden` field. `FORM_MEMBER_MANAGER` looks one
945
- * person's manager up, so its field must not be `multiple` either.
1074
+ * person's manager up, so its field must not be `multiple` either; so does an
1075
+ * `ORG_MANAGER_CHAIN` whose `startFrom` is a `FORM_MEMBER`.
946
1076
  */
947
1077
  function lintWorkflowMemberReferences(definition, schema) {
948
1078
  const memberFields = new Map((schema?.fields ?? [])
@@ -961,6 +1091,19 @@ function lintWorkflowMemberReferences(definition, schema) {
961
1091
  },
962
1092
  ];
963
1093
  }
1094
+ // A manager chain that starts from a form pick reads one person there,
1095
+ // the same as FORM_MEMBER_MANAGER.
1096
+ if (node.type === 'userTask' &&
1097
+ node.data.approverResolver?.type === 'ORG_MANAGER_CHAIN' &&
1098
+ node.data.approverResolver.startFrom?.type === 'FORM_MEMBER') {
1099
+ return [
1100
+ {
1101
+ formPath: node.data.approverResolver.startFrom.formPath,
1102
+ label: `workflow.nodes.${node.id}.approverResolver.startFrom.formPath`,
1103
+ single: true,
1104
+ },
1105
+ ];
1106
+ }
964
1107
  if (node.type === 'serviceTask' &&
965
1108
  node.data.action?.type === 'NOTIFY' &&
966
1109
  node.data.action.recipients?.type === 'DYNAMIC_FORM') {
@@ -1032,19 +1175,45 @@ function lintWorkflowOrgUnitReferences(definition, schema) {
1032
1175
  });
1033
1176
  }
1034
1177
  /**
1035
- * The graph checks behind `NODE_ACTOR` / `NODE_ACTOR_MANAGER` nodes (shared
1036
- * with the designer): every source is an unskippable, upstream, non-parallel
1037
- * user task and every path to the node passes one.
1178
+ * The graph checks behind `NODE_ACTOR` / `NODE_ACTOR_MANAGER` nodes, and the
1179
+ * `NODE_ACTOR` start of an `ORG_MANAGER_CHAIN` (shared with the designer):
1180
+ * every source is an unskippable, upstream, non-parallel user task and every
1181
+ * path to the node passes one.
1038
1182
  */
1039
1183
  function lintWorkflowNodeActorSources(definition) {
1040
1184
  return definition.nodes.flatMap((node) => {
1041
1185
  const resolver = node.type === 'userTask' ? node.data.approverResolver : null;
1186
+ // A manager chain that starts from an earlier stage reads that stage's
1187
+ // signer the way NODE_ACTOR does, so it meets the same graph rules. It
1188
+ // also needs that signer to be one person: when the last of several
1189
+ // signers approves, the engine's "which one?" Conflict would land in that
1190
+ // approval's own transaction and it could never commit. NODE_ACTOR and
1191
+ // NODE_ACTOR_MANAGER take every signer and are not held to this.
1192
+ if (resolver?.type === 'ORG_MANAGER_CHAIN' &&
1193
+ resolver.startFrom?.type === 'NODE_ACTOR') {
1194
+ return [
1195
+ ...(0, workflow_graph_1.readNodeActorSourceIssues)(definition, node.id, resolver.startFrom.nodeIds),
1196
+ ...(0, workflow_graph_1.readSingleApproverSourceIssues)(definition, resolver.startFrom.nodeIds),
1197
+ ].map((issue) => `workflow.nodes.${node.id}.approverResolver.startFrom.nodeIds: ${issue}`);
1198
+ }
1042
1199
  if (resolver?.type !== 'NODE_ACTOR' && resolver?.type !== 'NODE_ACTOR_MANAGER') {
1043
1200
  return [];
1044
1201
  }
1045
1202
  return (0, workflow_graph_1.readNodeActorSourceIssues)(definition, node.id, resolver.nodeIds).map((issue) => `workflow.nodes.${node.id}.approverResolver.nodeIds: ${issue}`);
1046
1203
  });
1047
1204
  }
1205
+ /**
1206
+ * The graph checks behind `excludeNodeActors` (shared with the designer): every
1207
+ * listed node, on its own, is an upstream user task that is on every path to
1208
+ * the node and always has a person sign (the `NODE_ACTOR` source rules, worded
1209
+ * for the exclusion). Runs after the structural lint, like the `NODE_ACTOR`
1210
+ * checks.
1211
+ */
1212
+ function lintWorkflowExcludeNodeActors(definition) {
1213
+ return definition.nodes.flatMap((node) => node.type === 'userTask'
1214
+ ? (0, workflow_graph_1.readExcludeNodeActorsIssues)(definition, node.id, node.data.excludeNodeActors).map((issue) => `workflow.nodes.${node.id}.excludeNodeActors: ${issue}`)
1215
+ : []);
1216
+ }
1048
1217
  /**
1049
1218
  * A condition that reads a field an approver fills in, before that approver
1050
1219
  * has filled it in, finds the field absent: the applicant's form is submitted