Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

exporter: external api publisher #636

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Conversation

bangarang
Copy link
Collaborator

  • chore: add /validators folder
  • koala: initial commit

Please explain how to summarize this PR for the Changelog:

Tell code reviewer how and what to test:

@flatfile-nullify
Copy link

flatfile-nullify bot commented Sep 25, 2024

Nullify Code Vulnerabilities

1 findings found in this pull request

🔴 CRITICAL 🟡 HIGH 🔵 MEDIUM ⚪ LOW
0 1 0 0

You can find a list of all findings here

@bangarang bangarang changed the title exporters: external api publisher exporter: external api publisher Sep 25, 2024
@carlbrugger carlbrugger force-pushed the feat/ExternalAPIPublisher branch from dc31ba5 to 49f395e Compare October 17, 2024 20:22
Comment on lines +23 to +30
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullify Code Language: TypeScript 🟡 HIGH Severity CWE-918

Rules lgpl javascript ssrf rule node ssrf

This application allows user-controlled URLs to be passed directly to HTTP client libraries. This can result in Server-Side Request Forgery (SSRF). SSRF refers to an attack where the attacker can abuse functionality on the server to force it to make requests to other internal systems within your infrastructure that are not directly exposed to the internet. This allows the attacker to access internal resources they do not have direct access to.
Some risks of SSRF are:

  • Access and manipulation of internal databases, APIs, or administrative panels - Ability to scan internal network architecture and services - Can be used to pivot attacks into the internal network - Circumvent network segregation and firewall rules
    To avoid this, try using hardcoded HTTP request calls or a whitelisting object to check whether the user input is trying to access allowed resources or not.
    Here is an example: var whitelist = [ "https://example.com", "https://example.com/sample" ] app.get('/ssrf/node-ssrf/axios/safe/3', function (req, res) { if(whitelist.includes(req.query.url)){ axios.get(url, {}) .then(function (response) { console.log(response); }) .catch(function (response) { console.log(response); }) } }); For more information on SSRF see OWASP: https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

Here's how you might fix this potential vulnerability

In the initial code, the 'apiEndpoint' parameter is passed directly to the 'fetch' function without any validation, which could lead to Server Side Request Forgery (SSRF) if an attacker can control the value of 'apiEndpoint'. The modified code mitigates this vulnerability by validating the 'apiEndpoint' parameter using the 'URL' class before passing it to the 'fetch' function. It checks the protocol and hostname of the URL against a whitelist of allowed values, and throws an error if the URL is not allowed.

autoFixesExperimental

Add URL validation before making the fetch request

Suggested change
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
const url = new URL(apiEndpoint);
if (url.protocol !== 'https:' || !['api.example.com', 'api2.example.com'].includes(url.hostname)) {
throw new Error('Invalid API endpoint');
}
const response = await fetch(apiEndpoint, {

poweredByNullify

Reply with /nullify to interact with me like another developer
(you will need to refresh the page for updates)

@bangarang bangarang marked this pull request as ready for review October 22, 2024 15:24
Copy link
Contributor

coderabbitai bot commented Oct 22, 2024

Walkthrough

This pull request introduces the @flatfile/plugin-export-external-api, which includes comprehensive documentation detailing its capabilities for exporting data from Flatfile to an external API. The changes encompass configuration files for Jest and Rollup, a new TypeScript plugin implementation, utility functions for handling records, and updates to the main export module. Key features of the plugin include batch processing, error handling, retry logic, and user notifications.

Changes

File Path Change Summary
export/external-api/README.MD Documentation for @flatfile/plugin-export-external-api, outlining features and parameters.
export/external-api/jest.config.js New Jest configuration file for testing in a Node.js environment.
export/external-api/rollup.config.mjs New Rollup configuration file for customizing the build process.
export/external-api/src/external.api.plugin.ts Introduced PluginConfig interface and exportToExternalAPIPlugin function for the new plugin.
export/external-api/src/external.api.utils.ts Added utility functions: exportToExternalAPI and retryOperation.
export/external-api/src/index.ts New export for all entities from external.api.plugin.
.changeset/short-papayas-share.md Initial release noted for package @flatfile/plugin-export-external-api.
flatfilers/sandbox/src/index.ts Modified functionality to replace pivot table export with export to external API, updating listener configuration and actions.

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (4)
export/external-api/jest.config.js (1)

14-14: Consider removing forceExit: true.

While forceExit: true ensures Jest exits after tests complete, it might mask issues with asynchronous operations or resources not being properly cleaned up. Consider removing this option and addressing any hanging processes directly in your test teardown.

export/external-api/README.MD (1)

22-43: Adjust heading levels for consistency.

The Parameters section provides clear and concise descriptions for all required parameters. However, the heading levels should be adjusted for consistency with markdown best practices.

Apply this change to fix the heading levels:

-## Parameters
+## Parameters
 
-#### `job` - `string` - (required)
+### `job` - `string` - (required)
 The type of job to create.
 
-#### `apiEndpoint` - `string` - (required)
+### `apiEndpoint` - `string` - (required)
 The URL of the external API endpoint to send data to.
 
-#### `authToken` - `string` - (required)
+### `authToken` - `string` - (required)
 The authentication token for the external API.
 
-#### `dataMapping` - `object` - (required)
+### `dataMapping` - `object` - (required)
 A mapping of Flatfile field names to external API field names.
 
-#### `batchSize` - `number` - (required)
+### `batchSize` - `number` - (required)
 The number of records to process in each batch.
 
-#### `maxRetries` - `number` - (required)
+### `maxRetries` - `number` - (required)
 The maximum number of retry attempts for failed API calls.
 
-#### `retryDelay` - `number` - (required)
+### `retryDelay` - `number` - (required)
 The delay (in milliseconds) between retry attempts.
🧰 Tools
🪛 Markdownlint

24-24: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

export/external-api/src/external.api.plugin.ts (2)

84-88: Provide more informative error messages

When an error occurs during export, the thrown error message is generic: 'An error occurred during export.' Consider including the original error message to aid in debugging.

Update the throw statement to include the original error message:

     console.error('Error during export:', (error as Error).message)

-    throw new Error('An error occurred during export.')
+    throw new Error(`An error occurred during export: ${(error as Error).message}`)

70-70: Use a logging library instead of console.error

Using console.error for logging errors may not align with the application's logging practices. Consider using a dedicated logging library or the application's standard logging mechanism to handle error logging.

Replace console.error calls with the appropriate logger methods.

Also applies to: 85-85

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d7c7d7e and 438dfb7.

⛔ Files ignored due to path filters (2)
  • export/external-api/package.json is excluded by !**/*.json
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json
📒 Files selected for processing (6)
  • export/external-api/README.MD (1 hunks)
  • export/external-api/jest.config.js (1 hunks)
  • export/external-api/rollup.config.mjs (1 hunks)
  • export/external-api/src/external.api.plugin.ts (1 hunks)
  • export/external-api/src/external.api.utils.ts (1 hunks)
  • export/external-api/src/index.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • export/external-api/rollup.config.mjs
  • export/external-api/src/index.ts
🧰 Additional context used
🪛 Markdownlint
export/external-api/README.MD

24-24: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

🔇 Additional comments (8)
export/external-api/jest.config.js (2)

1-16: LGTM! Comprehensive Jest configuration.

The Jest configuration is well-structured and includes essential settings for a Node.js environment with TypeScript support. It covers important aspects such as environment setup, TypeScript transformation, and test timeouts.


15-15: Caution: passWithNoTests: true might hide issues.

Setting passWithNoTests: true allows the test suite to pass even if no tests are found. This could potentially hide issues where tests are accidentally skipped or not detected. Consider setting this to false to ensure that tests are always run as expected.

To ensure that tests are present and being detected, you can run the following command:

✅ Verification successful

Tests are present and being detected as expected.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for the presence of test files in the project

# Test: Search for test files. Expect: At least one test file should be found.
fd -e test.ts -e test.js -e spec.ts -e spec.js

Length of output: 2015

export/external-api/README.MD (3)

1-9: LGTM: Infocard section is informative and well-structured.

The infocard provides a clear and concise overview of the plugin's purpose and functionality. The inclusion of the event type "job:ready" is particularly helpful for integration purposes.


11-20: LGTM: Features section is comprehensive and well-presented.

The list of features provides a clear overview of the plugin's capabilities, covering crucial aspects such as data mapping, batch processing, error handling, and user notifications. This section effectively communicates the value proposition of the plugin to potential users.


1-75: Overall, excellent documentation with minor improvements needed.

The README provides comprehensive and well-structured documentation for the @flatfile/plugin-export-external-api plugin. It effectively communicates the plugin's purpose, features, configuration, and usage. To further enhance the documentation:

  1. Address the heading level inconsistencies in the Parameters section.
  2. Ensure consistency in the plugin/package name across all sections.
  3. Update the import statement in the Example Usage section to match the correct package name and export.

These minor adjustments will improve the overall accuracy and consistency of the documentation.

🧰 Tools
🪛 Markdownlint

24-24: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

export/external-api/src/external.api.utils.ts (3)

5-16: LGTM!

The processRecord function accurately processes records based on the provided data mapping.


23-30: Potential SSRF vulnerability in 'exportToExternalAPI' function.

The issue raised in the previous review comment regarding SSRF risks is still applicable.


37-52: LGTM!

The retryOperation function effectively implements retry logic with a configurable delay.

Comment on lines 46 to 52
## Installation

To install the plugin, use npm:

```bash
npm install @flatfile/plugin-external-api-export
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update the npm package name in the installation command.

The installation instructions are clear, but there's a discrepancy between the plugin name mentioned earlier and the npm package name in the installation command.

Update the npm package name to match the plugin name:

-npm install @flatfile/plugin-external-api-export
+npm install @flatfile/plugin-export-external-api
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Installation
To install the plugin, use npm:
```bash
npm install @flatfile/plugin-external-api-export
```
## Installation
To install the plugin, use npm:
```bash
npm install @flatfile/plugin-export-external-api
```

Comment on lines 54 to 75
## Example Usage

```javascript
import externalApiExportPlugin from '@flatfile/plugin-external-api-export';

export default function (listener) {
listener.use(
externalApiExportPlugin({
apiEndpoint: 'https://api.example.com/import',
authToken: 'your-api-token',
dataMapping: {
'First Name': 'firstName',
'Last Name': 'lastName',
'Email': 'emailAddress'
},
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})
);
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update import statement and function name in the example usage.

The example usage provides a clear demonstration of how to configure and use the plugin. However, there are discrepancies in the import statement that need to be addressed.

Apply the following changes to correct the import statement and function name:

-import externalApiExportPlugin from '@flatfile/plugin-external-api-export';
+import { externalApiExportPlugin } from '@flatfile/plugin-export-external-api';

Also, ensure that the function name externalApiExportPlugin is correct and matches the actual export from the plugin.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Example Usage
```javascript
import externalApiExportPlugin from '@flatfile/plugin-external-api-export';
export default function (listener) {
listener.use(
externalApiExportPlugin({
apiEndpoint: 'https://api.example.com/import',
authToken: 'your-api-token',
dataMapping: {
'First Name': 'firstName',
'Last Name': 'lastName',
'Email': 'emailAddress'
},
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})
);
}
```
## Example Usage
```javascript
import { externalApiExportPlugin } from '@flatfile/plugin-export-external-api';
export default function (listener) {
listener.use(
externalApiExportPlugin({
apiEndpoint: 'https://api.example.com/import',
authToken: 'your-api-token',
dataMapping: {
'First Name': 'firstName',
'Last Name': 'lastName',
'Email': 'emailAddress'
},
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})
);
}
```

Comment on lines 57 to 66
await retryOperation(
() =>
exportToExternalAPI(
processedBatch,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Implement exponential backoff for retries

The current retry mechanism uses a constant delay defined by config.retryDelay. Consider implementing an exponential backoff strategy to improve resilience against transient failures when exporting to the external API.

Comment on lines 46 to 79
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})

const processedBatch = records.map((record) =>
processRecord(record, config.dataMapping)
)

try {
await retryOperation(
() =>
exportToExternalAPI(
processedBatch,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += processedBatch.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += processedBatch.length
}

const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix missing pageNumber increment to prevent infinite loop

The pageNumber variable is not incremented within the while loop, which will cause an infinite loop because pageNumber will always be less than or equal to batchCount. Increment pageNumber at the end of each loop iteration to ensure the loop progresses correctly.

Apply this diff to fix the issue:

 while (pageNumber <= batchCount) {
   const records = await Simplified.getAllRecords(sheetId, {
     pageSize: config.batchSize,
     pageNumber,
   })

   // ... processing code ...

   await tick(
     progress,
     `Exported ${totalExported} records. Failed to export ${failedRecords} records.`
   )
+  pageNumber++
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})
const processedBatch = records.map((record) =>
processRecord(record, config.dataMapping)
)
try {
await retryOperation(
() =>
exportToExternalAPI(
processedBatch,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += processedBatch.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += processedBatch.length
}
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
}
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})
const processedBatch = records.map((record) =>
processRecord(record, config.dataMapping)
)
try {
await retryOperation(
() =>
exportToExternalAPI(
processedBatch,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += processedBatch.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += processedBatch.length
}
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
pageNumber++
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (2)
export/external-api/README.MD (2)

21-40: Adjust heading levels for parameters

The parameters section is well-documented, but the heading levels are inconsistent with the document structure. To improve readability and adhere to markdown best practices, consider changing the parameter headings from h4 (####) to h3 (###).

Apply this change to all parameter headings. For example:

-#### `job` - `string` - (required)
+### `job` - `string` - (required)
🧰 Tools
🪛 Markdownlint

23-23: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)


1-66: Great job on the comprehensive README

The README for @flatfile/plugin-export-external-api is well-structured and provides all necessary information for users to understand, install, and use the plugin. It effectively covers the plugin's purpose, features, parameters, installation process, and usage example. Previous issues have been addressed, resulting in a high-quality documentation file.

One minor suggestion for improvement would be to consider adding a section on error handling or troubleshooting, which could be helpful for users implementing the plugin.

Consider adding a brief section on error handling or troubleshooting tips to further enhance the README's usefulness.

🧰 Tools
🪛 Markdownlint

23-23: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 438dfb7 and af5556a.

📒 Files selected for processing (5)
  • .changeset/short-papayas-share.md (1 hunks)
  • export/external-api/README.MD (1 hunks)
  • export/external-api/src/external.api.plugin.ts (1 hunks)
  • export/external-api/src/external.api.utils.ts (1 hunks)
  • export/external-api/src/index.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .changeset/short-papayas-share.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • export/external-api/src/external.api.utils.ts
  • export/external-api/src/index.ts
🧰 Additional context used
🪛 Markdownlint
export/external-api/README.MD

23-23: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

🔇 Additional comments (6)
export/external-api/README.MD (3)

1-20: LGTM: Clear and informative introduction

The plugin introduction and features section provide a comprehensive overview of the @flatfile/plugin-export-external-api. The description effectively communicates the plugin's purpose and key functionalities.


41-49: LGTM: Installation instructions are correct

The installation section provides clear instructions for installing the plugin. The npm package name has been correctly updated to @flatfile/plugin-export-external-api, addressing the previous review comment.


50-66: LGTM: Example usage is correct and comprehensive

The example usage section provides a clear and accurate demonstration of how to use the plugin. The import statement and function name have been corrected, addressing the previous review comment. All required parameters are included in the example, making it a helpful reference for users.

export/external-api/src/external.api.plugin.ts (3)

1-5: Imports look good and are relevant to the plugin's functionality.

The necessary dependencies from @flatfile libraries and local utility file are correctly imported.


7-14: PluginConfig interface is well-defined and comprehensive.

The interface includes all necessary configuration options with proper type definitions.


16-84: Overall structure and implementation of the plugin function is well-designed

The exportToExternalAPIPlugin function is well-structured and implements the necessary logic for batch processing, error handling, and progress reporting. It effectively utilizes the jobHandler and other utility functions to manage the export process.

Some notable strengths:

  1. Clear separation of concerns with the use of jobHandler.
  2. Proper error handling and logging.
  3. Effective use of batch processing to handle large datasets.
  4. Progress reporting using the tick function.

Consider addressing the previously mentioned issues (pageNumber increment and retry logic) to further improve the robustness of the plugin.

Comment on lines 46 to 55
await retryOperation(
() =>
exportToExternalAPI(
records,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider implementing exponential backoff for retries

The current retry mechanism uses a constant delay defined by config.retryDelay. Consider implementing an exponential backoff strategy to improve resilience against transient failures when exporting to the external API.

Here's a suggested implementation of exponential backoff:

const retryWithExponentialBackoff = async (
  operation: () => Promise<any>,
  maxRetries: number,
  initialDelay: number
): Promise<any> => {
  let retries = 0;
  while (retries < maxRetries) {
    try {
      return await operation();
    } catch (error) {
      if (retries === maxRetries - 1) throw error;
      const delay = initialDelay * Math.pow(2, retries);
      await new Promise(resolve => setTimeout(resolve, delay));
      retries++;
    }
  }
};

// Usage in the plugin
await retryWithExponentialBackoff(
  () => exportToExternalAPI(records, config.apiEndpoint, config.authToken),
  config.maxRetries,
  config.retryDelay
);

Comment on lines 38 to 68
let pageNumber = 1
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})

try {
await retryOperation(
() =>
exportToExternalAPI(
records,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += records.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += records.length
}

const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix missing pageNumber increment to prevent infinite loop

The pageNumber variable is not incremented within the while loop, which will cause an infinite loop because pageNumber will always be less than or equal to batchCount. Increment pageNumber at the end of each loop iteration to ensure the loop progresses correctly.

Apply this diff to fix the issue:

 while (pageNumber <= batchCount) {
   const records = await Simplified.getAllRecords(sheetId, {
     pageSize: config.batchSize,
     pageNumber,
   })

   // ... processing code ...

   await tick(
     progress,
     `Exported ${totalExported} records. Failed to export ${failedRecords} records.`
   )
+  pageNumber++
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let pageNumber = 1
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})
try {
await retryOperation(
() =>
exportToExternalAPI(
records,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += records.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += records.length
}
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
}
let pageNumber = 1
while (pageNumber <= batchCount) {
const records = await Simplified.getAllRecords(sheetId, {
pageSize: config.batchSize,
pageNumber,
})
try {
await retryOperation(
() =>
exportToExternalAPI(
records,
config.apiEndpoint,
config.authToken
),
config.maxRetries,
config.retryDelay
)
totalExported += records.length
successfulBatches++
} catch (error) {
console.error('Failed to export batch after retries:', error)
failedRecords += records.length
}
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
pageNumber++
}

export default function (listener) {
listener.use(
exportToExternalAPIPlugin({
apiEndpoint: 'https://api.example.com/import',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing job

Copy link
Contributor

@carlbrugger carlbrugger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the readme comment, 👍🏼

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (2)
export/external-api/src/external.api.plugin.ts (2)

7-14: Add validation for configuration fields

The PluginConfig interface should include runtime validation to ensure:

  • batchSize is a positive integer
  • maxRetries is a non-negative integer
  • retryDelay is a positive number
  • apiEndpoint is a valid URL

Consider adding a validation function:

function validateConfig(config: PluginConfig): void {
  if (config.batchSize <= 0) throw new Error('batchSize must be positive');
  if (config.maxRetries < 0) throw new Error('maxRetries must be non-negative');
  if (config.retryDelay <= 0) throw new Error('retryDelay must be positive');
  try {
    new URL(config.apiEndpoint);
  } catch {
    throw new Error('apiEndpoint must be a valid URL');
  }
}

40-43: Optimize memory usage for large datasets

The records array holds all records in memory until the batch is processed. For large batches, this could lead to memory pressure.

Consider implementing a streaming approach or adding a memory limit check:

const memoryLimit = 100 * 1024 * 1024; // 100MB
const records = await Simplified.getAllRecords(sheetId, {
  pageSize: Math.min(config.batchSize, Math.floor(memoryLimit / averageRecordSize)),
  pageNumber,
});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between af5556a and 01dc296.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !**/*.json
📒 Files selected for processing (2)
  • export/external-api/src/external.api.plugin.ts (1 hunks)
  • flatfilers/sandbox/src/index.ts (2 hunks)
🧰 Additional context used
🔇 Additional comments (2)
flatfilers/sandbox/src/index.ts (1)

2-2: Verify plugin availability and version

Let's ensure the plugin is properly published and available in the package registry.

export/external-api/src/external.api.plugin.ts (1)

16-27: LGTM! Well-structured plugin setup.

The plugin initialization follows best practices with proper type safety and event handling setup.

@@ -1,14 +1,15 @@
import type { FlatfileListener } from '@flatfile/listener'
import { pivotTablePlugin } from '@flatfile/plugin-export-pivot-table'
import { exportToExternalAPIPlugin } from '@flatfile/plugin-export-external-api'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security and configuration concerns in plugin setup

  1. The authentication token should not be hardcoded in the source code. Consider using environment variables or a secure configuration management system.
  2. The API endpoint is set to a local development URL (http://localhost:5678), which won't work in production.
  3. Configuration values like batchSize, maxRetries, and retryDelay should be externalized for easier maintenance.

Consider refactoring to:

 import { exportToExternalAPIPlugin } from '@flatfile/plugin-export-external-api'
+import { config } from './config'  // Create a separate config file

 listener.use(
   exportToExternalAPIPlugin({
     job: 'export-external-api',
-    apiEndpoint: 'http://localhost:5678/api/import',
-    authToken: 'your-api-token',
-    batchSize: 100,
-    maxRetries: 3,
-    retryDelay: 1000,
+    apiEndpoint: config.apiEndpoint,
+    authToken: config.authToken,
+    batchSize: config.batchSize,
+    maxRetries: config.maxRetries,
+    retryDelay: config.retryDelay,
   })
 )

Also applies to: 6-12

Comment on lines +51 to +61
actions: [
{
operation: 'export-external-api',
label: 'Export to External API',
description:
'This custom action code exports the records in the Sales sheet to an external API.',
primary: false,
mode: 'foreground',
type: 'string',
},
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review and enhance action configuration

  1. The type: 'string' seems incorrect for an export action. This property typically defines the return type of the action.
  2. Consider adding a confirmation dialog for this operation since it's exporting data to an external system.
  3. The description could be more informative about error handling and retry behavior.

Consider applying these improvements:

 actions: [
   {
     operation: 'export-external-api',
     label: 'Export to External API',
     description:
-      'This custom action code exports the records in the Sales sheet to an external API.',
+      'Exports records from the Sales sheet to an external API. Failed requests will be retried up to 3 times. Data is sent in batches of 100 records.',
     primary: false,
     mode: 'foreground',
-    type: 'string',
+    confirm: {
+      title: 'Confirm Export',
+      description: 'Are you sure you want to export all records to the external API?'
+    }
   },
 ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
actions: [
{
operation: 'export-external-api',
label: 'Export to External API',
description:
'This custom action code exports the records in the Sales sheet to an external API.',
primary: false,
mode: 'foreground',
type: 'string',
},
],
actions: [
{
operation: 'export-external-api',
label: 'Export to External API',
description:
'Exports records from the Sales sheet to an external API. Failed requests will be retried up to 3 times. Data is sent in batches of 100 records.',
primary: false,
mode: 'foreground',
confirm: {
title: 'Confirm Export',
description: 'Are you sure you want to export all records to the external API?'
}
},
],

Comment on lines 63 to 67
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Adjust progress calculation to account for failed batches

The progress calculation only considers successful batches, which could be misleading if some batches fail.

Consider updating the progress calculation:

-              const progress = (successfulBatches / batchCount) * 100
+              const progress = ((pageNumber - 1) / batchCount) * 100
               await tick(
                 progress,
                 `Exported ${totalExported} records. Failed to export ${failedRecords} records.`
               )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const progress = (successfulBatches / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)
const progress = ((pageNumber - 1) / batchCount) * 100
await tick(
progress,
`Exported ${totalExported} records. Failed to export ${failedRecords} records.`
)

Comment on lines +74 to +78
} catch (error) {
console.error('Error during export:', (error as Error).message)

throw new Error('An error occurred during export.')
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling and propagation

The generic error message hides potentially useful information from the user.

Consider propagating the error details:

-            console.error('Error during export:', (error as Error).message)
-            throw new Error('An error occurred during export.')
+            const errorMessage = error instanceof Error ? error.message : 'Unknown error'
+            console.error('Error during export:', errorMessage)
+            throw new Error(`Export failed: ${errorMessage}`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error('Error during export:', (error as Error).message)
throw new Error('An error occurred during export.')
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error('Error during export:', errorMessage)
throw new Error(`Export failed: ${errorMessage}`)
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between 01dc296 and f78601e.

📒 Files selected for processing (3)
  • export/external-api/README.MD (1 hunks)
  • export/external-api/src/external.api.plugin.ts (1 hunks)
  • flatfilers/sandbox/src/index.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • export/external-api/src/external.api.plugin.ts
  • flatfilers/sandbox/src/index.ts
🧰 Additional context used
🪛 Markdownlint
export/external-api/README.MD

23-23: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

Comment on lines +57 to +63
exportToExternalAPIPlugin({
apiEndpoint: 'https://api.example.com/import',
secretName: 'YOUR_SECRET_NAME',
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add missing required job parameter in example configuration.

The example configuration is missing the required job parameter that was documented in the parameters section.

Add the missing parameter to the configuration:

 exportToExternalAPIPlugin({
+      job: 'export-data',
       apiEndpoint: 'https://api.example.com/import',
       secretName: 'YOUR_SECRET_NAME',
       batchSize: 100,
       maxRetries: 3,
       retryDelay: 1000
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
exportToExternalAPIPlugin({
apiEndpoint: 'https://api.example.com/import',
secretName: 'YOUR_SECRET_NAME',
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})
exportToExternalAPIPlugin({
job: 'export-data',
apiEndpoint: 'https://api.example.com/import',
secretName: 'YOUR_SECRET_NAME',
batchSize: 100,
maxRetries: 3,
retryDelay: 1000
})

Comment on lines +23 to +39
#### `job` - `string` - (required)
The type of job to create.

#### `apiEndpoint` - `string` - (required)
The URL of the external API endpoint to send data to.

#### `secretName` - `string` - (required)
The name of the Flatfile Secret that contains the authentication token for the external API.

#### `batchSize` - `number` - (required)
The number of records to process in each batch.

#### `maxRetries` - `number` - (required)
The maximum number of retry attempts for failed API calls.

#### `retryDelay` - `number` - (required)
The delay (in milliseconds) between retry attempts.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix heading hierarchy in parameters section.

The parameter headings should be h3 (###) instead of h4 (####) to maintain proper heading hierarchy.

Apply this change to all parameter headings:

-#### `job` - `string` - (required)
+### `job` - `string` - (required)

-#### `apiEndpoint` - `string` - (required)
+### `apiEndpoint` - `string` - (required)

-#### `secretName` - `string` - (required)
+### `secretName` - `string` - (required)

-#### `batchSize` - `number` - (required)
+### `batchSize` - `number` - (required)

-#### `maxRetries` - `number` - (required)
+### `maxRetries` - `number` - (required)

-#### `retryDelay` - `number` - (required)
+### `retryDelay` - `number` - (required)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#### `job` - `string` - (required)
The type of job to create.
#### `apiEndpoint` - `string` - (required)
The URL of the external API endpoint to send data to.
#### `secretName` - `string` - (required)
The name of the Flatfile Secret that contains the authentication token for the external API.
#### `batchSize` - `number` - (required)
The number of records to process in each batch.
#### `maxRetries` - `number` - (required)
The maximum number of retry attempts for failed API calls.
#### `retryDelay` - `number` - (required)
The delay (in milliseconds) between retry attempts.
### `job` - `string` - (required)
The type of job to create.
### `apiEndpoint` - `string` - (required)
The URL of the external API endpoint to send data to.
### `secretName` - `string` - (required)
The name of the Flatfile Secret that contains the authentication token for the external API.
### `batchSize` - `number` - (required)
The number of records to process in each batch.
### `maxRetries` - `number` - (required)
The maximum number of retry attempts for failed API calls.
### `retryDelay` - `number` - (required)
The delay (in milliseconds) between retry attempts.
🧰 Tools
🪛 Markdownlint

23-23: Expected: h3; Actual: h4
Heading levels should only increment by one level at a time

(MD001, heading-increment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants