@onlineapps/conn-orch-api-mapper 1.0.28 → 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 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
- - [API Standards](/docs/STANDARDS_OVERVIEW.md#api-structure) - Endpoint conventions
272
+ - [Error Handling Standard](/docs/standards/ERROR_HANDLING.md) - Business error classes and response format
260
273
 
261
274
  ---
262
- *Version: 1.0.0 | License: MIT*
275
+ *Version: 1.0.31 | License: MIT*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-api-mapper",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "API mapping connector for OA Drive - maps cookbook operations to HTTP endpoints",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
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 (may include _system.account_id)
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,19 +757,39 @@ class ApiMapper {
761
757
  }
762
758
  }
763
759
 
764
- // Multitenancy: propagate account context from workflow _system into HTTP header.
765
- // This is the single source of truth for workflow-scoped tenant.
766
- // NOTE: Gateway must set cookbook._system.account_id explicitly (no fallbacks).
767
760
  const sys = context && typeof context === 'object' ? context._system : null;
768
- if (sys && Object.prototype.hasOwnProperty.call(sys, 'account_id')) {
769
- const accountId = Number.parseInt(String(sys.account_id), 10);
770
- if (!Number.isInteger(accountId) || accountId <= 0) {
771
- throw new Error(`[ApiMapper][AccountContext] Invalid account context - Expected context._system.account_id to be a positive integer, got: ${sys.account_id}`);
772
- }
773
- // Do not allow step input to override tenant header
774
- request.headers['account-id'] = String(accountId);
761
+ if (!sys) {
762
+ throw new Error(
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.'
765
+ );
775
766
  }
776
767
 
768
+ if (sys.tenant_id === undefined || sys.workspace_id === undefined) {
769
+ throw new Error(
770
+ '[ApiMapper][TenantContext] Missing tenant context - Expected _system.tenant_id + _system.workspace_id'
771
+ );
772
+ }
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);
792
+
777
793
  return request;
778
794
  }
779
795
 
@@ -836,11 +852,37 @@ class ApiMapper {
836
852
  return response;
837
853
  } catch (error) {
838
854
  if (error.response) {
839
- // Server responded with error
840
- throw new Error(`API returned ${error.response.status}: ${error.response.statusText}`);
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;
841
880
  } else if (error.request) {
842
- // Request made but no response
843
- throw new Error(`No response from service: ${request.url}`);
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;
844
886
  } else {
845
887
  throw error;
846
888
  }