@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/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';