@usearazzo/runner 1.0.1-alpha.14 → 1.0.1-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usearazzo/runner",
3
- "version": "1.0.1-alpha.14",
3
+ "version": "1.0.1-alpha.16",
4
4
  "description": "Runner for Arazzo Workflows.",
5
5
  "keywords": [
6
6
  "arazzo",
@@ -73,8 +73,8 @@
73
73
  "@swaggerexpert/json-pointer": "^3.0.2",
74
74
  "@swaggerexpert/jsonpath": "^4.0.5",
75
75
  "@swaggerexpert/media-types": "^1.0.1",
76
- "@usearazzo/parser": "1.0.1-alpha.14",
77
- "@usearazzo/resolver": "1.0.1-alpha.14",
76
+ "@usearazzo/parser": "1.0.1-alpha.16",
77
+ "@usearazzo/resolver": "1.0.1-alpha.16",
78
78
  "@xmldom/xmldom": "^0.9.12",
79
79
  "fontoxpath": "^3.34.0",
80
80
  "ramda": "^0.32.0",
@@ -94,5 +94,5 @@
94
94
  "devDependencies": {
95
95
  "swagger-client": "^3.38.4"
96
96
  },
97
- "gitHead": "40a494596c0eed2f984fb87f544f3cf69488a6a7"
97
+ "gitHead": "9ff2a68d209a5d178658f9a568c2d136178d40b8"
98
98
  }
@@ -16,6 +16,7 @@ var _OutputResolver = _interopRequireDefault(require("../resolver/OutputResolver
16
16
  var _ActionResolver = _interopRequireDefault(require("../action/ActionResolver.cjs"));
17
17
  var _OpenAPIOperationLocatorNormalizer = _interopRequireDefault(require("./OpenAPIOperationLocatorNormalizer.cjs"));
18
18
  var _ExecutionError = _interopRequireDefault(require("../errors/ExecutionError.cjs"));
19
+ var _ResolverError = _interopRequireDefault(require("../errors/ResolverError.cjs"));
19
20
  var _RuntimeExpressionError = _interopRequireDefault(require("../errors/RuntimeExpressionError.cjs"));
20
21
  var _abort = require("./abort.cjs");
21
22
  /**
@@ -136,6 +137,7 @@ class StepExecutor {
136
137
  reason: 'workflow-step'
137
138
  });
138
139
  }
140
+ this.assertSuccessCriteria(step);
139
141
 
140
142
  // locate which operation the step invokes — reduced to a (document, JSON
141
143
  // Pointer) locator that the operation executor extracts, normalizes,
@@ -231,9 +233,48 @@ class StepExecutor {
231
233
  });
232
234
  }
233
235
 
236
+ /**
237
+ * Throws {@link ResolverError} unless the step's `successCriteria` is absent
238
+ * or a list of Criterion Objects: `malformed-criteria` for one that is
239
+ * present but not a list, `malformed-criterion` for an entry that is not a
240
+ * Criterion Object. Skipping an unreadable entry would turn the author's
241
+ * assertion into a vacuous pass, so it is an authoring error, as an action's
242
+ * unreadable `criteria` is in {@link ActionResolver}.
243
+ *
244
+ * Called before the step's request is sent, so no live side effect precedes
245
+ * the error. Public because the workflow executor checks every step of a
246
+ * workflow with it before any of them runs, a step targeting a `workflowId`
247
+ * included. It passes `workflowId`, which the error then carries: step ids
248
+ * are only unique within a workflow.
249
+ */
250
+ assertSuccessCriteria(step, workflowId) {
251
+ const criteria = step.successCriteria;
252
+ if (criteria === undefined) return;
253
+ const scope = {
254
+ ...(workflowId === undefined ? {} : {
255
+ workflowId
256
+ }),
257
+ stepId: (0, _apidomCore.toValue)(step.stepId),
258
+ target: 'successCriteria'
259
+ };
260
+ if (!(0, _apidomDatamodel.isArrayElement)(criteria)) {
261
+ throw new _ResolverError.default("A step's `successCriteria` is present but is not a list", {
262
+ ...scope,
263
+ reason: 'malformed-criteria'
264
+ });
265
+ }
266
+ if (![...criteria].every(_apidomNsArazzo.isCriterionElement)) {
267
+ throw new _ResolverError.default("A step's `successCriteria` contains an entry that is not a Criterion Object", {
268
+ ...scope,
269
+ reason: 'malformed-criterion'
270
+ });
271
+ }
272
+ }
273
+
234
274
  /**
235
275
  * Evaluates the step's `successCriteria`; all criteria must pass. A step with
236
- * no criteria succeeds on a received response.
276
+ * no criteria succeeds on a received response. An unreadable
277
+ * `successCriteria` throws, see {@link StepExecutor.assertSuccessCriteria}.
237
278
  *
238
279
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
239
280
  * targeting a `workflowId` is run by the workflow executor and never reaches
@@ -241,15 +282,12 @@ class StepExecutor {
241
282
  * assertion about the step and must be evaluated somewhere.
242
283
  */
243
284
  evaluateCriteria(step, context) {
285
+ this.assertSuccessCriteria(step);
244
286
  const criteria = step.successCriteria;
245
- // no (or non-array) successCriteria: the step succeeds on a received response.
246
- if (!(0, _apidomDatamodel.isArrayElement)(criteria)) return true;
247
- for (const criterion of criteria) {
248
- if (!(0, _apidomNsArazzo.isCriterionElement)(criterion)) continue;
249
- const met = this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
250
- if (!met) return false;
251
- }
252
- return true;
287
+ if (criteria === undefined) return true;
288
+
289
+ // assertSuccessCriteria above guarantees every entry is a Criterion Object
290
+ return [...criteria].every(criterion => this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression)));
253
291
  }
254
292
 
255
293
  /**
@@ -9,6 +9,7 @@ import OutputResolver from "../resolver/OutputResolver.mjs";
9
9
  import ActionResolver from "../action/ActionResolver.mjs";
10
10
  import OpenAPIOperationLocatorNormalizer from "./OpenAPIOperationLocatorNormalizer.mjs";
11
11
  import ExecutionError from "../errors/ExecutionError.mjs";
12
+ import ResolverError from "../errors/ResolverError.mjs";
12
13
  import RuntimeExpressionError from "../errors/RuntimeExpressionError.mjs";
13
14
  import { readAbortSignal, throwIfAborted } from "./abort.mjs";
14
15
  /**
@@ -129,6 +130,7 @@ class StepExecutor {
129
130
  reason: 'workflow-step'
130
131
  });
131
132
  }
133
+ this.assertSuccessCriteria(step);
132
134
 
133
135
  // locate which operation the step invokes — reduced to a (document, JSON
134
136
  // Pointer) locator that the operation executor extracts, normalizes,
@@ -224,9 +226,48 @@ class StepExecutor {
224
226
  });
225
227
  }
226
228
 
229
+ /**
230
+ * Throws {@link ResolverError} unless the step's `successCriteria` is absent
231
+ * or a list of Criterion Objects: `malformed-criteria` for one that is
232
+ * present but not a list, `malformed-criterion` for an entry that is not a
233
+ * Criterion Object. Skipping an unreadable entry would turn the author's
234
+ * assertion into a vacuous pass, so it is an authoring error, as an action's
235
+ * unreadable `criteria` is in {@link ActionResolver}.
236
+ *
237
+ * Called before the step's request is sent, so no live side effect precedes
238
+ * the error. Public because the workflow executor checks every step of a
239
+ * workflow with it before any of them runs, a step targeting a `workflowId`
240
+ * included. It passes `workflowId`, which the error then carries: step ids
241
+ * are only unique within a workflow.
242
+ */
243
+ assertSuccessCriteria(step, workflowId) {
244
+ const criteria = step.successCriteria;
245
+ if (criteria === undefined) return;
246
+ const scope = {
247
+ ...(workflowId === undefined ? {} : {
248
+ workflowId
249
+ }),
250
+ stepId: toValue(step.stepId),
251
+ target: 'successCriteria'
252
+ };
253
+ if (!isArrayElement(criteria)) {
254
+ throw new ResolverError("A step's `successCriteria` is present but is not a list", {
255
+ ...scope,
256
+ reason: 'malformed-criteria'
257
+ });
258
+ }
259
+ if (![...criteria].every(isCriterionElement)) {
260
+ throw new ResolverError("A step's `successCriteria` contains an entry that is not a Criterion Object", {
261
+ ...scope,
262
+ reason: 'malformed-criterion'
263
+ });
264
+ }
265
+ }
266
+
227
267
  /**
228
268
  * Evaluates the step's `successCriteria`; all criteria must pass. A step with
229
- * no criteria succeeds on a received response.
269
+ * no criteria succeeds on a received response. An unreadable
270
+ * `successCriteria` throws, see {@link StepExecutor.assertSuccessCriteria}.
230
271
  *
231
272
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
232
273
  * targeting a `workflowId` is run by the workflow executor and never reaches
@@ -234,15 +275,12 @@ class StepExecutor {
234
275
  * assertion about the step and must be evaluated somewhere.
235
276
  */
236
277
  evaluateCriteria(step, context) {
278
+ this.assertSuccessCriteria(step);
237
279
  const criteria = step.successCriteria;
238
- // no (or non-array) successCriteria: the step succeeds on a received response.
239
- if (!isArrayElement(criteria)) return true;
240
- for (const criterion of criteria) {
241
- if (!isCriterionElement(criterion)) continue;
242
- const met = this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression));
243
- if (!met) return false;
244
- }
245
- return true;
280
+ if (criteria === undefined) return true;
281
+
282
+ // assertSuccessCriteria above guarantees every entry is a Criterion Object
283
+ return [...criteria].every(criterion => this.#criterionEvaluator.evaluate(criterion, expression => this.#evaluate(context, expression)));
246
284
  }
247
285
 
248
286
  /**
@@ -297,6 +297,10 @@ class WorkflowExecutor {
297
297
  // at `#result` where it used to be resolved, means the throw lands before
298
298
  // the first request rather than after the last.
299
299
  this.#validateWorkflowOutputsShape(workflow, workflowId);
300
+ // and to every step's `successCriteria`, a step calling a workflow
301
+ // included: an unreadable assertion found at step three must not cost the
302
+ // run the requests of steps one and two.
303
+ for (const step of steps) this.#stepExecutor.assertSuccessCriteria(step, workflowId);
300
304
  const dependencies = await this.#runDependencies(workflow, invocation, scope);
301
305
  if (dependencies.some(dependency => this.#settledFailed(dependency))) {
302
306
  // a declared prerequisite did not complete, so this workflow cannot be
@@ -283,6 +283,10 @@ class WorkflowExecutor {
283
283
  // at `#result` where it used to be resolved, means the throw lands before
284
284
  // the first request rather than after the last.
285
285
  this.#validateWorkflowOutputsShape(workflow, workflowId);
286
+ // and to every step's `successCriteria`, a step calling a workflow
287
+ // included: an unreadable assertion found at step three must not cost the
288
+ // run the requests of steps one and two.
289
+ for (const step of steps) this.#stepExecutor.assertSuccessCriteria(step, workflowId);
286
290
  const dependencies = await this.#runDependencies(workflow, invocation, scope);
287
291
  if (dependencies.some(dependency => this.#settledFailed(dependency))) {
288
292
  // a declared prerequisite did not complete, so this workflow cannot be
@@ -159,23 +159,32 @@ class ArazzoWorkflowNormalizer {
159
159
  }
160
160
 
161
161
  /**
162
- * Inherits the workflow's `successActions` / `failureActions` into each step
163
- * that declares no `onSuccess` / `onFailure` of its own.
162
+ * Merges the workflow's `successActions` / `failureActions` into each step's
163
+ * `onSuccess` / `onFailure`.
164
164
  *
165
- * Per Arazzo 1.0.1 both lists are "applicable for all steps described under
166
- * this workflow", and a step's own list **overrides** the corresponding
167
- * workflow one — wholesale, with no per-action merge, which is where this
168
- * differs from the parameters above. Success and failure fall back
169
- * independently: a step may override only its `onFailure` and still inherit
170
- * the workflow's `successActions`.
165
+ * Per Arazzo both lists are "applicable for all steps described under this
166
+ * workflow" and "can be overridden at the step level but cannot be removed
167
+ * there". The required action `name` is what makes both halves hold: a
168
+ * step's action replaces the workflow action of the same `name`, and every
169
+ * workflow action the step does not name still applies. The effective list is
170
+ * the step's own actions in order, then the workflow actions it did not
171
+ * override, in theirs — order matters, as selection is first-match-wins on
172
+ * `criteria`. So a step with no list, or with `[]`, gets every workflow
173
+ * action. The step's own list is kept exactly as written, even a duplicate
174
+ * `name` in it, which the validator reports: inheritance only adds to it.
175
+ * Within the workflow's list the first action with a given `name` wins.
176
+ * Success and failure merge independently.
171
177
  *
172
- * Presence is tested with `hasKey`, not truthiness: a step declaring an empty
173
- * list (`onSuccess: []`) has *overridden* the default with an empty set of
174
- * actions, not asked for one — it is skipped, as is a step whose `onSuccess`
175
- * is present but not a list, for the same reason the parameters pass leaves a
176
- * malformed list alone. Testing the key is also what makes the pass
177
- * idempotent: after the first run the step has it, so a workflow reached again
178
- * through the registry cache is skipped.
178
+ * An entry without a readable `name` (an unresolved Reusable Object, a
179
+ * scalar someone put in the list) neither overrides nor is overridden: it
180
+ * keeps its place, and is added only if that same element is not already in
181
+ * the list. A step whose `onSuccess` is present but not a list is left
182
+ * alone, for the same reason the parameters pass leaves a malformed list
183
+ * alone — overwriting it would erase what makes the document invalid.
184
+ *
185
+ * The pass is idempotent, so a workflow reached again through the registry
186
+ * cache is merged into once: on a second pass every inherited action is
187
+ * already in the step's list, under its own `name` or as the same element.
179
188
  *
180
189
  * Unlike parameters there is no step-kind filter — an action names a
181
190
  * transition within the workflow, which is as meaningful for a step targeting
@@ -202,13 +211,34 @@ class ArazzoWorkflowNormalizer {
202
211
  if (!inheritSuccess && !inheritFailure) return;
203
212
  for (const step of workflow.steps) {
204
213
  if (!(0, _apidomNsArazzo.isStepElement)(step)) continue;
205
- if (inheritSuccess && !step.hasKey('onSuccess')) {
206
- step.onSuccess = new _apidomNsArazzo.StepOnSuccessElement([...successActions]);
214
+ if (inheritSuccess && (!step.hasKey('onSuccess') || (0, _apidomDatamodel.isArrayElement)(step.onSuccess))) {
215
+ step.onSuccess = new _apidomNsArazzo.StepOnSuccessElement(ArazzoWorkflowNormalizer.#mergeActions([...(step.onSuccess ?? [])], [...successActions]));
216
+ }
217
+ if (inheritFailure && (!step.hasKey('onFailure') || (0, _apidomDatamodel.isArrayElement)(step.onFailure))) {
218
+ step.onFailure = new _apidomNsArazzo.StepOnFailureElement(ArazzoWorkflowNormalizer.#mergeActions([...(step.onFailure ?? [])], [...failureActions]));
207
219
  }
208
- if (inheritFailure && !step.hasKey('onFailure')) {
209
- step.onFailure = new _apidomNsArazzo.StepOnFailureElement([...failureActions]);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * The step's actions as written, followed by each workflow action whose
225
+ * `name` neither the step nor an earlier workflow action carries; a nameless
226
+ * workflow entry is added unless that same element is already present.
227
+ */
228
+ static #mergeActions(own, inherited) {
229
+ const nameOf = action => (0, _apidomDatamodel.isObjectElement)(action) ? (0, _apidomCore.toValue)(action.get('name')) : undefined;
230
+ const names = new Set(own.map(nameOf));
231
+ const merged = [...own];
232
+ for (const action of inherited) {
233
+ const name = nameOf(action);
234
+ if (typeof name !== 'string') {
235
+ if (!merged.includes(action)) merged.push(action);
236
+ } else if (!names.has(name)) {
237
+ names.add(name);
238
+ merged.push(action);
210
239
  }
211
240
  }
241
+ return merged;
212
242
  }
213
243
  }
214
244
  var _default = exports.default = ArazzoWorkflowNormalizer;
@@ -1,7 +1,7 @@
1
1
  import { uniqWith } from 'ramda';
2
2
  import { isParameterElement, isStepElement, StepParametersElement, StepOnSuccessElement, StepOnFailureElement } from '@speclynx/apidom-ns-arazzo-1';
3
3
  import { toValue } from '@speclynx/apidom-core';
4
- import { isArrayElement, isStringElement } from '@speclynx/apidom-datamodel';
4
+ import { isArrayElement, isObjectElement, isStringElement } from '@speclynx/apidom-datamodel';
5
5
  import { dereferenceArazzoElement, defaultDereferenceArazzoOptions } from '@usearazzo/resolver';
6
6
  import { mergeOptions } from '@speclynx/apidom-reference/configuration/empty';
7
7
  import ParameterIdentity from "../document/ParameterIdentity.mjs";
@@ -151,23 +151,32 @@ class ArazzoWorkflowNormalizer {
151
151
  }
152
152
 
153
153
  /**
154
- * Inherits the workflow's `successActions` / `failureActions` into each step
155
- * that declares no `onSuccess` / `onFailure` of its own.
154
+ * Merges the workflow's `successActions` / `failureActions` into each step's
155
+ * `onSuccess` / `onFailure`.
156
156
  *
157
- * Per Arazzo 1.0.1 both lists are "applicable for all steps described under
158
- * this workflow", and a step's own list **overrides** the corresponding
159
- * workflow one — wholesale, with no per-action merge, which is where this
160
- * differs from the parameters above. Success and failure fall back
161
- * independently: a step may override only its `onFailure` and still inherit
162
- * the workflow's `successActions`.
157
+ * Per Arazzo both lists are "applicable for all steps described under this
158
+ * workflow" and "can be overridden at the step level but cannot be removed
159
+ * there". The required action `name` is what makes both halves hold: a
160
+ * step's action replaces the workflow action of the same `name`, and every
161
+ * workflow action the step does not name still applies. The effective list is
162
+ * the step's own actions in order, then the workflow actions it did not
163
+ * override, in theirs — order matters, as selection is first-match-wins on
164
+ * `criteria`. So a step with no list, or with `[]`, gets every workflow
165
+ * action. The step's own list is kept exactly as written, even a duplicate
166
+ * `name` in it, which the validator reports: inheritance only adds to it.
167
+ * Within the workflow's list the first action with a given `name` wins.
168
+ * Success and failure merge independently.
163
169
  *
164
- * Presence is tested with `hasKey`, not truthiness: a step declaring an empty
165
- * list (`onSuccess: []`) has *overridden* the default with an empty set of
166
- * actions, not asked for one — it is skipped, as is a step whose `onSuccess`
167
- * is present but not a list, for the same reason the parameters pass leaves a
168
- * malformed list alone. Testing the key is also what makes the pass
169
- * idempotent: after the first run the step has it, so a workflow reached again
170
- * through the registry cache is skipped.
170
+ * An entry without a readable `name` (an unresolved Reusable Object, a
171
+ * scalar someone put in the list) neither overrides nor is overridden: it
172
+ * keeps its place, and is added only if that same element is not already in
173
+ * the list. A step whose `onSuccess` is present but not a list is left
174
+ * alone, for the same reason the parameters pass leaves a malformed list
175
+ * alone — overwriting it would erase what makes the document invalid.
176
+ *
177
+ * The pass is idempotent, so a workflow reached again through the registry
178
+ * cache is merged into once: on a second pass every inherited action is
179
+ * already in the step's list, under its own `name` or as the same element.
171
180
  *
172
181
  * Unlike parameters there is no step-kind filter — an action names a
173
182
  * transition within the workflow, which is as meaningful for a step targeting
@@ -194,13 +203,34 @@ class ArazzoWorkflowNormalizer {
194
203
  if (!inheritSuccess && !inheritFailure) return;
195
204
  for (const step of workflow.steps) {
196
205
  if (!isStepElement(step)) continue;
197
- if (inheritSuccess && !step.hasKey('onSuccess')) {
198
- step.onSuccess = new StepOnSuccessElement([...successActions]);
206
+ if (inheritSuccess && (!step.hasKey('onSuccess') || isArrayElement(step.onSuccess))) {
207
+ step.onSuccess = new StepOnSuccessElement(ArazzoWorkflowNormalizer.#mergeActions([...(step.onSuccess ?? [])], [...successActions]));
208
+ }
209
+ if (inheritFailure && (!step.hasKey('onFailure') || isArrayElement(step.onFailure))) {
210
+ step.onFailure = new StepOnFailureElement(ArazzoWorkflowNormalizer.#mergeActions([...(step.onFailure ?? [])], [...failureActions]));
199
211
  }
200
- if (inheritFailure && !step.hasKey('onFailure')) {
201
- step.onFailure = new StepOnFailureElement([...failureActions]);
212
+ }
213
+ }
214
+
215
+ /**
216
+ * The step's actions as written, followed by each workflow action whose
217
+ * `name` neither the step nor an earlier workflow action carries; a nameless
218
+ * workflow entry is added unless that same element is already present.
219
+ */
220
+ static #mergeActions(own, inherited) {
221
+ const nameOf = action => isObjectElement(action) ? toValue(action.get('name')) : undefined;
222
+ const names = new Set(own.map(nameOf));
223
+ const merged = [...own];
224
+ for (const action of inherited) {
225
+ const name = nameOf(action);
226
+ if (typeof name !== 'string') {
227
+ if (!merged.includes(action)) merged.push(action);
228
+ } else if (!names.has(name)) {
229
+ names.add(name);
230
+ merged.push(action);
202
231
  }
203
232
  }
233
+ return merged;
204
234
  }
205
235
  }
206
236
  export default ArazzoWorkflowNormalizer;
package/types/runner.d.ts CHANGED
@@ -1889,9 +1889,25 @@ export declare class StepExecutor {
1889
1889
  * — rather than as the withdrawal it is.
1890
1890
  */
1891
1891
  execute(step: StepElement, state: ContextSource, executeOptions?: Record<string, unknown>): Promise<StepExecutionResult>;
1892
+ /**
1893
+ * Throws {@link ResolverError} unless the step's `successCriteria` is absent
1894
+ * or a list of Criterion Objects: `malformed-criteria` for one that is
1895
+ * present but not a list, `malformed-criterion` for an entry that is not a
1896
+ * Criterion Object. Skipping an unreadable entry would turn the author's
1897
+ * assertion into a vacuous pass, so it is an authoring error, as an action's
1898
+ * unreadable `criteria` is in {@link ActionResolver}.
1899
+ *
1900
+ * Called before the step's request is sent, so no live side effect precedes
1901
+ * the error. Public because the workflow executor checks every step of a
1902
+ * workflow with it before any of them runs, a step targeting a `workflowId`
1903
+ * included. It passes `workflowId`, which the error then carries: step ids
1904
+ * are only unique within a workflow.
1905
+ */
1906
+ assertSuccessCriteria(step: StepElement, workflowId?: string): void;
1892
1907
  /**
1893
1908
  * Evaluates the step's `successCriteria`; all criteria must pass. A step with
1894
- * no criteria succeeds on a received response.
1909
+ * no criteria succeeds on a received response. An unreadable
1910
+ * `successCriteria` throws, see {@link StepExecutor.assertSuccessCriteria}.
1895
1911
  *
1896
1912
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
1897
1913
  * targeting a `workflowId` is run by the workflow executor and never reaches