@onlineapps/conn-orch-api-mapper 1.0.7 → 1.0.9

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 (2) hide show
  1. package/package.json +2 -2
  2. package/src/ApiMapper.js +192 -61
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-api-mapper",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "API mapping connector for OA Drive - maps cookbook operations to HTTP endpoints",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
23
  "axios": "^1.4.0",
24
- "@onlineapps/content-resolver": "^1.1.2"
24
+ "@onlineapps/content-resolver": "^1.1.3"
25
25
  },
26
26
  "devDependencies": {
27
27
  "jest": "^29.5.0",
package/src/ApiMapper.js CHANGED
@@ -125,94 +125,225 @@ class ApiMapper {
125
125
 
126
126
  /**
127
127
  * Transform HTTP response to cookbook format
128
- * Normalizes output fields to Content Descriptor based on operations.json output schema
128
+ *
129
+ * CRITICAL RULES:
130
+ * 1. Handler returns RAW data (temp_path, filename, content_type) - NEVER Descriptors!
131
+ * 2. ApiMapper is the ONLY place that creates Descriptors
132
+ * 3. FAIL-FAST on unexpected formats - no fallbacks!
133
+ *
129
134
  * @method transformResponse
130
- * @param {Object} httpResponse - HTTP response
131
- * @param {Object} [operation] - Operation schema from operations.json (for output normalization)
132
- * @param {Object} [context] - Workflow context (for ContentAccessor initialization)
133
- * @returns {Promise<Object>} Transformed response for cookbook
135
+ * @param {Object} httpResponse - HTTP response from handler
136
+ * @param {Object} [operation] - Operation schema from operations.json
137
+ * @param {Object} [context] - Workflow context { workflow_id, step_id }
138
+ * @returns {Promise<Object>} Transformed response (Descriptor for file types)
134
139
  */
135
140
  async transformResponse(httpResponse, operation = null, context = {}) {
136
141
  // Extract relevant data from HTTP response
137
142
  let output = httpResponse.data || httpResponse;
138
143
 
139
- // If no operation schema, return as-is
144
+ const workflowId = context.workflow_id || 'standalone';
145
+ const stepId = context.step_id || 'unknown';
146
+ const logPrefix = `[${workflowId}:${stepId}:ApiMapper`;
147
+
148
+ // === LOGGING: Input ===
149
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
150
+ action: 'transform_start',
151
+ hasOperation: !!operation,
152
+ outputType: typeof output,
153
+ outputKeys: output && typeof output === 'object' ? Object.keys(output) : null
154
+ })}`);
155
+
156
+ // === FAIL-FAST: Handler should NEVER return Descriptors ===
157
+ if (output && typeof output === 'object') {
158
+ if (output._descriptor === true) {
159
+ const errorMsg = 'Handler returned _descriptor:true - handlers must return RAW data, not Descriptors!';
160
+ console.error(`${logPrefix}:FAIL] ${JSON.stringify({
161
+ error: 'HANDLER_RETURNED_DESCRIPTOR',
162
+ message: errorMsg,
163
+ handler_output_keys: Object.keys(output),
164
+ expected: 'object with temp_path/filename or raw data'
165
+ })}`);
166
+ throw new Error(`[ApiMapper] FAIL-FAST: ${errorMsg}`);
167
+ }
168
+
169
+ // Also fail if handler returned storage_ref directly (handler shouldn't know about MinIO)
170
+ if (output.storage_ref && output.storage_ref.startsWith('minio://')) {
171
+ const errorMsg = 'Handler returned storage_ref - handlers must not interact with MinIO directly!';
172
+ console.error(`${logPrefix}:FAIL] ${JSON.stringify({
173
+ error: 'HANDLER_RETURNED_STORAGE_REF',
174
+ message: errorMsg,
175
+ storage_ref: output.storage_ref,
176
+ expected: 'object with temp_path/filename'
177
+ })}`);
178
+ throw new Error(`[ApiMapper] FAIL-FAST: ${errorMsg}`);
179
+ }
180
+ }
181
+
182
+ // If no operation schema, return as-is (for non-file outputs)
140
183
  if (!operation || !operation.output) {
184
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
185
+ action: 'no_schema',
186
+ result: 'returning_as_is'
187
+ })}`);
141
188
  return output;
142
189
  }
143
190
 
144
- // Normalize output fields according to operations.json output schema
145
- // Fields with type: "file" or type: "content" should be normalized to Descriptor
146
191
  const outputSchema = operation.output;
147
- const normalizedOutput = { ...output };
148
192
 
149
- // Initialize ContentAccessor if needed (lazy initialization)
150
- let contentAccessor = null;
151
- const getContentAccessor = () => {
152
- if (!contentAccessor) {
153
- contentAccessor = new ContentAccessor({
154
- context: {
155
- workflow_id: context.workflow_id || 'standalone',
156
- step_id: context.step_id || 'api-mapper'
157
- }
193
+ // Initialize ContentResolver (lazy)
194
+ let contentResolver = null;
195
+ const getContentResolver = () => {
196
+ if (!contentResolver) {
197
+ contentResolver = new ContentAccessor({
198
+ context: { workflow_id: workflowId, step_id: stepId }
158
199
  });
159
200
  }
160
- return contentAccessor;
201
+ return contentResolver;
161
202
  };
162
203
 
163
- // Process each output field
204
+ // === DETECT OUTPUT SCHEMA FORMAT ===
205
+ // Format A: Direct type - { type: "file", fields: {...} }
206
+ // Format B: Named fields - { "fieldName": { type: "string" }, ... }
207
+ const isDirectType = outputSchema.type &&
208
+ ['file', 'content', 'string', 'object', 'array'].includes(outputSchema.type);
209
+
210
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
211
+ action: 'schema_detected',
212
+ format: isDirectType ? 'DIRECT_TYPE' : 'NAMED_FIELDS',
213
+ schemaType: outputSchema.type
214
+ })}`);
215
+
216
+ // === FORMAT A: Direct type (entire output is one value) ===
217
+ if (isDirectType) {
218
+ const schemaType = outputSchema.type;
219
+
220
+ // For file/content types, convert to Descriptor
221
+ if (schemaType === 'file' || schemaType === 'content') {
222
+ return await this._convertToDescriptor(output, outputSchema, getContentResolver, logPrefix, context);
223
+ }
224
+
225
+ // For other types, return as-is
226
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
227
+ action: 'pass_through',
228
+ schemaType: schemaType
229
+ })}`);
230
+ return output;
231
+ }
232
+
233
+ // === FORMAT B: Named fields ===
234
+ const normalizedOutput = { ...output };
235
+
164
236
  for (const [fieldName, fieldSchema] of Object.entries(outputSchema)) {
237
+ if (!fieldSchema || typeof fieldSchema !== 'object') continue;
238
+
165
239
  const fieldType = fieldSchema.type;
166
240
  const fieldValue = output[fieldName];
167
241
 
168
- // Skip if field not present or null
169
- if (fieldValue === undefined || fieldValue === null) {
170
- continue;
171
- }
242
+ if (fieldValue === undefined || fieldValue === null) continue;
172
243
 
173
- // Normalize fields with type: "file" or type: "content" to Descriptor
244
+ // For file/content fields, convert to Descriptor
174
245
  if (fieldType === 'file' || fieldType === 'content') {
175
- try {
176
- const accessor = getContentAccessor();
177
-
178
- // If field is already a Descriptor (has _descriptor flag), use as-is
179
- if (typeof fieldValue === 'object' && fieldValue !== null && (fieldValue._descriptor === true || fieldValue.type === 'file' || fieldValue.type === 'inline')) {
180
- // Already a Descriptor, normalize to ensure completeness and _descriptor flag
181
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue, {
182
- filename: fieldValue.filename,
183
- content_type: fieldValue.content_type
184
- });
185
- } else if (typeof fieldValue === 'string') {
186
- // String value - normalize to Descriptor
187
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue, {
188
- filename: fieldSchema.filename,
189
- content_type: fieldSchema.content_type
190
- });
191
- } else if (typeof fieldValue === 'object' && fieldValue !== null) {
192
- // Object value (e.g., {storage_ref: "minio://..."}) - extract and normalize
193
- if (fieldValue.storage_ref) {
194
- // If it's an object with storage_ref, normalize the storage_ref
195
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue.storage_ref, {
196
- filename: fieldValue.filename || fieldValue.output_name,
197
- content_type: fieldValue.content_type
198
- });
199
- } else {
200
- // Unknown object format, try to normalize as-is
201
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue, {
202
- filename: fieldSchema.filename,
203
- content_type: fieldSchema.content_type
204
- });
205
- }
206
- }
207
- } catch (error) {
208
- this.logger?.warn(`Failed to normalize output field ${fieldName} to Descriptor: ${error.message}`);
209
- // Keep original value on error
210
- }
246
+ normalizedOutput[fieldName] = await this._convertToDescriptor(
247
+ fieldValue, fieldSchema, getContentResolver, logPrefix, context, fieldName
248
+ );
211
249
  }
212
250
  }
213
251
 
252
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
253
+ action: 'transform_complete',
254
+ outputKeys: Object.keys(normalizedOutput)
255
+ })}`);
256
+
214
257
  return normalizedOutput;
215
258
  }
259
+
260
+ /**
261
+ * Convert handler output to Descriptor
262
+ * @private
263
+ */
264
+ async _convertToDescriptor(output, schema, getContentResolver, logPrefix, context, fieldName = null) {
265
+ const fieldLabel = fieldName ? `field '${fieldName}'` : 'output';
266
+
267
+ // Handler should return object with temp_path for file operations
268
+ if (output && typeof output === 'object' && output.temp_path) {
269
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
270
+ action: 'convert_temp_file',
271
+ field: fieldName,
272
+ temp_path: output.temp_path,
273
+ filename: output.filename
274
+ })}`);
275
+
276
+ const resolver = getContentResolver();
277
+ const descriptor = await resolver.createDescriptorFromFile(output.temp_path, {
278
+ filename: output.filename,
279
+ content_type: output.content_type,
280
+ context: {
281
+ workflow_id: context.workflow_id || 'standalone',
282
+ step_id: context.step_id || 'api-mapper'
283
+ },
284
+ deleteAfterUpload: true
285
+ });
286
+
287
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
288
+ action: 'descriptor_created',
289
+ field: fieldName,
290
+ storage_ref: descriptor.storage_ref,
291
+ size: descriptor.size
292
+ })}`);
293
+
294
+ return descriptor;
295
+ }
296
+
297
+ // Handler returned Buffer - store it
298
+ if (Buffer.isBuffer(output)) {
299
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
300
+ action: 'convert_buffer',
301
+ field: fieldName,
302
+ size: output.length
303
+ })}`);
304
+
305
+ const resolver = getContentResolver();
306
+ return await resolver.createDescriptor(output, {
307
+ filename: schema.filename || 'output.bin',
308
+ content_type: schema.content_type,
309
+ context: {
310
+ workflow_id: context.workflow_id || 'standalone',
311
+ step_id: context.step_id || 'api-mapper'
312
+ },
313
+ forceFile: true
314
+ });
315
+ }
316
+
317
+ // Handler returned string (for content type) - may be inline or stored
318
+ if (typeof output === 'string') {
319
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
320
+ action: 'convert_string',
321
+ field: fieldName,
322
+ length: output.length
323
+ })}`);
324
+
325
+ const resolver = getContentResolver();
326
+ return await resolver.createDescriptor(output, {
327
+ filename: schema.filename,
328
+ content_type: schema.content_type,
329
+ context: {
330
+ workflow_id: context.workflow_id || 'standalone',
331
+ step_id: context.step_id || 'api-mapper'
332
+ }
333
+ });
334
+ }
335
+
336
+ // FAIL-FAST: Unexpected format
337
+ const errorMsg = `${fieldLabel} expects file/content but got unexpected format`;
338
+ console.error(`${logPrefix}:FAIL] ${JSON.stringify({
339
+ error: 'UNEXPECTED_OUTPUT_FORMAT',
340
+ field: fieldName,
341
+ expected: 'object with temp_path, Buffer, or string',
342
+ received: typeof output,
343
+ receivedKeys: output && typeof output === 'object' ? Object.keys(output) : null
344
+ })}`);
345
+ throw new Error(`[ApiMapper] FAIL-FAST: ${errorMsg}. Expected temp_path/Buffer/string, got ${typeof output}`);
346
+ }
216
347
 
217
348
  /**
218
349
  * Call operation with parameters