@zauto/api-connector 0.0.1 → 0.0.2

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/index.ts +136 -123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauto/api-connector",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A NestJS-based API connector for managing integrations.",
5
5
  "author": "Saurabh Jain <saurabh@zautoai.com>",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -489,9 +489,13 @@ export async function callApiWithMapping(
489
489
  * @param input - Input data for the function
490
490
  * @returns The final output of the function
491
491
  */
492
- export async function executeDynamicFunction(prisma: PrismaClient, orgId: string, functionName: string, input: Record<string, any>) {
492
+ export async function executeDynamicFunction(
493
+ prisma: PrismaClient,
494
+ orgId: string,
495
+ functionName: string,
496
+ input: Record<string, any>,
497
+ ) {
493
498
  try {
494
-
495
499
  const functionConfig = await prisma.functionConfig.findFirst({
496
500
  where: { name: functionName, orgId },
497
501
  });
@@ -501,45 +505,78 @@ export async function executeDynamicFunction(prisma: PrismaClient, orgId: string
501
505
  }
502
506
 
503
507
  const { logic } = functionConfig;
504
-
505
508
  let currentData = input;
506
509
 
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.`);
510
+ for (const stepKey of Object.keys(logic)) {
511
+ const step = logic[stepKey];
512
+ const {
513
+ apiId,
514
+ requestMapping,
515
+ responseMapping,
516
+ queryParamsMapping,
517
+ pathParamsMapping,
518
+ condition,
519
+ loopUntil,
520
+ onFailure,
521
+ maxIterations,
522
+ } = step;
523
+
524
+ // Evaluate the condition, if provided
525
+ if (condition && !evaluateCondition(condition, currentData)) {
526
+ console.warn(`Condition failed for step ${stepKey}.`);
527
+ if (onFailure === 'exit') {
528
+ console.warn(`Exiting execution due to failure in step ${stepKey}.`);
529
+ return currentData; // Return current data if exiting
530
+ }
531
+ continue; // Skip to the next step if onFailure is "continue"
525
532
  }
526
533
 
527
- const suburl = substitutePathParams(endpoint.suburl, currentData, pathParamsMapping);
534
+ console.log(`Executing step ${stepKey}...`);
535
+ let iterations = 0;
536
+
537
+ // Execute the step in a loop if `loopUntil` is specified
538
+ do {
539
+ if (maxIterations && iterations >= maxIterations) {
540
+ console.warn(`Max iterations reached for step ${stepKey}.`);
541
+ break;
542
+ }
543
+ iterations++;
544
+ const requestData = mapData(currentData, requestMapping);
545
+ const endpoint = await prisma.endpointConfig.findUnique({ where: { id: apiId } });
528
546
 
529
- console.log(`Substituted URL: ${suburl}`);
547
+ if (!endpoint) {
548
+ throw new Error(`Endpoint with ID ${apiId} not found.`);
549
+ }
530
550
 
531
- const mappedQueryParams = mapData(currentData, queryParamsMapping, true);
551
+ const suburl = substitutePathParams(endpoint.suburl, currentData, pathParamsMapping);
552
+ const mappedQueryParams = mapData(currentData, queryParamsMapping, true);
532
553
 
533
- console.log(`Mapped Query Params: ${JSON.stringify(mappedQueryParams)}`);
554
+ try {
555
+ const apiResponse = await callApiWithMapping(prisma, apiId, requestData, mappedQueryParams, null, suburl);
556
+ const newData = mapData(apiResponse, responseMapping);
557
+ currentData = { ...currentData, ...newData };
534
558
 
535
- const apiResponse = await callApiWithMapping(prisma, apiId, requestData, mappedQueryParams, null, suburl);
536
- console.log(`API Response: ${JSON.stringify(apiResponse)}`);
559
+ // Log the loop status
560
+ console.log(`Step ${stepKey} loop iteration completed.`);
537
561
 
538
- const newData = mapData(apiResponse, responseMapping);
539
- currentData = { ...currentData, ...newData }
562
+ // Break the loop if `loopUntil` evaluates to true
563
+ if (loopUntil && evaluateCondition(loopUntil, currentData)) {
564
+ console.log(`LoopUntil condition met for step ${stepKey}, exiting loop.`);
565
+ break;
566
+ }
567
+ } catch (error) {
568
+ console.error(`API call failed for step ${stepKey}:`, error);
569
+ if (onFailure === 'exit') {
570
+ console.warn(`Exiting execution due to API call failure in step ${stepKey}.`);
571
+ throw new Error(`Execution stopped at step ${stepKey}.`);
572
+ }
573
+ console.warn(`Continuing to next step after failure in step ${stepKey}.`);
574
+ break; // Exit loop on failure
575
+ }
576
+ } while (loopUntil && !evaluateCondition(loopUntil, currentData));
540
577
  }
541
578
 
542
- return currentData;
579
+ return currentData; // Final data after executing all steps
543
580
  } catch (error) {
544
581
  console.error('Error executing dynamic function:', error);
545
582
  throw new Error(`Failed to execute function "${functionName}".`);
@@ -590,67 +627,40 @@ function mapData(data: any, mapping: Record<string, any>, isQueryParams: boolean
590
627
 
591
628
  return mapObject(data, mapping);
592
629
  }
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
630
 
617
- const args = parseFunctionArgs(fnArgs);
618
631
 
619
- switch (fnName.toLowerCase()) {
620
- case 'tolowercase':
621
- if (typeof value === 'string') {
632
+ function evaluateExpression(template: string, data: Record<string, any>): any {
633
+ // Remove surrounding {} if present
634
+ const cleanTemplate = template.replace(/^\{|\}$/g, '');
635
+
636
+ // Check for method chaining
637
+ const parts = cleanTemplate.split('.');
638
+ let value = getNestedValue(data, parts[0]);
639
+
640
+ // Apply method chain
641
+ for (let i = 1; i < parts.length; i++) {
642
+ const methodMatch = parts[i].match(/(\w+)\(([^)]*)\)/);
643
+ if (methodMatch) {
644
+ const method = methodMatch[1];
645
+ const args = methodMatch[2] ? parseFunctionArgs(methodMatch[2]) : [];
646
+
647
+ switch (method) {
648
+ case 'split':
649
+ value = value.split(args[0] || ' ');
650
+ break;
651
+ case 'slice':
652
+ value = value.slice(args[0], args[1]);
653
+ break;
654
+ case 'join':
655
+ value = value.join(args[0] || '');
656
+ break;
657
+ case 'toLowerCase':
622
658
  value = value.toLowerCase();
623
- }
624
- break;
625
- case 'touppercase':
626
- if (typeof value === 'string') {
659
+ break;
660
+ case 'toUpperCase':
627
661
  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;
662
+ break;
663
+ }
654
664
  }
655
665
  }
656
666
 
@@ -679,35 +689,29 @@ function parseFunctionArgs(argString: string): any[] {
679
689
  });
680
690
  }
681
691
 
682
-
683
692
  function replaceValue(template: string, data: any): any {
684
693
  if (!template.includes('{')) {
685
694
  return template;
686
695
  }
687
696
 
688
- return template.replace(/\{([^}]+)\}/g, (match, field) => {
689
- const value = data[field];
690
- return value !== undefined ? value : '';
697
+ if (/^\{[^}]+\}$/.test(template)) {
698
+ return evaluateExpression(template, data);
699
+ }
700
+ return template.replace(/\{[^}]+\}/g, (match) => {
701
+ try {
702
+ const evaluated = evaluateExpression(match, data);
703
+ return evaluated?.toString ? evaluated.toString() : '';
704
+ } catch (error) {
705
+ console.warn(`Error evaluating ${match}:`, error);
706
+ return match;
707
+ }
691
708
  });
692
709
  }
693
710
 
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
711
  function mapObject(obj: Record<string, any>, mapping: Record<string, any>): Record<string, any> {
702
712
  const result: Record<string, any> = {};
703
713
 
704
- if (mapping.resourceType) {
705
- result.resourceType = mapping.resourceType;
706
- }
707
-
708
714
  for (const [mappingKey, mappingValue] of Object.entries(mapping)) {
709
- if (mappingKey === 'resourceType') continue;
710
-
711
715
  if (typeof mappingValue === 'string') {
712
716
  if (mappingValue.includes('[?]')) {
713
717
  handleArrayShortcut(result, obj, mappingValue, mappingKey);
@@ -729,7 +733,7 @@ function mapObject(obj: Record<string, any>, mapping: Record<string, any>): Reco
729
733
  }
730
734
  });
731
735
  } else if (typeof mappingValue === 'object' && mappingValue !== null) {
732
- // For nested objects, process each field with the original data
736
+
733
737
  const nestedResult: Record<string, any> = {};
734
738
  for (const [nestedKey, nestedValue] of Object.entries(mappingValue)) {
735
739
  if (typeof nestedValue === 'string') {
@@ -761,28 +765,22 @@ function handleArrayShortcut(
761
765
  const templateParts = entryTemplate.split(',');
762
766
  const entry: Record<string, any> = {};
763
767
 
764
- templateParts.forEach(part => {
768
+ templateParts.forEach((part) => {
765
769
  const [field, valueTemplate] = part.split('=');
766
770
  const fieldValue = replaceValue(valueTemplate, sourceObj);
767
771
  entry[field] = fieldValue;
768
772
  });
769
773
 
770
- const existingIndex = resultObj[arrayField].findIndex((e: any) => e.system === entry.system);
771
- if (existingIndex === -1) {
772
- resultObj[arrayField].push(entry);
773
- }
774
+ // Simply push the new entry every time, no uniqueness check:
775
+ resultObj[arrayField].push(entry);
774
776
  }
777
+
775
778
  function getNestedValue(obj: Record<string, any>, path: string): any {
776
779
  return path.split('.').reduce((current, part) => {
777
780
  return current?.[part];
778
781
  }, obj);
779
782
  }
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
- */
783
+
786
784
  function getAuthHeaders(apiConfig: any): Record<string, string> {
787
785
  switch (apiConfig.authType) {
788
786
  case AuthType.BASIC:
@@ -812,13 +810,6 @@ function getAuthHeaders(apiConfig: any): Record<string, string> {
812
810
  }
813
811
  }
814
812
 
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
813
  function applyResponseMapping(
823
814
  response: any,
824
815
  responseMapping: Record<string, any>,
@@ -846,5 +837,27 @@ function applyResponseMapping(
846
837
  }
847
838
  }
848
839
 
840
+ /**
841
+ * Evaluates a condition dynamically.
842
+ *
843
+ * @param condition - A string representing the condition (e.g., "{id} !== null").
844
+ * @param data - The data context for evaluating the condition.
845
+ * @returns A boolean indicating whether the condition is met.
846
+ */
847
+ function evaluateCondition(condition: string, data: Record<string, any>): boolean {
848
+ try {
849
+ const evaluatedCondition = condition.replace(/\{([^}]+)\}/g, (_, field) => {
850
+ const value = getNestedValue(data, field);
851
+ return JSON.stringify(value); // Safely inject values into the expression
852
+ });
853
+
854
+ return eval(evaluatedCondition); // Evaluate the resulting condition expression
855
+ } catch (error) {
856
+ console.error('Error evaluating condition:', error);
857
+ return false; // Default to false if condition evaluation fails
858
+ }
859
+ }
860
+
861
+
849
862
  export { ApiConfigDto } from './dto/api-config.dto';
850
863
  export { AuthType } from './dto/api-config.dto';