@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.
- package/package.json +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.
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
132
|
-
* @param {Object} [context] - Workflow context
|
|
133
|
-
* @returns {Promise<Object>} Transformed response for
|
|
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
|
-
|
|
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
|
|
150
|
-
let
|
|
151
|
-
const
|
|
152
|
-
if (!
|
|
153
|
-
|
|
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
|
|
201
|
+
return contentResolver;
|
|
161
202
|
};
|
|
162
203
|
|
|
163
|
-
//
|
|
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
|
-
|
|
169
|
-
if (fieldValue === undefined || fieldValue === null) {
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
242
|
+
if (fieldValue === undefined || fieldValue === null) continue;
|
|
172
243
|
|
|
173
|
-
//
|
|
244
|
+
// For file/content fields, convert to Descriptor
|
|
174
245
|
if (fieldType === 'file' || fieldType === 'content') {
|
|
175
|
-
|
|
176
|
-
|
|
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
|