@onlineapps/conn-orch-api-mapper 1.0.29 → 1.0.30
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/README.md +15 -2
- package/package.json +1 -1
- package/src/ApiMapper.js +56 -20
package/README.md
CHANGED
|
@@ -252,11 +252,24 @@ npm run test:component # Component tests
|
|
|
252
252
|
- `axios` - HTTP client
|
|
253
253
|
- `openapi-validator` - Schema validation
|
|
254
254
|
|
|
255
|
+
## Error Propagation
|
|
256
|
+
|
|
257
|
+
When a downstream service returns an HTTP error, `_callViaHttp` preserves the full response body instead of discarding it. The thrown error carries:
|
|
258
|
+
|
|
259
|
+
- `statusCode` -- HTTP status code (enables `ErrorClassifier` classification)
|
|
260
|
+
- `errorCode` -- machine-readable code from the response (e.g. `RESOURCE_NOT_FOUND`)
|
|
261
|
+
- `type` -- error type from the response (e.g. `BUSINESS`, `VALIDATION`)
|
|
262
|
+
- `details` -- array of detail strings from the response
|
|
263
|
+
- `service` / `operation` -- originating service and operation
|
|
264
|
+
- `responseBody` -- raw response data for debugging
|
|
265
|
+
|
|
266
|
+
This enables the WorkflowOrchestrator to classify errors accurately and skip retries for permanent failures (BUSINESS/VALIDATION). See [Error Handling Standard](/docs/standards/ERROR_HANDLING.md#business-service-error-standard).
|
|
267
|
+
|
|
255
268
|
## Related Documentation
|
|
256
269
|
- [Service Wrapper](/shared/connector/service-wrapper/README.md) - Integration guide
|
|
257
270
|
- [Registry System](/docs/modules/registry.md) - Service discovery
|
|
258
271
|
- [Cookbook Connector](/shared/connector/conn-orch-cookbook/README.md) - Operation format
|
|
259
|
-
- [
|
|
272
|
+
- [Error Handling Standard](/docs/standards/ERROR_HANDLING.md) - Business error classes and response format
|
|
260
273
|
|
|
261
274
|
---
|
|
262
|
-
*Version: 1.0.
|
|
275
|
+
*Version: 1.0.31 | License: MIT*
|
package/package.json
CHANGED
package/src/ApiMapper.js
CHANGED
|
@@ -561,7 +561,6 @@ class ApiMapper {
|
|
|
561
561
|
method: (operation.method || 'POST').toUpperCase(),
|
|
562
562
|
path: operation.endpoint || `/${operationId}`,
|
|
563
563
|
// Static headers from operations.json (e.g. x-validation-request).
|
|
564
|
-
// NOTE: account-id is controlled by workflow context (_system.account_id) and must not be overridden here.
|
|
565
564
|
headers: operation.headers || null,
|
|
566
565
|
// Store original operations.json input schema for type-driven descriptor handling
|
|
567
566
|
input: operation.input || null,
|
|
@@ -687,7 +686,7 @@ class ApiMapper {
|
|
|
687
686
|
* @private
|
|
688
687
|
* @param {Object} operation - Operation definition
|
|
689
688
|
* @param {Object} input - Resolved input
|
|
690
|
-
* @param {Object} context - Workflow context (
|
|
689
|
+
* @param {Object} context - Workflow context (must include _system.tenant_id + _system.workspace_id)
|
|
691
690
|
* @returns {Object} Request object
|
|
692
691
|
*/
|
|
693
692
|
_buildRequest(operation, input, context = {}) {
|
|
@@ -703,9 +702,6 @@ class ApiMapper {
|
|
|
703
702
|
if (operation && operation.headers && typeof operation.headers === 'object') {
|
|
704
703
|
Object.entries(operation.headers).forEach(([k, v]) => {
|
|
705
704
|
if (!k) return;
|
|
706
|
-
// Never allow operation-level config to override tenant header
|
|
707
|
-
if (String(k).toLowerCase() === 'account-id') return;
|
|
708
|
-
|
|
709
705
|
// Optional: allow ${context.path} variable resolution (no env side-effects here).
|
|
710
706
|
if (typeof v === 'string' && v.startsWith('${') && v.endsWith('}')) {
|
|
711
707
|
const path = v.slice(2, -1);
|
|
@@ -761,24 +757,38 @@ class ApiMapper {
|
|
|
761
757
|
}
|
|
762
758
|
}
|
|
763
759
|
|
|
764
|
-
// Multitenancy: propagate account context from workflow _system into HTTP header.
|
|
765
|
-
// FAIL-FAST: _system.account_id is REQUIRED for all workflow requests.
|
|
766
|
-
// Gateway sets this, so missing value indicates configuration error.
|
|
767
760
|
const sys = context && typeof context === 'object' ? context._system : null;
|
|
768
|
-
if (!sys
|
|
761
|
+
if (!sys) {
|
|
769
762
|
throw new Error(
|
|
770
|
-
'[ApiMapper][
|
|
771
|
-
'Fix: Gateway must set _system.
|
|
763
|
+
'[ApiMapper][TenantContext] Missing _system context - Expected context._system with tenant_id + workspace_id. ' +
|
|
764
|
+
'Fix: Gateway must set _system.tenant_id + _system.workspace_id.'
|
|
772
765
|
);
|
|
773
766
|
}
|
|
774
|
-
|
|
775
|
-
if (
|
|
767
|
+
|
|
768
|
+
if (sys.tenant_id === undefined || sys.workspace_id === undefined) {
|
|
776
769
|
throw new Error(
|
|
777
|
-
|
|
770
|
+
'[ApiMapper][TenantContext] Missing tenant context - Expected _system.tenant_id + _system.workspace_id'
|
|
778
771
|
);
|
|
779
772
|
}
|
|
780
|
-
|
|
781
|
-
|
|
773
|
+
|
|
774
|
+
const tenantId = Number.parseInt(String(sys.tenant_id), 10);
|
|
775
|
+
const workspaceId = Number.parseInt(String(sys.workspace_id), 10);
|
|
776
|
+
if (!Number.isInteger(tenantId) || tenantId <= 0) {
|
|
777
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid tenant_id - Expected positive integer, got: ${sys.tenant_id}`);
|
|
778
|
+
}
|
|
779
|
+
if (!Number.isInteger(workspaceId) || workspaceId <= 0) {
|
|
780
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid workspace_id - Expected positive integer, got: ${sys.workspace_id}`);
|
|
781
|
+
}
|
|
782
|
+
const personId = sys.person_id !== undefined && sys.person_id !== null
|
|
783
|
+
? Number.parseInt(String(sys.person_id), 10)
|
|
784
|
+
: undefined;
|
|
785
|
+
if (personId === undefined || !Number.isInteger(personId) || personId <= 0) {
|
|
786
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid person_id - Expected positive integer, got: ${sys.person_id}`);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
request.headers['x-tenant-id'] = String(tenantId);
|
|
790
|
+
request.headers['x-workspace-id'] = String(workspaceId);
|
|
791
|
+
request.headers['x-person-id'] = String(personId);
|
|
782
792
|
|
|
783
793
|
return request;
|
|
784
794
|
}
|
|
@@ -842,11 +852,37 @@ class ApiMapper {
|
|
|
842
852
|
return response;
|
|
843
853
|
} catch (error) {
|
|
844
854
|
if (error.response) {
|
|
845
|
-
|
|
846
|
-
|
|
855
|
+
const status = error.response.status;
|
|
856
|
+
const data = error.response.data;
|
|
857
|
+
|
|
858
|
+
const businessBody = data?.error || data;
|
|
859
|
+
const remoteCode = businessBody?.code || businessBody?.errorCode || null;
|
|
860
|
+
const remoteMessage = businessBody?.message || error.response.statusText;
|
|
861
|
+
const remoteDetails = businessBody?.details || [];
|
|
862
|
+
const remoteService = businessBody?.service || null;
|
|
863
|
+
const remoteOperation = businessBody?.operation || null;
|
|
864
|
+
|
|
865
|
+
const richMessage = remoteCode
|
|
866
|
+
? `[${remoteService || 'service'}] ${remoteCode}: ${remoteMessage}`
|
|
867
|
+
: `API returned ${status}: ${remoteMessage}`;
|
|
868
|
+
|
|
869
|
+
const richError = new Error(richMessage);
|
|
870
|
+
richError.statusCode = status;
|
|
871
|
+
richError.errorCode = remoteCode;
|
|
872
|
+
richError.type = businessBody?.type || null;
|
|
873
|
+
richError.details = remoteDetails;
|
|
874
|
+
richError.service = remoteService;
|
|
875
|
+
richError.operation = remoteOperation;
|
|
876
|
+
richError.responseBody = data;
|
|
877
|
+
richError.requestUrl = request.url;
|
|
878
|
+
|
|
879
|
+
throw richError;
|
|
847
880
|
} else if (error.request) {
|
|
848
|
-
|
|
849
|
-
|
|
881
|
+
const noResponseError = new Error(`No response from service: ${request.url}`);
|
|
882
|
+
noResponseError.statusCode = 503;
|
|
883
|
+
noResponseError.type = 'TRANSIENT';
|
|
884
|
+
noResponseError.requestUrl = request.url;
|
|
885
|
+
throw noResponseError;
|
|
850
886
|
} else {
|
|
851
887
|
throw error;
|
|
852
888
|
}
|