@onlineapps/conn-orch-api-mapper 1.0.21 → 1.0.23

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.
@@ -1,599 +0,0 @@
1
- 'use strict';
2
-
3
- const ApiMapper = require('../../src/ApiMapper');
4
- const axios = require('axios');
5
-
6
- jest.mock('axios');
7
- jest.mock('@onlineapps/content-resolver', () => {
8
- return jest.fn().mockImplementation(() => ({
9
- getAsString: jest.fn(async (ref) => {
10
- if (typeof ref === 'string') return `resolved:${ref}`;
11
- if (ref && ref.content) return ref.content;
12
- return 'resolved-descriptor';
13
- })
14
- }));
15
- });
16
-
17
- describe('ApiMapper Unit Tests @unit', () => {
18
- let apiMapper;
19
- let mockLogger;
20
- let mockOpenApiSpec;
21
-
22
- beforeEach(() => {
23
- jest.clearAllMocks();
24
-
25
- mockLogger = {
26
- info: jest.fn(),
27
- error: jest.fn(),
28
- debug: jest.fn(),
29
- warn: jest.fn()
30
- };
31
-
32
- mockOpenApiSpec = {
33
- openapi: '3.0.0',
34
- info: { title: 'Test API', version: '1.0.0' },
35
- servers: [{ url: 'http://localhost:3000' }],
36
- paths: {
37
- '/users/{userId}': {
38
- get: {
39
- operationId: 'getUser',
40
- parameters: [
41
- { name: 'userId', in: 'path', required: true, schema: { type: 'string' } }
42
- ]
43
- },
44
- put: {
45
- operationId: 'updateUser',
46
- parameters: [
47
- { name: 'userId', in: 'path', required: true }
48
- ],
49
- requestBody: {
50
- content: {
51
- 'application/json': {
52
- schema: { type: 'object' }
53
- }
54
- }
55
- }
56
- }
57
- },
58
- '/users': {
59
- get: {
60
- operationId: 'listUsers',
61
- parameters: [
62
- { name: 'limit', in: 'query', schema: { type: 'integer' } },
63
- { name: 'offset', in: 'query', schema: { type: 'integer' } }
64
- ]
65
- },
66
- post: {
67
- operationId: 'createUser',
68
- requestBody: {
69
- required: true,
70
- content: {
71
- 'application/json': {
72
- schema: { type: 'object' }
73
- }
74
- }
75
- }
76
- }
77
- }
78
- }
79
- };
80
-
81
- apiMapper = new ApiMapper({
82
- openApiSpec: mockOpenApiSpec,
83
- serviceUrl: 'http://test-service:3000',
84
- logger: mockLogger
85
- });
86
- });
87
-
88
- describe('Constructor', () => {
89
- it('should create instance with required config', () => {
90
- expect(apiMapper).toBeInstanceOf(ApiMapper);
91
- expect(apiMapper.serviceUrl).toBe('http://test-service:3000');
92
- expect(apiMapper.openApiSpec).toEqual(mockOpenApiSpec);
93
- });
94
-
95
- it('should throw error without OpenAPI spec', () => {
96
- expect(() => new ApiMapper({})).toThrow('OpenAPI specification is required');
97
- });
98
-
99
- it('should use default service URL if not provided', () => {
100
- const mapper = new ApiMapper({
101
- openApiSpec: mockOpenApiSpec
102
- });
103
- expect(mapper.serviceUrl).toBe('http://localhost:3000');
104
- });
105
-
106
- it('should parse operations from OpenAPI spec', () => {
107
- expect(apiMapper.operations).toBeDefined();
108
- expect(apiMapper.operations['getUser']).toBeDefined();
109
- expect(apiMapper.operations['updateUser']).toBeDefined();
110
- expect(apiMapper.operations['listUsers']).toBeDefined();
111
- expect(apiMapper.operations['createUser']).toBeDefined();
112
- });
113
- });
114
-
115
- describe('_parseOpenApiSpec', () => {
116
- it('should extract operations with correct details', () => {
117
- const operations = apiMapper._parseOpenApiSpec(mockOpenApiSpec);
118
-
119
- expect(operations['getUser']).toEqual({
120
- method: 'GET',
121
- path: '/users/{userId}',
122
- parameters: mockOpenApiSpec.paths['/users/{userId}'].get.parameters,
123
- requestBody: undefined,
124
- responses: undefined,
125
- summary: undefined,
126
- description: undefined
127
- });
128
-
129
- expect(operations['createUser']).toEqual({
130
- method: 'POST',
131
- path: '/users',
132
- parameters: [],
133
- requestBody: mockOpenApiSpec.paths['/users'].post.requestBody,
134
- responses: undefined,
135
- summary: undefined,
136
- description: undefined
137
- });
138
- });
139
-
140
- it('should generate operation ID if missing', () => {
141
- const specWithoutId = {
142
- paths: {
143
- '/test': {
144
- get: {
145
- // No operationId
146
- parameters: []
147
- }
148
- }
149
- }
150
- };
151
-
152
- const operations = apiMapper._parseOpenApiSpec(specWithoutId);
153
- expect(operations['get__test']).toBeDefined();
154
- });
155
-
156
- it('should handle empty spec', () => {
157
- const operations = apiMapper._parseOpenApiSpec({});
158
- expect(operations).toEqual({});
159
- });
160
- });
161
-
162
- describe('operations.json format (type-driven descriptor handling)', () => {
163
- it('should store original input schema on parsed operation', () => {
164
- const operationsJson = {
165
- operations: {
166
- 'test-file-info': {
167
- description: 'Test',
168
- endpoint: '/api/test-file-info',
169
- method: 'POST',
170
- input: {
171
- file: { type: 'file', required: true }
172
- },
173
- output: {
174
- ok: { type: 'boolean' }
175
- }
176
- }
177
- }
178
- };
179
-
180
- const mapper = new ApiMapper({
181
- openApiSpec: operationsJson,
182
- serviceUrl: 'http://test-service:3000',
183
- logger: mockLogger
184
- });
185
-
186
- expect(mapper.operations['test-file-info']).toBeDefined();
187
- expect(mapper.operations['test-file-info'].input).toEqual({
188
- file: { type: 'file', required: true }
189
- });
190
- });
191
-
192
- it('should pass through descriptors for input fields typed as file', async () => {
193
- const operationsJson = {
194
- operations: {
195
- 'test-file-info': {
196
- description: 'Test',
197
- endpoint: '/api/test-file-info',
198
- method: 'POST',
199
- input: {
200
- file: { type: 'file', required: true }
201
- },
202
- output: {
203
- ok: { type: 'boolean' }
204
- }
205
- }
206
- }
207
- };
208
-
209
- apiMapper = new ApiMapper({
210
- openApiSpec: operationsJson,
211
- serviceUrl: 'http://test-service:3000',
212
- logger: mockLogger
213
- });
214
-
215
- // Ensure we do NOT hit axios
216
- axios.mockImplementation(async () => ({ status: 200, data: { ok: true } }));
217
-
218
- const descriptor = { _descriptor: true, type: 'file', storage_ref: 'minio://bucket/path' };
219
- await apiMapper.callOperation('test-file-info', { file: descriptor }, {});
220
-
221
- expect(axios).toHaveBeenCalledWith(
222
- expect.objectContaining({
223
- method: 'POST',
224
- url: 'http://test-service:3000/api/test-file-info',
225
- data: { file: descriptor }
226
- })
227
- );
228
- });
229
- });
230
-
231
- describe('mapOperationToEndpoint', () => {
232
- it('should return operation details for valid operation', () => {
233
- const endpoint = apiMapper.mapOperationToEndpoint('getUser');
234
-
235
- expect(endpoint).toEqual({
236
- method: 'GET',
237
- path: '/users/{userId}',
238
- parameters: expect.any(Array),
239
- requestBody: undefined,
240
- responses: undefined,
241
- summary: undefined,
242
- description: undefined
243
- });
244
- });
245
-
246
- it('should throw error for unknown operation', () => {
247
- expect(() => apiMapper.mapOperationToEndpoint('unknownOp'))
248
- .toThrow('Operation not found: unknownOp');
249
- });
250
- });
251
-
252
- describe('transformRequest', () => {
253
- it('should separate parameters by type', () => {
254
- const params = [
255
- { name: 'userId', in: 'path' },
256
- { name: 'limit', in: 'query' },
257
- { name: 'auth', in: 'header' }
258
- ];
259
-
260
- const input = {
261
- userId: '123',
262
- limit: 10,
263
- auth: 'Bearer token'
264
- };
265
-
266
- const result = apiMapper.transformRequest(input, params);
267
-
268
- expect(result.params.userId).toBe('123');
269
- expect(result.query.limit).toBe(10);
270
- expect(result.headers.auth).toBe('Bearer token');
271
- });
272
-
273
- it('should handle missing optional parameters', () => {
274
- const params = [
275
- { name: 'required', in: 'query', required: true },
276
- { name: 'optional', in: 'query' }
277
- ];
278
-
279
- const result = apiMapper.transformRequest({ required: 'value' }, params);
280
-
281
- expect(result.query.required).toBe('value');
282
- expect(result.query.optional).toBeUndefined();
283
- });
284
-
285
- it('should throw for missing required parameters', () => {
286
- const params = [
287
- { name: 'required', in: 'path', required: true }
288
- ];
289
-
290
- expect(() => apiMapper.transformRequest({}, params))
291
- .toThrow('Required parameter missing: required');
292
- });
293
-
294
- it('should extract body from input', () => {
295
- const input = {
296
- userId: '123',
297
- body: {
298
- name: 'John',
299
- email: 'john@example.com'
300
- }
301
- };
302
-
303
- const params = [{ name: 'userId', in: 'path' }];
304
- const result = apiMapper.transformRequest(input, params);
305
-
306
- expect(result.body).toEqual({
307
- name: 'John',
308
- email: 'john@example.com'
309
- });
310
- });
311
- });
312
-
313
- describe('transformResponse', () => {
314
- it('should extract data from response', async () => {
315
- const response = {
316
- data: { id: '1', name: 'John' },
317
- status: 200,
318
- headers: {}
319
- };
320
-
321
- const result = await apiMapper.transformResponse(response);
322
- expect(result).toEqual({ id: '1', name: 'John' });
323
- });
324
-
325
- it('should handle null response', async () => {
326
- const response = {
327
- data: null,
328
- status: 204
329
- };
330
-
331
- const result = await apiMapper.transformResponse(response);
332
- expect(result).toEqual(response);
333
- });
334
-
335
- it('should pass through response without data property', async () => {
336
- const response = { status: 200 };
337
- const result = await apiMapper.transformResponse(response);
338
- expect(result).toEqual(response);
339
- });
340
- });
341
-
342
- describe('_buildRequest', () => {
343
- it('should build request with path parameters', () => {
344
- const operation = {
345
- method: 'GET',
346
- path: '/users/{userId}',
347
- parameters: [
348
- { name: 'userId', in: 'path' }
349
- ]
350
- };
351
-
352
- const input = { userId: '123' };
353
- const request = apiMapper._buildRequest(operation, input);
354
-
355
- expect(request.method).toBe('GET');
356
- expect(request.url).toBe('http://test-service:3000/users/123');
357
- });
358
-
359
- it('should add query parameters', () => {
360
- const operation = {
361
- method: 'GET',
362
- path: '/users',
363
- parameters: [
364
- { name: 'limit', in: 'query' },
365
- { name: 'offset', in: 'query' }
366
- ]
367
- };
368
-
369
- const input = { limit: 10, offset: 20 };
370
- const request = apiMapper._buildRequest(operation, input);
371
-
372
- expect(request.params).toEqual({ limit: 10, offset: 20 });
373
- });
374
-
375
- it('should add headers', () => {
376
- const operation = {
377
- method: 'GET',
378
- path: '/users',
379
- parameters: [
380
- { name: 'X-API-Key', in: 'header' }
381
- ]
382
- };
383
-
384
- const input = { 'X-API-Key': 'secret' };
385
- const request = apiMapper._buildRequest(operation, input);
386
-
387
- expect(request.headers['X-API-Key']).toBe('secret');
388
- });
389
-
390
- it('should add request body', () => {
391
- const operation = {
392
- method: 'POST',
393
- path: '/users',
394
- parameters: [],
395
- requestBody: {
396
- content: {
397
- 'application/json': {}
398
- }
399
- }
400
- };
401
-
402
- const input = {
403
- body: { name: 'John', email: 'john@example.com' }
404
- };
405
- const request = apiMapper._buildRequest(operation, input);
406
-
407
- expect(request.data).toEqual(input.body);
408
- expect(request.headers['Content-Type']).toBe('application/json');
409
- });
410
- });
411
-
412
- describe('_resolveVariables', () => {
413
- it('should resolve simple variables', () => {
414
- const input = {
415
- userId: '${context.user.id}',
416
- name: 'static'
417
- };
418
-
419
- const context = {
420
- context: {
421
- user: { id: '123' }
422
- }
423
- };
424
-
425
- const result = apiMapper._resolveVariables(input, context);
426
- expect(result.userId).toBe('123');
427
- expect(result.name).toBe('static');
428
- });
429
-
430
- it('should handle missing context gracefully', () => {
431
- const input = {
432
- userId: '${context.user.id}'
433
- };
434
-
435
- const result = apiMapper._resolveVariables(input, {});
436
- expect(result.userId).toBeUndefined();
437
- });
438
-
439
- it('should resolve nested objects', () => {
440
- const input = {
441
- user: {
442
- id: '${context.userId}',
443
- name: '${input.userName}'
444
- }
445
- };
446
-
447
- const context = {
448
- context: { userId: '123' },
449
- input: { userName: 'John' }
450
- };
451
-
452
- const result = apiMapper._resolveVariables(input, context);
453
- expect(result.user.id).toBe('123');
454
- expect(result.user.name).toBe('John');
455
- });
456
- });
457
-
458
- describe('callOperation', () => {
459
- it('should execute operation successfully', async () => {
460
- const mockResponse = {
461
- data: { id: '1', name: 'John' },
462
- status: 200
463
- };
464
- axios.mockResolvedValue(mockResponse);
465
-
466
- const result = await apiMapper.callOperation('getUser', { userId: '1' });
467
-
468
- expect(axios).toHaveBeenCalledWith(
469
- expect.objectContaining({
470
- method: 'GET',
471
- url: 'http://test-service:3000/users/1',
472
- params: {}
473
- })
474
- );
475
-
476
- expect(result).toEqual({ id: '1', name: 'John' });
477
- });
478
-
479
- it('should handle operation errors', async () => {
480
- const error = new Error('Network error');
481
- axios.mockRejectedValue(error);
482
-
483
- await expect(apiMapper.callOperation('getUser', { userId: '1' }))
484
- .rejects.toThrow('Network error');
485
-
486
- expect(mockLogger.error).toHaveBeenCalledWith(
487
- 'API call failed for getUser',
488
- expect.any(Object)
489
- );
490
- });
491
-
492
- it('should throw for unknown operation', async () => {
493
- await expect(apiMapper.callOperation('unknownOp', {}))
494
- .rejects.toThrow('Operation not found: unknownOp');
495
- });
496
- });
497
-
498
- describe('loadOpenApiSpec', () => {
499
- it('should reload spec and update operations', () => {
500
- const newSpec = {
501
- paths: {
502
- '/new': {
503
- get: {
504
- operationId: 'newOp'
505
- }
506
- }
507
- }
508
- };
509
-
510
- apiMapper.loadOpenApiSpec(newSpec);
511
-
512
- expect(apiMapper.openApiSpec).toEqual(newSpec);
513
- expect(apiMapper.operations['newOp']).toBeDefined();
514
- expect(apiMapper.operations['getUser']).toBeUndefined();
515
- });
516
- });
517
-
518
- describe('Direct Call Mode', () => {
519
- it('should use direct call when configured', async () => {
520
- const mockApp = {};
521
- const directMapper = new ApiMapper({
522
- openApiSpec: mockOpenApiSpec,
523
- service: mockApp,
524
- directCall: true,
525
- logger: mockLogger
526
- });
527
-
528
- directMapper._callDirectly = jest.fn().mockResolvedValue({
529
- data: { id: '1' },
530
- status: 200
531
- });
532
-
533
- const result = await directMapper.callOperation('getUser', { userId: '1' });
534
-
535
- expect(directMapper._callDirectly).toHaveBeenCalled();
536
- expect(result).toEqual({ id: '1' });
537
- });
538
- });
539
-
540
- describe('_resolveContentDescriptors', () => {
541
- it('should return input if not object', async () => {
542
- const result = await apiMapper._resolveContentDescriptors(null);
543
- expect(result).toBeNull();
544
- });
545
-
546
- it('should resolve nested descriptors based on schema types', async () => {
547
- const input = {
548
- doc: {
549
- _descriptor: true,
550
- type: 'file',
551
- storage_ref: 'minio://workflow/file1'
552
- },
553
- meta: {
554
- title: {
555
- _descriptor: true,
556
- type: 'inline',
557
- content: 'Hello'
558
- },
559
- items: [
560
- {
561
- note: {
562
- _descriptor: true,
563
- type: 'inline',
564
- content: 'Note 1'
565
- }
566
- }
567
- ]
568
- }
569
- };
570
-
571
- const operation = {
572
- input: {
573
- doc: { type: 'file' },
574
- meta: {
575
- type: 'object',
576
- properties: {
577
- title: { type: 'string' },
578
- items: {
579
- type: 'array',
580
- items: {
581
- type: 'object',
582
- properties: {
583
- note: { type: 'string' }
584
- }
585
- }
586
- }
587
- }
588
- }
589
- }
590
- };
591
-
592
- const result = await apiMapper._resolveContentDescriptors(input, operation);
593
-
594
- expect(result.doc).toEqual(input.doc); // file passthrough
595
- expect(result.meta.title).toBe('Hello');
596
- expect(result.meta.items[0].note).toBe('Note 1');
597
- });
598
- });
599
- });