@onlineapps/conn-orch-api-mapper 1.0.8 → 1.0.10

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 +1 -1
  2. package/src/ApiMapper.js +167 -162
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-orch-api-mapper",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
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
@@ -125,220 +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
129
128
  *
130
- * Supports two operations.json output formats:
131
- * 1. Named fields: { "message": { "type": "string" }, "pdf": { "type": "file" } }
132
- * 2. Direct type: { "type": "file", "description": "..." } (entire output is one value)
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
133
  *
134
134
  * @method transformResponse
135
- * @param {Object} httpResponse - HTTP response
136
- * @param {Object} [operation] - Operation schema from operations.json (for output normalization)
137
- * @param {Object} [context] - Workflow context (for ContentAccessor initialization)
138
- * @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)
139
139
  */
140
140
  async transformResponse(httpResponse, operation = null, context = {}) {
141
141
  // Extract relevant data from HTTP response
142
142
  let output = httpResponse.data || httpResponse;
143
143
 
144
+ const workflowId = context.workflow_id || 'standalone';
145
+ const stepId = context.step_id || 'unknown';
146
+ const logPrefix = `[${workflowId}:${stepId}:ApiMapper`;
147
+
144
148
  // === LOGGING: Input ===
145
- const logPrefix = '[ApiMapper:transformResponse]';
146
- console.log(`${logPrefix} INPUT:`, JSON.stringify({
149
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
150
+ action: 'transform_start',
147
151
  hasOperation: !!operation,
148
- operationOutput: operation?.output,
149
152
  outputType: typeof output,
150
- outputKeys: output && typeof output === 'object' ? Object.keys(output) : null,
151
- output_descriptor: output?._descriptor,
152
- output_type: output?.type
153
- }));
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
+ }
154
181
 
155
- // If no operation schema, return as-is
182
+ // If no operation schema, return as-is (for non-file outputs)
156
183
  if (!operation || !operation.output) {
157
- console.log(`${logPrefix} OUTPUT: No operation schema, returning as-is`);
184
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
185
+ action: 'no_schema',
186
+ result: 'returning_as_is'
187
+ })}`);
158
188
  return output;
159
189
  }
160
190
 
161
191
  const outputSchema = operation.output;
162
192
 
163
- // Initialize ContentAccessor if needed (lazy initialization)
164
- let contentAccessor = null;
165
- const getContentAccessor = () => {
166
- if (!contentAccessor) {
167
- contentAccessor = new ContentAccessor({
168
- context: {
169
- workflow_id: context.workflow_id || 'standalone',
170
- step_id: context.step_id || 'api-mapper'
171
- }
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 }
172
199
  });
173
200
  }
174
- return contentAccessor;
201
+ return contentResolver;
175
202
  };
176
203
 
177
204
  // === DETECT OUTPUT SCHEMA FORMAT ===
178
- // Format A: Direct type - { type: "file", description: "..." }
205
+ // Format A: Direct type - { type: "file", fields: {...} }
179
206
  // Format B: Named fields - { "fieldName": { type: "string" }, ... }
180
207
  const isDirectType = outputSchema.type &&
181
- (outputSchema.type === 'file' ||
182
- outputSchema.type === 'content' ||
183
- outputSchema.type === 'string' ||
184
- outputSchema.type === 'object' ||
185
- outputSchema.type === 'array');
208
+ ['file', 'content', 'string', 'object', 'array'].includes(outputSchema.type);
186
209
 
187
- console.log(`${logPrefix} Schema format: ${isDirectType ? 'DIRECT_TYPE' : 'NAMED_FIELDS'}`);
210
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
211
+ action: 'schema_detected',
212
+ format: isDirectType ? 'DIRECT_TYPE' : 'NAMED_FIELDS',
213
+ schemaType: outputSchema.type
214
+ })}`);
188
215
 
189
216
  // === FORMAT A: Direct type (entire output is one value) ===
190
217
  if (isDirectType) {
191
218
  const schemaType = outputSchema.type;
192
219
 
193
- // If schema expects file/content, ensure output is Descriptor
220
+ // For file/content types, convert to Descriptor
194
221
  if (schemaType === 'file' || schemaType === 'content') {
195
- // Check if output is already a valid Descriptor
196
- if (output && typeof output === 'object' && output._descriptor === true) {
197
- console.log(`${logPrefix} OUTPUT: Already Descriptor with _descriptor=true`);
198
- return output;
199
- }
200
-
201
- // Check if output looks like Descriptor but missing _descriptor flag
202
- if (output && typeof output === 'object' && (output.type === 'file' || output.type === 'inline')) {
203
- console.log(`${logPrefix} Normalizing Descriptor (missing _descriptor flag)`);
204
- try {
205
- const accessor = getContentAccessor();
206
- const normalized = await accessor.normalizeToDescriptor(output, {
207
- filename: output.filename,
208
- content_type: output.content_type
209
- });
210
- console.log(`${logPrefix} OUTPUT: Normalized Descriptor:`, JSON.stringify({
211
- _descriptor: normalized._descriptor,
212
- type: normalized.type,
213
- storage_ref: normalized.storage_ref,
214
- filename: normalized.filename
215
- }));
216
- return normalized;
217
- } catch (error) {
218
- console.error(`${logPrefix} FAIL: Failed to normalize Descriptor: ${error.message}`);
219
- // FAIL-FAST: Don't silently continue with broken data
220
- throw new Error(`[ApiMapper] FAIL-FAST: Output is ${schemaType} but failed to normalize: ${error.message}`);
221
- }
222
- }
223
-
224
- // Output is string (storage reference) - convert to Descriptor
225
- if (typeof output === 'string') {
226
- console.log(`${logPrefix} Converting string to Descriptor: ${output.substring(0, 50)}...`);
227
- try {
228
- const accessor = getContentAccessor();
229
- const normalized = await accessor.normalizeToDescriptor(output, {
230
- filename: outputSchema.filename,
231
- content_type: outputSchema.content_type
232
- });
233
- console.log(`${logPrefix} OUTPUT: String converted to Descriptor`);
234
- return normalized;
235
- } catch (error) {
236
- console.error(`${logPrefix} FAIL: Failed to convert string to Descriptor: ${error.message}`);
237
- throw new Error(`[ApiMapper] FAIL-FAST: Output is ${schemaType} but string conversion failed: ${error.message}`);
238
- }
239
- }
240
-
241
- // Output is object with storage_ref - extract and convert
242
- if (output && typeof output === 'object' && output.storage_ref) {
243
- console.log(`${logPrefix} Converting object with storage_ref to Descriptor`);
244
- try {
245
- const accessor = getContentAccessor();
246
- const normalized = await accessor.normalizeToDescriptor(output.storage_ref, {
247
- filename: output.filename || output.output_name,
248
- content_type: output.content_type
249
- });
250
- console.log(`${logPrefix} OUTPUT: Object converted to Descriptor`);
251
- return normalized;
252
- } catch (error) {
253
- console.error(`${logPrefix} FAIL: Failed to convert object to Descriptor: ${error.message}`);
254
- throw new Error(`[ApiMapper] FAIL-FAST: Output is ${schemaType} but object conversion failed: ${error.message}`);
255
- }
256
- }
257
-
258
- // FAIL-FAST: Schema expects file/content but got unexpected format
259
- console.error(`${logPrefix} FAIL-FAST: Schema expects ${schemaType} but got unexpected output format:`, {
260
- outputType: typeof output,
261
- outputKeys: output && typeof output === 'object' ? Object.keys(output) : null
262
- });
263
- throw new Error(`[ApiMapper] FAIL-FAST: Schema expects '${schemaType}' but received unexpected format. Output type: ${typeof output}`);
222
+ return await this._convertToDescriptor(output, outputSchema, getContentResolver, logPrefix, context);
264
223
  }
265
224
 
266
- // For other direct types (string, object, array), return as-is
267
- console.log(`${logPrefix} OUTPUT: Direct type '${schemaType}', returning as-is`);
225
+ // For other types, return as-is
226
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
227
+ action: 'pass_through',
228
+ schemaType: schemaType
229
+ })}`);
268
230
  return output;
269
231
  }
270
232
 
271
233
  // === FORMAT B: Named fields ===
272
234
  const normalizedOutput = { ...output };
273
235
 
274
- // Process each output field
275
236
  for (const [fieldName, fieldSchema] of Object.entries(outputSchema)) {
276
- // Skip if fieldSchema is not an object (e.g., "type" key from Format A detected wrongly)
277
- if (!fieldSchema || typeof fieldSchema !== 'object') {
278
- continue;
279
- }
237
+ if (!fieldSchema || typeof fieldSchema !== 'object') continue;
280
238
 
281
239
  const fieldType = fieldSchema.type;
282
240
  const fieldValue = output[fieldName];
283
241
 
284
- console.log(`${logPrefix} Processing field '${fieldName}': type=${fieldType}, hasValue=${fieldValue !== undefined}`);
242
+ if (fieldValue === undefined || fieldValue === null) continue;
285
243
 
286
- // Skip if field not present or null
287
- if (fieldValue === undefined || fieldValue === null) {
288
- continue;
289
- }
290
-
291
- // Normalize fields with type: "file" or type: "content" to Descriptor
244
+ // For file/content fields, convert to Descriptor
292
245
  if (fieldType === 'file' || fieldType === 'content') {
293
- try {
294
- const accessor = getContentAccessor();
295
-
296
- // If field is already a Descriptor (has _descriptor flag), pass through
297
- if (typeof fieldValue === 'object' && fieldValue !== null && fieldValue._descriptor === true) {
298
- console.log(`${logPrefix} Field '${fieldName}' already Descriptor with _descriptor=true`);
299
- normalizedOutput[fieldName] = fieldValue;
300
- } else if (typeof fieldValue === 'object' && fieldValue !== null && (fieldValue.type === 'file' || fieldValue.type === 'inline')) {
301
- // Looks like Descriptor but missing _descriptor flag - normalize
302
- console.log(`${logPrefix} Field '${fieldName}' normalizing Descriptor (missing _descriptor flag)`);
303
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue, {
304
- filename: fieldValue.filename,
305
- content_type: fieldValue.content_type
306
- });
307
- } else if (typeof fieldValue === 'string') {
308
- // String value - normalize to Descriptor
309
- console.log(`${logPrefix} Field '${fieldName}' converting string to Descriptor`);
310
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue, {
311
- filename: fieldSchema.filename,
312
- content_type: fieldSchema.content_type
313
- });
314
- } else if (typeof fieldValue === 'object' && fieldValue !== null && fieldValue.storage_ref) {
315
- // Object with storage_ref - extract and normalize
316
- console.log(`${logPrefix} Field '${fieldName}' converting object with storage_ref to Descriptor`);
317
- normalizedOutput[fieldName] = await accessor.normalizeToDescriptor(fieldValue.storage_ref, {
318
- filename: fieldValue.filename || fieldValue.output_name,
319
- content_type: fieldValue.content_type
320
- });
321
- } else {
322
- // FAIL-FAST: Unexpected format for file/content field
323
- console.error(`${logPrefix} FAIL-FAST: Field '${fieldName}' expects ${fieldType} but got unexpected format:`, {
324
- valueType: typeof fieldValue,
325
- valueKeys: fieldValue && typeof fieldValue === 'object' ? Object.keys(fieldValue) : null
326
- });
327
- throw new Error(`[ApiMapper] FAIL-FAST: Field '${fieldName}' expects '${fieldType}' but received unexpected format`);
328
- }
329
- } catch (error) {
330
- if (error.message.includes('FAIL-FAST')) {
331
- throw error; // Re-throw fail-fast errors
332
- }
333
- console.error(`${logPrefix} FAIL: Failed to normalize field '${fieldName}': ${error.message}`);
334
- throw new Error(`[ApiMapper] FAIL-FAST: Failed to normalize field '${fieldName}' to Descriptor: ${error.message}`);
335
- }
246
+ normalizedOutput[fieldName] = await this._convertToDescriptor(
247
+ fieldValue, fieldSchema, getContentResolver, logPrefix, context, fieldName
248
+ );
336
249
  }
337
250
  }
338
251
 
339
- console.log(`${logPrefix} OUTPUT: Normalized fields:`, JSON.stringify(Object.keys(normalizedOutput)));
252
+ console.log(`${logPrefix}:PROCESS] ${JSON.stringify({
253
+ action: 'transform_complete',
254
+ outputKeys: Object.keys(normalizedOutput)
255
+ })}`);
256
+
340
257
  return normalizedOutput;
341
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
+ }
342
347
 
343
348
  /**
344
349
  * Call operation with parameters