diff --git a/components/netsuite/actions/get-invoice/get-invoice.mjs b/components/netsuite/actions/get-invoice/get-invoice.mjs new file mode 100644 index 0000000000000..d39ec6623d9ea --- /dev/null +++ b/components/netsuite/actions/get-invoice/get-invoice.mjs @@ -0,0 +1,63 @@ +import app from "../../netsuite.app.mjs"; + +export default { + key: "netsuite-get-invoice", + name: "Get Invoice", + description: "Retrieves an invoice by ID. [See the documentation](https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2025.2/index.html#tag-invoice)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + app, + invoiceId: { + propDefinition: [ + app, + "invoiceId", + ], + }, + expandSubResources: { + propDefinition: [ + app, + "expandSubResources", + ], + }, + simpleEnumFormat: { + propDefinition: [ + app, + "simpleEnumFormat", + ], + }, + fields: { + propDefinition: [ + app, + "fields", + ], + }, + }, + async run({ $ }) { + const { + app, + invoiceId, + expandSubResources, + simpleEnumFormat, + fields, + } = this; + + const response = await app.getInvoice({ + $, + invoiceId, + params: { + expandSubResources, + simpleEnumFormat, + fields, + }, + }); + + $.export("$summary", `Successfully retrieved invoice with ID ${invoiceId}`); + return response; + }, +}; diff --git a/components/netsuite/actions/get-sales-order/get-sales-order.mjs b/components/netsuite/actions/get-sales-order/get-sales-order.mjs new file mode 100644 index 0000000000000..60541f3c4e8b2 --- /dev/null +++ b/components/netsuite/actions/get-sales-order/get-sales-order.mjs @@ -0,0 +1,63 @@ +import app from "../../netsuite.app.mjs"; + +export default { + key: "netsuite-get-sales-order", + name: "Get Sales Order", + description: "Retrieves a sales order by ID from NetSuite. [See the documentation](https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2025.2/index.html#tag-salesOrder)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + app, + salesOrderId: { + propDefinition: [ + app, + "salesOrderId", + ], + }, + expandSubResources: { + propDefinition: [ + app, + "expandSubResources", + ], + }, + simpleEnumFormat: { + propDefinition: [ + app, + "simpleEnumFormat", + ], + }, + fields: { + propDefinition: [ + app, + "fields", + ], + }, + }, + async run({ $ }) { + const { + app, + salesOrderId, + expandSubResources, + simpleEnumFormat, + fields, + } = this; + + const response = await app.getSalesOrder({ + $, + salesOrderId, + params: { + expandSubResources, + simpleEnumFormat, + fields, + }, + }); + + $.export("$summary", `Successfully retrieved sales order with ID ${salesOrderId}`); + return response; + }, +}; diff --git a/components/netsuite/actions/list-invoices/list-invoices.mjs b/components/netsuite/actions/list-invoices/list-invoices.mjs new file mode 100644 index 0000000000000..43256387f4746 --- /dev/null +++ b/components/netsuite/actions/list-invoices/list-invoices.mjs @@ -0,0 +1,60 @@ +import app from "../../netsuite.app.mjs"; + +export default { + key: "netsuite-list-invoices", + name: "List Invoices", + description: "Retrieves a list of invoices from NetSuite. [See the documentation](https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2025.2/index.html#tag-invoice)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + app, + searchQuery: { + propDefinition: [ + app, + "searchQuery", + ], + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of invoices to return", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + searchQuery, + maxResults, + } = this; + + const params = {}; + if (searchQuery) params.q = searchQuery; + + const items = []; + const paginator = app.paginate({ + fn: app.listInvoices, + fnArgs: { + $, + params: Object.keys(params).length > 0 + ? params + : undefined, + }, + maxResults, + }); + + for await (const item of paginator) { + items.push(item); + } + + $.export("$summary", `Successfully retrieved ${items.length} invoice${items.length === 1 + ? "" + : "s"}`); + return items; + }, +}; diff --git a/components/netsuite/actions/list-sales-orders/list-sales-orders.mjs b/components/netsuite/actions/list-sales-orders/list-sales-orders.mjs new file mode 100644 index 0000000000000..a72de8c69e18e --- /dev/null +++ b/components/netsuite/actions/list-sales-orders/list-sales-orders.mjs @@ -0,0 +1,57 @@ +import app from "../../netsuite.app.mjs"; + +export default { + key: "netsuite-list-sales-orders", + name: "List Sales Orders", + description: "Retrieves a list of sales orders. [See the documentation](https://system.netsuite.com/help/helpcenter/en_US/APIs/REST_API_Browser/record/v1/2025.2/index.html#tag-salesOrder)", + version: "0.0.1", + type: "action", + annotations: { + readOnlyHint: true, + destructiveHint: false, + openWorldHint: true, + }, + props: { + app, + searchQuery: { + propDefinition: [ + app, + "searchQuery", + ], + }, + maxResults: { + type: "integer", + label: "Max Results", + description: "The maximum number of sales orders to return", + optional: true, + }, + }, + async run({ $ }) { + const { + app, + searchQuery, + maxResults, + } = this; + + const items = []; + const paginator = app.paginate({ + fn: app.listSalesOrders, + fnArgs: { + $, + params: { + q: searchQuery, + }, + }, + maxResults, + }); + + for await (const item of paginator) { + items.push(item); + } + + $.export("$summary", `Successfully retrieved ${items.length} sales order${items.length === 1 + ? "" + : "s"}`); + return items; + }, +}; diff --git a/components/netsuite/common/constants.mjs b/components/netsuite/common/constants.mjs new file mode 100644 index 0000000000000..0069ab5b7eee5 --- /dev/null +++ b/components/netsuite/common/constants.mjs @@ -0,0 +1,9 @@ +export default { + DEFAULT_LIMIT: 20, + DEFAULT_MAX_RESULTS: 100, + VALIDATION_OPTIONS: { + ERROR: "Error", + WARNING: "Warning", + IGNORE: "Ignore", + }, +}; diff --git a/components/netsuite/netsuite.app.mjs b/components/netsuite/netsuite.app.mjs index 2b9bb9fdf7913..a80343db1e9f5 100644 --- a/components/netsuite/netsuite.app.mjs +++ b/components/netsuite/netsuite.app.mjs @@ -1,11 +1,309 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "netsuite", - propDefinitions: {}, + propDefinitions: { + salesOrderId: { + type: "string", + label: "Sales Order ID", + description: "The internal identifier of the sales order", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const offset = page * limit; + const { items } = await this.listSalesOrders({ + params: { + limit, + offset, + }, + }); + return items?.map((order) => ({ + label: order.tranId || `Order ${order.id}`, + value: order.id, + })) || []; + }, + }, + invoiceId: { + type: "string", + label: "Invoice ID", + description: "The internal identifier of the invoice", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const offset = page * limit; + const { items } = await this.listInvoices({ + params: { + limit, + offset, + }, + }); + return items?.map((invoice) => ({ + label: invoice.tranId || `Invoice ${invoice.id}`, + value: invoice.id, + })) || []; + }, + }, + subsidiaryId: { + type: "string", + label: "Subsidiary ID", + description: "The internal identifier of the subsidiary", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const offset = page * limit; + const { items } = await this.listSubsidiaries({ + params: { + limit, + offset, + }, + }); + return items?.map((subsidiary) => ({ + label: subsidiary.name || `Subsidiary ${subsidiary.id}`, + value: subsidiary.id, + })) || []; + }, + }, + customerId: { + type: "string", + label: "Customer ID", + description: "The internal identifier of the customer", + async options({ page }) { + const limit = constants.DEFAULT_LIMIT; + const offset = page * limit; + const { items } = await this.listCustomers({ + params: { + limit, + offset, + }, + }); + return items?.map((customer) => ({ + label: customer.entityId || customer.companyName || `Customer ${customer.id}`, + value: customer.id, + })) || []; + }, + }, + searchQuery: { + type: "string", + label: "Search Query", + description: "The search query used to filter results", + optional: true, + }, + expandSubResources: { + type: "boolean", + label: "Expand Sub Resources", + description: "Set to true to automatically expand all sublists, sublist lines, and subrecords", + optional: true, + default: false, + }, + simpleEnumFormat: { + type: "boolean", + label: "Simple Enum Format", + description: "Set to true to return enumeration values showing only the internal ID value", + optional: true, + default: false, + }, + fields: { + type: "string", + label: "Fields", + description: "Comma-separated list of field names to return (only selected fields will be returned)", + optional: true, + }, + items: { + type: "string[]", + label: "Items", + description: `Array of item objects to add to the sales order. Each item should be a JSON object with the following properties: + +**Required:** +- \`item\` - Item ID or reference object (e.g., \`{ "id": "123" }\`) +- \`quantity\` - Quantity to order (number) + +**Important Optional:** +- \`rate\` - Price per unit (number) +- \`amount\` - Total line amount (number, usually \`quantity * rate\`) +- \`description\` - Custom description for the line item (string) +- \`location\` - Inventory location ID or reference object + +**Example:** +\`\`\`json +[ + { + "item": { "id": "456" }, + "quantity": 2, + "rate": 99.99, + "amount": 199.98, + "description": "Custom widget - blue", + "location": { "id": "1" } + } +] +\`\`\``, + optional: true, + }, + propertyNameValidation: { + type: "string", + label: "Property Name Validation", + description: "Sets the strictness of property name validation", + options: Object.values(constants.VALIDATION_OPTIONS), + optional: true, + }, + propertyValueValidation: { + type: "string", + label: "Property Value Validation", + description: "Sets the strictness of property value validation", + options: Object.values(constants.VALIDATION_OPTIONS), + optional: true, + }, + replace: { + type: "string", + label: "Replace Sublists", + description: "Comma-separated names of sublists on this record. All sublist lines will be replaced with lines specified in the request.", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://${this.$auth.account_id}.suitetalk.api.netsuite.com/services/rest/record/v1${path}`; + }, + _headers(headers = {}) { + return { + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": `Bearer ${this.$auth.oauth_access_token}`, + ...headers, + }; + }, + async _makeRequest({ + $ = this, path, headers, ...opts + }) { + return axios($, { + debug: true, + url: this.getUrl(path), + headers: this._headers(headers), + ...opts, + }); + }, + listSalesOrders(opts = {}) { + return this._makeRequest({ + path: "/salesOrder", + ...opts, + }); + }, + getSalesOrder({ + salesOrderId, opts, + } = {}) { + return this._makeRequest({ + path: `/salesOrder/${salesOrderId}`, + ...opts, + }); + }, + createSalesOrder(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/salesOrder", + ...opts, + }); + }, + updateSalesOrder({ + salesOrderId, opts, + } = {}) { + return this._makeRequest({ + method: "PATCH", + path: `/salesOrder/${salesOrderId}`, + ...opts, + }); + }, + deleteSalesOrder({ + salesOrderId, opts, + } = {}) { + return this._makeRequest({ + method: "DELETE", + path: `/salesOrder/${salesOrderId}`, + ...opts, + }); + }, + listInvoices(opts = {}) { + return this._makeRequest({ + path: "/invoice", + ...opts, + }); + }, + getInvoice({ + invoiceId, opts, + } = {}) { + return this._makeRequest({ + path: `/invoice/${invoiceId}`, + ...opts, + }); + }, + createInvoice(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "/invoice", + ...opts, + }); + }, + updateInvoice({ + invoiceId, opts, + } = {}) { + return this._makeRequest({ + method: "PATCH", + path: `/invoice/${invoiceId}`, + ...opts, + }); + }, + deleteInvoice({ + invoiceId, opts, + } = {}) { + return this._makeRequest({ + method: "DELETE", + path: `/invoice/${invoiceId}`, + ...opts, + }); + }, + listSubsidiaries(opts = {}) { + return this._makeRequest({ + path: "/subsidiary", + ...opts, + }); + }, + listCustomers(opts = {}) { + return this._makeRequest({ + path: "/customer", + ...opts, + }); + }, + async *paginate({ + fn, fnArgs = {}, maxResults = constants.DEFAULT_MAX_RESULTS, fieldKey = "items", + }) { + const limit = constants.DEFAULT_LIMIT; + let offset = 0; + let total = 0; + + while (true) { + const response = await fn({ + ...fnArgs, + params: { + ...fnArgs.params, + limit, + offset, + }, + }); + + const items = response[fieldKey] || []; + + for (const item of items) { + yield item; + total++; + + if (maxResults && total >= maxResults) { + return; + } + } + + if (items.length < limit) { + return; + } + + offset += limit; + } }, }, }; diff --git a/components/netsuite/package.json b/components/netsuite/package.json index 424a8e8d8cf0d..045a5d4989d58 100644 --- a/components/netsuite/package.json +++ b/components/netsuite/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/netsuite", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream NetSuite Components", "main": "netsuite.app.mjs", "keywords": [ @@ -12,4 +12,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +}