@usearazzo/runner 1.0.1-alpha.22 → 1.0.1-alpha.24

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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/arazzo-runner.browser.js +473 -293
  4. package/dist/arazzo-runner.browser.min.js +1 -1
  5. package/package.json +4 -4
  6. package/src/action/ActionResolver.cjs +4 -4
  7. package/src/action/ActionResolver.mjs +4 -4
  8. package/src/criterion/CriterionEvaluator.cjs +8 -9
  9. package/src/criterion/CriterionEvaluator.mjs +8 -9
  10. package/src/criterion/JSONPathCriterionEvaluator.cjs +1 -2
  11. package/src/criterion/JSONPathCriterionEvaluator.mjs +1 -2
  12. package/src/criterion/RegexCriterionEvaluator.cjs +1 -2
  13. package/src/criterion/RegexCriterionEvaluator.mjs +1 -2
  14. package/src/criterion/SimpleCriterionEvaluator.cjs +1 -2
  15. package/src/criterion/SimpleCriterionEvaluator.mjs +1 -2
  16. package/src/criterion/XPathCriterionEvaluator.cjs +4 -5
  17. package/src/criterion/XPathCriterionEvaluator.mjs +4 -5
  18. package/src/errors/CriterionError.cjs +52 -2
  19. package/src/errors/CriterionError.mjs +53 -2
  20. package/src/errors/ExecutionError.cjs +73 -2
  21. package/src/errors/ExecutionError.mjs +74 -2
  22. package/src/errors/ResolverError.cjs +53 -2
  23. package/src/errors/ResolverError.mjs +54 -2
  24. package/src/errors/RuntimeExpressionError.cjs +32 -2
  25. package/src/errors/RuntimeExpressionError.mjs +33 -2
  26. package/src/executor/ArazzoWorkflowLocatorNormalizer.cjs +4 -4
  27. package/src/executor/ArazzoWorkflowLocatorNormalizer.mjs +4 -4
  28. package/src/executor/OpenAPIOperationLocatorNormalizer.cjs +79 -47
  29. package/src/executor/OpenAPIOperationLocatorNormalizer.mjs +80 -48
  30. package/src/executor/StepExecutor.cjs +42 -32
  31. package/src/executor/StepExecutor.mjs +42 -32
  32. package/src/executor/StepRetryRunner.cjs +1 -1
  33. package/src/executor/StepRetryRunner.mjs +1 -1
  34. package/src/executor/StepTransitionInterpreter.cjs +3 -3
  35. package/src/executor/StepTransitionInterpreter.mjs +3 -3
  36. package/src/executor/WorkflowCallStack.cjs +2 -2
  37. package/src/executor/WorkflowCallStack.mjs +2 -2
  38. package/src/executor/WorkflowExecutor.cjs +17 -16
  39. package/src/executor/WorkflowExecutor.mjs +17 -16
  40. package/src/executor/abort.cjs +3 -3
  41. package/src/executor/abort.mjs +3 -3
  42. package/src/expression/RuntimeExpressionEvaluator.cjs +5 -5
  43. package/src/expression/RuntimeExpressionEvaluator.mjs +5 -5
  44. package/src/index.cjs +0 -7
  45. package/src/index.mjs +0 -1
  46. package/src/resolver/OpenAPIOperationParameterResolver.cjs +6 -6
  47. package/src/resolver/OpenAPIOperationParameterResolver.mjs +6 -6
  48. package/src/resolver/OutputResolver.cjs +3 -3
  49. package/src/resolver/OutputResolver.mjs +3 -3
  50. package/src/resolver/RequestBodyResolver.cjs +6 -6
  51. package/src/resolver/RequestBodyResolver.mjs +6 -6
  52. package/src/resolver/WorkflowParameterResolver.cjs +1 -1
  53. package/src/resolver/WorkflowParameterResolver.mjs +1 -1
  54. package/types/runner.d.ts +236 -60
  55. package/src/criterion/CriterionErrorReason.cjs +0 -51
  56. package/src/criterion/CriterionErrorReason.mjs +0 -46
@@ -1,9 +1,10 @@
1
- import { test as isRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
1
+ import { extract, test as isRuntimeExpression } from '@swaggerexpert/arazzo-runtime-expression';
2
2
  import { URIFragmentIdentifier, testJSONPointer } from '@swaggerexpert/json-pointer';
3
3
  import { url } from '@speclynx/apidom-reference/configuration/empty';
4
4
  import OpenAPIDocument from "../document/OpenAPIDocument.mjs";
5
5
  import RuntimeExpressionEvaluator from "../expression/RuntimeExpressionEvaluator.mjs";
6
- import ExtractionError from "../errors/ExtractionError.mjs";
6
+ import OpenAPIOperationExtractor from "../extractor/OpenAPIOperationExtractor.mjs";
7
+ import ExecutionError from "../errors/ExecutionError.mjs";
7
8
  import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNormalizer.mjs";
8
9
  /**
9
10
  * A canonical OpenAPI operation locator: the OpenAPI document that owns the
@@ -26,7 +27,7 @@ import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNorm
26
27
  * `$sourceDescriptions.{name}.{operationId}`. Per the Arazzo Specification, a
27
28
  * plain name is only valid when a single (non-arazzo) source description is
28
29
  * defined; multiple sources require the expression form. A plain name is
29
- * therefore resolved against the first source description that has it.
30
+ * therefore looked up in the first OpenAPI source description only.
30
31
  * - `operationPath` embeds a `{$sourceDescriptions.{name}.url}` runtime
31
32
  * expression followed by a JSON Pointer to the operation.
32
33
  *
@@ -34,21 +35,31 @@ import SourceDescriptionsLocatorNormalizer from "./SourceDescriptionsLocatorNorm
34
35
  * or wrong); a source is classified by what it actually parses to. The
35
36
  * normalizer acquires the referenced source documents from the registry on
36
37
  * demand.
38
+ *
39
+ * A reference that cannot be resolved throws {@link ExecutionError}, attributed
40
+ * to the referencing step when a {@link StepScope} is given:
41
+ * `operation-not-found` when no operation answers it,
42
+ * `source-description-not-found` for a source name the document does not
43
+ * define, `source-description-not-openapi` for a source that is not an OpenAPI
44
+ * document, and `invalid-operation-reference` for a reference malformed as
45
+ * written. Every returned locator names an existing operation.
37
46
  * @public
38
47
  */
39
48
  class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormalizer {
49
+ #extractor = new OpenAPIOperationExtractor();
50
+
40
51
  /**
41
52
  * Normalizes a plain or expression `operationId` to a canonical locator.
42
53
  *
43
54
  * A whole runtime expression (`$sourceDescriptions.{name}.{operationId}`) is
44
- * resolved against its named source; a plain name is resolved against the
45
- * first source description that has it.
55
+ * resolved against its named source; a plain name is looked up in the first
56
+ * OpenAPI source description only.
46
57
  */
47
- async normalizeOperationId(operationId, document) {
58
+ async normalizeOperationId(operationId, document, scope) {
48
59
  if (isRuntimeExpression(operationId)) {
49
- return this.#resolveSourced(operationId, document);
60
+ return this.#resolveSourced(operationId, document, scope);
50
61
  }
51
- return this.#resolvePlainOperationId(operationId, document);
62
+ return this.#resolvePlainOperationId(operationId, document, scope);
52
63
  }
53
64
 
54
65
  /**
@@ -64,10 +75,21 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
64
75
  * The runner only needs a URL and a JSON Pointer, so it does not enforce that
65
76
  * the source description is identified by a runtime expression rather than a
66
77
  * literal URL (a literal URL interpolates to itself and still resolves) — that
67
- * is a conformance concern for the validator. It fails only when the URL does
68
- * not resolve to an OpenAPI source or the JSON Pointer is invalid.
78
+ * is a conformance concern for the validator. A source name the document does
79
+ * not define is detected before interpolation, so it is reported as such
80
+ * rather than as the empty URL it would interpolate to; the JSON Pointer must
81
+ * locate an operation in the acquired OpenAPI document.
69
82
  */
70
- async normalizeOperationPath(operationPath, document) {
83
+ async normalizeOperationPath(operationPath, document, scope) {
84
+ for (const expression of extract(operationPath) ?? []) {
85
+ const parsed = this.parseSourceDescriptionsExpression(expression);
86
+ if (parsed !== undefined && document.resolveSourceDescriptionURI(parsed.sourceName) === undefined) {
87
+ throw new ExecutionError(`operationPath "${operationPath}" names source description "${parsed.sourceName}", which the Arazzo document at "${document.uri}" does not define`, {
88
+ ...scope,
89
+ reason: ExecutionError.SourceDescriptionNotFound
90
+ });
91
+ }
92
+ }
71
93
  const evaluator = new RuntimeExpressionEvaluator({}, {
72
94
  strict: false,
73
95
  document,
@@ -76,23 +98,35 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
76
98
  const uriReference = evaluator.interpolate(operationPath);
77
99
  const documentURI = url.sanitize(url.stripHash(uriReference));
78
100
  if (documentURI === '') {
79
- throw new ExtractionError(`operationPath "${operationPath}" does not resolve to a source description URL`, {
80
- pointer: operationPath,
81
- uri: document.uri
101
+ throw new ExecutionError(`operationPath "${operationPath}" does not resolve to a source description URL`, {
102
+ ...scope,
103
+ reason: ExecutionError.InvalidOperationReference
82
104
  });
83
105
  }
84
106
  const jsonPointer = URIFragmentIdentifier.fromURIReference(uriReference);
85
107
  if (!testJSONPointer(jsonPointer)) {
86
- throw new ExtractionError(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
87
- pointer: operationPath,
88
- uri: document.uri
108
+ throw new ExecutionError(`operationPath "${operationPath}" has an invalid JSON Pointer`, {
109
+ ...scope,
110
+ reason: ExecutionError.InvalidOperationReference
89
111
  });
90
112
  }
91
113
  const source = await this.registry.acquire(documentURI);
92
114
  if (!OpenAPIDocument.is(source)) {
93
- throw new ExtractionError(`operationPath "${operationPath}" does not reference an OpenAPI document`, {
94
- pointer: operationPath,
95
- uri: documentURI
115
+ throw new ExecutionError(`operationPath "${operationPath}" references "${documentURI}", which is not an OpenAPI document`, {
116
+ ...scope,
117
+ reason: ExecutionError.SourceDescriptionNotOpenAPI
118
+ });
119
+ }
120
+
121
+ // the extractor owns locating an operation by pointer; its failure here
122
+ // means the pointer names no operation in this document.
123
+ try {
124
+ this.#extractor.extractByPointer(source, jsonPointer);
125
+ } catch (error) {
126
+ throw new ExecutionError(`operationPath "${operationPath}" does not locate an operation in the OpenAPI document at "${source.uri}"`, {
127
+ cause: error,
128
+ ...scope,
129
+ reason: ExecutionError.OperationNotFound
96
130
  });
97
131
  }
98
132
  return {
@@ -100,31 +134,24 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
100
134
  jsonPointer
101
135
  };
102
136
  }
103
-
104
- /**
105
- * Parses a `$sourceDescriptions.{name}.{reference}` expression, asserting
106
- * it is a source-descriptions expression.
107
- */
108
- #parseSourceDescriptionsExpression(expression) {
137
+ async #resolveSourced(expression, document, scope) {
109
138
  const parsed = this.parseSourceDescriptionsExpression(expression);
110
139
  if (parsed === undefined) {
111
- throw new ExtractionError(`Unsupported operationId expression "${expression}"`, {
112
- operationId: expression
140
+ throw new ExecutionError(`operationId "${expression}" is not a $sourceDescriptions.{name}.{operationId} expression`, {
141
+ ...scope,
142
+ reason: ExecutionError.InvalidOperationReference
113
143
  });
114
144
  }
115
- return parsed;
116
- }
117
- async #resolveSourced(expression, document) {
118
145
  const {
119
146
  sourceName,
120
147
  reference: operationId
121
- } = this.#parseSourceDescriptionsExpression(expression);
122
- const openapiDocument = await this.#sourceDocument(sourceName, document);
148
+ } = parsed;
149
+ const openapiDocument = await this.#sourceDocument(sourceName, document, scope);
123
150
  const jsonPointer = openapiDocument.operationIndex.get(operationId);
124
151
  if (jsonPointer === undefined) {
125
- throw new ExtractionError(`Operation "${operationId}" not found in source description "${sourceName}"`, {
126
- operationId,
127
- uri: openapiDocument.uri
152
+ throw new ExecutionError(`Operation "${operationId}" not found in source description "${sourceName}" at "${openapiDocument.uri}"`, {
153
+ ...scope,
154
+ reason: ExecutionError.OperationNotFound
128
155
  });
129
156
  }
130
157
  return {
@@ -145,15 +172,15 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
145
172
  * Non-OpenAPI (arazzo) sources are skipped. Sources are acquired one at a time
146
173
  * until the first OpenAPI source is reached.
147
174
  */
148
- async #resolvePlainOperationId(operationId, document) {
175
+ async #resolvePlainOperationId(operationId, document, scope) {
149
176
  for (const uri of document.sourceDescriptionURIs()) {
150
177
  const source = await this.registry.acquire(uri);
151
178
  if (!OpenAPIDocument.is(source)) continue;
152
179
  const jsonPointer = source.operationIndex.get(operationId);
153
180
  if (jsonPointer === undefined) {
154
- throw new ExtractionError(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
155
- operationId,
156
- uri: source.uri
181
+ throw new ExecutionError(`Operation "${operationId}" not found in source description at "${source.uri}"`, {
182
+ ...scope,
183
+ reason: ExecutionError.OperationNotFound
157
184
  });
158
185
  }
159
186
  return {
@@ -161,26 +188,31 @@ class OpenAPIOperationLocatorNormalizer extends SourceDescriptionsLocatorNormali
161
188
  jsonPointer
162
189
  };
163
190
  }
164
- throw new ExtractionError(`Operation "${operationId}" has no OpenAPI source description`, {
165
- operationId,
166
- uri: document.uri
191
+
192
+ // no source was named, so this is not a missing source description: there
193
+ // is simply no OpenAPI document for the operation to be in.
194
+ throw new ExecutionError(`Operation "${operationId}" not found: the Arazzo document at "${document.uri}" has no OpenAPI source description`, {
195
+ ...scope,
196
+ reason: ExecutionError.OperationNotFound
167
197
  });
168
198
  }
169
199
 
170
200
  /**
171
201
  * Acquires a source description by name and asserts it is an OpenAPI document.
172
202
  */
173
- async #sourceDocument(sourceName, document) {
203
+ async #sourceDocument(sourceName, document, scope) {
174
204
  const uri = document.resolveSourceDescriptionURI(sourceName);
175
205
  if (uri === undefined) {
176
- throw new ExtractionError(`Source description "${sourceName}" not found`, {
177
- uri: document.uri
206
+ throw new ExecutionError(`Source description "${sourceName}" not found in the Arazzo document at "${document.uri}"`, {
207
+ ...scope,
208
+ reason: ExecutionError.SourceDescriptionNotFound
178
209
  });
179
210
  }
180
211
  const source = await this.registry.acquire(uri);
181
212
  if (!OpenAPIDocument.is(source)) {
182
- throw new ExtractionError(`Source description "${sourceName}" is not an OpenAPI document`, {
183
- uri
213
+ throw new ExecutionError(`Source description "${sourceName}" at "${uri}" is not an OpenAPI document`, {
214
+ ...scope,
215
+ reason: ExecutionError.SourceDescriptionNotOpenAPI
184
216
  });
185
217
  }
186
218
  return source;
@@ -130,42 +130,49 @@ class StepExecutor {
130
130
  * take precedence over it.
131
131
  *
132
132
  * A `signal` in that bag is honored here and not only at the transport: an
133
- * already-aborted signal throws `reason: 'aborted'` instead of issuing a
133
+ * already-aborted signal throws with reason `ExecutionError.Aborted` instead of issuing a
134
134
  * request nobody is waiting for, whose wire-level cancellation would surface
135
135
  * as a transport {@link ClientError} — a request that failed on its own terms
136
136
  * — rather than as the withdrawal it is.
137
+ *
138
+ * `workflowId` names the workflow the step belongs to; every error raised
139
+ * here then carries it alongside the `stepId`, since step ids are only
140
+ * unique within a workflow. The workflow executor always passes it.
137
141
  */
138
- async execute(step, state, executeOptions = {}) {
142
+ async execute(step, state, executeOptions = {}, workflowId) {
139
143
  const stepId = (0, _apidomCore.toValue)(step.stepId);
144
+ const scope = {
145
+ ...(workflowId === undefined ? {} : {
146
+ workflowId
147
+ }),
148
+ stepId
149
+ };
140
150
 
141
151
  // a step's target fields are mutually exclusive; declaring more than one is
142
152
  // malformed and has no defined resolution.
143
153
  const declaredTargets = STEP_TARGET_FIELDS.filter(field => (0, _apidomDatamodel.isStringElement)(step[field]));
144
154
  if (declaredTargets.length > 1) {
145
155
  throw new _ExecutionError.default(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
146
- stepId,
147
- reason: 'ambiguous-target'
156
+ ...scope,
157
+ reason: _ExecutionError.default.AmbiguousTarget
148
158
  });
149
159
  }
150
160
  if ((0, _apidomDatamodel.isStringElement)(step.workflowId)) {
151
161
  throw new _ExecutionError.default(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
152
- stepId,
153
- reason: 'workflow-step'
162
+ ...scope,
163
+ reason: _ExecutionError.default.WorkflowStep
154
164
  });
155
165
  }
156
- this.assertSuccessCriteria(step);
166
+ this.assertSuccessCriteria(step, workflowId);
157
167
 
158
168
  // locate which operation the step invokes — reduced to a (document, JSON
159
169
  // Pointer) locator that the operation executor extracts, normalizes,
160
170
  // assembles into a standalone doc, and runs.
161
- const locator = await this.#locateOperation(step, stepId);
171
+ const locator = await this.#locateOperation(step, scope);
162
172
 
163
173
  // resolve parameters and request body against the pre-request context.
164
174
  // keyed by '{in}.{name}' rather than bare name, so two parameters that
165
175
  // legally differ only in their location each reach their own place.
166
- const scope = {
167
- stepId
168
- };
169
176
  const preContext = state.toContext();
170
177
  const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
171
178
  const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
@@ -176,9 +183,7 @@ class StepExecutor {
176
183
  // live side effect. The signal stays in the bag, so a request already in
177
184
  // flight is aborted by the transport as before.
178
185
  const signal = (0, _abort.readAbortSignal)(executeOptions);
179
- (0, _abort.throwIfAborted)(signal, {
180
- stepId
181
- });
186
+ (0, _abort.throwIfAborted)(signal, scope);
182
187
 
183
188
  // the Arazzo-derived parameters and request body are spread after the opaque
184
189
  // executeOptions bag so they take precedence over it. the operation executor
@@ -197,22 +202,20 @@ class StepExecutor {
197
202
  // cancelled, it did not: reporting it as the abort keeps one error shape
198
203
  // whether the run was withdrawn between two steps or mid-flight, and keeps
199
204
  // the whole feature from reading differently per transport.
200
- (0, _abort.throwIfAborted)(signal, {
201
- stepId
202
- });
205
+ (0, _abort.throwIfAborted)(signal, scope);
203
206
  throw error;
204
207
  }
205
208
 
206
209
  // evaluate criteria, outputs, and the next action against the post-request
207
210
  // context (with $request / $url / $method and $response / $statusCode).
208
211
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
209
- const successCriteria = this.evaluateCriteria(step, postContext);
212
+ const successCriteria = this.evaluateCriteria(step, postContext, workflowId);
210
213
  const successful = successCriteria.every(criterion => criterion.met);
211
214
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
212
215
  const {
213
216
  matchedActions,
214
217
  actionCriteria
215
- } = this.selectActions(step, successful, postContext);
218
+ } = this.selectActions(step, successful, postContext, workflowId);
216
219
  return {
217
220
  stepId,
218
221
  response,
@@ -242,16 +245,16 @@ class StepExecutor {
242
245
  requestContentType: requestBody.contentType
243
246
  };
244
247
  }
245
- async #locateOperation(step, stepId) {
248
+ async #locateOperation(step, scope) {
246
249
  if ((0, _apidomDatamodel.isStringElement)(step.operationId)) {
247
- return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document);
250
+ return this.#locatorNormalizer.normalizeOperationId((0, _apidomCore.toValue)(step.operationId), this.#document, scope);
248
251
  }
249
252
  if ((0, _apidomDatamodel.isStringElement)(step.operationPath)) {
250
- return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document);
253
+ return this.#locatorNormalizer.normalizeOperationPath((0, _apidomCore.toValue)(step.operationPath), this.#document, scope);
251
254
  }
252
- throw new _ExecutionError.default(`Step "${stepId}" has no operationId or operationPath`, {
253
- stepId,
254
- reason: 'missing-operation'
255
+ throw new _ExecutionError.default(`Step "${scope.stepId}" has no operationId or operationPath`, {
256
+ ...scope,
257
+ reason: _ExecutionError.default.MissingOperation
255
258
  });
256
259
  }
257
260
 
@@ -282,13 +285,13 @@ class StepExecutor {
282
285
  if (!(0, _apidomDatamodel.isArrayElement)(criteria)) {
283
286
  throw new _ResolverError.default("A step's `successCriteria` is present but is not a list", {
284
287
  ...scope,
285
- reason: 'malformed-criteria'
288
+ reason: _ResolverError.default.MalformedCriteria
286
289
  });
287
290
  }
288
291
  if (![...criteria].every(_apidomNsArazzo.isCriterionElement)) {
289
292
  throw new _ResolverError.default("A step's `successCriteria` contains an entry that is not a Criterion Object", {
290
293
  ...scope,
291
- reason: 'malformed-criterion'
294
+ reason: _ResolverError.default.MalformedCriterion
292
295
  });
293
296
  }
294
297
  }
@@ -304,10 +307,12 @@ class StepExecutor {
304
307
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
305
308
  * targeting a `workflowId` is run by the workflow executor and never reaches
306
309
  * {@link StepExecutor.execute}, but its criteria are still the author's
307
- * assertion about the step and must be evaluated somewhere.
310
+ * assertion about the step and must be evaluated somewhere. `workflowId`
311
+ * names the step's workflow on a thrown error, as for
312
+ * {@link StepExecutor.execute}.
308
313
  */
309
- evaluateCriteria(step, context) {
310
- this.assertSuccessCriteria(step);
314
+ evaluateCriteria(step, context, workflowId) {
315
+ this.assertSuccessCriteria(step, workflowId);
311
316
  const criteria = step.successCriteria;
312
317
  if (criteria === undefined) return [];
313
318
 
@@ -335,8 +340,10 @@ class StepExecutor {
335
340
  * it runs itself and which therefore never reaches
336
341
  * {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
337
342
  * the expression context through this one method rather than wiring their own.
343
+ * `workflowId` names the step's workflow on a thrown error, as for
344
+ * {@link StepExecutor.execute}.
338
345
  */
339
- selectActions(step, successful, context) {
346
+ selectActions(step, successful, context, workflowId) {
340
347
  const actionCriteria = [];
341
348
  const isCriterionMet = (criterion, action) => {
342
349
  const record = {
@@ -347,6 +354,9 @@ class StepExecutor {
347
354
  return record.met;
348
355
  };
349
356
  const scope = {
357
+ ...(workflowId === undefined ? {} : {
358
+ workflowId
359
+ }),
350
360
  stepId: (0, _apidomCore.toValue)(step.stepId)
351
361
  };
352
362
  const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
@@ -384,7 +394,7 @@ class StepExecutor {
384
394
  // a step that calls a workflow made no HTTP call, so a condition on one
385
395
  // fails like any unevaluable condition; the diagnostic keeps the
386
396
  // authoring mistake visible.
387
- if (error instanceof _RuntimeExpressionError.default && error.reason === 'unavailable-expression') {
397
+ if (error instanceof _RuntimeExpressionError.default && error.reason === _RuntimeExpressionError.default.UnavailableExpression) {
388
398
  return {
389
399
  condition,
390
400
  met: false,
@@ -123,42 +123,49 @@ class StepExecutor {
123
123
  * take precedence over it.
124
124
  *
125
125
  * A `signal` in that bag is honored here and not only at the transport: an
126
- * already-aborted signal throws `reason: 'aborted'` instead of issuing a
126
+ * already-aborted signal throws with reason `ExecutionError.Aborted` instead of issuing a
127
127
  * request nobody is waiting for, whose wire-level cancellation would surface
128
128
  * as a transport {@link ClientError} — a request that failed on its own terms
129
129
  * — rather than as the withdrawal it is.
130
+ *
131
+ * `workflowId` names the workflow the step belongs to; every error raised
132
+ * here then carries it alongside the `stepId`, since step ids are only
133
+ * unique within a workflow. The workflow executor always passes it.
130
134
  */
131
- async execute(step, state, executeOptions = {}) {
135
+ async execute(step, state, executeOptions = {}, workflowId) {
132
136
  const stepId = toValue(step.stepId);
137
+ const scope = {
138
+ ...(workflowId === undefined ? {} : {
139
+ workflowId
140
+ }),
141
+ stepId
142
+ };
133
143
 
134
144
  // a step's target fields are mutually exclusive; declaring more than one is
135
145
  // malformed and has no defined resolution.
136
146
  const declaredTargets = STEP_TARGET_FIELDS.filter(field => isStringElement(step[field]));
137
147
  if (declaredTargets.length > 1) {
138
148
  throw new ExecutionError(`Step "${stepId}" declares more than one of ${STEP_TARGET_FIELDS.join(', ')} (mutually exclusive)`, {
139
- stepId,
140
- reason: 'ambiguous-target'
149
+ ...scope,
150
+ reason: ExecutionError.AmbiguousTarget
141
151
  });
142
152
  }
143
153
  if (isStringElement(step.workflowId)) {
144
154
  throw new ExecutionError(`Step "${stepId}" targets a workflowId; use the workflow executor`, {
145
- stepId,
146
- reason: 'workflow-step'
155
+ ...scope,
156
+ reason: ExecutionError.WorkflowStep
147
157
  });
148
158
  }
149
- this.assertSuccessCriteria(step);
159
+ this.assertSuccessCriteria(step, workflowId);
150
160
 
151
161
  // locate which operation the step invokes — reduced to a (document, JSON
152
162
  // Pointer) locator that the operation executor extracts, normalizes,
153
163
  // assembles into a standalone doc, and runs.
154
- const locator = await this.#locateOperation(step, stepId);
164
+ const locator = await this.#locateOperation(step, scope);
155
165
 
156
166
  // resolve parameters and request body against the pre-request context.
157
167
  // keyed by '{in}.{name}' rather than bare name, so two parameters that
158
168
  // legally differ only in their location each reach their own place.
159
- const scope = {
160
- stepId
161
- };
162
169
  const preContext = state.toContext();
163
170
  const parameters = this.#parameterResolver.resolve(step.parameters, expression => this.#evaluate(preContext, expression), scope);
164
171
  const requestBody = this.#requestBodyResolver.resolve(step.requestBody, expression => this.#evaluate(preContext, expression), scope);
@@ -169,9 +176,7 @@ class StepExecutor {
169
176
  // live side effect. The signal stays in the bag, so a request already in
170
177
  // flight is aborted by the transport as before.
171
178
  const signal = readAbortSignal(executeOptions);
172
- throwIfAborted(signal, {
173
- stepId
174
- });
179
+ throwIfAborted(signal, scope);
175
180
 
176
181
  // the Arazzo-derived parameters and request body are spread after the opaque
177
182
  // executeOptions bag so they take precedence over it. the operation executor
@@ -190,22 +195,20 @@ class StepExecutor {
190
195
  // cancelled, it did not: reporting it as the abort keeps one error shape
191
196
  // whether the run was withdrawn between two steps or mid-flight, and keeps
192
197
  // the whole feature from reading differently per transport.
193
- throwIfAborted(signal, {
194
- stepId
195
- });
198
+ throwIfAborted(signal, scope);
196
199
  throw error;
197
200
  }
198
201
 
199
202
  // evaluate criteria, outputs, and the next action against the post-request
200
203
  // context (with $request / $url / $method and $response / $statusCode).
201
204
  const postContext = state.toContext(this.#requestContext(response), this.#responseContext(response));
202
- const successCriteria = this.evaluateCriteria(step, postContext);
205
+ const successCriteria = this.evaluateCriteria(step, postContext, workflowId);
203
206
  const successful = successCriteria.every(criterion => criterion.met);
204
207
  const outputs = this.#outputResolver.resolve(step.outputs, expression => this.#evaluate(postContext, expression), scope);
205
208
  const {
206
209
  matchedActions,
207
210
  actionCriteria
208
- } = this.selectActions(step, successful, postContext);
211
+ } = this.selectActions(step, successful, postContext, workflowId);
209
212
  return {
210
213
  stepId,
211
214
  response,
@@ -235,16 +238,16 @@ class StepExecutor {
235
238
  requestContentType: requestBody.contentType
236
239
  };
237
240
  }
238
- async #locateOperation(step, stepId) {
241
+ async #locateOperation(step, scope) {
239
242
  if (isStringElement(step.operationId)) {
240
- return this.#locatorNormalizer.normalizeOperationId(toValue(step.operationId), this.#document);
243
+ return this.#locatorNormalizer.normalizeOperationId(toValue(step.operationId), this.#document, scope);
241
244
  }
242
245
  if (isStringElement(step.operationPath)) {
243
- return this.#locatorNormalizer.normalizeOperationPath(toValue(step.operationPath), this.#document);
246
+ return this.#locatorNormalizer.normalizeOperationPath(toValue(step.operationPath), this.#document, scope);
244
247
  }
245
- throw new ExecutionError(`Step "${stepId}" has no operationId or operationPath`, {
246
- stepId,
247
- reason: 'missing-operation'
248
+ throw new ExecutionError(`Step "${scope.stepId}" has no operationId or operationPath`, {
249
+ ...scope,
250
+ reason: ExecutionError.MissingOperation
248
251
  });
249
252
  }
250
253
 
@@ -275,13 +278,13 @@ class StepExecutor {
275
278
  if (!isArrayElement(criteria)) {
276
279
  throw new ResolverError("A step's `successCriteria` is present but is not a list", {
277
280
  ...scope,
278
- reason: 'malformed-criteria'
281
+ reason: ResolverError.MalformedCriteria
279
282
  });
280
283
  }
281
284
  if (![...criteria].every(isCriterionElement)) {
282
285
  throw new ResolverError("A step's `successCriteria` contains an entry that is not a Criterion Object", {
283
286
  ...scope,
284
- reason: 'malformed-criterion'
287
+ reason: ResolverError.MalformedCriterion
285
288
  });
286
289
  }
287
290
  }
@@ -297,10 +300,12 @@ class StepExecutor {
297
300
  * Public for the same reason as {@link StepExecutor.selectActions}: a step
298
301
  * targeting a `workflowId` is run by the workflow executor and never reaches
299
302
  * {@link StepExecutor.execute}, but its criteria are still the author's
300
- * assertion about the step and must be evaluated somewhere.
303
+ * assertion about the step and must be evaluated somewhere. `workflowId`
304
+ * names the step's workflow on a thrown error, as for
305
+ * {@link StepExecutor.execute}.
301
306
  */
302
- evaluateCriteria(step, context) {
303
- this.assertSuccessCriteria(step);
307
+ evaluateCriteria(step, context, workflowId) {
308
+ this.assertSuccessCriteria(step, workflowId);
304
309
  const criteria = step.successCriteria;
305
310
  if (criteria === undefined) return [];
306
311
 
@@ -328,8 +333,10 @@ class StepExecutor {
328
333
  * it runs itself and which therefore never reaches
329
334
  * {@link StepExecutor.execute}. Both callers reach the criteria evaluator and
330
335
  * the expression context through this one method rather than wiring their own.
336
+ * `workflowId` names the step's workflow on a thrown error, as for
337
+ * {@link StepExecutor.execute}.
331
338
  */
332
- selectActions(step, successful, context) {
339
+ selectActions(step, successful, context, workflowId) {
333
340
  const actionCriteria = [];
334
341
  const isCriterionMet = (criterion, action) => {
335
342
  const record = {
@@ -340,6 +347,9 @@ class StepExecutor {
340
347
  return record.met;
341
348
  };
342
349
  const scope = {
350
+ ...(workflowId === undefined ? {} : {
351
+ workflowId
352
+ }),
343
353
  stepId: toValue(step.stepId)
344
354
  };
345
355
  const matchedActions = successful ? this.#actionResolver.resolveOnSuccess(step.onSuccess, isCriterionMet, scope) : this.#actionResolver.resolveOnFailure(step.onFailure, isCriterionMet, scope);
@@ -377,7 +387,7 @@ class StepExecutor {
377
387
  // a step that calls a workflow made no HTTP call, so a condition on one
378
388
  // fails like any unevaluable condition; the diagnostic keeps the
379
389
  // authoring mistake visible.
380
- if (error instanceof RuntimeExpressionError && error.reason === 'unavailable-expression') {
390
+ if (error instanceof RuntimeExpressionError && error.reason === RuntimeExpressionError.UnavailableExpression) {
381
391
  return {
382
392
  condition,
383
393
  met: false,
@@ -209,7 +209,7 @@ class StepRetryRunner {
209
209
  throw new _ExecutionError.default(`retry action on step "${context.stepId}" in workflow "${context.workflowId}" carries a stepId/workflowId reference, but no runReference was given to run it`, {
210
210
  stepId: context.stepId,
211
211
  workflowId: context.workflowId,
212
- reason: 'retry-reference-unsupported'
212
+ reason: _ExecutionError.default.RetryReferenceUnsupported
213
213
  });
214
214
  }
215
215
  }
@@ -199,7 +199,7 @@ class StepRetryRunner {
199
199
  throw new ExecutionError(`retry action on step "${context.stepId}" in workflow "${context.workflowId}" carries a stepId/workflowId reference, but no runReference was given to run it`, {
200
200
  stepId: context.stepId,
201
201
  workflowId: context.workflowId,
202
- reason: 'retry-reference-unsupported'
202
+ reason: ExecutionError.RetryReferenceUnsupported
203
203
  });
204
204
  }
205
205
  }
@@ -104,7 +104,7 @@ class StepTransitionInterpreter {
104
104
  throw new _ExecutionError.default(`goto action on step "${stepId}" in workflow "${workflowId}" declares both a stepId and a workflowId (mutually exclusive)`, {
105
105
  stepId,
106
106
  workflowId,
107
- reason: 'ambiguous-target'
107
+ reason: _ExecutionError.default.AmbiguousTarget
108
108
  });
109
109
  }
110
110
  if (hasStepId) {
@@ -122,13 +122,13 @@ class StepTransitionInterpreter {
122
122
  throw new _ExecutionError.default(`goto action on step "${stepId}" in workflow "${workflowId}" has neither stepId nor workflowId`, {
123
123
  stepId,
124
124
  workflowId,
125
- reason: 'goto-target-missing'
125
+ reason: _ExecutionError.default.GotoTargetMissing
126
126
  });
127
127
  }
128
128
  throw new _ExecutionError.default(`action on step "${stepId}" in workflow "${workflowId}" has unsupported type "${type}"`, {
129
129
  stepId,
130
130
  workflowId,
131
- reason: 'unknown-action-type'
131
+ reason: _ExecutionError.default.UnknownActionType
132
132
  });
133
133
  }
134
134