Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions components/csvbox/actions/submit-spreadsheet/submit-spreadsheet.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import FormData from "form-data";
import { getFileStream } from "@pipedream/platform";
import app from "../../csvbox.app.mjs";

export default {
key: "csvbox-submit-spreadsheet",
name: "Submit Spreadsheet",
description: "Submit a spreadsheet file via public URL or local file path to CSVBox for processing. [See documentation](https://help.csvbox.io/advanced-installation/rest-file-api)",
version: "0.0.1",
type: "action",
props: {
app,
file: {
type: "string",
label: "File Path Or URL",
description: "Provide either a file URL or a path to a file in the `/tmp` directory (for example, `/tmp/spreadsheet.csv`).",
},
sheetLicenseKey: {
propDefinition: [
app,
"sheetLicenseKey",
],
},
userId: {
propDefinition: [
app,
"userId",
],
},
hasHeaders: {
propDefinition: [
app,
"hasHeaders",
],
},
syncDir: {
type: "dir",
accessMode: "read",
sync: true,
optional: true,
},
},
annotations: {
readOnlyHint: false,
destructiveHint: false,
openWorldHint: true,
},
methods: {
booleanToNumber(value) {
return value === true || value === "true" || value === "1" || value === 1
? 1
: 0;
},
},
async run({ $ }) {
const {
app,
booleanToNumber,
file,
sheetLicenseKey,
userId,
hasHeaders,
} = this;
let data;

const isUrl = file?.startsWith("http://") || file?.startsWith("https://");

const otherFields = {
...(userId
? {
user: {
user_id: userId,
},
}
: {}
),
...(hasHeaders
? {
options: {
has_header: booleanToNumber(hasHeaders),
},
}
: {}
),
Comment on lines +77 to +84
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 | 🟠 Major

Handle explicit false value for hasHeaders.

The current truthiness check means that when hasHeaders is explicitly set to false, the options object won't be included in the request. This prevents users from explicitly indicating that the spreadsheet has no headers.

Apply this diff to handle explicit false values:

-      ...(hasHeaders
+      ...(hasHeaders !== undefined
        ? {
          options: {
            has_header: booleanToNumber(hasHeaders),
          },
        }
        : {}
      ),
📝 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
...(hasHeaders
? {
options: {
has_header: booleanToNumber(hasHeaders),
},
}
: {}
),
...(hasHeaders !== undefined
? {
options: {
has_header: booleanToNumber(hasHeaders),
},
}
: {}
),
🤖 Prompt for AI Agents
In components/csvbox/actions/submit-spreadsheet/submit-spreadsheet.mjs around
lines 77 to 84, the current truthiness check removes the options object when
hasHeaders is explicitly false; change the condition to detect an explicit
boolean (e.g., hasHeaders !== undefined or typeof hasHeaders === 'boolean') so
options is included for both true and false, keeping options: { has_header:
booleanToNumber(hasHeaders) } when hasHeaders is provided.

};

if (isUrl) {
data = {
import: {
public_file_url: file,
sheet_license_key: sheetLicenseKey,
...otherFields,
},
};

} else {
data = new FormData();
data.append("file", await getFileStream(file));
data.append("import", JSON.stringify({
sheet_license_key: sheetLicenseKey,
...otherFields,
}));
}

const response = await app.submitFile({
$,
headers: !isUrl
? {
"Content-Type": "multipart/form-data",
}
: undefined,
Comment on lines +107 to +111
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 | 🔴 Critical

Fix multipart Content-Type to include boundary.

Manually setting Content-Type: multipart/form-data without the boundary parameter will cause the request to fail. The form-data package's FormData instance has a getHeaders() method that returns the correct headers including the boundary.

Apply this diff:

      headers: !isUrl
-        ? {
-          "Content-Type": "multipart/form-data",
-        }
+        ? data.getHeaders()
        : undefined,

This will properly include the boundary parameter in the Content-Type header (e.g., multipart/form-data; boundary=----WebKitFormBoundary...), which is required for the server to parse the multipart request correctly.

📝 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
headers: !isUrl
? {
"Content-Type": "multipart/form-data",
}
: undefined,
headers: !isUrl
? data.getHeaders()
: undefined,
🤖 Prompt for AI Agents
In components/csvbox/actions/submit-spreadsheet/submit-spreadsheet.mjs around
lines 107 to 111, the code manually sets "Content-Type": "multipart/form-data"
which omits the required boundary; replace that manual header with the FormData
instance's getHeaders() (e.g., const formHeaders = form.getHeaders()) and merge
those headers into the request when !isUrl so the Content-Type includes the
boundary parameter returned by getHeaders().

data,
});

$.export("$summary", "Successfully submitted spreadsheet");

return response;
},
};
54 changes: 49 additions & 5 deletions components/csvbox/csvbox.app.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,55 @@
import { axios } from "@pipedream/platform";

export default {
type: "app",
app: "csvbox",
propDefinitions: {},
propDefinitions: {
sheetLicenseKey: {
type: "string",
label: "Sheet License Key",
description: "The unique identifier for your CSVBox sheet. You can find it in **Sheets - Edit - Code Snippet - Sheet License Key**.",
},
userId: {
type: "string",
label: "User ID",
description: "The unique identifier for the user. You can find it in the **Dashboard - Edit - Code Snippet**.",
optional: true,
},
hasHeaders: {
type: "boolean",
label: "Has Headers",
description: "Whether the spreadsheet has headers.",
optional: true,
},
},
methods: {
// this.$auth contains connected account data
authKeys() {
console.log(Object.keys(this.$auth));
getUrl(path) {
return `https://api.csvbox.io/1.1${path}`;
},
getHeaders(headers) {
return {
"Content-Type": "application/json",
"x-csvbox-api-key": `${this.$auth.api_key}`,
"x-csvbox-secret-api-key": `${this.$auth.secret_api_key}`,
...headers,
};
},
_makeRequest({
$ = this, path, headers, ...args
} = {}) {
return axios($, {
debug: true,
url: this.getUrl(path),
headers: this.getHeaders(headers),
...args,
});
},
submitFile(args = {}) {
return this._makeRequest({
method: "POST",
path: "/file",
...args,
});
},
},
};
};
8 changes: 6 additions & 2 deletions components/csvbox/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/csvbox",
"version": "0.0.1",
"version": "0.1.0",
"description": "Pipedream CSVbox Components",
"main": "csvbox.app.mjs",
"keywords": [
Expand All @@ -11,5 +11,9 @@
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.1.0",
"form-data": "^4.0.4"
}
}
}
33 changes: 33 additions & 0 deletions components/csvbox/sources/new-import/new-import.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ConfigurationError } from "@pipedream/platform";
import app from "../../csvbox.app.mjs";
import sampleEvent from "./test-event.mjs";

export default {
key: "csvbox-new-import",
name: "New Import",
description: "Emit new event when CSVBox receives and processes a new import. [See documentation](https://help.csvbox.io/destinations#api-webhook)",
version: "0.0.1",
type: "source",
dedupe: "unique",
props: {
app,
http: "$.interface.http",
},
async run({
headers, body,
}) {

if (!headers["content-type"] || headers["content-type"] !== "application/json") {
throw new ConfigurationError("Invalid content type. Please check your CSVBox webhook configuration so that the content type is set to `JSON`.");
}
Comment on lines +20 to +22
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 | 🟠 Major

Content-Type validation is too strict.

The exact equality check headers["content-type"] !== "application/json" will reject valid Content-Type headers that include charset or other parameters (e.g., "application/json; charset=utf-8").

Apply this diff to use a more flexible check:

-    if (!headers["content-type"] || headers["content-type"] !== "application/json") {
+    if (!headers["content-type"]?.startsWith("application/json")) {
      throw new ConfigurationError("Invalid content type. Please check your CSVBox webhook configuration so that the content type is set to `JSON`.");
    }
📝 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
if (!headers["content-type"] || headers["content-type"] !== "application/json") {
throw new ConfigurationError("Invalid content type. Please check your CSVBox webhook configuration so that the content type is set to `JSON`.");
}
if (!headers["content-type"]?.startsWith("application/json")) {
throw new ConfigurationError("Invalid content type. Please check your CSVBox webhook configuration so that the content type is set to `JSON`.");
}
🤖 Prompt for AI Agents
In components/csvbox/sources/new-import/new-import.mjs around lines 20 to 22,
the Content-Type check uses strict equality against "application/json" which
rejects valid headers with parameters (e.g., "application/json; charset=utf-8");
change the validation to parse or prefix-match the header value (e.g., check
that headers["content-type"] startsWith "application/json" or split on ";" and
trim before comparing) and ensure you handle missing header case as before and
normalize casing when comparing.


const ts = Date.now();

this.$emit(body, {
id: ts,
summary: "New import has been processed",
ts,
});
},
sampleEvent,
};
97 changes: 97 additions & 0 deletions components/csvbox/sources/new-import/test-event.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export default [
{
"import_id": 1841488,
"sheet_id": 19247,
"sheet_name": "Test 100",
"row_number": 1,
"total_rows": 5,
"env_name": "default",
"original_filename": "basicScience.csv",
"custom_fields": [],
"row_data": {
"ID": "2",
"Question": "What type of paper detects acids and alkali in liquid?",
"Answer": "Litmus Paper",
"Option B": "Filter paper",
"Option C": "Hydrion Paper",
"Option D": "Proton Paper"
},
"import_description": ""
},
{
"import_id": 1841488,
"sheet_id": 19247,
"sheet_name": "Test 100",
"row_number": 2,
"total_rows": 5,
"env_name": "default",
"original_filename": "basicScience.csv",
"custom_fields": [],
"row_data": {
"ID": "3",
"Question": "What is the only metal to be liquid at room temperature? ",
"Answer": "Mercury",
"Option B": "Nickel",
"Option C": "Chromium",
"Option D": "Zirconium"
},
"import_description": ""
},
{
"import_id": 1841488,
"sheet_id": 19247,
"sheet_name": "Test 100",
"row_number": 3,
"total_rows": 5,
"env_name": "default",
"original_filename": "basicScience.csv",
"custom_fields": [],
"row_data": {
"ID": "4",
"Question": "What is the chemical name of aqua fortis?",
"Answer": "Nitric Acid",
"Option B": "Hydrochloric Acid",
"Option C": "Benzoic Acid",
"Option D": "Acetic Acid"
},
"import_description": ""
},
{
"import_id": 1841488,
"sheet_id": 19247,
"sheet_name": "Test 100",
"row_number": 4,
"total_rows": 5,
"env_name": "default",
"original_filename": "basicScience.csv",
"custom_fields": [],
"row_data": {
"ID": "5",
"Question": "What is the fourth state of matter after solid and liquid and gas?",
"Answer": "Plasma",
"Option B": "Ground State",
"Option C": "Metal ",
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 | 🟡 Minor

Remove trailing whitespace.

"Metal " contains trailing spaces, which is likely unintentional.

Apply this diff:

-      "Option C": "Metal  ",
+      "Option C": "Metal",
📝 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
"Option C": "Metal ",
"Option C": "Metal",
🤖 Prompt for AI Agents
In components/csvbox/sources/new-import/test-event.mjs around line 73, the value
"Metal  " contains unintended trailing whitespace; remove the extra spaces so
the string becomes "Metal" (update the literal in that line and run a quick
check/lint to ensure no other entries have trailing spaces).

"Option D": "Flora"
},
"import_description": ""
},
{
"import_id": 1841488,
"sheet_id": 19247,
"sheet_name": "Test 100",
"row_number": 5,
"total_rows": 5,
"env_name": "default",
"original_filename": "basicScience.csv",
"custom_fields": [],
"row_data": {
"ID": "6",
"Question": "What is the chemical symbol for Plutonium? ",
"Answer": "Pu",
"Option B": "Pa",
"Option C": "Po",
"Option D": "P"
},
"import_description": ""
}
];
Loading
Loading