-
Notifications
You must be signed in to change notification settings - Fork 11
exporter: external api publisher #636
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
base: main
Are you sure you want to change the base?
Changes from all commits
49f395e
438dfb7
7f58c34
af5556a
01dc296
f78601e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@flatfile/plugin-export-external-api': minor | ||
| --- | ||
|
|
||
| Initial Release |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||||||||||||||||
| <!-- START_INFOCARD --> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| # @flatfile/plugin-export-external-api | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| This plugin for Flatfile enables exporting data to an external API with advanced features such as batching, error handling, retry logic, and user notifications. It processes records from Flatfile, maps them to the desired format, and exports them in batches to a specified external API endpoint. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| **Event Type:** `job:ready` | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| <!-- END_INFOCARD --> | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Features | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| - Batch processing for efficient exports | ||||||||||||||||||||||||||||||||
| - Configurable batch size | ||||||||||||||||||||||||||||||||
| - Retry logic with configurable max retries and delay | ||||||||||||||||||||||||||||||||
| - Detailed progress tracking and job status updates | ||||||||||||||||||||||||||||||||
| - Error handling and logging | ||||||||||||||||||||||||||||||||
| - User notifications for export completion or failure | ||||||||||||||||||||||||||||||||
| - Support for custom record validation | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Parameters | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| #### `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. | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Installation | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| To install the plugin, use npm: | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ```bash | ||||||||||||||||||||||||||||||||
| npm install @flatfile/plugin-export-external-api | ||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ## Example Usage | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| ```javascript | ||||||||||||||||||||||||||||||||
| import { exportToExternalAPIPlugin } from '@flatfile/plugin-export-external-api'; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export default function (listener) { | ||||||||||||||||||||||||||||||||
| listener.use( | ||||||||||||||||||||||||||||||||
| exportToExternalAPIPlugin({ | ||||||||||||||||||||||||||||||||
| apiEndpoint: 'https://api.example.com/import', | ||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing |
||||||||||||||||||||||||||||||||
| secretName: 'YOUR_SECRET_NAME', | ||||||||||||||||||||||||||||||||
| batchSize: 100, | ||||||||||||||||||||||||||||||||
| maxRetries: 3, | ||||||||||||||||||||||||||||||||
| retryDelay: 1000 | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add missing required The example configuration is missing the required 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
Suggested change
|
||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| module.exports = { | ||
| testEnvironment: 'node', | ||
|
|
||
| transform: { | ||
| '^.+\\.tsx?$': 'ts-jest', | ||
| }, | ||
| setupFiles: ['../../test/dotenv-config.js'], | ||
| setupFilesAfterEnv: [ | ||
| '../../test/betterConsoleLog.js', | ||
| '../../test/unit.cleanup.js', | ||
| ], | ||
| testTimeout: 60_000, | ||
| globalSetup: '../../test/setup-global.js', | ||
| forceExit: true, | ||
| passWithNoTests: true, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "@flatfile/plugin-export-external-api", | ||
| "version": "0.0.0", | ||
| "url": "https://github.com/FlatFilers/flatfile-plugins/tree/main/export/external-api", | ||
| "description": "A Flatfile plugin for exporting data to an external API with batching, error handling, and retry logic", | ||
| "registryMetadata": { | ||
| "category": "export" | ||
| }, | ||
| "engines": { | ||
| "node": ">= 16" | ||
| }, | ||
| "browserslist": [ | ||
| "> 0.5%", | ||
| "last 2 versions", | ||
| "not dead" | ||
| ], | ||
| "browser": { | ||
| "./dist/index.js": "./dist/index.browser.js", | ||
| "./dist/index.mjs": "./dist/index.browser.mjs" | ||
| }, | ||
| "exports": { | ||
| "types": "./dist/index.d.ts", | ||
| "node": { | ||
| "import": "./dist/index.mjs", | ||
| "require": "./dist/index.js" | ||
| }, | ||
| "browser": { | ||
| "require": "./dist/index.browser.js", | ||
| "import": "./dist/index.browser.mjs" | ||
| }, | ||
| "default": "./dist/index.mjs" | ||
| }, | ||
| "main": "./dist/index.js", | ||
| "module": "./dist/index.mjs", | ||
| "types": "./dist/index.d.ts", | ||
| "source": "./src/index.ts", | ||
| "files": [ | ||
| "dist/**" | ||
| ], | ||
| "scripts": { | ||
| "build": "rollup -c", | ||
| "build:watch": "rollup -c --watch", | ||
| "build:prod": "NODE_ENV=production rollup -c", | ||
| "check": "tsc ./**/*.ts --noEmit --esModuleInterop", | ||
| "test": "jest src/*.spec.ts --detectOpenHandles", | ||
| "test:unit": "jest src/*.spec.ts --testPathIgnorePatterns=.*\\.e2e\\.spec\\.ts$ --detectOpenHandles", | ||
| "test:e2e": "jest src/*.e2e.spec.ts --detectOpenHandles" | ||
| }, | ||
| "keywords": [ | ||
| "flatfile-plugins", | ||
| "category-export" | ||
| ], | ||
| "author": "Flatfile, Inc.", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/FlatFilers/flatfile-plugins.git", | ||
| "directory": "export/external-api" | ||
| }, | ||
| "license": "ISC", | ||
| "dependencies": { | ||
| "@flatfile/plugin-job-handler": "^0.6.1", | ||
| "cross-fetch": "^4.0.0" | ||
| }, | ||
| "peerDependencies": { | ||
| "@flatfile/util-common": "^1.4.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@flatfile/api": "^1.9.15", | ||
| "@flatfile/listener": "^1.0.5", | ||
| "@flatfile/rollup-config": "^0.1.1" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { buildConfig } from '@flatfile/rollup-config' | ||
|
|
||
| const config = buildConfig({}) | ||
|
|
||
| export default config |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,81 @@ | ||||||||||||||||||||||
| import type { Flatfile } from '@flatfile/api' | ||||||||||||||||||||||
| import type { FlatfileEvent, FlatfileListener } from '@flatfile/listener' | ||||||||||||||||||||||
| import { jobHandler } from '@flatfile/plugin-job-handler' | ||||||||||||||||||||||
| import { getSheetLength, Simplified } from '@flatfile/util-common' | ||||||||||||||||||||||
| import { exportToExternalAPI, retryOperation } from './external.api.utils' | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export interface PluginConfig { | ||||||||||||||||||||||
| job: string | ||||||||||||||||||||||
| apiEndpoint: string | ||||||||||||||||||||||
| secretName: string | ||||||||||||||||||||||
| batchSize: number | ||||||||||||||||||||||
| maxRetries: number | ||||||||||||||||||||||
| retryDelay: number | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export const exportToExternalAPIPlugin = (config: PluginConfig) => { | ||||||||||||||||||||||
| return (listener: FlatfileListener) => { | ||||||||||||||||||||||
| listener.use( | ||||||||||||||||||||||
| jobHandler( | ||||||||||||||||||||||
| `sheet:${config.job}`, | ||||||||||||||||||||||
| async ( | ||||||||||||||||||||||
| event: FlatfileEvent, | ||||||||||||||||||||||
| tick: ( | ||||||||||||||||||||||
| progress: number, | ||||||||||||||||||||||
| message?: string | ||||||||||||||||||||||
| ) => Promise<Flatfile.JobResponse> | ||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||
| const { sheetId } = event.context | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const authToken = await event.secrets(config.secretName) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| let totalExported = 0 | ||||||||||||||||||||||
| let failedRecords = 0 | ||||||||||||||||||||||
| let successfulBatches = 0 | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const sheetLength = await getSheetLength(sheetId) | ||||||||||||||||||||||
| const batchCount = Math.ceil(sheetLength / config.batchSize) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let pageNumber = 1 | ||||||||||||||||||||||
| while (pageNumber <= batchCount) { | ||||||||||||||||||||||
| const records = await Simplified.getAllRecords(sheetId, { | ||||||||||||||||||||||
| pageSize: config.batchSize, | ||||||||||||||||||||||
| pageNumber, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await retryOperation( | ||||||||||||||||||||||
| () => | ||||||||||||||||||||||
| exportToExternalAPI(records, config.apiEndpoint, 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 = ((pageNumber - 1) / batchCount) * 100 | ||||||||||||||||||||||
| await tick( | ||||||||||||||||||||||
| progress, | ||||||||||||||||||||||
| `Exported ${totalExported} records. Failed to export ${failedRecords} records.` | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| pageNumber++ | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return { | ||||||||||||||||||||||
| info: `Exported ${totalExported} records. Failed to export ${failedRecords} records.`, | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||
| console.error('Error during export:', (error as Error).message) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| throw new Error('An error occurred during export.') | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+72
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||
| import fetch from 'cross-fetch' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export async function exportToExternalAPI( | ||||||||||||||||||||||||||||
| data: Record<string, unknown>[], | ||||||||||||||||||||||||||||
| apiEndpoint: string, | ||||||||||||||||||||||||||||
| authToken: string | ||||||||||||||||||||||||||||
| ): Promise<void> { | ||||||||||||||||||||||||||||
| const response = await fetch(apiEndpoint, { | ||||||||||||||||||||||||||||
| method: 'POST', | ||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||
| Authorization: `Bearer ${authToken}`, | ||||||||||||||||||||||||||||
| 'Content-Type': 'application/json', | ||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||
| body: JSON.stringify(data), | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Rules lgpl javascript ssrf rule node ssrfThis 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.
⚡ 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
poweredByNullify Reply with |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||
| throw new Error(`API request failed: ${response.statusText}`) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export async function retryOperation<T>( | ||||||||||||||||||||||||||||
| operation: () => Promise<T>, | ||||||||||||||||||||||||||||
| maxRetries: number, | ||||||||||||||||||||||||||||
| delay: number | ||||||||||||||||||||||||||||
| ): Promise<T> { | ||||||||||||||||||||||||||||
| let lastError: Error | null = null | ||||||||||||||||||||||||||||
| for (let attempt = 0; attempt < maxRetries; attempt++) { | ||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||
| return await operation() | ||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||
| lastError = error instanceof Error ? error : new Error(String(error)) | ||||||||||||||||||||||||||||
| await new Promise((resolve) => setTimeout(resolve, delay)) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| throw lastError || new Error('Operation failed after max retries') | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export * from './external.api.plugin' |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security and configuration concerns in plugin setup
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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { configureSpace } from '@flatfile/plugin-space-configure' | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default async function (listener: FlatfileListener) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| listener.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pivotTablePlugin({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| pivotColumn: 'product', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aggregateColumn: 'salesAmount', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| aggregationMethod: 'sum', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| groupByColumn: 'region', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| exportToExternalAPIPlugin({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| job: 'export-external-api', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiEndpoint: 'http://localhost:5678/api/import', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| secretName: 'EXTERNAL_API_AUTH_SECRET', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| batchSize: 100, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| maxRetries: 3, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| retryDelay: 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| listener.use( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,17 +48,17 @@ export default async function (listener: FlatfileListener) { | |||||||||||||||||||||||||||||||||||||||||||||||||||
| label: 'Sales Amount', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| actions: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| operation: 'generatePivotTable', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: 'Generate Pivot Table', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'This custom action code generates a pivot table from the records in the People sheet.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| primary: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| mode: 'foreground', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'string', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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', | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+61
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review and enhance action configuration
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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
📝 Committable suggestion
🧰 Tools
🪛 Markdownlint