@onlineapps/conn-orch-api-mapper 1.0.29 → 1.0.31
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 +72 -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.default_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,54 @@ 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. ' +
|
|
764
|
+
'Fix: Gateway must set _system.tenant_id.'
|
|
772
765
|
);
|
|
773
766
|
}
|
|
774
|
-
|
|
775
|
-
if (
|
|
767
|
+
|
|
768
|
+
if (sys.tenant_id === undefined) {
|
|
769
|
+
throw new Error(
|
|
770
|
+
'[ApiMapper][TenantContext] Missing tenant context - Expected _system.tenant_id'
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const tenantId = Number.parseInt(String(sys.tenant_id), 10);
|
|
775
|
+
if (!Number.isInteger(tenantId) || tenantId <= 0) {
|
|
776
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid tenant_id - Expected positive integer, got: ${sys.tenant_id}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// workspace_id: per-step override → cookbook default → error
|
|
780
|
+
// See: .cursor/rules/workspace-architecture.mdc
|
|
781
|
+
const stepDef = context._pointer?.currentStep || operation;
|
|
782
|
+
const rawStepWs = stepDef?.workspace_id;
|
|
783
|
+
const rawDefaultWs = sys.default_workspace_id;
|
|
784
|
+
const rawWorkspaceId = rawStepWs !== undefined && rawStepWs !== null ? rawStepWs : rawDefaultWs;
|
|
785
|
+
|
|
786
|
+
if (rawWorkspaceId === undefined || rawWorkspaceId === null) {
|
|
776
787
|
throw new Error(
|
|
777
|
-
|
|
788
|
+
'[ApiMapper][TenantContext] Missing workspace_id - set per-step workspace_id or cookbook defaults.workspace_id'
|
|
778
789
|
);
|
|
779
790
|
}
|
|
780
|
-
|
|
781
|
-
|
|
791
|
+
|
|
792
|
+
const workspaceId = Number.parseInt(String(rawWorkspaceId), 10);
|
|
793
|
+
if (!Number.isInteger(workspaceId) || workspaceId <= 0) {
|
|
794
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid workspace_id - Expected positive integer, got: ${rawWorkspaceId}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const personId = sys.person_id !== undefined && sys.person_id !== null
|
|
798
|
+
? Number.parseInt(String(sys.person_id), 10)
|
|
799
|
+
: undefined;
|
|
800
|
+
if (personId === undefined || !Number.isInteger(personId) || personId <= 0) {
|
|
801
|
+
throw new Error(`[ApiMapper][TenantContext] Invalid person_id - Expected positive integer, got: ${sys.person_id}`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// See: docs/standards/tenant-context-contract.md (§3 Data Flow)
|
|
805
|
+
request.headers['x-tenant-id'] = String(tenantId);
|
|
806
|
+
request.headers['x-workspace-id'] = String(workspaceId);
|
|
807
|
+
request.headers['x-person-id'] = String(personId);
|
|
782
808
|
|
|
783
809
|
return request;
|
|
784
810
|
}
|
|
@@ -842,11 +868,37 @@ class ApiMapper {
|
|
|
842
868
|
return response;
|
|
843
869
|
} catch (error) {
|
|
844
870
|
if (error.response) {
|
|
845
|
-
|
|
846
|
-
|
|
871
|
+
const status = error.response.status;
|
|
872
|
+
const data = error.response.data;
|
|
873
|
+
|
|
874
|
+
const businessBody = data?.error || data;
|
|
875
|
+
const remoteCode = businessBody?.code || businessBody?.errorCode || null;
|
|
876
|
+
const remoteMessage = businessBody?.message || error.response.statusText;
|
|
877
|
+
const remoteDetails = businessBody?.details || [];
|
|
878
|
+
const remoteService = businessBody?.service || null;
|
|
879
|
+
const remoteOperation = businessBody?.operation || null;
|
|
880
|
+
|
|
881
|
+
const richMessage = remoteCode
|
|
882
|
+
? `[${remoteService || 'service'}] ${remoteCode}: ${remoteMessage}`
|
|
883
|
+
: `API returned ${status}: ${remoteMessage}`;
|
|
884
|
+
|
|
885
|
+
const richError = new Error(richMessage);
|
|
886
|
+
richError.statusCode = status;
|
|
887
|
+
richError.errorCode = remoteCode;
|
|
888
|
+
richError.type = businessBody?.type || null;
|
|
889
|
+
richError.details = remoteDetails;
|
|
890
|
+
richError.service = remoteService;
|
|
891
|
+
richError.operation = remoteOperation;
|
|
892
|
+
richError.responseBody = data;
|
|
893
|
+
richError.requestUrl = request.url;
|
|
894
|
+
|
|
895
|
+
throw richError;
|
|
847
896
|
} else if (error.request) {
|
|
848
|
-
|
|
849
|
-
|
|
897
|
+
const noResponseError = new Error(`No response from service: ${request.url}`);
|
|
898
|
+
noResponseError.statusCode = 503;
|
|
899
|
+
noResponseError.type = 'TRANSIENT';
|
|
900
|
+
noResponseError.requestUrl = request.url;
|
|
901
|
+
throw noResponseError;
|
|
850
902
|
} else {
|
|
851
903
|
throw error;
|
|
852
904
|
}
|