@zauto/api-connector 0.0.1
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 +208 -0
- package/dist/dto/api-config.dto.d.ts +24 -0
- package/dist/dto/api-config.dto.js +94 -0
- package/dist/dto/api-config.dto.js.map +1 -0
- package/dist/dto/endpoint-config.dto.d.ts +7 -0
- package/dist/dto/endpoint-config.dto.js +39 -0
- package/dist/dto/endpoint-config.dto.js.map +1 -0
- package/dist/dto/field-mapping.dto.d.ts +7 -0
- package/dist/dto/field-mapping.dto.js +34 -0
- package/dist/dto/field-mapping.dto.js.map +1 -0
- package/dist/enums/resources.enum.d.ts +8 -0
- package/dist/enums/resources.enum.js +13 -0
- package/dist/enums/resources.enum.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +611 -0
- package/dist/index.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +86 -0
- package/src/dto/api-config.dto.ts +115 -0
- package/src/dto/endpoint-config.dto.ts +23 -0
- package/src/dto/field-mapping.dto.ts +19 -0
- package/src/enums/resources.enum.ts +9 -0
- package/src/index.ts +850 -0
package/src/index.ts
ADDED
@@ -0,0 +1,850 @@
|
|
1
|
+
import { PrismaClient } from '@prisma/client';
|
2
|
+
import { ApiConfigDto, AuthType } from './dto/api-config.dto';
|
3
|
+
import { EndpointConfigDto } from './dto/endpoint-config.dto';
|
4
|
+
import { FieldMappingDto } from './dto/field-mapping.dto';
|
5
|
+
import axios, { AxiosRequestConfig } from 'axios';
|
6
|
+
|
7
|
+
/**
|
8
|
+
* Saves or updates API configuration in the database.
|
9
|
+
*
|
10
|
+
* @param prisma - PrismaClient instance
|
11
|
+
* @param apiConfig - API configuration object of type ApiConfigDto
|
12
|
+
* @param additionalParams - Additional fields to include in the database (e.g., orgId)
|
13
|
+
* @param searchCriteria - Optional search criteria for finding an existing record
|
14
|
+
* @returns A promise that resolves to the stored or updated configuration
|
15
|
+
*/
|
16
|
+
export async function saveApiConfig(
|
17
|
+
prisma: PrismaClient,
|
18
|
+
apiConfig: ApiConfigDto,
|
19
|
+
additionalParams: Record<string, any> = {},
|
20
|
+
searchCriteria: Record<string, any> = {},
|
21
|
+
) {
|
22
|
+
try {
|
23
|
+
|
24
|
+
const existingConfig = await prisma.apiConfig.findFirst({
|
25
|
+
where: searchCriteria,
|
26
|
+
});
|
27
|
+
|
28
|
+
if (existingConfig) {
|
29
|
+
|
30
|
+
const updatedConfig = await prisma.apiConfig.update({
|
31
|
+
where: { id: existingConfig.id },
|
32
|
+
data: {
|
33
|
+
baseUrl: apiConfig.baseUrl,
|
34
|
+
authType: apiConfig.authType,
|
35
|
+
infoForAuthType: apiConfig.infoForAuthType
|
36
|
+
? JSON.stringify(apiConfig.infoForAuthType)
|
37
|
+
: '{}',
|
38
|
+
timeout: apiConfig.timeout || null,
|
39
|
+
retryPolicy: apiConfig.retryPolicy
|
40
|
+
? JSON.stringify(apiConfig.retryPolicy)
|
41
|
+
: '{}',
|
42
|
+
...additionalParams,
|
43
|
+
},
|
44
|
+
});
|
45
|
+
|
46
|
+
return {
|
47
|
+
operation: 'updated',
|
48
|
+
config: updatedConfig,
|
49
|
+
};
|
50
|
+
} else {
|
51
|
+
|
52
|
+
const createdConfig = await prisma.apiConfig.create({
|
53
|
+
data: {
|
54
|
+
baseUrl: apiConfig.baseUrl,
|
55
|
+
authType: apiConfig.authType,
|
56
|
+
infoForAuthType: typeof apiConfig.infoForAuthType === 'string'
|
57
|
+
? apiConfig.infoForAuthType
|
58
|
+
: JSON.stringify(apiConfig.infoForAuthType || '{}'),
|
59
|
+
timeout: apiConfig.timeout || null,
|
60
|
+
retryPolicy: apiConfig.retryPolicy
|
61
|
+
? JSON.stringify(apiConfig.retryPolicy)
|
62
|
+
: '{}',
|
63
|
+
...additionalParams,
|
64
|
+
},
|
65
|
+
});
|
66
|
+
|
67
|
+
return {
|
68
|
+
operation: 'created',
|
69
|
+
config: createdConfig,
|
70
|
+
};
|
71
|
+
}
|
72
|
+
} catch (error) {
|
73
|
+
console.error('Error saving or updating API configuration:', error);
|
74
|
+
throw new Error('Failed to save or update API configuration');
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Updates an existing API configuration in the database.
|
80
|
+
*
|
81
|
+
* @param prisma - PrismaClient instance
|
82
|
+
* @param updateData - Data to update (fields in ApiConfigDto or additional fields)
|
83
|
+
* @param searchCriteria - Criteria to find the record to update
|
84
|
+
* @returns A promise that resolves to the updated configuration, or throws an error if not found
|
85
|
+
*/
|
86
|
+
export async function updateApiConfig(
|
87
|
+
prisma: PrismaClient,
|
88
|
+
updateData: Partial<ApiConfigDto> & Record<string, any>,
|
89
|
+
searchCriteria: Record<string, any>,
|
90
|
+
) {
|
91
|
+
try {
|
92
|
+
|
93
|
+
const existingConfig = await prisma.apiConfig.findFirst({
|
94
|
+
where: searchCriteria,
|
95
|
+
});
|
96
|
+
|
97
|
+
if (!existingConfig) {
|
98
|
+
throw new Error('API configuration not found for the given criteria.');
|
99
|
+
}
|
100
|
+
|
101
|
+
const updatedConfig = await prisma.apiConfig.update({
|
102
|
+
where: { id: existingConfig.id },
|
103
|
+
data: {
|
104
|
+
...updateData,
|
105
|
+
infoForAuthType: typeof updateData.infoForAuthType === 'string' ? updateData.infoForAuthType : updateData.infoForAuthType
|
106
|
+
? JSON.stringify(updateData.infoForAuthType)
|
107
|
+
: undefined,
|
108
|
+
retryPolicy: updateData.retryPolicy
|
109
|
+
? JSON.stringify(updateData.retryPolicy)
|
110
|
+
: undefined,
|
111
|
+
},
|
112
|
+
});
|
113
|
+
|
114
|
+
return updatedConfig;
|
115
|
+
} catch (error) {
|
116
|
+
console.error('Error updating API configuration:', error);
|
117
|
+
throw new Error('Failed to update API configuration');
|
118
|
+
}
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Saves an endpoint configuration linked to a specific API configuration.
|
123
|
+
*
|
124
|
+
* @param prisma - PrismaClient instance
|
125
|
+
* @param endpointConfig - Endpoint configuration object
|
126
|
+
* @returns A promise that resolves to the stored configuration
|
127
|
+
*/
|
128
|
+
export async function saveEndpointConfig(
|
129
|
+
prisma: PrismaClient,
|
130
|
+
endpointConfig: EndpointConfigDto,
|
131
|
+
) {
|
132
|
+
try {
|
133
|
+
|
134
|
+
const apiConfig = await prisma.apiConfig.findFirst({
|
135
|
+
where: { id: endpointConfig.apiConfigId },
|
136
|
+
});
|
137
|
+
|
138
|
+
if (!apiConfig) {
|
139
|
+
throw new Error(`API Configuration with ID ${endpointConfig.apiConfigId} not found.`);
|
140
|
+
}
|
141
|
+
|
142
|
+
const existingConfig = await prisma.endpointConfig.findFirst({
|
143
|
+
where: { name: endpointConfig.name, apiConfigId: apiConfig.id },
|
144
|
+
});
|
145
|
+
|
146
|
+
if (existingConfig) {
|
147
|
+
|
148
|
+
return {
|
149
|
+
message: "An endpoint with the same name already exists for this API configuration",
|
150
|
+
config: existingConfig
|
151
|
+
};
|
152
|
+
} else {
|
153
|
+
|
154
|
+
const createdConfig = await prisma.endpointConfig.create({
|
155
|
+
data: {
|
156
|
+
name: endpointConfig.name,
|
157
|
+
method: endpointConfig.method,
|
158
|
+
suburl: endpointConfig.suburl,
|
159
|
+
headers: endpointConfig.headers || {},
|
160
|
+
apiConfigId: endpointConfig.apiConfigId,
|
161
|
+
},
|
162
|
+
});
|
163
|
+
return {
|
164
|
+
operation: 'created',
|
165
|
+
config: createdConfig,
|
166
|
+
};
|
167
|
+
}
|
168
|
+
} catch (error) {
|
169
|
+
console.error('Error saving endpoint configuration:', error);
|
170
|
+
throw new Error('Failed to save endpoint configuration');
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
/**
|
175
|
+
* Updates an existing endpoint configuration.
|
176
|
+
*
|
177
|
+
* @param prisma - PrismaClient instance
|
178
|
+
* @param endpointId - ID of the endpoint configuration to update
|
179
|
+
* @param updateData - Partial data to update (fields in EndpointConfigDto)
|
180
|
+
* @returns A promise that resolves to the updated endpoint configuration
|
181
|
+
*/
|
182
|
+
export async function updateEndpointConfig(
|
183
|
+
prisma: PrismaClient,
|
184
|
+
endpointId: string,
|
185
|
+
updateData: Partial<EndpointConfigDto>,
|
186
|
+
) {
|
187
|
+
try {
|
188
|
+
|
189
|
+
const existingEndpoint = await prisma.endpointConfig.findUnique({
|
190
|
+
where: { id: endpointId },
|
191
|
+
});
|
192
|
+
|
193
|
+
if (!existingEndpoint) {
|
194
|
+
throw new Error('Endpoint configuration not found for the given ID.');
|
195
|
+
}
|
196
|
+
|
197
|
+
if (updateData.name && updateData.name !== existingEndpoint.name) {
|
198
|
+
const conflict = await prisma.endpointConfig.findFirst({
|
199
|
+
where: {
|
200
|
+
name: updateData.name,
|
201
|
+
apiConfigId: existingEndpoint.apiConfigId,
|
202
|
+
},
|
203
|
+
});
|
204
|
+
if (conflict) {
|
205
|
+
throw new Error(
|
206
|
+
`Endpoint with the name "${updateData.name}" already exists for this API configuration.`,
|
207
|
+
);
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
211
|
+
const updatedEndpoint = await prisma.endpointConfig.update({
|
212
|
+
where: { id: endpointId },
|
213
|
+
data: {
|
214
|
+
name: updateData.name ?? existingEndpoint.name,
|
215
|
+
method: updateData.method ?? existingEndpoint.method,
|
216
|
+
suburl: updateData.suburl ?? existingEndpoint.suburl,
|
217
|
+
headers: updateData.headers ?? existingEndpoint.headers,
|
218
|
+
},
|
219
|
+
});
|
220
|
+
|
221
|
+
return {
|
222
|
+
operation: 'updated',
|
223
|
+
config: updatedEndpoint,
|
224
|
+
};
|
225
|
+
} catch (error) {
|
226
|
+
console.error('Error updating endpoint configuration:', error);
|
227
|
+
throw new Error('Failed to update endpoint configuration');
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Saves a field mapping configuration associated with an endpoint.
|
233
|
+
*
|
234
|
+
* @param prisma - PrismaClient instance
|
235
|
+
* @param fieldMapping - Field mapping configuration object
|
236
|
+
* @returns A promise that resolves to the stored configuration
|
237
|
+
*/
|
238
|
+
export async function saveFieldMapping(
|
239
|
+
prisma: PrismaClient,
|
240
|
+
fieldMapping: FieldMappingDto,
|
241
|
+
) {
|
242
|
+
try {
|
243
|
+
|
244
|
+
const endpoint = await prisma.endpointConfig.findFirst({
|
245
|
+
where: { id: fieldMapping.endpointId },
|
246
|
+
});
|
247
|
+
|
248
|
+
if (!endpoint) {
|
249
|
+
throw new Error(`Endpoint with ID ${fieldMapping.endpointId} not found.`);
|
250
|
+
}
|
251
|
+
|
252
|
+
const existingMapping = await prisma.fieldMapping.findFirst({
|
253
|
+
where: {
|
254
|
+
resourceType: fieldMapping.resourceType,
|
255
|
+
endpointId: fieldMapping.endpointId,
|
256
|
+
},
|
257
|
+
});
|
258
|
+
|
259
|
+
if (existingMapping) {
|
260
|
+
|
261
|
+
return {
|
262
|
+
message: "Field mapping already exists for this resource and endpoint"
|
263
|
+
};
|
264
|
+
} else {
|
265
|
+
|
266
|
+
const createdMapping = await prisma.fieldMapping.create({
|
267
|
+
data: {
|
268
|
+
resourceType: fieldMapping.resourceType,
|
269
|
+
requestMapping: fieldMapping.requestMapping,
|
270
|
+
responseMapping: fieldMapping.responseMapping,
|
271
|
+
endpointId: fieldMapping.endpointId,
|
272
|
+
},
|
273
|
+
});
|
274
|
+
return {
|
275
|
+
operation: 'created',
|
276
|
+
mapping: createdMapping,
|
277
|
+
};
|
278
|
+
}
|
279
|
+
} catch (error) {
|
280
|
+
console.error('Error saving field mapping:', error);
|
281
|
+
throw new Error('Failed to save field mapping');
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
/**
|
286
|
+
* Updates an existing field mapping configuration associated with an endpoint.
|
287
|
+
*
|
288
|
+
* @param prisma - PrismaClient instance
|
289
|
+
* @param fieldMappingId - ID of the field mapping configuration to update
|
290
|
+
* @param updateData - Partial data to update (fields in FieldMappingDto)
|
291
|
+
* @returns A promise that resolves to the updated mapping configuration
|
292
|
+
*/
|
293
|
+
export async function updateFieldMapping(
|
294
|
+
prisma: PrismaClient,
|
295
|
+
fieldMappingId: string,
|
296
|
+
updateData: Partial<FieldMappingDto>,
|
297
|
+
) {
|
298
|
+
try {
|
299
|
+
|
300
|
+
const existingMapping = await prisma.fieldMapping.findUnique({
|
301
|
+
where: { id: fieldMappingId },
|
302
|
+
});
|
303
|
+
|
304
|
+
if (!existingMapping) {
|
305
|
+
throw new Error(`Field mapping not found for the given ID: ${fieldMappingId}`);
|
306
|
+
}
|
307
|
+
|
308
|
+
if (updateData.endpointId && updateData.endpointId !== existingMapping.endpointId) {
|
309
|
+
const endpoint = await prisma.endpointConfig.findUnique({
|
310
|
+
where: { id: updateData.endpointId },
|
311
|
+
});
|
312
|
+
if (!endpoint) {
|
313
|
+
throw new Error(`Endpoint with ID ${updateData.endpointId} not found.`);
|
314
|
+
}
|
315
|
+
}
|
316
|
+
|
317
|
+
if (
|
318
|
+
updateData.resourceType &&
|
319
|
+
updateData.resourceType !== existingMapping.resourceType &&
|
320
|
+
(updateData.endpointId ?? existingMapping.endpointId)
|
321
|
+
) {
|
322
|
+
const conflict = await prisma.fieldMapping.findFirst({
|
323
|
+
where: {
|
324
|
+
resourceType: updateData.resourceType,
|
325
|
+
endpointId: updateData.endpointId ?? existingMapping.endpointId,
|
326
|
+
},
|
327
|
+
});
|
328
|
+
if (conflict) {
|
329
|
+
throw new Error(
|
330
|
+
`Field mapping already exists for resource "${updateData.resourceType}" on endpoint ID ${updateData.endpointId ?? existingMapping.endpointId
|
331
|
+
}.`,
|
332
|
+
);
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
const updatedMapping = await prisma.fieldMapping.update({
|
337
|
+
where: { id: fieldMappingId },
|
338
|
+
data: {
|
339
|
+
resourceType: updateData.resourceType ?? existingMapping.resourceType,
|
340
|
+
requestMapping: updateData.requestMapping ?? existingMapping.requestMapping,
|
341
|
+
responseMapping: updateData.responseMapping ?? existingMapping.responseMapping,
|
342
|
+
endpointId: updateData.endpointId ?? existingMapping.endpointId,
|
343
|
+
},
|
344
|
+
});
|
345
|
+
|
346
|
+
return {
|
347
|
+
operation: 'updated',
|
348
|
+
mapping: updatedMapping,
|
349
|
+
};
|
350
|
+
} catch (error) {
|
351
|
+
console.error('Error updating field mapping:', error);
|
352
|
+
throw new Error('Failed to update field mapping');
|
353
|
+
}
|
354
|
+
}
|
355
|
+
|
356
|
+
/**
|
357
|
+
* Calls an API by endpoint ID, applying field mappings dynamically for request body, query params, response, and authentication.
|
358
|
+
*
|
359
|
+
* @param prisma - PrismaClient instance
|
360
|
+
* @param endpointId - The ID of the endpoint to call
|
361
|
+
* @param data - Data to send in the API request (local fields)
|
362
|
+
* @returns The API response with fields mapped back to local format
|
363
|
+
*/
|
364
|
+
export async function callApiWithMapping(
|
365
|
+
prisma: PrismaClient,
|
366
|
+
endpointId: string,
|
367
|
+
data?: Record<string, any>,
|
368
|
+
queryParams?: Record<string, any>,
|
369
|
+
pathParams?: string,
|
370
|
+
subUrl?: string
|
371
|
+
) {
|
372
|
+
try {
|
373
|
+
|
374
|
+
const endpoint = await prisma.endpointConfig.findFirst({
|
375
|
+
where: { id: endpointId },
|
376
|
+
include: { apiConfig: true },
|
377
|
+
});
|
378
|
+
|
379
|
+
if (!endpoint) {
|
380
|
+
throw new Error(`Endpoint with ID ${endpointId} not found.`);
|
381
|
+
}
|
382
|
+
|
383
|
+
const { apiConfig } = endpoint;
|
384
|
+
if (!apiConfig) {
|
385
|
+
throw new Error(`Top-level API configuration not found for endpoint ID ${endpointId}.`);
|
386
|
+
}
|
387
|
+
|
388
|
+
const fieldMapping = await prisma.fieldMapping.findFirst({
|
389
|
+
where: { endpointId },
|
390
|
+
});
|
391
|
+
|
392
|
+
const mappedRequestData = fieldMapping?.requestMapping
|
393
|
+
? Object.entries(fieldMapping.requestMapping as Record<string, string>).reduce(
|
394
|
+
(acc, [localField, apiField]) => {
|
395
|
+
if (data[localField] !== undefined) {
|
396
|
+
acc[apiField] = data[localField];
|
397
|
+
}
|
398
|
+
return acc;
|
399
|
+
},
|
400
|
+
{} as Record<string, any>,
|
401
|
+
)
|
402
|
+
: data;
|
403
|
+
|
404
|
+
const mappedQueryParams = fieldMapping?.requestMapping
|
405
|
+
? Object.entries(fieldMapping.requestMapping as Record<string, string>).reduce(
|
406
|
+
(acc, [localField, apiField]) => {
|
407
|
+
if (queryParams[localField] !== undefined) {
|
408
|
+
acc[apiField] = queryParams[localField];
|
409
|
+
}
|
410
|
+
return acc;
|
411
|
+
},
|
412
|
+
{} as Record<string, any>,
|
413
|
+
)
|
414
|
+
: queryParams;
|
415
|
+
|
416
|
+
const authHeaders = getAuthHeaders(apiConfig);
|
417
|
+
|
418
|
+
let url = apiConfig.baseUrl
|
419
|
+
if (subUrl) {
|
420
|
+
url = url + subUrl
|
421
|
+
} else {
|
422
|
+
url = url + endpoint.suburl
|
423
|
+
}
|
424
|
+
|
425
|
+
let config: AxiosRequestConfig = {
|
426
|
+
method: endpoint.method as AxiosRequestConfig['method'],
|
427
|
+
url,
|
428
|
+
headers: {
|
429
|
+
...endpoint.headers,
|
430
|
+
...authHeaders,
|
431
|
+
},
|
432
|
+
params: mappedQueryParams,
|
433
|
+
data: mappedRequestData,
|
434
|
+
};
|
435
|
+
|
436
|
+
if (pathParams) {
|
437
|
+
config.url = config.url + "/" + pathParams;
|
438
|
+
}
|
439
|
+
|
440
|
+
if (apiConfig.timeout) {
|
441
|
+
config.timeout = apiConfig.timeout;
|
442
|
+
}
|
443
|
+
const maxRetries = apiConfig.retryPolicy?.maxRetries || 0;
|
444
|
+
const retryDelayMs = apiConfig.retryPolicy?.retryDelayMs || 1000;
|
445
|
+
|
446
|
+
let attempt = 0;
|
447
|
+
console.log(config);
|
448
|
+
|
449
|
+
while (attempt <= maxRetries) {
|
450
|
+
try {
|
451
|
+
|
452
|
+
const response = await axios(config);
|
453
|
+
|
454
|
+
const mappedResponseData = fieldMapping?.responseMapping
|
455
|
+
? applyResponseMapping(response.data, fieldMapping.responseMapping)
|
456
|
+
: response.data;
|
457
|
+
|
458
|
+
return mappedResponseData;
|
459
|
+
} catch (error: any) {
|
460
|
+
attempt++;
|
461
|
+
|
462
|
+
const retryOnStatusCodes = apiConfig.retryPolicy?.retryOnStatusCodes || [];
|
463
|
+
const shouldRetry =
|
464
|
+
retryOnStatusCodes.length === 0 ||
|
465
|
+
retryOnStatusCodes.includes(String(error.response?.status)) ||
|
466
|
+
!error.response;
|
467
|
+
|
468
|
+
if (attempt <= maxRetries && shouldRetry) {
|
469
|
+
console.warn(
|
470
|
+
`Retrying request... Attempt ${attempt} of ${maxRetries}. Waiting ${retryDelayMs}ms.`,
|
471
|
+
);
|
472
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
473
|
+
} else {
|
474
|
+
console.error('Error calling API:', error);
|
475
|
+
throw new Error('Failed to call API with mapping');
|
476
|
+
}
|
477
|
+
}
|
478
|
+
}
|
479
|
+
} catch (error) {
|
480
|
+
console.error('Error calling API:', error);
|
481
|
+
throw new Error('Failed to call API with mapping');
|
482
|
+
}
|
483
|
+
}
|
484
|
+
|
485
|
+
/**
|
486
|
+
* Executes a dynamic function based on its configuration.
|
487
|
+
*
|
488
|
+
* @param functionName - Name of the function to execute
|
489
|
+
* @param input - Input data for the function
|
490
|
+
* @returns The final output of the function
|
491
|
+
*/
|
492
|
+
export async function executeDynamicFunction(prisma: PrismaClient, orgId: string, functionName: string, input: Record<string, any>) {
|
493
|
+
try {
|
494
|
+
|
495
|
+
const functionConfig = await prisma.functionConfig.findFirst({
|
496
|
+
where: { name: functionName, orgId },
|
497
|
+
});
|
498
|
+
|
499
|
+
if (!functionConfig) {
|
500
|
+
throw new Error(`Function with name "${functionName}" not found.`);
|
501
|
+
}
|
502
|
+
|
503
|
+
const { logic } = functionConfig;
|
504
|
+
|
505
|
+
let currentData = input;
|
506
|
+
|
507
|
+
for (const step of Object.values(logic) as any[]) {
|
508
|
+
const { apiId, requestMapping, responseMapping, queryParamsMapping, pathParamsMapping } = step;
|
509
|
+
|
510
|
+
console.log(`currentData: ${JSON.stringify(currentData)}`);
|
511
|
+
console.log(`Request Mapping: ${JSON.stringify(requestMapping)}`);
|
512
|
+
console.log(`Response Mapping: ${JSON.stringify(responseMapping)}`);
|
513
|
+
console.log(`Query Params Mapping: ${JSON.stringify(queryParamsMapping)}`);
|
514
|
+
console.log(`Path Params Mapping: ${JSON.stringify(pathParamsMapping)}`);
|
515
|
+
|
516
|
+
const requestData = mapData(currentData, requestMapping);
|
517
|
+
console.log(`Request Data: ${JSON.stringify(requestData)}`);
|
518
|
+
|
519
|
+
const endpoint = await prisma.endpointConfig.findUnique({
|
520
|
+
where: { id: apiId },
|
521
|
+
});
|
522
|
+
|
523
|
+
if (!endpoint) {
|
524
|
+
throw new Error(`Endpoint with ID ${apiId} not found.`);
|
525
|
+
}
|
526
|
+
|
527
|
+
const suburl = substitutePathParams(endpoint.suburl, currentData, pathParamsMapping);
|
528
|
+
|
529
|
+
console.log(`Substituted URL: ${suburl}`);
|
530
|
+
|
531
|
+
const mappedQueryParams = mapData(currentData, queryParamsMapping, true);
|
532
|
+
|
533
|
+
console.log(`Mapped Query Params: ${JSON.stringify(mappedQueryParams)}`);
|
534
|
+
|
535
|
+
const apiResponse = await callApiWithMapping(prisma, apiId, requestData, mappedQueryParams, null, suburl);
|
536
|
+
console.log(`API Response: ${JSON.stringify(apiResponse)}`);
|
537
|
+
|
538
|
+
const newData = mapData(apiResponse, responseMapping);
|
539
|
+
currentData = { ...currentData, ...newData }
|
540
|
+
}
|
541
|
+
|
542
|
+
return currentData;
|
543
|
+
} catch (error) {
|
544
|
+
console.error('Error executing dynamic function:', error);
|
545
|
+
throw new Error(`Failed to execute function "${functionName}".`);
|
546
|
+
}
|
547
|
+
}
|
548
|
+
|
549
|
+
/**
|
550
|
+
* Substitutes path parameters in the URL using the provided mapping and data.
|
551
|
+
*
|
552
|
+
* @param url - The URL with placeholders
|
553
|
+
* @param data - The data to use for substitution
|
554
|
+
* @param pathParamsMapping - Mapping of placeholders to data fields
|
555
|
+
* @returns The URL with substituted path parameters
|
556
|
+
*/
|
557
|
+
function substitutePathParams(
|
558
|
+
url: string,
|
559
|
+
data: Record<string, any>,
|
560
|
+
pathParamsMapping: Record<string, string>,
|
561
|
+
): string {
|
562
|
+
if (!pathParamsMapping) return url
|
563
|
+
return Object.entries(pathParamsMapping).reduce((updatedUrl, [placeholder, field]) => {
|
564
|
+
const value = data[field];
|
565
|
+
if (!value) {
|
566
|
+
throw new Error(`Missing value for path parameter "${placeholder}"`);
|
567
|
+
}
|
568
|
+
return updatedUrl.replace(`:${placeholder}`, value);
|
569
|
+
}, url);
|
570
|
+
}
|
571
|
+
|
572
|
+
/**
|
573
|
+
* Maps data based on a given mapping configuration.
|
574
|
+
*
|
575
|
+
* @param data - The input data
|
576
|
+
* @param mapping - The mapping configuration
|
577
|
+
* @returns The mapped data
|
578
|
+
*/
|
579
|
+
function mapData(data: any, mapping: Record<string, any>, isQueryParams: boolean = false): any {
|
580
|
+
if (!mapping || typeof data !== 'object' || data === null) {
|
581
|
+
if (isQueryParams) {
|
582
|
+
return {};
|
583
|
+
}
|
584
|
+
return data;
|
585
|
+
}
|
586
|
+
|
587
|
+
if (Array.isArray(data)) {
|
588
|
+
return data.map((item) => mapObject(item, mapping));
|
589
|
+
}
|
590
|
+
|
591
|
+
return mapObject(data, mapping);
|
592
|
+
}
|
593
|
+
function evaluateExpression(template: string, data: Record<string, any>): any {
|
594
|
+
const expressionRegex = /\{([^}]+)\}((?:\.[a-zA-Z]+\([^)]*\))*)/;
|
595
|
+
const match = template.match(expressionRegex);
|
596
|
+
|
597
|
+
if (!match) {
|
598
|
+
return template.replace(/\{([^}]+)\}/g, (_, path) => {
|
599
|
+
// Handle nested paths (e.g., "address.line1")
|
600
|
+
return getNestedValue(data, path) ?? '';
|
601
|
+
});
|
602
|
+
}
|
603
|
+
|
604
|
+
const fieldPath = match[1];
|
605
|
+
const functionChain = match[2];
|
606
|
+
let value = getNestedValue(data, fieldPath);
|
607
|
+
|
608
|
+
if (!functionChain) return value;
|
609
|
+
|
610
|
+
const functionCallRegex = /\.([a-zA-Z]+)\(([^)]*)\)/g;
|
611
|
+
let fnMatch;
|
612
|
+
|
613
|
+
while ((fnMatch = functionCallRegex.exec(functionChain)) !== null) {
|
614
|
+
const fnName = fnMatch[1];
|
615
|
+
const fnArgs = fnMatch[2];
|
616
|
+
|
617
|
+
const args = parseFunctionArgs(fnArgs);
|
618
|
+
|
619
|
+
switch (fnName.toLowerCase()) {
|
620
|
+
case 'tolowercase':
|
621
|
+
if (typeof value === 'string') {
|
622
|
+
value = value.toLowerCase();
|
623
|
+
}
|
624
|
+
break;
|
625
|
+
case 'touppercase':
|
626
|
+
if (typeof value === 'string') {
|
627
|
+
value = value.toUpperCase();
|
628
|
+
}
|
629
|
+
break;
|
630
|
+
case 'split':
|
631
|
+
if (typeof value === 'string') {
|
632
|
+
|
633
|
+
const delimiter = args[0] ?? '';
|
634
|
+
value = value.split(delimiter);
|
635
|
+
}
|
636
|
+
break;
|
637
|
+
case 'slice':
|
638
|
+
if (typeof value === 'string' || Array.isArray(value)) {
|
639
|
+
const start = args[0] !== undefined ? args[0] : 0;
|
640
|
+
const end = args[1] !== undefined ? args[1] : undefined;
|
641
|
+
value = value.slice(start, end);
|
642
|
+
}
|
643
|
+
break;
|
644
|
+
case 'join':
|
645
|
+
if (Array.isArray(value)) {
|
646
|
+
const delimiter = args[0] || '';
|
647
|
+
value = value.join(delimiter);
|
648
|
+
}
|
649
|
+
break;
|
650
|
+
default:
|
651
|
+
|
652
|
+
console.warn(`Unrecognized function: ${fnName}`);
|
653
|
+
break;
|
654
|
+
}
|
655
|
+
}
|
656
|
+
|
657
|
+
return value;
|
658
|
+
}
|
659
|
+
|
660
|
+
function parseFunctionArgs(argString: string): any[] {
|
661
|
+
if (!argString.trim()) return [];
|
662
|
+
|
663
|
+
return argString.split(',').map((arg) => {
|
664
|
+
arg = arg.trim();
|
665
|
+
|
666
|
+
if (
|
667
|
+
(arg.startsWith("'") && arg.endsWith("'")) ||
|
668
|
+
(arg.startsWith('"') && arg.endsWith('"'))
|
669
|
+
) {
|
670
|
+
return arg.slice(1, -1);
|
671
|
+
}
|
672
|
+
|
673
|
+
const num = Number(arg);
|
674
|
+
if (!isNaN(num)) {
|
675
|
+
return num;
|
676
|
+
}
|
677
|
+
|
678
|
+
return arg;
|
679
|
+
});
|
680
|
+
}
|
681
|
+
|
682
|
+
|
683
|
+
function replaceValue(template: string, data: any): any {
|
684
|
+
if (!template.includes('{')) {
|
685
|
+
return template;
|
686
|
+
}
|
687
|
+
|
688
|
+
return template.replace(/\{([^}]+)\}/g, (match, field) => {
|
689
|
+
const value = data[field];
|
690
|
+
return value !== undefined ? value : '';
|
691
|
+
});
|
692
|
+
}
|
693
|
+
|
694
|
+
/**
|
695
|
+
* Maps an object's fields based on a mapping configuration.
|
696
|
+
*
|
697
|
+
* @param obj - The input object
|
698
|
+
* @param mapping - The mapping configuration
|
699
|
+
* @returns The mapped object
|
700
|
+
*/
|
701
|
+
function mapObject(obj: Record<string, any>, mapping: Record<string, any>): Record<string, any> {
|
702
|
+
const result: Record<string, any> = {};
|
703
|
+
|
704
|
+
if (mapping.resourceType) {
|
705
|
+
result.resourceType = mapping.resourceType;
|
706
|
+
}
|
707
|
+
|
708
|
+
for (const [mappingKey, mappingValue] of Object.entries(mapping)) {
|
709
|
+
if (mappingKey === 'resourceType') continue;
|
710
|
+
|
711
|
+
if (typeof mappingValue === 'string') {
|
712
|
+
if (mappingValue.includes('[?]')) {
|
713
|
+
handleArrayShortcut(result, obj, mappingValue, mappingKey);
|
714
|
+
} else {
|
715
|
+
result[mappingKey] = replaceValue(mappingValue, obj);
|
716
|
+
}
|
717
|
+
} else if (Array.isArray(mappingValue)) {
|
718
|
+
result[mappingKey] = mappingValue.map((element) => {
|
719
|
+
if (typeof element === 'string') {
|
720
|
+
return replaceValue(element, obj);
|
721
|
+
} else if (Array.isArray(element)) {
|
722
|
+
return element.map((sub) =>
|
723
|
+
typeof sub === 'string' ? replaceValue(sub, obj) : sub
|
724
|
+
);
|
725
|
+
} else if (typeof element === 'object' && element !== null) {
|
726
|
+
return mapObject(obj, element);
|
727
|
+
} else {
|
728
|
+
return element;
|
729
|
+
}
|
730
|
+
});
|
731
|
+
} else if (typeof mappingValue === 'object' && mappingValue !== null) {
|
732
|
+
// For nested objects, process each field with the original data
|
733
|
+
const nestedResult: Record<string, any> = {};
|
734
|
+
for (const [nestedKey, nestedValue] of Object.entries(mappingValue)) {
|
735
|
+
if (typeof nestedValue === 'string') {
|
736
|
+
nestedResult[nestedKey] = replaceValue(nestedValue, obj);
|
737
|
+
} else {
|
738
|
+
nestedResult[nestedKey] = mapObject(obj, nestedValue);
|
739
|
+
}
|
740
|
+
}
|
741
|
+
result[mappingKey] = nestedResult;
|
742
|
+
} else {
|
743
|
+
result[mappingKey] = obj[mappingKey];
|
744
|
+
}
|
745
|
+
}
|
746
|
+
|
747
|
+
return result;
|
748
|
+
}
|
749
|
+
|
750
|
+
function handleArrayShortcut(
|
751
|
+
resultObj: Record<string, any>,
|
752
|
+
sourceObj: Record<string, any>,
|
753
|
+
mappingValue: string,
|
754
|
+
mappingKey: string,
|
755
|
+
): void {
|
756
|
+
const [arrayField, entryTemplate] = mappingValue.split('[?].');
|
757
|
+
if (!resultObj[arrayField]) {
|
758
|
+
resultObj[arrayField] = [];
|
759
|
+
}
|
760
|
+
|
761
|
+
const templateParts = entryTemplate.split(',');
|
762
|
+
const entry: Record<string, any> = {};
|
763
|
+
|
764
|
+
templateParts.forEach(part => {
|
765
|
+
const [field, valueTemplate] = part.split('=');
|
766
|
+
const fieldValue = replaceValue(valueTemplate, sourceObj);
|
767
|
+
entry[field] = fieldValue;
|
768
|
+
});
|
769
|
+
|
770
|
+
const existingIndex = resultObj[arrayField].findIndex((e: any) => e.system === entry.system);
|
771
|
+
if (existingIndex === -1) {
|
772
|
+
resultObj[arrayField].push(entry);
|
773
|
+
}
|
774
|
+
}
|
775
|
+
function getNestedValue(obj: Record<string, any>, path: string): any {
|
776
|
+
return path.split('.').reduce((current, part) => {
|
777
|
+
return current?.[part];
|
778
|
+
}, obj);
|
779
|
+
}
|
780
|
+
/**
|
781
|
+
* Prepares authentication headers based on the top-level API configuration.
|
782
|
+
*
|
783
|
+
* @param apiConfig - The top-level API configuration
|
784
|
+
* @returns A Record of authentication headers
|
785
|
+
*/
|
786
|
+
function getAuthHeaders(apiConfig: any): Record<string, string> {
|
787
|
+
switch (apiConfig.authType) {
|
788
|
+
case AuthType.BASIC:
|
789
|
+
if (!apiConfig.infoForAuthType?.username || !apiConfig.infoForAuthType?.password) {
|
790
|
+
throw new Error('Missing credentials for BASIC auth.');
|
791
|
+
}
|
792
|
+
const credentials = Buffer.from(
|
793
|
+
`${apiConfig.infoForAuthType.username}:${apiConfig.infoForAuthType.password}`,
|
794
|
+
).toString('base64');
|
795
|
+
return { Authorization: `Basic ${credentials}` };
|
796
|
+
|
797
|
+
case AuthType.BEARER:
|
798
|
+
if (!apiConfig.infoForAuthType?.token) {
|
799
|
+
throw new Error('Missing token for BEARER auth.');
|
800
|
+
}
|
801
|
+
return { Authorization: `Bearer ${apiConfig.infoForAuthType.token}` };
|
802
|
+
|
803
|
+
case AuthType.API_KEY:
|
804
|
+
if (!apiConfig.infoForAuthType?.apiKeyHeader || !apiConfig.infoForAuthType?.apiKey) {
|
805
|
+
throw new Error('Missing API key information.');
|
806
|
+
}
|
807
|
+
return { [apiConfig.infoForAuthType.apiKeyHeader]: apiConfig.infoForAuthType.apiKey };
|
808
|
+
|
809
|
+
case AuthType.NONE:
|
810
|
+
default:
|
811
|
+
return {};
|
812
|
+
}
|
813
|
+
}
|
814
|
+
|
815
|
+
/**
|
816
|
+
* Recursively applies the response mapping to handle nested structures.
|
817
|
+
*
|
818
|
+
* @param response - The API response data
|
819
|
+
* @param responseMapping - The field mapping configuration
|
820
|
+
* @returns The mapped response
|
821
|
+
*/
|
822
|
+
function applyResponseMapping(
|
823
|
+
response: any,
|
824
|
+
responseMapping: Record<string, any>,
|
825
|
+
): any {
|
826
|
+
if (Array.isArray(response)) {
|
827
|
+
|
828
|
+
return response.map((item) => applyResponseMapping(item, responseMapping));
|
829
|
+
} else if (typeof response === 'object' && response !== null) {
|
830
|
+
|
831
|
+
return Object.entries(responseMapping).reduce((mappedObject, [apiField, localField]) => {
|
832
|
+
if (response[apiField] !== undefined) {
|
833
|
+
if (typeof localField === 'string') {
|
834
|
+
|
835
|
+
mappedObject[localField] = response[apiField];
|
836
|
+
} else if (typeof localField === 'object') {
|
837
|
+
|
838
|
+
mappedObject[localField] = applyResponseMapping(response[apiField], localField);
|
839
|
+
}
|
840
|
+
}
|
841
|
+
return mappedObject;
|
842
|
+
}, {} as Record<string, any>);
|
843
|
+
} else {
|
844
|
+
|
845
|
+
return response;
|
846
|
+
}
|
847
|
+
}
|
848
|
+
|
849
|
+
export { ApiConfigDto } from './dto/api-config.dto';
|
850
|
+
export { AuthType } from './dto/api-config.dto';
|