diff --git a/components/coda/actions/copy-doc/copy-doc.mjs b/components/coda/actions/copy-doc/copy-doc.mjs new file mode 100644 index 0000000000000..afd662c8e74c0 --- /dev/null +++ b/components/coda/actions/copy-doc/copy-doc.mjs @@ -0,0 +1,45 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-copy-doc", + name: "Copy Doc", + description: "Creates a copy of the specified doc. [See docs](https://coda.io/developers/apis/v1#operation/createDoc)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + label: "Source Doc ID", + description: "The doc from which to create a copy", + }, + title: { + propDefinition: [ + coda, + "title", + ], + description: "Title of the newly copied doc. Defaults to `\"Copy of \"`", + }, + folderId: { + propDefinition: [ + coda, + "folderId", + ], + description: "The folder within which to copy this doc", + }, + }, + async run({ $ }) { + let data = { + title: this.title, + folderId: this.folderId, + sourceDoc: this.docId, + }; + + let response = await this.coda.createDoc($, data); + $.export("$summary", `Copied to new doc "${response.name}" in folderId: "${response.folderId}" and workspaceId: "${response.workspaceId}"`); + return response; + }, +}; diff --git a/components/coda/actions/create-doc/create-doc.mjs b/components/coda/actions/create-doc/create-doc.mjs new file mode 100644 index 0000000000000..8c20d95923215 --- /dev/null +++ b/components/coda/actions/create-doc/create-doc.mjs @@ -0,0 +1,35 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-create-doc", + name: "Create Doc", + description: "Creates a new doc. [See docs](https://coda.io/developers/apis/v1#operation/createDoc)", + version: "0.0.1", + type: "action", + props: { + coda, + title: { + propDefinition: [ + coda, + "title", + ], + }, + folderId: { + propDefinition: [ + coda, + "folderId", + ], + description: "The folder within which to create this doc", + }, + }, + async run({ $ }) { + let data = { + title: this.title, + folderId: this.folderId, + }; + + let response = await this.coda.createDoc($, data); + $.export("$summary", `Created doc "${response.name}" in folderId: "${response.folderId}" and workspaceId: "${response.workspaceId}"`); + return response; + }, +}; diff --git a/components/coda/actions/create-rows/create-rows.mjs b/components/coda/actions/create-rows/create-rows.mjs new file mode 100644 index 0000000000000..bd62ed4025325 --- /dev/null +++ b/components/coda/actions/create-rows/create-rows.mjs @@ -0,0 +1,71 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-create-rows", + name: "Create Rows", + description: "Insert a row in a selected table. [See docs](https://coda.io/developers/apis/v1#operation/upsertRows)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + tableId: { + propDefinition: [ + coda, + "tableId", + (c) => ({ + docId: c.docId, + }), + ], + reloadProps: true, + }, + disableParsing: { + propDefinition: [ + coda, + "disableParsing", + ], + }, + }, + async additionalProps() { + const props = {}; + const { items } = await this.coda.listColumns(this, this.docId, this.tableId); + for (const item of items) { + props[`col_${item.id}`] = { + type: "string", + label: `Column: ${item.name}`, + description: "Leave blank to ignore this column", + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const params = { + disableParsing: this.disableParsing, + }; + + const data = { + rows: [ + { + cells: this.coda.createRowCells(this), + }, + ], + }; + + const response = await this.coda.createRows( + $, + this.docId, + this.tableId, + data, + params, + ); + + $.export("$summary", "Created row successfully"); + return response; + }, +}; diff --git a/components/coda/actions/find-row/find-row.mjs b/components/coda/actions/find-row/find-row.mjs new file mode 100644 index 0000000000000..f1176684308b7 --- /dev/null +++ b/components/coda/actions/find-row/find-row.mjs @@ -0,0 +1,124 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-find-row", + name: "Find Row", + description: "Searches for a row in the selected table using a column match search. [See docs](https://coda.io/developers/apis/v1#operation/listRows)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + tableId: { + propDefinition: [ + coda, + "tableId", + (c) => ({ + docId: c.docId, + }), + ], + }, + columnId: { + propDefinition: [ + coda, + "columnId", + (c) => ({ + docId: c.docId, + tableId: c.tableId, + }), + ], + description: "ID of the column. This field is required if querying", + optional: true, + }, + query: { + propDefinition: [ + coda, + "query", + ], + description: "Query used to filter returned rows", + optional: true, + }, + sortBy: { + propDefinition: [ + coda, + "sortBy", + ], + description: "Specifies the sort order of the rows returned. If left unspecified, rows are returned by creation time ascending", + options: [ + "createdAt", + "natural", + "updatedAt", + ], + }, + visibleOnly: { + propDefinition: [ + coda, + "visibleOnly", + ], + }, + useColumnNames: { + type: "boolean", + label: "Use Column Names", + description: "Use column names instead of column IDs in the returned output", + optional: true, + }, + valueFormat: { + type: "string", + label: "Value Format", + description: "The format that individual cell values are returned as", + optional: true, + options: [ + "simple", + "simpleWithArrays", + "rich", + ], + }, + max: { + propDefinition: [ + coda, + "max", + ], + }, + }, + async run({ $ }) { + let params = { + sortBy: this.sortBy, + visibleOnly: this.visibleOnly, + useColumnNames: this.useColumnNames, + valueFormat: this.valueFormat, + }; + + if (this.columnId && this.query) { + params.query = `${this.columnId}:"${this.query}"`; + } + + let items = []; + let response; + do { + response = await this.coda.findRow( + $, + this.docId, + this.tableId, + params, + ); + items.push(...response.items); + params.pageToken = response.nextPageToken; + } while (params.pageToken && items.length < this.max); + + if (items.length > this.max) items.length = this.max; + + if (items.length > 0) { + $.export("$summary", `Found ${items.length} rows`); + } else { + $.export("$summary", `No rows found with the search query: ${this.query}`); + } + return { + items, + }; + }, +}; diff --git a/components/coda/actions/list-columns/list-columns.mjs b/components/coda/actions/list-columns/list-columns.mjs new file mode 100644 index 0000000000000..1311e8be230a7 --- /dev/null +++ b/components/coda/actions/list-columns/list-columns.mjs @@ -0,0 +1,63 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-list-columns", + name: "List Columns", + description: "Lists columns in a table. [See docs](https://coda.io/developers/apis/v1#operation/listColumns)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + tableId: { + propDefinition: [ + coda, + "tableId", + (c) => ({ + docId: c.docId, + }), + ], + }, + visibleOnly: { + propDefinition: [ + coda, + "visibleOnly", + ], + }, + max: { + propDefinition: [ + coda, + "max", + ], + }, + }, + async run({ $ }) { + let params = { + visibleOnly: this.visibleOnly, + }; + + let items = []; + let response; + do { + response = await this.coda.listColumns( + $, + this.docId, + this.tableId, + params, + ); + items.push(...response.items); + params.pageToken = response.nextPageToken; + } while (params.pageToken && items.length < this.max); + + if (items.length > this.max) items.length = this.max; + + $.export("$summary", `Retrieved ${items.length} column(s)`); + + return items; + }, +}; diff --git a/components/coda/actions/list-docs/list-docs.mjs b/components/coda/actions/list-docs/list-docs.mjs new file mode 100644 index 0000000000000..1e5bde05322f2 --- /dev/null +++ b/components/coda/actions/list-docs/list-docs.mjs @@ -0,0 +1,96 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-list-docs", + name: "List Docs", + description: "Returns a list of docs accessible by the user. These are returned in the same order as on the docs page: reverse chronological by the latest event relevant to the user (last viewed, edited, or shared). [See docs](https://coda.io/developers/apis/v1#operation/listDocs)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + description: "Show only docs copied from the specified doc", + optional: true, + }, + workspaceId: { + type: "string", + label: "Workspace", + description: "Show only docs belonging to the given workspace", + optional: true, + }, + folderId: { + propDefinition: [ + coda, + "folderId", + ], + description: "Show only docs belonging to the given folder", + }, + query: { + propDefinition: [ + coda, + "query", + ], + }, + isOwner: { + type: "boolean", + label: "Is Owner Docs", + description: "Show only docs owned by the user", + optional: true, + }, + isPublished: { + type: "boolean", + label: "Is Published Docs", + description: "Show only published docs", + optional: true, + }, + isStarred: { + type: "boolean", + label: "Is Starred Docs", + description: "If true, returns docs that are starred. If false, returns docs that are not starred", + optional: true, + }, + inGallery: { + type: "boolean", + label: "In Gallery Docs", + description: "Show only docs visible within the gallery", + optional: true, + }, + max: { + propDefinition: [ + coda, + "max", + ], + label: "Max Items", + }, + }, + async run({ $ }) { + let params = { + sourceDoc: this.docId, + workspaceId: this.workspaceId, + folderId: this.folderId, + query: this.query, + isOwner: this.isOwner, + isPublished: this.isPublished, + isStarred: this.isStarred, + inGallery: this.inGallery, + }; + + let items = []; + let response; + do { + response = await this.coda.listDocs($, params); + items.push(...response.items); + params.pageToken = response.nextPageToken; + } while (params.pageToken && items.length < this.max); + + if (items.length > this.max) items.length = this.max; + + $.export("$summary", `Retrieved ${items.length} doc(s)`); + + return items; + }, +}; diff --git a/components/coda/actions/list-tables/list-tables.mjs b/components/coda/actions/list-tables/list-tables.mjs new file mode 100644 index 0000000000000..e040c5aa27fbe --- /dev/null +++ b/components/coda/actions/list-tables/list-tables.mjs @@ -0,0 +1,63 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-list-tables", + name: "List Tables", + description: "Lists tables in a doc. [See docs](https://coda.io/developers/apis/v1#operation/listTables)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + sortBy: { + propDefinition: [ + coda, + "sortBy", + ], + }, + tableTypes: { + type: "string[]", + label: "tableTypes", + description: "Comma-separated list of table types to include in results. Items: `\"table\"`,`\"view\"`", + optional: true, + default: [ + "table", + "view", + ], + }, + max: { + propDefinition: [ + coda, + "max", + ], + }, + }, + async run({ $ }) { + let params = { + sortBy: this.sortBy, + tableTypes: this.tableTypes.toString(), + }; + + let items = []; + let response; + do { + response = await this.coda.listTables( + $, + this.docId, + params, + ); + items.push(...response.items); + params.pageToken = response.nextPageToken; + } while (params.pageToken && items.length < this.max); + + if (items.length > this.max) items.length = this.max; + + $.export("$summary", `Retrieved ${items.length} ${this.tableTypes}(s)`); + return items; + }, +}; diff --git a/components/coda/actions/update-row/update-row.mjs b/components/coda/actions/update-row/update-row.mjs new file mode 100644 index 0000000000000..d7a30acb9eb52 --- /dev/null +++ b/components/coda/actions/update-row/update-row.mjs @@ -0,0 +1,80 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-update-row", + name: "Update a Row", + description: "Updates the specified row in the table. [See docs](https://coda.io/developers/apis/v1#operation/updateRow)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + tableId: { + propDefinition: [ + coda, + "tableId", + (c) => ({ + docId: c.docId, + }), + ], + reloadProps: true, + }, + rowId: { + propDefinition: [ + coda, + "rowId", + (c) => ({ + docId: c.docId, + tableId: c.tableId, + }), + ], + }, + disableParsing: { + propDefinition: [ + coda, + "disableParsing", + ], + }, + }, + async additionalProps() { + const props = {}; + const { items } = await this.coda.listColumns(this, this.docId, this.tableId); + for (const item of items) { + props[`col_${item.id}`] = { + type: "string", + label: `Column: ${item.name}`, + description: "Leave blank to ignore this column", + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const params = { + disableParsing: this.disableParsing, + }; + + const data = { + row: { + cells: this.coda.createRowCells(this), + }, + }; + + const response = await this.coda.updateRow( + $, + this.docId, + this.tableId, + this.rowId, + data, + params, + ); + + $.export("$summary", "Updated row successfully"); + return response; + }, +}; diff --git a/components/coda/actions/upsert-rows/upsert-rows.mjs b/components/coda/actions/upsert-rows/upsert-rows.mjs new file mode 100644 index 0000000000000..6401a3a02bbea --- /dev/null +++ b/components/coda/actions/upsert-rows/upsert-rows.mjs @@ -0,0 +1,82 @@ +import coda from "../../coda.app.mjs"; + +export default { + key: "coda-upsert-rows", + name: "Upsert Rows", + description: "Creates a new row or updates existing rows if any upsert key columns are provided. When upserting, if multiple rows match the specified key column(s), they will all be updated with the specified value. [See docs](https://coda.io/developers/apis/v1#operation/upsertRows)", + version: "0.0.1", + type: "action", + props: { + coda, + docId: { + propDefinition: [ + coda, + "docId", + ], + }, + tableId: { + propDefinition: [ + coda, + "tableId", + (c) => ({ + docId: c.docId, + }), + ], + reloadProps: true, + }, + keyColumns: { + propDefinition: [ + coda, + "keyColumns", + (c) => ({ + docId: c.docId, + tableId: c.tableId, + }), + ], + }, + disableParsing: { + propDefinition: [ + coda, + "disableParsing", + ], + }, + }, + async additionalProps() { + const props = {}; + const { items } = await this.coda.listColumns(this, this.docId, this.tableId); + for (const item of items) { + props[`col_${item.id}`] = { + type: "string", + label: `Column: ${item.name}`, + description: "Leave blank to ignore this column", + optional: true, + }; + } + return props; + }, + async run({ $ }) { + const params = { + disableParsing: this.disableParsing, + }; + + const data = { + keyColumns: this.keyColumns, + rows: [ + { + cells: this.coda.createRowCells(this), + }, + ], + }; + + const response = await this.coda.createRows( + $, + this.docId, + this.tableId, + data, + params, + ); + + $.export("$summary", "Upserted row(s) successfully"); + return response; + }, +}; diff --git a/components/coda/coda.app.mjs b/components/coda/coda.app.mjs index 8097e2568f0d3..b5cf9692c74d1 100644 --- a/components/coda/coda.app.mjs +++ b/components/coda/coda.app.mjs @@ -1,11 +1,310 @@ +import { axios } from "@pipedream/platform"; + export default { type: "app", app: "coda", - propDefinitions: {}, + propDefinitions: { + title: { + type: "string", + label: "Title", + description: "Title of the doc. Defaults to `\"Untitled\"`", + optional: true, + }, + docId: { + type: "string", + label: "Doc ID", + description: "ID of the Doc", + async options({ prevContext }) { + const response = await this.listDocs(this, { + pageToken: prevContext.nextPageToken, + }); + return this._makeOptionsResponse(response); + }, + }, + folderId: { + type: "string", + label: "Folder ID", + description: "ID of the folder", + optional: true, + }, + tableId: { + type: "string", + label: "Table ID", + description: "ID of the table", + async options({ + docId, + prevContext, + }) { + const response = await this.listTables(this, docId, { + pageToken: prevContext.nextPageToken, + }); + return this._makeOptionsResponse(response); + }, + }, + rowId: { + type: "string", + label: "Row ID", + description: "ID of the row", + async options({ + docId, tableId, prevContext, + }) { + let { counter = 0 } = prevContext; + const response = await this.findRow(this, docId, tableId, { + pageToken: prevContext.nextPageToken, + sortBy: "natural", + }); + + const { + options, + context, + } = this._makeOptionsResponse(response); + + return { + options: options + .map((row) => ({ + label: `${counter++}: ${row.label}`, + value: row.value, + })), + context: { + ...context, + counter, + }, + }; + }, + }, + columnId: { + type: "string", + label: "Column ID", + description: "ID of the column", + optional: true, + async options({ + docId, tableId, prevContext, + }) { + const response = await this.listColumns(this, docId, tableId, { + pageToken: prevContext.nextPageToken, + }); + return this._makeOptionsResponse(response); + }, + }, + keyColumns: { + type: "string[]", + label: "Key of columns to be upserted", + description: "Optional column IDs, specifying columns to be used as upsert keys", + async options({ + docId, tableId, prevContext, + }) { + const response = await this.listColumns(this, docId, tableId, { + pageToken: prevContext.nextPageToken, + }); + return this._makeOptionsResponse(response); + }, + }, + query: { + type: "string", + label: "Search Query", + description: "Search term used to filter down results", + optional: true, + }, + sortBy: { + type: "string", + label: "Sort By", + description: "Determines how to sort the given objects", + optional: true, + options: [ + "name", + ], + }, + disableParsing: { + type: "boolean", + label: "Disable Parsing", + description: "If true, the API will not attempt to parse the data in any way", + optional: true, + }, + visibleOnly: { + type: "boolean", + label: "Visible Only", + description: "If true, returns only visible rows and columns for the table", + optional: true, + }, + max: { + type: "integer", + label: "Max Items", + description: "Maximum number of results to return in this query", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + async _makeRequest($, opts) { + if (!opts.headers) opts.headers = {}; + opts.headers.Authorization = `Bearer ${this.$auth.api_token}`; + opts.headers["user-agent"] = "@PipedreamHQ/pipedream v0.1"; + if (!opts.method) opts.method = "get"; + const { path } = opts; + delete opts.path; + opts.url = `https://coda.io/apis/v1${path[0] === "/" + ? "" + : "/"}${path}`; + try { + return await axios($ ?? this, opts); + } catch (err) { + this._throwFormattedError(err); + } + }, + _throwFormattedError(err) { + err = err.response.data; + throw Error(`${err.statusCode} - ${err.statusMessage} - ${err.message}`); + }, + _makeOptionsResponse(response) { + return { + options: response.items + .map((e) => ({ + label: e.name, + value: e.id, + })), + context: { + nextPageToken: response.nextPageToken, + }, + }; + }, + createRowCells(component) { + return Object.keys(component) + .filter((k) => k.startsWith("col_") && component[k]) + .map((k) => ({ + column: k.slice(4), + value: component[k], + })); + }, + /** + * Creates a new doc or copies a doc from a source docId + * @param {object} $ + * @param {object} [data] + * @param {object} [data.title] + * @param {object} [data.folderId] + * @param {object} [data.sourceDoc] + * @return {object} Created or copied doc + */ + async createDoc($, data = {}) { + let opts = { + method: "post", + path: "/docs", + data, + }; + return await this._makeRequest($, opts); + }, + /** + * List docs according to query parameters + * @param {object} $ + * @param {object} [params] + * @param {string} [params.docId] + * @param {string} [params.workspaceId] + * @param {string} [params.folderId] + * @param {string} [params.query] + * @param {boolean} [params.isOwner] + * @param {boolean} [params.isPublished] + * @param {boolean} [params.isStarred] + * @param {boolean} [params.inGallery] + * @return {object[]} List of docs + */ + async listDocs($, params = {}) { + let opts = { + path: "/docs", + params, + }; + return await this._makeRequest($, opts); + }, + /** + * Lists tables in a doc according to parameters + * @param {object} $ + * @param {string} docId + * @param {object} [params] + * @param {string} [params.sortBy] + * @param {string} [params.tableTypes] + * @return {object[]} List of tables + */ + async listTables($, docId, params = {}) { + let opts = { + path: `/docs/${docId}/tables`, + params, + }; + return await this._makeRequest($, opts); + }, + /** + * Searches for a row in the selected table using a column match search + * @param {object} $ + * @param {string} docId + * @param {string} tableId + * @param {object} [params] + * @param {string} [params.query] + * @param {string} [params.sortBy] + * @param {boolean} [params.visibleOnly] + * @param {boolean} [params.useColumnNames] + * @param {string} [params.valueFormat] + * @return {object[]} List of rows + */ + async findRow($, docId, tableId, params = {}) { + let opts = { + path: `/docs/${docId}/tables/${tableId}/rows`, + params, + }; + return await this._makeRequest($, opts); + }, + /** + * Returns a list of columns in a doc table. + * @param {object} $ + * @param {string} docId + * @param {string} tableId + * @param {object} [params] + * @param {object} [params.visibleOnly] + * @return {object[]} List of columns + */ + async listColumns($, docId, tableId, params = {}) { + let opts = { + path: `/docs/${docId}/tables/${tableId}/columns`, + params, + }; + return await this._makeRequest($, opts); + }, + /** + * Inserts rows into a table, optionally updating existing rows using upsert key columns + * @param {object} $ + * @param {string} docId + * @param {string} tableId + * @param {object} data + * @param {object} data.rows + * @param {string[]} [data.keyColumns] + * @param {object} [params] + * @param {boolean} [params.disableParsing] + * @return {object[]} List of added rows and requestId + */ + async createRows($, docId, tableId, data, params = {}) { + let opts = { + method: "post", + path: `/docs/${docId}/tables/${tableId}/rows`, + params, + data, + }; + return await this._makeRequest($, opts); + }, + /** + * Updates the specified row in the table + * @param {object} $ + * @param {string} docId + * @param {string} tableId + * @param {string} rowId + * @param {object} data + * @param {object} data.row + * @param {object} [params] + * @param {boolean} [params.disableParsing] + * @return {object[]} Updated rowId and requestId + */ + async updateRow($, docId, tableId, rowId, data, params = {}) { + let opts = { + method: "put", + path: `/docs/${docId}/tables/${tableId}/rows/${rowId}`, + params, + data, + }; + return await this._makeRequest($, opts); }, }, };