diff --git a/components/google_sheets/actions/add-conditional-format-rule/add-conditional-format-rule.mjs b/components/google_sheets/actions/add-conditional-format-rule/add-conditional-format-rule.mjs new file mode 100644 index 0000000000000..4abe7ae7d062a --- /dev/null +++ b/components/google_sheets/actions/add-conditional-format-rule/add-conditional-format-rule.mjs @@ -0,0 +1,223 @@ +import googleSheets from "../../google_sheets.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "google_sheets-add-conditional-format-rule", + name: "Add Conditional Format Rule", + description: "Create conditional formatting with color scales or custom formulas. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddConditionalFormatRuleRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + description: "The range of cells to format (e.g., `A1:A10`)", + }, + conditionType: { + type: "string", + label: "Validation Type", + description: "The type of data condition", + options: [ + "ONE_OF_LIST", + "NUMBER_GREATER", + "NUMBER_LESS", + "DATE_BEFORE", + "DATE_AFTER", + "TEXT_CONTAINS", + "TEXT_IS_EMAIL", + "TEXT_IS_URL", + "BOOLEAN", + ], + }, + conditionValues: { + type: "string[]", + label: "Condition Values", + description: "Values for condition (e.g., color scales or custom formulas)", + }, + formattingType: { + type: "string", + label: "Formatting Type", + description: "Choose between boolean condition or gradient color scale", + options: [ + "BOOLEAN_RULE", + "GRADIENT_RULE", + ], + default: "BOOLEAN_RULE", + }, + rgbColor: { + type: "object", + label: "RGB Color", + description: "The RGB color value (e.g., {\"red\": 1.0, \"green\": 0.5, \"blue\": 0.2})", + optional: true, + }, + textFormat: { + type: "object", + label: "Text Format", + description: "The text format options", + optional: true, + }, + bold: { + type: "boolean", + label: "Bold", + description: "Whether the text is bold", + optional: true, + }, + italic: { + type: "boolean", + label: "Italic", + description: "Whether the text is italic", + optional: true, + }, + strikethrough: { + type: "boolean", + label: "Strikethrough", + description: "Whether the text is strikethrough", + optional: true, + }, + interpolationPointType: { + type: "string", + label: "Interpolation Point Type", + description: "The interpolation point type", + options: [ + "MIN", + "MAX", + "NUMBER", + "PERCENT", + "PERCENTILE", + ], + optional: true, + }, + index: { + type: "integer", + label: "Index", + description: "The zero-based index of the rule", + }, + }, + async run({ $ }) { + const { + startCol, + endCol, + startRow, + endRow, + } = this.googleSheets._parseRangeString(`${this.worksheetId}!${this.range}`); + + const rule = { + ranges: [ + { + sheetId: this.worksheetId, + startRowIndex: startRow, + endRowIndex: endRow, + startColumnIndex: startCol.charCodeAt(0) - 65, + endColumnIndex: endCol.charCodeAt(0) - 64, + }, + ], + }; + + const parseRgbColor = (rgbColor = {}) => { + if (typeof rgbColor === "string") { + try { + rgbColor = JSON.parse(rgbColor); + } catch { + throw new ConfigurationError("Could not parse RGB Color. Please provide a valid JSON object."); + } + } + return rgbColor; + }; + + this.formattingType === "GRADIENT_RULE" ? + rule.gradientRule = { + minpoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.interpolationPointType, + value: "MIN", + }, + midpoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.interpolationPointType, + value: "MID", + }, + maxpoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.interpolationPointType, + value: "MAX", + }, + } : + rule.booleanRule = { + condition: { + type: this.conditionType, + values: this.conditionValues?.map((v) => ({ + userEnteredValue: v, + })) || [], + }, + format: { + backgroundColorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + textFormat: { + ...this.textFormat, + foregroundColorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + bold: this.bold, + italic: this.italic, + strikethrough: this.strikethrough, + }, + }, + }; + + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + addConditionalFormatRule: { + rule, + index: this.index, + }, + }, + ], + }, + }; + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully added conditional format rule."); + return response; + }, +}; diff --git a/components/google_sheets/actions/add-protected-range/add-protected-range.mjs b/components/google_sheets/actions/add-protected-range/add-protected-range.mjs new file mode 100644 index 0000000000000..374cfe7a9ddb3 --- /dev/null +++ b/components/google_sheets/actions/add-protected-range/add-protected-range.mjs @@ -0,0 +1,111 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +export default { + key: "google_sheets-add-protected-range", + name: "Add Protected Range", + description: "Add edit protection to cell range with permissions. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddProtectedRangeRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + protectedRangeId: { + type: "integer", + label: "Protected Range ID", + description: "The ID of the protected range (required for update and delete operations). This is a unique identifier assigned by Google Sheets", + optional: true, + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + description: "The range of cells to protect (e.g., `A1:A10`). Required for add and update operations", + }, + description: { + type: "string", + label: "Description", + description: "A description of the protected range", + optional: true, + }, + requestingUserCanEdit: { + type: "boolean", + label: "Requesting User Can Edit", + description: "If true, the user making this request can edit the protected range", + optional: true, + default: false, + }, + protectors: { + type: "string[]", + label: "Protectors", + description: "Email addresses of users/groups who can edit the protected range (e.g., user@example.com)", + optional: true, + }, + }, + async run({ $ }) { + const { + startCol, + endCol, + startRow, + endRow, + } = this.googleSheets._parseRangeString(`${this.worksheetId}!${this.range}`); + + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + addProtectedRange: { + protectedRange: { + protectedRangeId: this.protectedRangeId, + range: { + sheetId: this.worksheetId, + startRowIndex: startRow, + endRowIndex: endRow, + startColumnIndex: startCol.charCodeAt(0) - 65, + endColumnIndex: endCol.charCodeAt(0) - 64, + }, + description: this.description, + requestingUserCanEdit: this.requestingUserCanEdit, + editors: { + users: this.protectors || [], + }, + }, + }, + }, + ], + }, + }; + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully added protected range."); + return response; + }, +}; diff --git a/components/google_sheets/actions/delete-conditional-format-rule/delete-conditional-format-rule.mjs b/components/google_sheets/actions/delete-conditional-format-rule/delete-conditional-format-rule.mjs new file mode 100644 index 0000000000000..5fff05f0293e0 --- /dev/null +++ b/components/google_sheets/actions/delete-conditional-format-rule/delete-conditional-format-rule.mjs @@ -0,0 +1,64 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +export default { + key: "google_sheets-delete-conditional-format-rule", + name: "Delete Conditional Format Rule", + description: "Remove conditional formatting rule by index. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteConditionalFormatRuleRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: true, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + index: { + type: "integer", + label: "Index", + description: "The zero-based index of the rule", + }, + }, + async run({ $ }) { + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + deleteConditionalFormatRule: { + sheetId: this.worksheetId, + index: this.index, + }, + }, + ], + }, + }; + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully deleted conditional format rule."); + return response; + }, +}; diff --git a/components/google_sheets/actions/merge-cells/merge-cells.mjs b/components/google_sheets/actions/merge-cells/merge-cells.mjs new file mode 100644 index 0000000000000..d4312f374856c --- /dev/null +++ b/components/google_sheets/actions/merge-cells/merge-cells.mjs @@ -0,0 +1,90 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +export default { + key: "google_sheets-merge-cells", + name: "Merge Cells", + description: "Merge a range of cells into a single cell. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MergeCellsRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + description: "The range of cells to apply validation (e.g., `A1:A10`)", + }, + mergeType: { + type: "string", + label: "Merge Type", + description: "The type of merge to perform", + options: [ + "MERGE_ALL", + "MERGE_COLUMNS", + "MERGE_ROWS", + ], + default: "MERGE_ALL", + }, + }, + async run({ $ }) { + const { + startCol, + endCol, + startRow, + endRow, + } = this.googleSheets._parseRangeString(`${this.worksheetId}!${this.range}`); + + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + mergeCells: { + range: { + sheetId: this.worksheetId, + startRowIndex: startRow, + endRowIndex: endRow, + startColumnIndex: startCol.charCodeAt(0) - 65, + endColumnIndex: endCol.charCodeAt(0) - 64, + }, + mergeType: this.mergeType, + }, + }, + ], + }, + }; + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully merged cells."); + return response; + }, +}; diff --git a/components/google_sheets/actions/set-data-validation/set-data-validation.mjs b/components/google_sheets/actions/set-data-validation/set-data-validation.mjs new file mode 100644 index 0000000000000..00bae5364bb73 --- /dev/null +++ b/components/google_sheets/actions/set-data-validation/set-data-validation.mjs @@ -0,0 +1,122 @@ +import googleSheets from "../../google_sheets.app.mjs"; + +export default { + key: "google_sheets-set-data-validation", + name: "Set Data Validation", + description: "Add data validation rules to cells (dropdowns, checkboxes, date/number validation). [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#SetDataValidationRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + description: "The range of cells to apply validation (e.g., `A1:A10`)", + }, + validationType: { + type: "string", + label: "Validation Type", + description: "The type of data validation", + options: [ + "NUMBER_GREATER", + "NUMBER_GREATER_THAN_EQ", + "NUMBER_LESS_THAN_EQ", + "NUMBER_LESS", + "TEXT_CONTAINS", + "TEXT_NOT_CONTAINS", + "DATE_EQUAL_TO", + "ONE_OF_LIST", + "DATE_AFTER", + "DATE_ON_OR_AFTER", + "DATE_BEFORE", + "DATE_ON_OR_BEFORE", + "DATE_BETWEEN", + "TEXT_STARTS_WITH", + "TEXT_ENDS_WITH", + "TEXT_EQUAL_TO", + "TEXT_NOT_EQUAL_TO", + "CUSTOM_FORMULA", + "NUMBER_EQUAL_TO", + "NUMBER_NOT_EQUAL_TO", + "NUMBER_BETWEEN", + "NUMBER_NOT_BETWEEN", + ], + }, + validationValues: { + type: "string[]", + label: "Validation Values", + description: "Values for validation (e.g., dropdown options)", + }, + }, + async run({ $ }) { + const { + startCol, + endCol, + startRow, + endRow, + } = this.googleSheets._parseRangeString(`${this.worksheetId}!${this.range}`); + + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + setDataValidation: { + range: { + sheetId: this.worksheetId, + startRowIndex: startRow, + endRowIndex: endRow, + startColumnIndex: startCol.charCodeAt(0) - 65, + endColumnIndex: endCol.charCodeAt(0) - 64, + }, + rule: { + condition: { + type: this.validationType, + values: this.validationValues?.map((v) => ({ + userEnteredValue: v, + })) || [], + }, + showCustomUi: true, + strict: true, + }, + }, + }, + ], + }, + }; + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully set data validation."); + return response; + }, +}; diff --git a/components/google_sheets/actions/update-conditional-format-rule/update-conditional-format-rule.mjs b/components/google_sheets/actions/update-conditional-format-rule/update-conditional-format-rule.mjs new file mode 100644 index 0000000000000..ff8357f0df9a9 --- /dev/null +++ b/components/google_sheets/actions/update-conditional-format-rule/update-conditional-format-rule.mjs @@ -0,0 +1,238 @@ +import googleSheets from "../../google_sheets.app.mjs"; +import { ConfigurationError } from "@pipedream/platform"; + +export default { + key: "google_sheets-update-conditional-format-rule", + name: "Update Conditional Format Rule", + description: "Modify existing conditional formatting rule. [See the documentation](https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#UpdateConditionalFormatRuleRequest)", + version: "0.0.1", + type: "action", + annotations: { + destructiveHint: false, + openWorldHint: true, + readOnlyHint: false, + }, + props: { + googleSheets, + drive: { + propDefinition: [ + googleSheets, + "watchedDrive", + ], + }, + sheetId: { + propDefinition: [ + googleSheets, + "sheetID", + (c) => ({ + driveId: googleSheets.methods.getDriveId(c.drive), + }), + ], + }, + worksheetId: { + propDefinition: [ + googleSheets, + "worksheetIDs", + (c) => ({ + sheetId: c.sheetId, + }), + ], + }, + range: { + propDefinition: [ + googleSheets, + "range", + ], + description: "The range of cells to protect (e.g., `A1:A10`)", + }, + conditionType: { + type: "string", + label: "Validation Type", + description: "The type of data condition", + options: [ + "ONE_OF_LIST", + "NUMBER_GREATER", + "NUMBER_LESS", + "DATE_BEFORE", + "DATE_AFTER", + "TEXT_CONTAINS", + "TEXT_IS_EMAIL", + "TEXT_IS_URL", + "BOOLEAN", + ], + }, + conditionValues: { + type: "string[]", + label: "Condition Values", + description: "Values for condition (e.g., color scales or custom formulas)", + }, + formattingType: { + type: "string", + label: "Formatting Type", + description: "Choose between boolean condition or gradient color scale", + options: [ + "BOOLEAN_RULE", + "GRADIENT_RULE", + ], + default: "BOOLEAN_RULE", + }, + rgbColor: { + type: "object", + label: "RGB Color", + description: "The RGB color value (e.g., {\"red\": 1.0, \"green\": 0.5, \"blue\": 0.2})", + optional: true, + }, + textFormat: { + type: "object", + label: "Text Format", + description: "The text format options", + optional: true, + }, + bold: { + type: "boolean", + label: "Bold", + description: "Whether the text is bold", + optional: true, + }, + italic: { + type: "boolean", + label: "Italic", + description: "Whether the text is italic", + optional: true, + }, + strikethrough: { + type: "boolean", + label: "Strikethrough", + description: "Whether the text is strikethrough", + optional: true, + }, + interpolationPointType: { + type: "string", + label: "Interpolation Point Type", + description: "The interpolation point type", + options: [ + "MIN", + "MAX", + "NUMBER", + "PERCENT", + "PERCENTILE", + ], + optional: true, + }, + index: { + type: "integer", + label: "Index", + description: "The zero-based index of the rule", + }, + newIndex: { + type: "integer", + label: "New Index", + description: "The new zero-based index of the rule", + optional: true, + }, + }, + async run({ $ }) { + const { + startCol, + endCol, + startRow, + endRow, + } = this.googleSheets._parseRangeString(`${this.worksheetId}!${this.range}`); + + const rule = { + ranges: [ + { + sheetId: this.worksheetId, + startRowIndex: startRow, + endRowIndex: endRow, + startColumnIndex: startCol.charCodeAt(0) - 65, + endColumnIndex: endCol.charCodeAt(0) - 64, + }, + ], + }; + const parseRgbColor = (rgbColor = {}) => { + if (typeof rgbColor === "string") { + try { + rgbColor = JSON.parse(rgbColor); + } catch { + throw new ConfigurationError("Could not parse RGB Color. Please provide a valid JSON object."); + } + } + return rgbColor; + }; + + this.formattingType === "GRADIENT_RULE" ? + rule.gradientRule = { + minpoint: { + interpolationPoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.InterpolationPointType, + value: "MIN", + }, + }, + midpoint: { + interpolationPoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.InterpolationPointType, + value: "MID", + }, + }, + maxpoint: { + interpolationPoint: { + colorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + type: this.InterpolationPointType, + value: "MAX", + }, + }, + } : + rule.booleanRule = { + condition: { + type: this.conditionType, + values: this.conditionValues?.map((v) => ({ + userEnteredValue: v, + })) || [], + }, + format: { + backgroundColorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + }, + textFormat: { + ...this.textFormat, + foregroundColorStyle: { + rgbColor: parseRgbColor(this.rgbColor), + + }, + bold: this.bold, + italic: this.italic, + strikethrough: this.strikethrough, + }, + }, + }; + + const request = { + spreadsheetId: this.sheetId, + requestBody: { + requests: [ + { + updateConditionalFormatRule: { + index: this.index, + sheetId: this.worksheetId, + rule, + newIndex: this.newIndex, + }, + }, + ], + }, + }; + + const response = await this.googleSheets.batchUpdate(request); + $.export("$summary", "Successfully updated conditional format rule."); + return response; + }, +}; diff --git a/components/google_sheets/package.json b/components/google_sheets/package.json index 0b0021b697c69..65bc7365d07b2 100644 --- a/components/google_sheets/package.json +++ b/components/google_sheets/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_sheets", - "version": "0.10.0", + "version": "0.11.0", "description": "Pipedream Google_sheets Components", "main": "google_sheets.app.mjs", "keywords": [ diff --git a/components/google_sheets/sources/new-comment/new-comment.mjs b/components/google_sheets/sources/new-comment/new-comment.mjs index 96c020965eef3..add3c05e04741 100644 --- a/components/google_sheets/sources/new-comment/new-comment.mjs +++ b/components/google_sheets/sources/new-comment/new-comment.mjs @@ -1,16 +1,30 @@ -import httpBase from "../common/http-based/sheet.mjs"; +import app from "../../google_sheets.app.mjs"; import sampleEmit from "./test-event.mjs"; export default { - ...httpBase, key: "google_sheets-new-comment", name: "New Comment (Instant)", description: "Emit new event each time a comment is added to a spreadsheet.", - version: "0.1.3", + version: "0.2.0", dedupe: "unique", type: "source", + props: { + app, + db: "$.service.db", + timer: { + type: "$.interface.timer", + default: { + intervalSeconds: 60, + }, + }, + sheetID: { + propDefinition: [ + app, + "sheetID", + ], + }, + }, methods: { - ...httpBase.methods, _getLastTs() { return this.db.get("lastTs"); }, @@ -24,18 +38,20 @@ export default { ts: Date.parse(comment.createdTime), }; }, - takeSheetSnapshot() {}, - getSheetId() { - return this.sheetID.toString(); - }, async processSpreadsheet() { const comments = []; const lastTs = this._getLastTs(); - const results = this.googleSheets.listComments(this.sheetID, lastTs); - for await (const comment of results) { - comments.push(comment); + + try { + const results = this.app.listComments(this.sheetID, lastTs); + for await (const comment of results) { + comments.push(comment); + } + } catch (error) { + console.error("Error fetching comments:", error); } if (!comments.length) { + console.log("No new comments since last check"); return; } this._setLastTs(comments[0].createdTime); @@ -45,17 +61,8 @@ export default { }); }, }, - async run(event) { - if (event.timestamp) { - // Component was invoked by timer - return this.renewSubscription(); - } - - if (!this.isEventRelevant(event)) { - console.log("Sync notification, exiting early"); - return; - } - + async run() { + // Component was invoked by timer await this.processSpreadsheet(); }, sampleEmit, diff --git a/components/google_sheets/sources/new-row-added/new-row-added.mjs b/components/google_sheets/sources/new-row-added/new-row-added.mjs index 8ded968d86b7b..03c29758d30df 100644 --- a/components/google_sheets/sources/new-row-added/new-row-added.mjs +++ b/components/google_sheets/sources/new-row-added/new-row-added.mjs @@ -1,23 +1,167 @@ -import httpBase from "../common/http-based/sheet.mjs"; -import newRowAdded from "../common/new-row-added.mjs"; import sampleEmit from "./test-event.mjs"; export default { - ...httpBase, - ...newRowAdded, key: "google_sheets-new-row-added", - name: "New Row Added (Instant)", - description: "Emit new event each time a row or rows are added to the bottom of a spreadsheet.", - version: "0.2.3", - dedupe: "unique", type: "source", + name: "New Row Added (Polling)", + description: + "Emit new event each time a row or rows are added to the bottom of a spreadsheet.", + version: "0.3.0", + dedupe: "unique", props: { - ...httpBase.props, - ...newRowAdded.props, + db: "$.service.db", + http: "$.interface.http", + google_sheets: { + type: "app", + app: "google_sheets", + label: "Google Sheets", + description: "Google Sheets account used to access the Sheets API", + }, + spreadsheet_id: { + type: "string", + label: "Spreadsheet ID", + description: "The Google Sheets spreadsheet ID", + }, + sheet_name: { + type: "string", + label: "Sheet Name", + description: "The name of the sheet to monitor (e.g., 'Sheet1')", + }, }, methods: { - ...httpBase.methods, - ...newRowAdded.methods, + async getSheetData() { + const { + google_sheets, + spreadsheet_id, + sheet_name, + } = this; + + const response = await google_sheets.request({ + url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values/${encodeURIComponent(sheet_name)}`, + method: "GET", + }); + + if (response && response.values) { + return response.values; + } + return []; + }, + + async getHeaderRow() { + const data = await this.getSheetData(); + if (data.length > 0) { + return data[0]; + } + return []; + }, + + rowToObject(headers, rowData) { + const obj = {}; + headers.forEach((header, idx) => { + obj[header] = rowData[idx] || ""; + }); + return obj; + }, + + getStoredRowCount() { + const { + spreadsheet_id, + sheet_name, + db, + } = this; + const stateKey = `row-count-${spreadsheet_id}-${sheet_name}`; + return db.get(stateKey) || 0; + }, + + updateStoredRowCount(count) { + const { + spreadsheet_id, + sheet_name, + db, + } = this; + const stateKey = `row-count-${spreadsheet_id}-${sheet_name}`; + db.set(stateKey, count); + }, + + getStoredRows() { + const { + spreadsheet_id, + sheet_name, + db, + } = this; + const stateKey = `rows-${spreadsheet_id}-${sheet_name}`; + return db.get(stateKey) || []; + }, + + updateStoredRows(rows) { + const { + spreadsheet_id, + sheet_name, + db, + } = this; + const stateKey = `rows-${spreadsheet_id}-${sheet_name}`; + db.set(stateKey, rows); + }, + }, + async run() { + const { + spreadsheet_id, + sheet_name, + } = this; + + if (!spreadsheet_id || !sheet_name) { + throw new Error("Please provide both Spreadsheet ID and Sheet Name"); + } + + try { + const allData = await this.getSheetData(); + + // Sheet must have at least a header row + if (allData.length === 0) { + this.updateStoredRowCount(0); + return; + } + + const headers = allData[0]; + const currentRows = allData.slice(1); // Exclude header + const storedRowCount = this.getStoredRowCount(); + + // Detect new rows added to the bottom + if (currentRows.length > storedRowCount) { + const newRows = currentRows.slice(storedRowCount); + + // Emit event for new rows + newRows.forEach((rowData, idx) => { + const rowNumber = storedRowCount + idx + 2; // +2: 1 for header, 1 for 1-indexing + const rowObject = this.rowToObject(headers, rowData); + + this.$emit( + { + row_number: rowNumber, + values: rowData, + object: rowObject, + spreadsheet_id, + sheet_name, + timestamp: new Date().toISOString(), + }, + { + id: `${spreadsheet_id}-${sheet_name}-row-${rowNumber}`, + summary: `New row added at row ${rowNumber}`, + ts: Date.now(), + }, + ); + }); + + // Update stored row count + this.updateStoredRowCount(currentRows.length); + } else if (storedRowCount === 0) { + // First run: store current row count without emitting + this.updateStoredRowCount(currentRows.length); + } + } catch (error) { + console.error(`Error fetching sheet data: ${error.message}`); + throw error; + } }, sampleEmit, }; diff --git a/components/google_sheets/sources/new-updates/new-updates.mjs b/components/google_sheets/sources/new-updates/new-updates.mjs index 99583bda991cb..5b1627a14d601 100644 --- a/components/google_sheets/sources/new-updates/new-updates.mjs +++ b/components/google_sheets/sources/new-updates/new-updates.mjs @@ -1,23 +1,149 @@ -import httpBase from "../common/http-based/sheet.mjs"; import sampleEmit from "./test-event.mjs"; -import newUpdates from "../common/new-updates.mjs"; export default { - ...httpBase, - ...newUpdates, key: "google_sheets-new-updates", type: "source", - name: "New Updates (Instant)", + name: "New Updates (Polling)", description: "Emit new event each time a row or cell is updated in a spreadsheet.", - version: "0.3.3", + version: "0.4.0", dedupe: "unique", props: { - ...httpBase.props, - ...newUpdates.props, + db: "$.service.db", + http: "$.interface.http", + google_sheets: { + type: "app", + app: "google_sheets", + label: "Google Sheets", + description: "Google Sheets account to access spreadsheets", + }, + sheet_name: { + type: "string", + label: "Sheet Name", + description: "The name of the sheet to monitor (e.g., 'Sheet1')", + }, }, methods: { - ...httpBase.methods, - ...newUpdates.methods, + async getSheetData() { + const { google_sheets } = this; + const { + spreadsheet_id, + sheet_name, + } = this; + + const response = await google_sheets.request({ + url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}/values/${encodeURIComponent(sheet_name)}`, + method: "GET", + }); + + return response.values || []; + }, + + async getLastModifiedTime() { + const { google_sheets } = this; + const { spreadsheet_id } = this; + + const response = await google_sheets.request({ + url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}`, + method: "GET", + params: { + fields: "spreadsheet_metadata.last_update_time", + }, + }); + + return response.spreadsheet_metadata?.last_update_time; + }, + + async getAllSheetMetadata() { + const { google_sheets } = this; + const { spreadsheet_id } = this; + + const response = await google_sheets.request({ + url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}`, + method: "GET", + params: { + fields: "sheets(properties(sheetId,title))", + }, + }); + + return response.sheets || []; + }, + + async getDetailedChangeData(previousData) { + const currentData = await this.getSheetData(); + const changes = []; + + const maxRows = Math.max(previousData.length, currentData.length); + + for (let i = 0; i < maxRows; i++) { + const prevRow = previousData[i] || []; + const currRow = currentData[i] || []; + const maxCols = Math.max(prevRow.length, currRow.length); + + for (let j = 0; j < maxCols; j++) { + const prevCell = prevRow[j] || ""; + const currCell = currRow[j] || ""; + + if (prevCell !== currCell) { + changes.push({ + row: i + 1, + column: j + 1, + column_letter: this.getColumnLetter(j + 1), + previous_value: prevCell, + current_value: currCell, + }); + } + } + } + + return { + current_data: currentData, + changes, + timestamp: new Date().toISOString(), + row_count: currentData.length, + column_count: + currentData.length > 0 + ? currentData[0].length + : 0, + }; + }, + + getColumnLetter(colNumber) { + let letter = ""; + while (colNumber > 0) { + colNumber--; + letter = String.fromCharCode(65 + (colNumber % 26)) + letter; + colNumber = Math.floor(colNumber / 26); + } + return letter; + }, + }, + async run() { + const { + spreadsheet_id, + sheet_name, + db, + } = this; + + const stateKey = `${spreadsheet_id}-${sheet_name}`; + const previousData = db.get(stateKey) || []; + + try { + const changeData = await this.getDetailedChangeData(previousData); + const currentData = changeData.current_data; + + if (changeData.changes.length > 0) { + db.set(stateKey, currentData); + + this.$emit(changeData, { + id: `${stateKey}-${changeData.timestamp}`, + summary: `${changeData.changes.length} cell(s) updated`, + ts: Date.parse(changeData.timestamp), + }); + } + } catch (error) { + console.error(`Error fetching sheet data: ${error.message}`); + throw error; + } }, sampleEmit, }; diff --git a/components/google_sheets/sources/new-worksheet/new-worksheet.mjs b/components/google_sheets/sources/new-worksheet/new-worksheet.mjs index 5467e522b2982..554e0bc482a20 100644 --- a/components/google_sheets/sources/new-worksheet/new-worksheet.mjs +++ b/components/google_sheets/sources/new-worksheet/new-worksheet.mjs @@ -1,27 +1,116 @@ -import httpBase from "../common/http-based/sheet.mjs"; -import newWorksheet from "../common/new-worksheet.mjs"; import sampleEmit from "./test-event.mjs"; export default { - ...httpBase, - ...newWorksheet, key: "google_sheets-new-worksheet", type: "source", - name: "New Worksheet (Instant)", + name: "New Worksheet (Polling)", description: "Emit new event each time a new worksheet is created in a spreadsheet.", - version: "0.2.3", + version: "0.3.0", dedupe: "unique", - hooks: { - ...httpBase.hooks, - ...newWorksheet.hooks, - }, props: { - ...httpBase.props, - ...newWorksheet.props, + db: "$.service.db", + http: "$.interface.http", + google_sheets: { + type: "app", + app: "google_sheets", + label: "Google Sheets", + description: "Google Sheets account to access spreadsheets", + }, + spreadsheet_id: { + type: "string", + label: "Spreadsheet ID", + description: "The Google Sheets spreadsheet ID", + }, }, methods: { - ...httpBase.methods, - ...newWorksheet.methods, + async getSpreadsheetMetadata() { + const { google_sheets } = this; + const { spreadsheet_id } = this; + + const response = await google_sheets.request({ + url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheet_id}`, + method: "GET", + params: { + fields: "sheets(properties(sheetId,title,index,sheetType,gridProperties),data(rowData(values(userEnteredValue))))", + }, + }); + + return response; + }, + + async getSheets() { + const metadata = await this.getSpreadsheetMetadata(); + return metadata.sheets || []; + }, + + formatSheetData(sheet) { + const { properties } = sheet; + return { + sheet_id: properties.sheetId, + title: properties.title, + index: properties.index, + sheet_type: properties.sheetType || "GRID", + grid_properties: properties.gridProperties, + created_at: new Date().toISOString(), + }; + }, + + getStoredSheetIds() { + const { + spreadsheet_id, + db, + } = this; + const stateKey = `sheets-${spreadsheet_id}`; + return db.get(stateKey) || []; + }, + + updateStoredSheetIds(sheetIds) { + const { + spreadsheet_id, + db, + } = this; + const stateKey = `sheets-${spreadsheet_id}`; + db.set(stateKey, sheetIds); + }, + }, + async run() { + const { spreadsheet_id } = this; + + try { + const sheets = await this.getSheets(); + const currentSheetIds = sheets.map((s) => s.properties.sheetId); + const storedSheetIds = this.getStoredSheetIds(); + + const newSheets = sheets.filter( + (sheet) => !storedSheetIds.includes(sheet.properties.sheetId), + ); + + if (newSheets.length > 0) { + newSheets.forEach((sheet) => { + const formattedSheet = this.formatSheetData(sheet); + + this.$emit( + { + ...formattedSheet, + spreadsheet_id, + timestamp: new Date().toISOString(), + }, + { + id: `${spreadsheet_id}-${sheet.properties.sheetId}`, + summary: `New worksheet created: ${sheet.properties.title}`, + ts: Date.now(), + }, + ); + }); + + this.updateStoredSheetIds(currentSheetIds); + } else if (storedSheetIds.length === 0) { + this.updateStoredSheetIds(currentSheetIds); + } + } catch (error) { + console.error(`Error fetching spreadsheet metadata: ${error.message}`); + throw error; + } }, sampleEmit, };