Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { areSheetIdsValid, googleSheetsCommon } from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
export const clearSheetAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'clear_sheet',
|
||||
description: 'Clears all rows on an existing sheet',
|
||||
displayName: 'Clear Sheet',
|
||||
props: {
|
||||
...commonProps,
|
||||
is_first_row_headers: Property.Checkbox({
|
||||
displayName: 'Is First row Headers?',
|
||||
description: 'If the first row is headers',
|
||||
required: true,
|
||||
defaultValue: true,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const { spreadsheetId, sheetId, is_first_row_headers:isFirstRowHeaders, headerRow } = propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
await googleSheetsCommon.findSheetName(
|
||||
auth,
|
||||
spreadsheetId as string,
|
||||
sheetId as number
|
||||
);
|
||||
|
||||
const rowsToDelete: number[] = [];
|
||||
const values = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheetId as string,
|
||||
auth: auth,
|
||||
sheetId: sheetId as number,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: undefined,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
for (const key in values) {
|
||||
if (key === '0' && isFirstRowHeaders) {
|
||||
continue;
|
||||
}
|
||||
rowsToDelete.push(parseInt(key) + 1);
|
||||
}
|
||||
|
||||
const response = await googleSheetsCommon.clearSheet(
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
auth,
|
||||
isFirstRowHeaders ? 1 : 0,
|
||||
rowsToDelete.length
|
||||
);
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { includeTeamDrivesProp, sheetIdProp, spreadsheetIdProp } from '../common/props';
|
||||
import { google } from 'googleapis';
|
||||
import { createGoogleClient } from '../common/common';
|
||||
|
||||
export const copyWorksheetAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'copy-worksheet',
|
||||
displayName: 'Copy Worksheet',
|
||||
description: 'Creates a new worksheet by copying an existing one.',
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheetId: spreadsheetIdProp('Spreadsheet Containing the Worksheet to Copy', ''),
|
||||
sheetId: sheetIdProp('Worksheet to Copy', ''),
|
||||
desinationSpeadsheetId: spreadsheetIdProp('Spreadsheet to paste in', ''),
|
||||
},
|
||||
async run(context) {
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const response = await sheets.spreadsheets.sheets.copyTo({
|
||||
spreadsheetId: context.propsValue.spreadsheetId,
|
||||
sheetId: context.propsValue.sheetId,
|
||||
requestBody: {
|
||||
destinationSpreadsheetId: context.propsValue.desinationSpeadsheetId,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
areSheetIdsValid,
|
||||
columnToLabel,
|
||||
createGoogleClient,
|
||||
getHeaderRow,
|
||||
ValueInputOption,
|
||||
} from '../common/common';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import { getWorkSheetName } from '../triggers/helpers';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
export const createColumnAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'create-column',
|
||||
displayName: 'Create Spreadsheet Column',
|
||||
description: 'Adds a new column to a spreadsheet.',
|
||||
props: {
|
||||
...commonProps,
|
||||
columnName: Property.ShortText({
|
||||
displayName: 'Column Name',
|
||||
required: true,
|
||||
}),
|
||||
columnIndex: Property.Number({
|
||||
displayName: 'Column Index',
|
||||
description:
|
||||
'The column index starts from 1.For example, if you want to add a column to the third column, enter 3.Ff the input is less than 1 the column will be added after the last current column.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { spreadsheetId, sheetId, columnName, columnIndex } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
let columnLabel;
|
||||
|
||||
if (columnIndex && columnIndex > 0) {
|
||||
await sheets.spreadsheets.batchUpdate({
|
||||
spreadsheetId,
|
||||
requestBody: {
|
||||
requests: [
|
||||
{
|
||||
insertDimension: {
|
||||
range: {
|
||||
sheetId,
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: columnIndex -1,
|
||||
endIndex: columnIndex,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
columnLabel = columnToLabel(columnIndex-1);
|
||||
} else {
|
||||
const headers = await getHeaderRow({
|
||||
spreadsheetId:spreadsheetId as string,
|
||||
sheetId :sheetId as number,
|
||||
auth: context.auth,
|
||||
});
|
||||
|
||||
const newColumnIndex = headers === undefined ? 0 : headers.length;
|
||||
|
||||
await sheets.spreadsheets.batchUpdate({
|
||||
spreadsheetId,
|
||||
requestBody: {
|
||||
requests: [
|
||||
{
|
||||
insertDimension: {
|
||||
range: {
|
||||
sheetId,
|
||||
dimension: 'COLUMNS',
|
||||
startIndex: newColumnIndex,
|
||||
endIndex: newColumnIndex + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
columnLabel = columnToLabel(newColumnIndex);
|
||||
}
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId as string , sheetId as number);
|
||||
|
||||
const response = await sheets.spreadsheets.values.update({
|
||||
range: `${sheetName}!${columnLabel}1`,
|
||||
spreadsheetId,
|
||||
valueInputOption: ValueInputOption.USER_ENTERED,
|
||||
requestBody: {
|
||||
values: [[columnName]],
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
AppConnectionValueForAuthProperty,
|
||||
createAction,
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
AuthenticationType,
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
HttpRequest,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { google } from 'googleapis';
|
||||
import { includeTeamDrivesProp } from '../common/props';
|
||||
import { createGoogleClient, getAccessToken, googleSheetsAuth } from '../common/common';
|
||||
import { AppConnectionType, isNil } from '@activepieces/shared';
|
||||
|
||||
export const createSpreadsheetAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'create-spreadsheet',
|
||||
displayName: 'Create Spreadsheet',
|
||||
description: 'Creates a blank spreadsheet.',
|
||||
props: {
|
||||
title: Property.ShortText({
|
||||
displayName: 'Title',
|
||||
description: 'The title of the new spreadsheet.',
|
||||
required: true,
|
||||
}),
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
folder: Property.Dropdown({
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Parent Folder',
|
||||
description:
|
||||
'The folder to create the worksheet in.By default, the new worksheet is created in the root folder of drive.',
|
||||
required: false,
|
||||
refreshers: ['auth', 'includeTeamDrives'],
|
||||
options: async ({ auth, includeTeamDrives }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Please authenticate first',
|
||||
};
|
||||
}
|
||||
const authProp = auth;
|
||||
let folders: { id: string; name: string }[] = [];
|
||||
const isServiceAccountWithoutImpersonation = authProp.type === AppConnectionType.CUSTOM_AUTH && authProp.props.userEmail?.length === 0;
|
||||
let pageToken = null;
|
||||
do {
|
||||
const request: HttpRequest = {
|
||||
method: HttpMethod.GET,
|
||||
url: `https://www.googleapis.com/drive/v3/files`,
|
||||
queryParams: {
|
||||
q: "mimeType='application/vnd.google-apps.folder' and trashed = false",
|
||||
includeItemsFromAllDrives: includeTeamDrives || isServiceAccountWithoutImpersonation ? 'true' : 'false',
|
||||
supportsAllDrives: 'true',
|
||||
},
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(authProp),
|
||||
},
|
||||
};
|
||||
if (pageToken) {
|
||||
if (request.queryParams !== undefined) {
|
||||
request.queryParams['pageToken'] = pageToken;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await httpClient.sendRequest<{
|
||||
files: { id: string; name: string,teamDriveId?: string }[];
|
||||
nextPageToken: string;
|
||||
}>(request);
|
||||
folders = folders.concat(response.body.files.filter(file => !isNil(file.teamDriveId) || !isServiceAccountWithoutImpersonation));
|
||||
pageToken = response.body.nextPageToken;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to get folders\nError:${e}`);
|
||||
}
|
||||
} while (pageToken);
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options: folders.map((folder: { id: string; name: string }) => {
|
||||
return {
|
||||
label: folder.name,
|
||||
value: folder.id,
|
||||
};
|
||||
}),
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { title, folder } = context.propsValue;
|
||||
const response = await createSpreadsheet(context.auth, title, folder);
|
||||
const newSpreadsheetId = response.id;
|
||||
|
||||
|
||||
|
||||
return {
|
||||
id: newSpreadsheetId,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
async function createSpreadsheet(
|
||||
auth: AppConnectionValueForAuthProperty<typeof googleSheetsAuth>,
|
||||
title: string,
|
||||
folderId?: string,
|
||||
) {
|
||||
const googleClient = await createGoogleClient(auth);
|
||||
const driveApi = google.drive({ version: 'v3', auth: googleClient });
|
||||
const response = await driveApi.files.create({
|
||||
requestBody: {
|
||||
name: title,
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
parents: folderId ? [folderId] : undefined,
|
||||
},
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { createGoogleClient } from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { includeTeamDrivesProp, spreadsheetIdProp } from '../common/props';
|
||||
import { google } from 'googleapis';
|
||||
|
||||
export const createWorksheetAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'create-worksheet',
|
||||
displayName: 'Create Worksheet',
|
||||
description:'Create a blank worksheet with a title.',
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheetId: spreadsheetIdProp('Spreadsheet',''),
|
||||
title:Property.ShortText({
|
||||
displayName:'Title',
|
||||
description:'The title of the new worksheet.',
|
||||
required:true
|
||||
}),
|
||||
headers:Property.Array({
|
||||
displayName:'Headers',
|
||||
required:false
|
||||
})
|
||||
|
||||
},
|
||||
async run(context){
|
||||
const {spreadsheetId,title} = context.propsValue;
|
||||
const headers = context.propsValue.headers as string[] ?? [];
|
||||
const client = await createGoogleClient(context.auth);
|
||||
const sheetsApi = google.sheets({ version: 'v4', auth: client });
|
||||
const sheet = await sheetsApi.spreadsheets.batchUpdate({
|
||||
spreadsheetId:spreadsheetId,
|
||||
requestBody:{
|
||||
requests:[
|
||||
{
|
||||
addSheet:{
|
||||
properties:{
|
||||
title:title
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
const addHeadersResponse = await sheetsApi.spreadsheets.values.append({
|
||||
spreadsheetId,
|
||||
range:`${context.propsValue.title}!A1`,
|
||||
valueInputOption:'RAW',
|
||||
requestBody:{
|
||||
majorDimension:'ROWS',
|
||||
values:[headers]
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: sheet.data?.replies?.[0]?.addSheet?.properties?.sheetId,
|
||||
...addHeadersResponse.data
|
||||
}
|
||||
|
||||
|
||||
}})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { areSheetIdsValid, googleSheetsCommon } from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
export const deleteRowAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'delete_row',
|
||||
description: 'Delete a row on an existing sheet you have access to',
|
||||
displayName: 'Delete Row',
|
||||
props: {
|
||||
...commonProps,
|
||||
rowId: Property.Number({
|
||||
displayName: 'Row Number',
|
||||
description: 'The row number to remove',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { spreadsheetId, sheetId, rowId } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId,sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
// Subtract 1 from the row_id to convert it to 0-indexed
|
||||
const adjustedRowIndex = rowId - 1;
|
||||
const response = await googleSheetsCommon.deleteRow(
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
adjustedRowIndex,
|
||||
context.auth,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
body: response,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
AuthenticationType,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { commonProps } from '../common/props';
|
||||
import { areSheetIdsValid, getAccessToken } from '../common/common';
|
||||
|
||||
export const exportSheetAction = createAction({
|
||||
name: 'export_sheet',
|
||||
displayName: 'Export Sheet',
|
||||
description: 'Export a Google Sheets tab to CSV or TSV format.',
|
||||
auth: googleSheetsAuth,
|
||||
props: {
|
||||
...commonProps,
|
||||
format: Property.StaticDropdown({
|
||||
displayName: 'Export Format',
|
||||
description: 'The format to export the sheet to.',
|
||||
required: true,
|
||||
defaultValue: 'csv',
|
||||
options: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{ label: 'Comma Separated Values (.csv)', value: 'csv' },
|
||||
{ label: 'Tab Separated Values (.tsv)', value: 'tsv' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
returnAsText: Property.Checkbox({
|
||||
displayName: 'Return as Text',
|
||||
description: 'Return the exported data as text instead of a file.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth, files }) {
|
||||
const { spreadsheetId, sheetId, format, returnAsText } = propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const spreadsheet_id = spreadsheetId as string;
|
||||
const sheet_id = sheetId as number;
|
||||
|
||||
const exportUrl = `https://docs.google.com/spreadsheets/d/${spreadsheet_id}/export?format=${format}&id=${spreadsheet_id}&gid=${sheet_id}`;
|
||||
|
||||
try {
|
||||
const response = await httpClient.sendRequest({
|
||||
method: HttpMethod.GET,
|
||||
url: exportUrl,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
if (returnAsText) {
|
||||
const textData = Buffer.from(response.body).toString('utf-8');
|
||||
return {
|
||||
text: textData,
|
||||
format,
|
||||
};
|
||||
} else {
|
||||
const filename = `exported_sheet.${format}`;
|
||||
|
||||
const file = await files.write({
|
||||
fileName: filename,
|
||||
data: Buffer.from(response.body),
|
||||
});
|
||||
|
||||
return {
|
||||
file,
|
||||
filename,
|
||||
format,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to export sheet: ${error}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Property, createAction } from '@activepieces/pieces-framework';
|
||||
import { areSheetIdsValid, googleSheetsCommon } from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
export const findRowByNumAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'find_row_by_num',
|
||||
description: 'Get a row in a Google Sheet by row number',
|
||||
displayName: 'Get Row',
|
||||
props: {
|
||||
...commonProps,
|
||||
rowNumber: Property.Number({
|
||||
displayName: 'Row Number',
|
||||
description: 'The row number to get from the sheet',
|
||||
required: true,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {spreadsheetId,sheetId,rowNumber,headerRow} = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId,sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const row = await googleSheetsCommon.getGoogleSheetRows({
|
||||
auth: context.auth,
|
||||
sheetId: sheetId as number,
|
||||
spreadsheetId: spreadsheetId as string,
|
||||
rowIndex_s: rowNumber,
|
||||
rowIndex_e: rowNumber,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
return row[0];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
createAction,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import {
|
||||
areSheetIdsValid,
|
||||
googleSheetsCommon,
|
||||
labelToColumn,
|
||||
mapRowsToHeaderNames
|
||||
} from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { z } from 'zod';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { columnNameProp, commonProps } from '../common/props';
|
||||
|
||||
export const findRowsAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'find_rows',
|
||||
description:
|
||||
'Find or get rows in a Google Sheet by column name and search value',
|
||||
displayName: 'Find Rows',
|
||||
props: {
|
||||
...commonProps,
|
||||
columnName: columnNameProp(),
|
||||
searchValue: Property.ShortText({
|
||||
displayName: 'Search Value',
|
||||
description:
|
||||
'The value to search for in the specified column. If left empty, all rows will be returned.',
|
||||
required: false,
|
||||
}),
|
||||
matchCase: Property.Checkbox({
|
||||
displayName: 'Exact match',
|
||||
description:
|
||||
'Whether to choose the rows with exact match or choose the rows that contain the search value',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
}),
|
||||
startingRow: Property.Number({
|
||||
displayName: 'Starting Row',
|
||||
description: 'The row number to start searching from',
|
||||
required: false,
|
||||
}),
|
||||
numberOfRows: Property.Number({
|
||||
displayName: 'Number of Rows',
|
||||
description:
|
||||
'The number of rows to return ( the default is 1 if not specified )',
|
||||
required: false,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
useHeaderNames: Property.Checkbox({
|
||||
displayName: 'Use header names for keys',
|
||||
description: 'Map A/B/C… to the actual column headers (row specified above).',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
startingRow: z.number().min(1).optional(),
|
||||
numberOfRows: z.number().min(1).optional(),
|
||||
});
|
||||
|
||||
const spreadsheetId = propsValue.spreadsheetId;
|
||||
const sheetId = propsValue.sheetId;
|
||||
const startingRow = propsValue.startingRow ?? 1;
|
||||
const numberOfRowsToReturn = propsValue.numberOfRows ?? 1;
|
||||
const headerRow = propsValue.headerRow;
|
||||
const useHeaderNames = propsValue.useHeaderNames as boolean;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId,sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const rows = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheetId as string,
|
||||
auth: auth,
|
||||
sheetId: sheetId as number,
|
||||
rowIndex_s: startingRow,
|
||||
rowIndex_e: undefined,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
|
||||
const values = rows.map((row) => {
|
||||
return row.values;
|
||||
});
|
||||
|
||||
const matchingRows: any[] = [];
|
||||
const columnName = propsValue.columnName ? propsValue.columnName : 'A';
|
||||
const columnNumber:number = labelToColumn(columnName);
|
||||
const searchValue = propsValue.searchValue ?? '';
|
||||
|
||||
let matchedRowCount = 0;
|
||||
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const row:Record<string,any> = values[i];
|
||||
|
||||
if (matchedRowCount === numberOfRowsToReturn) break;
|
||||
|
||||
if (searchValue === '') {
|
||||
matchingRows.push(rows[i]);
|
||||
matchedRowCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const keys = Object.keys(row);
|
||||
if (keys.length <= columnNumber) continue;
|
||||
const entry_value = row[keys[columnNumber]];
|
||||
|
||||
if (entry_value === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (propsValue.matchCase) {
|
||||
if (entry_value === searchValue) {
|
||||
matchedRowCount += 1;
|
||||
matchingRows.push(rows[i]);
|
||||
}
|
||||
} else {
|
||||
if (entry_value.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
matchedRowCount += 1;
|
||||
matchingRows.push(rows[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finalRows = await mapRowsToHeaderNames(
|
||||
matchingRows,
|
||||
useHeaderNames,
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
headerRow,
|
||||
auth,
|
||||
);
|
||||
|
||||
return finalRows;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { httpClient, HttpMethod, AuthenticationType, HttpRequest } from '@activepieces/pieces-common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { includeTeamDrivesProp } from '../common/props';
|
||||
import { getAccessToken } from '../common/common';
|
||||
|
||||
export const findSpreadsheets = createAction({
|
||||
name: 'find_spreadsheets',
|
||||
displayName: 'Find Spreadsheet(s)',
|
||||
description: 'Find spreadsheet(s) by name.',
|
||||
auth: googleSheetsAuth,
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheet_name: Property.ShortText({
|
||||
displayName: 'Spreadsheet Name',
|
||||
description: 'The name of the spreadsheet(s) to find.',
|
||||
required: true,
|
||||
}),
|
||||
exact_match: Property.Checkbox({
|
||||
displayName: 'Exact Match',
|
||||
description: 'If true, only return spreadsheets that exactly match the name. If false, return spreadsheets that contain the name.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const searchValue = propsValue.spreadsheet_name;
|
||||
const queries = [
|
||||
"mimeType='application/vnd.google-apps.spreadsheet'",
|
||||
'trashed=false',
|
||||
];
|
||||
|
||||
if (propsValue.exact_match) {
|
||||
queries.push(`name = '${searchValue}'`);
|
||||
} else {
|
||||
queries.push(`name contains '${searchValue}'`);
|
||||
}
|
||||
|
||||
const files = [];
|
||||
let pageToken = null;
|
||||
|
||||
do
|
||||
{
|
||||
const request :HttpRequest = {
|
||||
method:HttpMethod.GET,
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
queryParams:{
|
||||
q: queries.join(' and '),
|
||||
includeItemsFromAllDrives: propsValue.includeTeamDrives ? 'true' : 'false',
|
||||
supportsAllDrives: 'true',
|
||||
fields: 'files(id,name,webViewLink,createdTime,modifiedTime),nextPageToken',
|
||||
},
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
|
||||
}
|
||||
if (pageToken) {
|
||||
if (request.queryParams !== undefined) {
|
||||
request.queryParams['pageToken'] = pageToken;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await httpClient.sendRequest<{
|
||||
files: { id: string; name: string }[];
|
||||
nextPageToken: string;
|
||||
}>(request);
|
||||
|
||||
files.push(...response.body.files);
|
||||
pageToken = response.body.nextPageToken;
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to get folders\nError:${e}`);
|
||||
}
|
||||
|
||||
}while(pageToken);
|
||||
|
||||
return {
|
||||
found: files.length > 0,
|
||||
spreadsheets:files,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import { includeTeamDrivesProp, spreadsheetIdProp } from '../common/props';
|
||||
import { createGoogleClient } from '../common/common';
|
||||
|
||||
export const findWorksheetAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'find-worksheet',
|
||||
displayName: 'Find Worksheet(s)',
|
||||
description: 'Finds a worksheet(s) by title.',
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheetId:spreadsheetIdProp('Spreadsheet',''),
|
||||
title: Property.ShortText({
|
||||
displayName: 'Title',
|
||||
required: true,
|
||||
}),
|
||||
exact_match: Property.Checkbox({
|
||||
displayName: 'Exact Match',
|
||||
description: 'If true, only return worksheets that exactly match the name. If false, return worksheets that contain the name.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const spreadsheetId = context.propsValue.spreadsheetId;
|
||||
const title = context.propsValue.title;
|
||||
const exactMatch = context.propsValue.exact_match ?? false;
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const response = await sheets.spreadsheets.get({
|
||||
spreadsheetId,
|
||||
});
|
||||
|
||||
const sheetsData = response.data.sheets ?? [];
|
||||
|
||||
const matchedSheets = sheetsData.filter((sheet) => {
|
||||
const sheetTitle = sheet.properties?.title ?? "";
|
||||
return exactMatch ? sheetTitle === title : sheetTitle.includes(title);
|
||||
});
|
||||
|
||||
return {
|
||||
found: matchedSheets.length > 0,
|
||||
worksheets: matchedSheets ,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createAction, Property } from "@activepieces/pieces-framework";
|
||||
import { areSheetIdsValid, googleSheetsAuth, googleSheetsCommon, mapRowsToHeaderNames } from "../common/common";
|
||||
import { commonProps } from "../common/props";
|
||||
|
||||
export const getManyRowsAction = createAction({
|
||||
name: 'get-many-rows',
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Get Many Rows',
|
||||
description: 'Get all values from the selected sheet.',
|
||||
props: {
|
||||
...commonProps,
|
||||
first_row_headers: Property.Checkbox({
|
||||
displayName: 'Does the first row contain headers?',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {first_row_headers,sheetId,spreadsheetId} = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
const rows = await googleSheetsCommon.getGoogleSheetRows({
|
||||
auth:context.auth,
|
||||
sheetId: sheetId as number,
|
||||
spreadsheetId: spreadsheetId as string,
|
||||
rowIndex_s:undefined,
|
||||
rowIndex_e:undefined,
|
||||
headerRow: 1,
|
||||
});
|
||||
|
||||
const useHeaderNames = first_row_headers;
|
||||
|
||||
const result = await mapRowsToHeaderNames(
|
||||
rows,
|
||||
useHeaderNames,
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
1,
|
||||
context.auth
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,207 @@
|
||||
import {
|
||||
PiecePropValueSchema,
|
||||
Property,
|
||||
Store,
|
||||
StoreScope,
|
||||
createAction,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import {
|
||||
areSheetIdsValid,
|
||||
GoogleSheetsAuthValue,
|
||||
googleSheetsCommon,
|
||||
mapRowsToHeaderNames,
|
||||
} from '../common/common';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
import { HttpError } from '@activepieces/pieces-common';
|
||||
import { z } from 'zod';
|
||||
import { propsValidation } from '@activepieces/pieces-common';
|
||||
import { getWorkSheetGridSize } from '../triggers/helpers';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
async function getRows(
|
||||
store: Store,
|
||||
auth: GoogleSheetsAuthValue,
|
||||
spreadsheetId: string,
|
||||
sheetId: number,
|
||||
memKey: string,
|
||||
groupSize: number,
|
||||
startRow: number,
|
||||
headerRow: number,
|
||||
useHeaderNames: boolean,
|
||||
testing: boolean
|
||||
) {
|
||||
const sheetGridRange = await getWorkSheetGridSize(auth,spreadsheetId,sheetId);
|
||||
const existingGridRowCount = sheetGridRange.rowCount ??0;
|
||||
const memVal = await store.get(memKey, StoreScope.FLOW);
|
||||
|
||||
let startingRow;
|
||||
if (isNil(memVal) || memVal === '') startingRow = startRow || 1;
|
||||
else {
|
||||
startingRow = parseInt(memVal as string);
|
||||
if (isNaN(startingRow)) {
|
||||
throw Error(
|
||||
'The value stored in memory key : ' +
|
||||
memKey +
|
||||
' is ' +
|
||||
memVal +
|
||||
' and it is not a number'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (startingRow < 1)
|
||||
throw Error('Starting row : ' + startingRow + ' is less than 1' + memVal);
|
||||
|
||||
|
||||
if(startingRow > existingGridRowCount-1){
|
||||
return [];
|
||||
}
|
||||
|
||||
const endRow = Math.min(startingRow + groupSize,existingGridRowCount);
|
||||
|
||||
if (testing == false) await store.put(memKey, endRow, StoreScope.FLOW);
|
||||
|
||||
const row = await googleSheetsCommon.getGoogleSheetRows({
|
||||
auth,
|
||||
sheetId: sheetId,
|
||||
spreadsheetId: spreadsheetId,
|
||||
rowIndex_s: startingRow,
|
||||
rowIndex_e: endRow - 1,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
|
||||
if (row.length == 0) {
|
||||
const allRows = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheetId,
|
||||
auth,
|
||||
sheetId: sheetId,
|
||||
rowIndex_s: undefined,
|
||||
rowIndex_e: undefined,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
const lastRow = allRows.length + 1;
|
||||
if (testing == false) await store.put(memKey, lastRow, StoreScope.FLOW);
|
||||
}
|
||||
|
||||
const finalRows = await mapRowsToHeaderNames(
|
||||
row,
|
||||
useHeaderNames,
|
||||
spreadsheetId,
|
||||
sheetId,
|
||||
headerRow,
|
||||
auth,
|
||||
);
|
||||
|
||||
return finalRows;
|
||||
}
|
||||
|
||||
const notes = `
|
||||
**Notes:**
|
||||
|
||||
- Memory key is used to remember where last row was processed and will be used in the following runs.
|
||||
- Republishing the flow **keeps** the memory key value, If you want to start over **change** the memory key.
|
||||
`
|
||||
export const getRowsAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'get_next_rows',
|
||||
description: 'Get next group of rows from a Google Sheet',
|
||||
displayName: 'Get next row(s)',
|
||||
props: {
|
||||
...commonProps,
|
||||
startRow: Property.Number({
|
||||
displayName: 'Start Row',
|
||||
description: 'Which row to start from?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
useHeaderNames: Property.Checkbox({
|
||||
displayName: 'Use header names for keys',
|
||||
description: 'Map A/B/C… to the actual column headers (row specified above).',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
markdown: Property.MarkDown({
|
||||
value: notes
|
||||
}),
|
||||
memKey: Property.ShortText({
|
||||
displayName: 'Memory Key',
|
||||
description: 'The key used to store the current row number in memory',
|
||||
required: true,
|
||||
defaultValue: 'row_number',
|
||||
}),
|
||||
groupSize: Property.Number({
|
||||
displayName: 'Group Size',
|
||||
description: 'The number of rows to get',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
},
|
||||
async run({ store, auth, propsValue }) {
|
||||
const { startRow, groupSize, memKey, headerRow, spreadsheetId, sheetId, useHeaderNames} = propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
await propsValidation.validateZod(propsValue, {
|
||||
startRow: z.number().min(1),
|
||||
groupSize: z.number().min(1),
|
||||
});
|
||||
|
||||
try {
|
||||
return await getRows(
|
||||
store,
|
||||
auth,
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
memKey,
|
||||
groupSize,
|
||||
startRow,
|
||||
headerRow,
|
||||
useHeaderNames as boolean,
|
||||
false
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
const errorBody = error.response.body as any;
|
||||
throw new Error(errorBody['error']['message']);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async test({ store, auth, propsValue }) {
|
||||
const { startRow, groupSize, memKey, headerRow, spreadsheetId, sheetId, useHeaderNames} = propsValue;
|
||||
|
||||
if (!areSheetIdsValid(spreadsheetId, sheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
try {
|
||||
return await getRows(
|
||||
store,
|
||||
auth,
|
||||
spreadsheetId as string,
|
||||
sheetId as number,
|
||||
memKey,
|
||||
groupSize,
|
||||
startRow,
|
||||
headerRow,
|
||||
useHeaderNames as boolean,
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpError) {
|
||||
const errorBody = error.response.body as any;
|
||||
throw new Error(errorBody['error']['message']);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,514 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import {
|
||||
createAction,
|
||||
DropdownOption,
|
||||
DynamicPropsValue,
|
||||
OAuth2PropertyValue,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { Dimension, googleSheetsCommon, objectToArray, ValueInputOption,columnToLabel, areSheetIdsValid, GoogleSheetsAuthValue, createGoogleClient } from '../common/common';
|
||||
import { getWorkSheetName, getWorkSheetGridSize } from '../triggers/helpers';
|
||||
import { google, sheets_v4 } from 'googleapis';
|
||||
import { MarkdownVariant } from '@activepieces/shared';
|
||||
import {parse} from 'csv-parse/sync';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
|
||||
type RowValueType = Record<string, any>
|
||||
|
||||
export const insertMultipleRowsAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'google-sheets-insert-multiple-rows',
|
||||
displayName: 'Insert Multiple Rows',
|
||||
description: 'Add one or more new rows in a specific spreadsheet.',
|
||||
props: {
|
||||
...commonProps,
|
||||
input_type: Property.StaticDropdown({
|
||||
displayName: 'Rows Input Format',
|
||||
description: 'Select the format of the input values to be inserted into the sheet.',
|
||||
required: true,
|
||||
defaultValue: 'column_names',
|
||||
options: {
|
||||
disabled: false,
|
||||
options: [
|
||||
{
|
||||
value: 'csv',
|
||||
label: 'CSV',
|
||||
},
|
||||
{
|
||||
value: 'json',
|
||||
label: 'JSON',
|
||||
},
|
||||
{
|
||||
value: 'column_names',
|
||||
label: 'Column Names',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
values: Property.DynamicProperties({
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Values',
|
||||
description: 'The values to insert.',
|
||||
required: true,
|
||||
refreshers: ['sheetId', 'spreadsheetId', 'input_type', 'headerRow'],
|
||||
props: async ({ auth, sheetId, spreadsheetId, input_type, headerRow }) => {
|
||||
const sheet_id = Number(sheetId);
|
||||
const spreadsheet_id = spreadsheetId as unknown as string;
|
||||
const valuesInputType = input_type as unknown as string;
|
||||
|
||||
if (
|
||||
!auth ||
|
||||
(spreadsheet_id ?? '').toString().length === 0 ||
|
||||
(sheet_id ?? '').toString().length === 0 ||
|
||||
!['csv', 'json', 'column_names'].includes(valuesInputType)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
|
||||
switch (valuesInputType) {
|
||||
case 'csv':
|
||||
fields['markdown'] = Property.MarkDown({
|
||||
value: `Ensure the first row contains column headers that match the sheet's column names.`,
|
||||
variant: MarkdownVariant.INFO,
|
||||
});
|
||||
fields['values'] = Property.LongText({
|
||||
displayName: 'CSV',
|
||||
required: true,
|
||||
});
|
||||
break;
|
||||
case 'json':
|
||||
fields['markdown'] = Property.MarkDown({
|
||||
value: `Provide values in JSON format. Ensure the column names match the sheet's header.`,
|
||||
variant: MarkdownVariant.INFO,
|
||||
});
|
||||
fields['values'] = Property.Json({
|
||||
displayName: 'JSON',
|
||||
required: true,
|
||||
defaultValue: [
|
||||
{
|
||||
column1: 'value1',
|
||||
column2: 'value2',
|
||||
},
|
||||
{
|
||||
column1: 'value3',
|
||||
column2: 'value4',
|
||||
},
|
||||
],
|
||||
});
|
||||
break;
|
||||
case 'column_names': {
|
||||
const headers = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheet_id,
|
||||
auth: auth,
|
||||
sheetId: sheet_id,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: 1,
|
||||
headerRow: (headerRow as unknown as number) || 1,
|
||||
});
|
||||
const firstRow = headers[0].values ?? {};
|
||||
|
||||
//check for empty headers
|
||||
if (Object.keys(firstRow).length === 0) {
|
||||
fields['markdown'] = Property.MarkDown({
|
||||
value: `We couldn't find any headers in the selected spreadsheet or worksheet. Please add headers to the sheet and refresh the page to reflect the columns.`,
|
||||
variant: MarkdownVariant.INFO,
|
||||
});
|
||||
} else {
|
||||
const columns: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
for (const key in firstRow) {
|
||||
columns[key] = Property.ShortText({
|
||||
displayName: firstRow[key],
|
||||
description: firstRow[key],
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
fields['values'] = Property.Array({
|
||||
displayName: 'Values',
|
||||
required: false,
|
||||
properties: columns,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
}),
|
||||
overwrite: Property.Checkbox({
|
||||
displayName: 'Overwrite Existing Data?',
|
||||
description:
|
||||
'Enable this option to replace all existing data in the sheet with new data from your input. This will clear any extra rows beyond the updated range.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
check_for_duplicate: Property.Checkbox({
|
||||
displayName: 'Avoid Duplicates?',
|
||||
description:
|
||||
'Enable this option to check for duplicate values before inserting data into the sheet. Only unique rows will be added based on the selected column.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
check_for_duplicate_column: Property.DynamicProperties({
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Duplicate Value Column',
|
||||
description: 'The column to check for duplicate values.',
|
||||
refreshers: ['spreadsheetId', 'sheetId', 'check_for_duplicate', 'headerRow'],
|
||||
required: false,
|
||||
props: async ({ auth, spreadsheetId, sheetId, check_for_duplicate, headerRow }) => {
|
||||
const sheet_id = Number(sheetId);
|
||||
const spreadsheet_id = spreadsheetId as unknown as string;
|
||||
const checkForExisting = check_for_duplicate as unknown as boolean;
|
||||
if (
|
||||
!auth ||
|
||||
(spreadsheet_id ?? '').toString().length === 0 ||
|
||||
(sheet_id ?? '').toString().length === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
|
||||
if (checkForExisting) {
|
||||
const headers = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheet_id,
|
||||
auth: auth as GoogleSheetsAuthValue,
|
||||
sheetId: sheet_id,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: 1,
|
||||
headerRow: (headerRow as unknown as number) || 1,
|
||||
});
|
||||
const firstRow = headers[0].values ?? {};
|
||||
|
||||
//check for empty headers
|
||||
if (Object.keys(firstRow).length === 0) {
|
||||
fields['markdown'] = Property.MarkDown({
|
||||
value: `No headers were found in the selected spreadsheet or worksheet. Please ensure that headers are added to the sheet and refresh the page to display the available columns.`,
|
||||
variant: MarkdownVariant.INFO,
|
||||
});
|
||||
} else {
|
||||
const headers: DropdownOption<string>[] = [];
|
||||
for (const key in firstRow) {
|
||||
headers.push({ label: firstRow[key].toString(), value: key.toString() });
|
||||
}
|
||||
|
||||
fields['column_name'] = Property.StaticDropdown({
|
||||
displayName: 'Column to Check for Duplicates',
|
||||
description:
|
||||
'Select the column to use for detecting duplicate values. Only rows with unique values in this column will be added to the sheet.',
|
||||
required: true,
|
||||
options: {
|
||||
disabled: false,
|
||||
options: headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
}),
|
||||
as_string: Property.Checkbox({
|
||||
displayName: 'As String',
|
||||
description:
|
||||
'Inserted values that are dates and formulas will be entered as strings and have no effect',
|
||||
required: false,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
const {
|
||||
spreadsheetId:inputSpreadsheetId,
|
||||
sheetId:inputSheetId,
|
||||
input_type: valuesInputType,
|
||||
overwrite: overwriteValues,
|
||||
check_for_duplicate: checkForDuplicateValues,
|
||||
values: { values: rowValuesInput },
|
||||
as_string: asString,
|
||||
headerRow,
|
||||
} = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const duplicateColumn = context.propsValue.check_for_duplicate_column?.['column_name'];
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
|
||||
const rowHeaders = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheetId,
|
||||
auth: context.auth,
|
||||
sheetId: sheetId,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: 1,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
|
||||
const sheetHeaders = rowHeaders[0]?.values ?? {};
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const formattedValues = await formatInputRows(sheets,spreadsheetId, sheetName,valuesInputType, rowValuesInput, sheetHeaders);
|
||||
|
||||
const valueInputOption = asString ? ValueInputOption.RAW : ValueInputOption.USER_ENTERED;
|
||||
|
||||
|
||||
if (overwriteValues) {
|
||||
const sheetGridRange = await getWorkSheetGridSize(context.auth, spreadsheetId, sheetId);
|
||||
const existingGridRowCount = sheetGridRange.rowCount ?? 0;
|
||||
return handleOverwrite(sheets, spreadsheetId, sheetName, formattedValues, existingGridRowCount, valueInputOption);
|
||||
}
|
||||
|
||||
if (checkForDuplicateValues) {
|
||||
const existingSheetValues = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheetId,
|
||||
auth: context.auth,
|
||||
sheetId: sheetId,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: undefined,
|
||||
headerRow: headerRow,
|
||||
});
|
||||
return handleDuplicates(
|
||||
sheets,
|
||||
spreadsheetId,
|
||||
sheetName,
|
||||
formattedValues,
|
||||
existingSheetValues,
|
||||
duplicateColumn,
|
||||
valueInputOption
|
||||
);
|
||||
}
|
||||
|
||||
return normalInsert(sheets, spreadsheetId, sheetName, formattedValues, valueInputOption);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleOverwrite(
|
||||
sheets: sheets_v4.Sheets,
|
||||
spreadSheetId: string,
|
||||
sheetName: string,
|
||||
formattedValues: any[],
|
||||
existingGridRowCount: number,
|
||||
valueInputOption: ValueInputOption
|
||||
) {
|
||||
const existingRowCount = existingGridRowCount;
|
||||
const inputRowCount = formattedValues.length;
|
||||
|
||||
const updateResponse = await sheets.spreadsheets.values.batchUpdate({
|
||||
spreadsheetId: spreadSheetId,
|
||||
requestBody: {
|
||||
data: [{
|
||||
range: `${sheetName}!A2:ZZZ${inputRowCount + 1}`,
|
||||
majorDimension: Dimension.ROWS,
|
||||
values: formattedValues.map(row => objectToArray(row)),
|
||||
}],
|
||||
valueInputOption
|
||||
},
|
||||
});
|
||||
|
||||
// Determine if clearing rows is necessary and within grid size
|
||||
const clearStartRow = inputRowCount + 2; // Start clearing after the last input row
|
||||
const clearEndRow = Math.max(clearStartRow, existingRowCount);
|
||||
|
||||
if (clearStartRow <= existingGridRowCount) {
|
||||
const boundedClearEndRow = Math.min(clearEndRow, existingGridRowCount);
|
||||
const clearRowsResponse = await sheets.spreadsheets.values.batchClear({
|
||||
spreadsheetId: spreadSheetId,
|
||||
requestBody: {
|
||||
ranges: [`${sheetName}!A${clearStartRow}:ZZZ${boundedClearEndRow}`],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...updateResponse.data,
|
||||
...clearRowsResponse.data,
|
||||
};
|
||||
}
|
||||
return updateResponse.data;
|
||||
|
||||
|
||||
}
|
||||
|
||||
async function handleDuplicates(
|
||||
sheets: sheets_v4.Sheets,
|
||||
spreadSheetId: string,
|
||||
sheetName: string,
|
||||
formattedInputRows: any[],
|
||||
existingSheetValues: any[],
|
||||
duplicateColumn: string,
|
||||
valueInputOption: ValueInputOption
|
||||
) {
|
||||
|
||||
const uniqueValues = formattedInputRows.filter(
|
||||
(inputRow) => !existingSheetValues.some(
|
||||
(existingRow) => {
|
||||
const existingValue = existingRow?.values?.[duplicateColumn];
|
||||
const inputValue = inputRow?.[duplicateColumn];
|
||||
return existingValue != null && inputValue != null &&
|
||||
String(existingValue).toLowerCase().trim() === String(inputValue).toLowerCase().trim();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const response = await sheets.spreadsheets.values.append({
|
||||
range: sheetName + '!A:A',
|
||||
spreadsheetId: spreadSheetId,
|
||||
valueInputOption,
|
||||
requestBody: {
|
||||
values: uniqueValues.map((row) => objectToArray(row)),
|
||||
majorDimension: Dimension.ROWS,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function normalInsert(
|
||||
sheets: sheets_v4.Sheets,
|
||||
spreadSheetId: string,
|
||||
sheetName: string,
|
||||
formattedValues: any[],
|
||||
valueInputOption: ValueInputOption
|
||||
) {
|
||||
const response = await sheets.spreadsheets.values.append({
|
||||
range: sheetName + '!A:A',
|
||||
spreadsheetId: spreadSheetId,
|
||||
valueInputOption,
|
||||
requestBody: {
|
||||
values: formattedValues.map(row => objectToArray(row)),
|
||||
majorDimension: Dimension.ROWS,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async function formatInputRows(
|
||||
sheets: sheets_v4.Sheets,
|
||||
spreadSheetId: string,
|
||||
sheetName: string,
|
||||
valuesInputType: string,
|
||||
rowValuesInput: any,
|
||||
sheetHeaders: RowValueType
|
||||
): Promise<RowValueType[]> {
|
||||
let formattedInputRows: any[] = [];
|
||||
|
||||
switch (valuesInputType) {
|
||||
case 'csv':
|
||||
formattedInputRows = convertCsvToRawValues(rowValuesInput as string, ',', sheetHeaders);
|
||||
break;
|
||||
case 'json':
|
||||
formattedInputRows = await convertJsonToRawValues(sheets,spreadSheetId, sheetName, rowValuesInput as string, sheetHeaders);
|
||||
break;
|
||||
case 'column_names':
|
||||
formattedInputRows = rowValuesInput as RowValueType[];
|
||||
break;
|
||||
}
|
||||
|
||||
return formattedInputRows;
|
||||
}
|
||||
|
||||
async function convertJsonToRawValues(
|
||||
sheets: sheets_v4.Sheets,
|
||||
spreadSheetId: string,
|
||||
sheetName: string,
|
||||
json: string | Record<string, any>[],
|
||||
labelHeaders: RowValueType
|
||||
): Promise<RowValueType[]> {
|
||||
|
||||
let data: RowValueType[];
|
||||
|
||||
// If the input is a JSON string
|
||||
if (typeof json === 'string') {
|
||||
try {
|
||||
data = JSON.parse(json);
|
||||
} catch (error) {
|
||||
throw new Error('Invalid JSON format for row values');
|
||||
}
|
||||
} else {
|
||||
// If the input is already an array of objects, use it directly
|
||||
data = json;
|
||||
}
|
||||
|
||||
// Ensure the input is an array of objects
|
||||
if (!Array.isArray(data) || typeof data[0] !== 'object') {
|
||||
throw new Error('Input must be an array of objects or a valid JSON string representing it.');
|
||||
}
|
||||
|
||||
// Collect all possible headers from the data
|
||||
const allHeaders = new Set<string>();
|
||||
data.forEach((row) => {
|
||||
Object.keys(row).forEach((key) => allHeaders.add(key));
|
||||
});
|
||||
|
||||
// Identify headers not present in labelHeaders
|
||||
const additionalHeaders = Array.from(allHeaders).filter(
|
||||
(header) => !Object.values(labelHeaders).includes(header)
|
||||
);
|
||||
|
||||
//add missing headers to labelHeaders
|
||||
additionalHeaders.forEach((header) => {
|
||||
labelHeaders[columnToLabel(Object.keys(labelHeaders).length)] = header;
|
||||
});
|
||||
|
||||
// update sheets with new headers
|
||||
if (additionalHeaders.length > 0) {
|
||||
await sheets.spreadsheets.values.update({
|
||||
range: `${sheetName}!A1:ZZZ1`,
|
||||
spreadsheetId: spreadSheetId,
|
||||
valueInputOption: ValueInputOption.USER_ENTERED,
|
||||
requestBody: {
|
||||
majorDimension:Dimension.ROWS,
|
||||
values: [objectToArray(labelHeaders)]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return data.map((row: RowValueType) => {
|
||||
return Object.entries(labelHeaders).reduce((acc, [labelColumn, csvHeader]) => {
|
||||
acc[labelColumn] = row[csvHeader] ?? "";
|
||||
return acc;
|
||||
}, {} as RowValueType);
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function convertCsvToRawValues(
|
||||
csvText: string,
|
||||
delimiter: string,
|
||||
labelHeaders: RowValueType,
|
||||
) {
|
||||
// Split CSV into rows
|
||||
const rows:Record<string,any>[] = parse(csvText,{delimiter: delimiter, columns: true});
|
||||
|
||||
const result = rows.map((row)=>{
|
||||
// Normalize record keys to lowercase
|
||||
const normalizedRow = Object.keys(row).reduce((acc, key) => {
|
||||
acc[key.toLowerCase().trim()] = row[key];
|
||||
return acc;
|
||||
},{} as Record<string,any>);
|
||||
|
||||
const transformedRow :Record<string,any>= {};
|
||||
for(const key in labelHeaders){
|
||||
// Match labels to normalized keys
|
||||
const normalizedKey = labelHeaders[key].toLowerCase();
|
||||
transformedRow[key] = normalizedRow[normalizedKey] || '';
|
||||
}
|
||||
return transformedRow;
|
||||
})
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
areSheetIdsValid,
|
||||
Dimension,
|
||||
getAccessToken,
|
||||
GoogleSheetsAuthValue,
|
||||
googleSheetsCommon,
|
||||
objectToArray,
|
||||
stringifyArray,
|
||||
ValueInputOption,
|
||||
} from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
import { AuthenticationType, httpClient, HttpMethod, HttpRequest } from '@activepieces/pieces-common';
|
||||
import { commonProps, rowValuesProp } from '../common/props';
|
||||
|
||||
export const insertRowAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'insert_row',
|
||||
description: 'Append a row of values to an existing sheet',
|
||||
displayName: 'Insert Row',
|
||||
props: {
|
||||
...commonProps,
|
||||
as_string: Property.Checkbox({
|
||||
displayName: 'As String',
|
||||
description: 'Inserted values that are dates and formulas will be entered strings and have no effect',
|
||||
required: false,
|
||||
}),
|
||||
first_row_headers: Property.Checkbox({
|
||||
displayName: 'Does the first row contain headers?',
|
||||
description: 'If the first row is headers',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
}),
|
||||
values: rowValuesProp(),
|
||||
},
|
||||
async run({ propsValue, auth }) {
|
||||
const { values, spreadsheetId:inputSpreadsheetId, sheetId:inputSheetId, as_string, first_row_headers } = propsValue;
|
||||
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await googleSheetsCommon.findSheetName(
|
||||
auth,
|
||||
spreadsheetId,
|
||||
sheetId
|
||||
);
|
||||
|
||||
const formattedValues = first_row_headers
|
||||
? objectToArray(values).map(val => isNil(val) ? '' : val)
|
||||
: values.values;
|
||||
|
||||
const res = await appendGoogleSheetValues({
|
||||
auth,
|
||||
majorDimension: Dimension.COLUMNS,
|
||||
range: sheetName,
|
||||
spreadSheetId: spreadsheetId,
|
||||
valueInputOption: as_string ? ValueInputOption.RAW : ValueInputOption.USER_ENTERED,
|
||||
values: stringifyArray(formattedValues),
|
||||
});
|
||||
|
||||
const updatedRowNumber = extractRowNumber(res.body.updates.updatedRange);
|
||||
return { ...res.body, row: updatedRowNumber };
|
||||
},
|
||||
});
|
||||
|
||||
function extractRowNumber(updatedRange: string): number {
|
||||
const rowRange = updatedRange.split('!')[1];
|
||||
return parseInt(rowRange.split(':')[0].substring(1), 10);
|
||||
}
|
||||
|
||||
async function appendGoogleSheetValues(params: AppendGoogleSheetValuesParams) {
|
||||
const { auth, majorDimension, range, spreadSheetId, valueInputOption, values } = params;
|
||||
const accessToken = await getAccessToken(auth);
|
||||
const request: HttpRequest = {
|
||||
method: HttpMethod.POST,
|
||||
url: `https://sheets.googleapis.com/v4/spreadsheets/${spreadSheetId}/values/${encodeURIComponent(`${range}!A:A`)}:append`,
|
||||
body: {
|
||||
majorDimension,
|
||||
range: `${range}!A:A`,
|
||||
values: values.map(val => ({ values: val })),
|
||||
},
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: accessToken,
|
||||
},
|
||||
queryParams: {
|
||||
valueInputOption,
|
||||
},
|
||||
};
|
||||
|
||||
return httpClient.sendRequest(request);
|
||||
}
|
||||
|
||||
type AppendGoogleSheetValuesParams = {
|
||||
values: string[];
|
||||
spreadSheetId: string;
|
||||
range: string;
|
||||
valueInputOption: ValueInputOption;
|
||||
majorDimension: Dimension;
|
||||
auth: GoogleSheetsAuthValue;
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import {
|
||||
createAction,
|
||||
DynamicPropsValue,
|
||||
OAuth2PropertyValue,
|
||||
Property,
|
||||
} from '@activepieces/pieces-framework';
|
||||
import { areSheetIdsValid, createGoogleClient, Dimension, GoogleSheetsAuthValue, googleSheetsCommon, objectToArray, ValueInputOption } from '../common/common';
|
||||
import { getAccessTokenOrThrow } from '@activepieces/pieces-common';
|
||||
import { isNil, isString, MarkdownVariant } from '@activepieces/shared';
|
||||
import { getWorkSheetName } from '../triggers/helpers';
|
||||
import { google, sheets_v4 } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
export const updateMultipleRowsAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'update-multiple-rows',
|
||||
displayName: 'Update Multiple Rows',
|
||||
description: 'Updates multiple rows in a specific spreadsheet.',
|
||||
props: {
|
||||
...commonProps,
|
||||
values: Property.DynamicProperties({
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Values',
|
||||
description: 'The values to update.',
|
||||
required: true,
|
||||
refreshers: ['sheetId', 'spreadsheetId', 'headerRow'],
|
||||
props: async ({ auth, spreadsheetId, sheetId, headerRow }) => {
|
||||
const sheet_Id = Number(sheetId);
|
||||
const spreadsheet_Id = spreadsheetId as unknown as string;
|
||||
const authentication = auth;
|
||||
|
||||
if (
|
||||
!auth ||
|
||||
(spreadsheet_Id ?? '').toString().length === 0 ||
|
||||
(sheet_Id ?? '').toString().length === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fields: DynamicPropsValue = {};
|
||||
|
||||
const headers = await googleSheetsCommon.getGoogleSheetRows({
|
||||
spreadsheetId: spreadsheet_Id,
|
||||
auth: auth as GoogleSheetsAuthValue,
|
||||
sheetId: sheet_Id,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: 1,
|
||||
headerRow: (headerRow as unknown as number) || 1,
|
||||
});
|
||||
const firstRow = headers[0].values ?? {};
|
||||
|
||||
//check for empty headers
|
||||
if (Object.keys(firstRow).length === 0) {
|
||||
fields['markdown'] = Property.MarkDown({
|
||||
value: `We couldn't find any headers in the selected spreadsheet or worksheet. Please add headers to the sheet and refresh the page to reflect the columns.`,
|
||||
variant: MarkdownVariant.INFO,
|
||||
});
|
||||
} else {
|
||||
const columns: {
|
||||
[key: string]: any;
|
||||
} = {
|
||||
rowId: Property.Number({
|
||||
displayName: 'Row Id',
|
||||
description: 'The row id to update',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
|
||||
for (const key in firstRow) {
|
||||
columns[key] = Property.ShortText({
|
||||
displayName: firstRow[key].toString(),
|
||||
description: firstRow[key].toString(),
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
fields['values'] = Property.Array({
|
||||
displayName: 'Values',
|
||||
required: false,
|
||||
properties: columns,
|
||||
});
|
||||
}
|
||||
|
||||
return fields;
|
||||
},
|
||||
}),
|
||||
as_string: Property.Checkbox({
|
||||
displayName: 'As String',
|
||||
description:
|
||||
'Inserted values that are dates and formulas will be entered as strings and have no effect',
|
||||
required: false,
|
||||
}),
|
||||
headerRow: Property.Number({
|
||||
displayName: 'Header Row',
|
||||
description: 'Which row contains the headers?',
|
||||
required: true,
|
||||
defaultValue: 1,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {
|
||||
spreadsheetId:inputSpreadsheetId,
|
||||
sheetId:inputSheetId,
|
||||
values: { values: rowValuesInput },
|
||||
as_string: asString,
|
||||
headerRow,
|
||||
} = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
const valueInputOption = asString ? ValueInputOption.RAW : ValueInputOption.USER_ENTERED;
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const values: sheets_v4.Schema$ValueRange[] = [];
|
||||
|
||||
for (const row of rowValuesInput) {
|
||||
const { rowId, ...rowValues } = row;
|
||||
if (rowId === undefined || rowId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const formattedValues = objectToArray(rowValues).map((value: string | null | undefined) => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
});
|
||||
|
||||
if (formattedValues.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
values.push({
|
||||
range: `${sheetName}!A${rowId}:ZZZ${rowId}`,
|
||||
majorDimension: Dimension.ROWS,
|
||||
values: [formattedValues],
|
||||
});
|
||||
}
|
||||
|
||||
const response = await sheets.spreadsheets.values.batchUpdate({
|
||||
spreadsheetId: spreadsheetId,
|
||||
|
||||
requestBody: {
|
||||
valueInputOption: valueInputOption,
|
||||
data: values,
|
||||
},
|
||||
});
|
||||
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,98 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { areSheetIdsValid, createGoogleClient, Dimension, objectToArray, ValueInputOption } from '../common/common';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { getWorkSheetName } from '../triggers/helpers';
|
||||
import { google } from 'googleapis';
|
||||
import { isString } from '@activepieces/shared';
|
||||
import { commonProps, rowValuesProp } from '../common/props';
|
||||
|
||||
export const updateRowAction = createAction({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'update_row',
|
||||
description: 'Overwrite values in an existing row',
|
||||
displayName: 'Update Row',
|
||||
props: {
|
||||
...commonProps,
|
||||
row_id: Property.Number({
|
||||
displayName: 'Row Number',
|
||||
description: 'The row number to update',
|
||||
required: true,
|
||||
}),
|
||||
first_row_headers: Property.Checkbox({
|
||||
displayName: 'Does the first row contain headers?',
|
||||
description: 'If the first row is headers',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
}),
|
||||
values: rowValuesProp(),
|
||||
},
|
||||
async run(context) {
|
||||
const inputSpreadsheetId = context.propsValue.spreadsheetId;
|
||||
const inputSheetId = context.propsValue.sheetId;
|
||||
const rowId = context.propsValue.row_id;
|
||||
const isFirstRowHeaders = context.propsValue.first_row_headers;
|
||||
const rowValuesInput = context.propsValue.values;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const sheetName = await getWorkSheetName(
|
||||
context.auth,
|
||||
spreadsheetId,
|
||||
sheetId
|
||||
);
|
||||
|
||||
// replace empty string with null to skip the cell value
|
||||
const formattedValues = (
|
||||
isFirstRowHeaders
|
||||
? objectToArray(rowValuesInput)
|
||||
: rowValuesInput['values']
|
||||
).map((value: string | null | undefined) => {
|
||||
if (value === '' || value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
return JSON.stringify(value, null, 2);
|
||||
});
|
||||
|
||||
|
||||
if (formattedValues.length > 0) {
|
||||
const response = await sheets.spreadsheets.values.update({
|
||||
range: `${sheetName}!A${rowId}:ZZZ${rowId}`,
|
||||
spreadsheetId: spreadsheetId,
|
||||
valueInputOption: ValueInputOption.USER_ENTERED,
|
||||
requestBody: {
|
||||
values: [formattedValues],
|
||||
majorDimension: Dimension.ROWS,
|
||||
},
|
||||
});
|
||||
|
||||
//Split the updatedRange string to extract the row number
|
||||
const updatedRangeParts = response.data.updatedRange?.split(
|
||||
'!'
|
||||
);
|
||||
const updatedRowRange = updatedRangeParts?.[1];
|
||||
const updatedRowNumber = parseInt(
|
||||
updatedRowRange?.split(':')[0].substring(1) ?? '0',
|
||||
10
|
||||
);
|
||||
|
||||
return { updates: { ...response.data }, row: updatedRowNumber };
|
||||
} else {
|
||||
throw Error(
|
||||
'Values passed are empty or not array ' +
|
||||
JSON.stringify(formattedValues)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,383 @@
|
||||
import { AppConnectionValueForAuthProperty, OAuth2PropertyValue, OAuth2Props, PieceAuth, PiecePropValueSchema, Property, ShortTextProperty, StaticPropsValue } from '@activepieces/pieces-framework';
|
||||
import {
|
||||
httpClient,
|
||||
HttpMethod,
|
||||
AuthenticationType,
|
||||
HttpRequest,
|
||||
} from '@activepieces/pieces-common';
|
||||
import { AppConnectionType, isNil, isString } from '@activepieces/shared';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import { mapRowsToColumnLabels } from '../triggers/helpers';
|
||||
|
||||
export type GoogleSheetsAuthValue = AppConnectionValueForAuthProperty<typeof googleSheetsAuth>
|
||||
export const googleSheetsCommon = {
|
||||
baseUrl: 'https://sheets.googleapis.com/v4/spreadsheets',
|
||||
getGoogleSheetRows,
|
||||
findSheetName,
|
||||
deleteRow,
|
||||
clearSheet,
|
||||
getHeaderRow,
|
||||
};
|
||||
|
||||
export async function findSheetName(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
spreadsheetId: string,
|
||||
sheetId: string | number,
|
||||
) {
|
||||
const sheets = await listSheetsName(auth, spreadsheetId);
|
||||
const sheetName = sheets.find((f) => f.properties.sheetId == sheetId)?.properties.title;
|
||||
if (!sheetName) {
|
||||
throw Error(`Sheet with ID ${sheetId} not found in spreadsheet ${spreadsheetId}`);
|
||||
}
|
||||
return sheetName;
|
||||
}
|
||||
|
||||
async function listSheetsName(auth: GoogleSheetsAuthValue, spreadsheet_id: string) {
|
||||
return (
|
||||
await httpClient.sendRequest<{
|
||||
sheets: { properties: { title: string; sheetId: number } }[];
|
||||
}>({
|
||||
method: HttpMethod.GET,
|
||||
url: `https://sheets.googleapis.com/v4/spreadsheets/` + spreadsheet_id,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
})
|
||||
).body.sheets;
|
||||
}
|
||||
|
||||
type GetGoogleSheetRowsProps = {
|
||||
spreadsheetId: string;
|
||||
auth: GoogleSheetsAuthValue;
|
||||
sheetId: number;
|
||||
rowIndex_s: number | undefined;
|
||||
rowIndex_e: number | undefined;
|
||||
headerRow?: number;
|
||||
};
|
||||
|
||||
async function getGoogleSheetRows({
|
||||
spreadsheetId,
|
||||
auth,
|
||||
sheetId,
|
||||
rowIndex_s,
|
||||
rowIndex_e,
|
||||
headerRow = 1,
|
||||
}: GetGoogleSheetRowsProps): Promise<{ row: number; values: { [x: string]: string } }[]> {
|
||||
const sheetName = await findSheetName(auth, spreadsheetId, sheetId);
|
||||
if (!sheetName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let range = '';
|
||||
if (rowIndex_s !== undefined) {
|
||||
range = `!A${rowIndex_s}:ZZZ`;
|
||||
}
|
||||
if (rowIndex_s !== undefined && rowIndex_e !== undefined) {
|
||||
range = `!A${rowIndex_s}:ZZZ${rowIndex_e}`;
|
||||
}
|
||||
const rowsResponse = await httpClient.sendRequest<{ values: [string[]][] }>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${googleSheetsCommon.baseUrl}/${spreadsheetId}/values/${encodeURIComponent(`${sheetName}${range}`)}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
});
|
||||
if (rowsResponse.body.values === undefined) return [];
|
||||
|
||||
const headerResponse = await httpClient.sendRequest<{ values: [string[]][] }>({
|
||||
method: HttpMethod.GET,
|
||||
url: `${googleSheetsCommon.baseUrl}/${spreadsheetId}/values/${encodeURIComponent(`${sheetName}!A${headerRow}:ZZZ${headerRow}`)}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
});
|
||||
|
||||
if (!headerResponse.body.values) {
|
||||
throw new Error(`Unable to read headers from row ${headerRow} in sheet "${sheetName}". The row appears to be empty or inaccessible.`);
|
||||
}
|
||||
|
||||
const headers = headerResponse.body.values[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
|
||||
const startingRow = rowIndex_s ? rowIndex_s - 1 : 0;
|
||||
|
||||
const labeledRowValues = mapRowsToColumnLabels(
|
||||
rowsResponse.body.values,
|
||||
startingRow,
|
||||
headerCount,
|
||||
);
|
||||
|
||||
return labeledRowValues;
|
||||
}
|
||||
|
||||
type GetHeaderRowProps = {
|
||||
spreadsheetId: string;
|
||||
auth: GoogleSheetsAuthValue;
|
||||
sheetId: number;
|
||||
};
|
||||
|
||||
export async function getHeaderRow({
|
||||
spreadsheetId,
|
||||
auth,
|
||||
sheetId,
|
||||
}: GetHeaderRowProps): Promise<string[] | undefined> {
|
||||
const rows = await getGoogleSheetRows({
|
||||
spreadsheetId,
|
||||
auth,
|
||||
sheetId,
|
||||
rowIndex_s: 1,
|
||||
rowIndex_e: 1,
|
||||
headerRow: 1,
|
||||
});
|
||||
if (rows.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return objectToArray(rows[0].values);
|
||||
}
|
||||
|
||||
export const columnToLabel = (columnIndex: number) => {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let label = '';
|
||||
|
||||
while (columnIndex >= 0) {
|
||||
label = alphabet[columnIndex % 26] + label;
|
||||
columnIndex = Math.floor(columnIndex / 26) - 1;
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
export const labelToColumn = (label: string) => {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let column = 0;
|
||||
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
column += (alphabet.indexOf(label[i]) + 1) * Math.pow(26, label.length - i - 1);
|
||||
}
|
||||
|
||||
return column - 1;
|
||||
};
|
||||
|
||||
export function objectToArray(obj: { [x: string]: any }) {
|
||||
const maxIndex = Math.max(...Object.keys(obj).map((key) => labelToColumn(key)));
|
||||
const arr = new Array(maxIndex + 1);
|
||||
for (const key in obj) {
|
||||
arr[labelToColumn(key)] = obj[key];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
export function stringifyArray(object: unknown[]): string[] {
|
||||
return object.map((val) => {
|
||||
if (isString(val)) {
|
||||
return val;
|
||||
}
|
||||
return JSON.stringify(val);
|
||||
});
|
||||
}
|
||||
|
||||
export async function mapRowsToHeaderNames(
|
||||
rows:any[],
|
||||
useHeaderNames: boolean,
|
||||
spreadsheetId: string,
|
||||
sheetId: number,
|
||||
headerRow: number,
|
||||
auth: GoogleSheetsAuthValue,
|
||||
): Promise<any[]> {
|
||||
if (!useHeaderNames) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const headerRows = await getGoogleSheetRows({
|
||||
spreadsheetId,
|
||||
auth,
|
||||
sheetId,
|
||||
rowIndex_s: headerRow,
|
||||
rowIndex_e: headerRow,
|
||||
});
|
||||
|
||||
if (headerRows.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const headers = Object.values(headerRows[0].values);
|
||||
if (headers.length === 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
// map rows to use header names as keys instead of column letters
|
||||
return rows.map(row => {
|
||||
const newValues: Record<string, any> = {};
|
||||
Object.keys(row.values).forEach((columnLetter) => {
|
||||
const columnIndex = labelToColumn(columnLetter);
|
||||
const headerName = headers[columnIndex];
|
||||
if (headerName) {
|
||||
newValues[headerName] = row.values[columnLetter];
|
||||
}
|
||||
else{
|
||||
newValues[columnLetter] = row.values[columnLetter];
|
||||
}
|
||||
});
|
||||
return { ...row, values: newValues };
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRow(
|
||||
spreadsheetId: string,
|
||||
sheetId: number,
|
||||
rowIndex: number,
|
||||
auth: GoogleSheetsAuthValue,
|
||||
) {
|
||||
const request: HttpRequest = {
|
||||
method: HttpMethod.POST,
|
||||
url: `${googleSheetsCommon.baseUrl}/${spreadsheetId}/:batchUpdate`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
body: {
|
||||
requests: [
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: sheetId,
|
||||
dimension: 'ROWS',
|
||||
startIndex: rowIndex,
|
||||
endIndex: rowIndex + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
await httpClient.sendRequest(request);
|
||||
}
|
||||
|
||||
async function clearSheet(
|
||||
spreadsheetId: string,
|
||||
sheetId: number,
|
||||
auth: GoogleSheetsAuthValue,
|
||||
rowIndex: number,
|
||||
numOfRows: number,
|
||||
) {
|
||||
const request: HttpRequest = {
|
||||
method: HttpMethod.POST,
|
||||
url: `${googleSheetsCommon.baseUrl}/${spreadsheetId}/:batchUpdate`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BEARER_TOKEN,
|
||||
token: await getAccessToken(auth),
|
||||
},
|
||||
body: {
|
||||
requests: [
|
||||
{
|
||||
deleteDimension: {
|
||||
range: {
|
||||
sheetId: sheetId,
|
||||
dimension: 'ROWS',
|
||||
startIndex: rowIndex,
|
||||
endIndex: rowIndex + numOfRows + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
return await httpClient.sendRequest(request);
|
||||
}
|
||||
|
||||
export enum ValueInputOption {
|
||||
RAW = 'RAW',
|
||||
USER_ENTERED = 'USER_ENTERED',
|
||||
}
|
||||
|
||||
export enum Dimension {
|
||||
ROWS = 'ROWS',
|
||||
COLUMNS = 'COLUMNS',
|
||||
}
|
||||
|
||||
export async function createGoogleClient(auth: GoogleSheetsAuthValue): Promise<OAuth2Client> {
|
||||
if(auth.type === AppConnectionType.CUSTOM_AUTH)
|
||||
{
|
||||
const serviceAccount = JSON.parse(auth.props.serviceAccount);
|
||||
return new google.auth.JWT({
|
||||
email: serviceAccount.client_email,
|
||||
key: serviceAccount.private_key,
|
||||
scopes: googleSheetsScopes,
|
||||
subject: auth.props.userEmail,
|
||||
});
|
||||
}
|
||||
const authClient = new OAuth2Client();
|
||||
authClient.setCredentials(auth);
|
||||
return authClient;
|
||||
}
|
||||
|
||||
export const getAccessToken = async (auth: GoogleSheetsAuthValue): Promise<string> => {
|
||||
if(auth.type === AppConnectionType.CUSTOM_AUTH)
|
||||
{
|
||||
const googleClient = await createGoogleClient(auth);
|
||||
const response = await googleClient.getAccessToken();
|
||||
if(response.token)
|
||||
{
|
||||
return response.token;
|
||||
}
|
||||
else {
|
||||
throw new Error('Could not retrieve access token from service account json');
|
||||
}
|
||||
}
|
||||
return auth.access_token;
|
||||
}
|
||||
|
||||
export function areSheetIdsValid(spreadsheetId: string | null | undefined, sheetId: string | number | null | undefined): boolean {
|
||||
return !isNil(spreadsheetId) && spreadsheetId !== "" &&
|
||||
!isNil(sheetId) && sheetId !== "";
|
||||
}
|
||||
|
||||
export const googleSheetsScopes = [
|
||||
'https://www.googleapis.com/auth/spreadsheets',
|
||||
'https://www.googleapis.com/auth/drive.readonly',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
]
|
||||
|
||||
export const googleSheetsAuth =[PieceAuth.OAuth2({
|
||||
description: '',
|
||||
authUrl: 'https://accounts.google.com/o/oauth2/auth',
|
||||
tokenUrl: 'https://oauth2.googleapis.com/token',
|
||||
required: true,
|
||||
scope:googleSheetsScopes ,
|
||||
}), PieceAuth.CustomAuth({
|
||||
displayName: 'Service Account (Advanced)',
|
||||
description: 'Authenticate via service account from https://console.cloud.google.com/ > IAM & Admin > Service Accounts > Create Service Account > Keys > Add key. <br> <br> You can optionally use domain-wide delegation (https://support.google.com/a/answer/162106?hl=en#zippy=%2Cset-up-domain-wide-delegation-for-a-client) to access spreadsheets without adding the service account to each one. <br> <br> **Note:** Without a user email, the service account only has access to files/folders you explicitly share with it.',
|
||||
required: true,
|
||||
props: {
|
||||
serviceAccount: Property.ShortText({
|
||||
displayName: 'Service Account JSON Key',
|
||||
required: true,
|
||||
}
|
||||
) ,
|
||||
userEmail: Property.ShortText({
|
||||
displayName: 'User Email',
|
||||
required: false,
|
||||
description: 'Email address of the user to impersonate for domain-wide delegation.',
|
||||
}),},
|
||||
validate: async ({auth})=>{
|
||||
try{
|
||||
await getAccessToken({
|
||||
type: AppConnectionType.CUSTOM_AUTH,
|
||||
props: {...auth}
|
||||
});
|
||||
}catch(e){
|
||||
return {
|
||||
valid: false,
|
||||
error: (e as Error).message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
})];
|
||||
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
import { DropdownOption, Property } from '@activepieces/pieces-framework';
|
||||
import { google, drive_v3 } from 'googleapis';
|
||||
import { columnToLabel, createGoogleClient, getHeaderRow, googleSheetsAuth, GoogleSheetsAuthValue, googleSheetsCommon } from './common';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
export const includeTeamDrivesProp = () =>
|
||||
Property.Checkbox({
|
||||
displayName: 'Include Team Drive Sheets ?',
|
||||
description: 'Determines if sheets from Team Drives should be included in the results.',
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
});
|
||||
|
||||
export const spreadsheetIdProp = (displayName: string, description: string, required = true) =>
|
||||
Property.Dropdown({
|
||||
displayName,
|
||||
description,
|
||||
auth: googleSheetsAuth,
|
||||
required,
|
||||
refreshers: ['includeTeamDrives'],
|
||||
options: async ({ auth, includeTeamDrives }, { searchValue }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Please authenticate first',
|
||||
};
|
||||
}
|
||||
const authValue = auth;
|
||||
|
||||
const authClient = await createGoogleClient(authValue);
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: authClient });
|
||||
|
||||
const q = ["mimeType='application/vnd.google-apps.spreadsheet'", 'trashed = false'];
|
||||
|
||||
if (searchValue) {
|
||||
q.push(`name contains '${searchValue}'`);
|
||||
}
|
||||
|
||||
let nextPageToken;
|
||||
const options: DropdownOption<string>[] = [];
|
||||
do {
|
||||
const response: any = await drive.files.list({
|
||||
q: q.join(' and '),
|
||||
pageToken: nextPageToken,
|
||||
orderBy: 'createdTime desc',
|
||||
fields: 'nextPageToken, files(id, name)',
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: includeTeamDrives ? true : false,
|
||||
});
|
||||
const fileList: drive_v3.Schema$FileList = response.data;
|
||||
|
||||
if (fileList.files) {
|
||||
for (const file of fileList.files) {
|
||||
options.push({
|
||||
label: file.name!,
|
||||
value: file.id!,
|
||||
});
|
||||
}
|
||||
}
|
||||
nextPageToken = response.data.nextPageToken;
|
||||
} while (nextPageToken);
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const sheetIdProp = (displayName: string, description: string, required = true) =>
|
||||
Property.Dropdown({
|
||||
displayName,
|
||||
description,
|
||||
auth: googleSheetsAuth,
|
||||
required,
|
||||
refreshers: ['spreadsheetId'],
|
||||
options: async ({ auth, spreadsheetId }) => {
|
||||
if (!auth || (spreadsheetId ?? '').toString().length === 0) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Please select a spreadsheet first.',
|
||||
};
|
||||
}
|
||||
|
||||
const authValue = auth as GoogleSheetsAuthValue;
|
||||
|
||||
const authClient = await createGoogleClient(authValue);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const response = await sheets.spreadsheets.get({
|
||||
spreadsheetId: spreadsheetId as unknown as string,
|
||||
});
|
||||
|
||||
const sheetsData = response.data.sheets ?? [];
|
||||
|
||||
const options: DropdownOption<number>[] = [];
|
||||
|
||||
for (const sheet of sheetsData) {
|
||||
const title = sheet.properties?.title;
|
||||
const sheetId = sheet.properties?.sheetId;
|
||||
if(isNil(title) || isNil(sheetId)){
|
||||
continue;
|
||||
}
|
||||
options.push({
|
||||
label: title,
|
||||
value: sheetId,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
disabled: false,
|
||||
options,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const commonProps = {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheetId: spreadsheetIdProp('Spreadsheet', 'The ID of the spreadsheet to use.'),
|
||||
sheetId: sheetIdProp('Sheet', 'The ID of the sheet to use.'),
|
||||
};
|
||||
|
||||
export const rowValuesProp = () =>
|
||||
Property.DynamicProperties({
|
||||
displayName: 'Values',
|
||||
description: 'The values to insert',
|
||||
required: true,
|
||||
auth: googleSheetsAuth,
|
||||
refreshers: ['sheetId', 'spreadsheetId', 'first_row_headers'],
|
||||
props: async ({ auth, spreadsheetId, sheetId, first_row_headers }) => {
|
||||
if (
|
||||
!auth ||
|
||||
(spreadsheetId ?? '').toString().length === 0 ||
|
||||
(sheetId ?? '').toString().length === 0
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
const sheet_id = Number(sheetId);
|
||||
const authValue = auth as GoogleSheetsAuthValue;
|
||||
|
||||
const headers = await googleSheetsCommon.getHeaderRow({
|
||||
spreadsheetId: spreadsheetId as unknown as string,
|
||||
auth: authValue,
|
||||
sheetId: sheet_id,
|
||||
});
|
||||
|
||||
if (!first_row_headers) {
|
||||
return {
|
||||
values: Property.Array({
|
||||
displayName: 'Values',
|
||||
required: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const firstRow = headers ?? [];
|
||||
const properties: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
for (let i = 0; i < firstRow.length; i++) {
|
||||
const label = columnToLabel(i);
|
||||
properties[label] = Property.ShortText({
|
||||
displayName: firstRow[i].toString(),
|
||||
description: firstRow[i].toString(),
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
});
|
||||
}
|
||||
return properties;
|
||||
},
|
||||
});
|
||||
|
||||
export const columnNameProp = () =>
|
||||
Property.Dropdown<string,true,typeof googleSheetsAuth>({
|
||||
description: 'Column Name',
|
||||
displayName: 'The name of the column to search in',
|
||||
required: true,
|
||||
auth: googleSheetsAuth,
|
||||
refreshers: ['sheetId', 'spreadsheetId'],
|
||||
options: async ({ auth, spreadsheetId, sheetId }) => {
|
||||
const spreadsheet_id = spreadsheetId as string;
|
||||
const sheet_id = Number(sheetId) as number;
|
||||
if (
|
||||
!auth ||
|
||||
(spreadsheet_id ?? '').toString().length === 0 ||
|
||||
(sheet_id ?? '').toString().length === 0
|
||||
) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Please select a sheet first',
|
||||
};
|
||||
}
|
||||
|
||||
const sheetName = await googleSheetsCommon.findSheetName(
|
||||
auth,
|
||||
spreadsheet_id,
|
||||
sheet_id,
|
||||
);
|
||||
|
||||
if (!sheetName) {
|
||||
throw Error('Sheet not found in spreadsheet');
|
||||
}
|
||||
|
||||
const headers = await getHeaderRow({
|
||||
spreadsheetId: spreadsheet_id,
|
||||
auth,
|
||||
sheetId: sheet_id,
|
||||
});
|
||||
|
||||
const ret = [];
|
||||
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
if (isNil(headers)) {
|
||||
return {
|
||||
options: [],
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
if (headers.length === 0) {
|
||||
const columnSize = headers.length;
|
||||
|
||||
for (let i = 0; i < columnSize; i++) {
|
||||
ret.push({
|
||||
label: alphabet[i].toUpperCase(),
|
||||
value: alphabet[i],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let index = 0;
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
let value = 'A';
|
||||
if (index >= alphabet.length) {
|
||||
// if the index is greater than the length of the alphabet, we need to add another letter
|
||||
const firstLetter = alphabet[Math.floor(index / alphabet.length) - 1];
|
||||
const secondLetter = alphabet[index % alphabet.length];
|
||||
value = firstLetter + secondLetter;
|
||||
} else {
|
||||
value = alphabet[index];
|
||||
}
|
||||
|
||||
ret.push({
|
||||
label: headers[i].toString(),
|
||||
value: value,
|
||||
});
|
||||
index++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
options: ret,
|
||||
disabled: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
import { google } from 'googleapis';
|
||||
import { nanoid } from 'nanoid';
|
||||
import dayjs from 'dayjs';
|
||||
import crypto from 'crypto';
|
||||
import { columnToLabel, createGoogleClient, GoogleSheetsAuthValue } from '../common/common';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
|
||||
export async function getWorkSheetName(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
spreadSheetId: string,
|
||||
sheetId: number,
|
||||
) {
|
||||
const authClient = await createGoogleClient(auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const res = await sheets.spreadsheets.get({ spreadsheetId: spreadSheetId });
|
||||
const sheetName = res.data.sheets?.find((f) => f.properties?.sheetId == sheetId)?.properties
|
||||
?.title;
|
||||
|
||||
if (!sheetName) {
|
||||
throw Error(`Sheet with ID ${sheetId} not found in spreadsheet ${spreadSheetId}`);
|
||||
}
|
||||
|
||||
return sheetName;
|
||||
}
|
||||
|
||||
export async function getWorkSheetGridSize(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
spreadSheetId: string,
|
||||
sheetId: number,
|
||||
) {
|
||||
const authClient = await createGoogleClient(auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const res = await sheets.spreadsheets.get({ spreadsheetId: spreadSheetId, includeGridData: true, fields: 'sheets.properties(sheetId,title,sheetType,gridProperties)' });
|
||||
const sheetRange = res.data.sheets?.find((f) => f.properties?.sheetId == sheetId)?.properties?.gridProperties;
|
||||
|
||||
if (!sheetRange) {
|
||||
throw Error(`Unable to get grid size for sheet ${sheetId} in spreadsheet ${spreadSheetId}`);
|
||||
}
|
||||
|
||||
return sheetRange
|
||||
}
|
||||
|
||||
export async function getWorkSheetValues(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
spreadsheetId: string,
|
||||
range?: string,
|
||||
) {
|
||||
const authClient = await createGoogleClient(auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const res = await sheets.spreadsheets.values.get({
|
||||
spreadsheetId: spreadsheetId,
|
||||
range: range,
|
||||
});
|
||||
|
||||
return res.data.values ?? [];
|
||||
}
|
||||
|
||||
export async function createFileNotification(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
fileId: string,
|
||||
url: string,
|
||||
includeTeamDrives?: boolean,
|
||||
) {
|
||||
const authClient = await createGoogleClient(auth);
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: authClient });
|
||||
|
||||
// create unique UUID for channel
|
||||
const channelId = nanoid();
|
||||
return await drive.files.watch({
|
||||
fileId: fileId,
|
||||
supportsAllDrives: includeTeamDrives,
|
||||
requestBody: {
|
||||
id: channelId,
|
||||
expiration: (dayjs().add(6, 'day').unix() * 1000).toString(),
|
||||
type: 'web_hook',
|
||||
address: url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteFileNotification(
|
||||
auth: GoogleSheetsAuthValue,
|
||||
channelId: string,
|
||||
resourceId: string,
|
||||
) {
|
||||
const authClient = await createGoogleClient(auth);
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: authClient });
|
||||
|
||||
return await drive.channels.stop({
|
||||
requestBody: {
|
||||
id: channelId,
|
||||
resourceId: resourceId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function isSyncMessage(headers: Record<string, string>) {
|
||||
return headers['x-goog-resource-state'] === 'sync';
|
||||
}
|
||||
|
||||
export function isChangeContentMessage(headers: Record<string, string>) {
|
||||
// https://developers.google.com/drive/api/guides/push#respond-to-notifications
|
||||
return (
|
||||
headers['x-goog-resource-state'] === 'update' &&
|
||||
['content', 'properties', 'content,properties'].includes(headers['x-goog-changed'])
|
||||
);
|
||||
}
|
||||
|
||||
export function hashObject(obj: Record<string, unknown>): string {
|
||||
const hash = crypto.createHash('sha256');
|
||||
hash.update(JSON.stringify(obj));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
// returns an array of row number and cells values mapped to column labels
|
||||
export function mapRowsToColumnLabels(rowValues: any[][], oldRowCount: number, headerCount: number) {
|
||||
const result = [];
|
||||
for (let i = 0; i < rowValues.length; i++) {
|
||||
const values: Record<string, string> = {};
|
||||
for (let j = 0; j < Math.max(headerCount, rowValues[i].length); j++) {
|
||||
const columnLabel = columnToLabel(j);
|
||||
if (isNil(rowValues[i][j])) {
|
||||
values[columnLabel] = "";
|
||||
} else if (typeof rowValues[i][j] === "string") {
|
||||
values[columnLabel] = rowValues[i][j];
|
||||
}
|
||||
else if ('toString' in rowValues[i][j]) {
|
||||
values[columnLabel] = rowValues[i][j].toString();
|
||||
}
|
||||
else {
|
||||
values[columnLabel] = `${rowValues[i][j]}`;
|
||||
}
|
||||
}
|
||||
result.push({
|
||||
row: oldRowCount + i + 1,
|
||||
values,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface WebhookInformation {
|
||||
kind?: string | null;
|
||||
id?: string | null;
|
||||
resourceId?: string | null;
|
||||
resourceUri?: string | null;
|
||||
expiration?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { isNil } from '@activepieces/shared';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { areSheetIdsValid, columnToLabel, GoogleSheetsAuthValue, labelToColumn } from '../common/common';
|
||||
import {
|
||||
createFileNotification,
|
||||
deleteFileNotification,
|
||||
getWorkSheetName,
|
||||
getWorkSheetValues,
|
||||
hashObject,
|
||||
isChangeContentMessage,
|
||||
isSyncMessage,
|
||||
mapRowsToColumnLabels,
|
||||
WebhookInformation,
|
||||
} from './helpers';
|
||||
|
||||
import {
|
||||
createTrigger,
|
||||
TriggerStrategy,
|
||||
DEDUPE_KEY_PROPERTY,
|
||||
WebhookRenewStrategy,
|
||||
Property,
|
||||
DropdownOption,
|
||||
} from '@activepieces/pieces-framework';
|
||||
|
||||
import crypto from 'crypto';
|
||||
import { commonProps } from '../common/props';
|
||||
|
||||
const ALL_COLUMNS = 'all_columns';
|
||||
|
||||
export const newOrUpdatedRowTrigger = createTrigger({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'google-sheets-new-or-updated-row',
|
||||
displayName: 'New or Updated Row',
|
||||
description: 'Triggers when a new row is added or modified in a spreadsheet.',
|
||||
props: {
|
||||
info: Property.MarkDown({
|
||||
value:
|
||||
'Please note that there might be a delay of up to 3 minutes for the trigger to be fired, due to a delay from Google.',
|
||||
}),
|
||||
...commonProps,
|
||||
trigger_column: Property.Dropdown({
|
||||
auth: googleSheetsAuth,
|
||||
displayName: 'Trigger Column',
|
||||
description: `Trigger on changes to cells in this column only. \nSelect **Any Column** if you want the flow to trigger on changes to any cell within the row.`,
|
||||
required: false,
|
||||
refreshers: ['spreadsheetId', 'sheetId'],
|
||||
defaultValue: ALL_COLUMNS,
|
||||
options: async ({ auth, spreadsheetId, sheetId }) => {
|
||||
if (!auth || !spreadsheetId || isNil(sheetId)) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: `Please select sheet first`,
|
||||
};
|
||||
}
|
||||
|
||||
const spreadsheet_id = spreadsheetId as string;
|
||||
const sheet_id = sheetId as number;
|
||||
|
||||
const sheetName = await getWorkSheetName(auth, spreadsheet_id, sheet_id);
|
||||
|
||||
const firstRowValues = await getWorkSheetValues(
|
||||
auth,
|
||||
spreadsheet_id,
|
||||
`${sheetName}!1:1`,
|
||||
);
|
||||
|
||||
const headers = firstRowValues[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
const labeledRowValues = mapRowsToColumnLabels(firstRowValues, 0, headerCount);
|
||||
const labledHeaders = labeledRowValues.length > 0 ? labeledRowValues[0].values : {};
|
||||
|
||||
const options = Object.entries(labledHeaders).reduce((accumlator:DropdownOption<string>[],[key,value]) => {
|
||||
accumlator.push({ label: value as string, value: key });
|
||||
return accumlator;
|
||||
}, [{ label: 'Any Column', value: ALL_COLUMNS }]);
|
||||
|
||||
return {
|
||||
disabled: options.length === 0,
|
||||
options,
|
||||
};
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
renewConfiguration: {
|
||||
strategy: WebhookRenewStrategy.CRON,
|
||||
cronExpression: '0 */12 * * *',
|
||||
},
|
||||
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
|
||||
async onEnable(context) {
|
||||
const inputSpreadsheetId = context.propsValue.spreadsheetId;
|
||||
const inputSheetId = context.propsValue.sheetId;
|
||||
const triggerColumn = context.propsValue.trigger_column ?? ALL_COLUMNS;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
|
||||
const sheetValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
|
||||
const rowHashes = [];
|
||||
|
||||
// create initial row level hashes and used it to check updated row
|
||||
for (const row of sheetValues) {
|
||||
let targetValue;
|
||||
if (triggerColumn === ALL_COLUMNS) {
|
||||
targetValue = row;
|
||||
} else {
|
||||
const currentTriggerColumnValue = row[labelToColumn(triggerColumn)];
|
||||
|
||||
targetValue =
|
||||
currentTriggerColumnValue !== undefined && currentTriggerColumnValue !== '' // if column value is empty
|
||||
? [currentTriggerColumnValue]
|
||||
: [];
|
||||
}
|
||||
|
||||
const rowHash = crypto.createHash('md5').update(JSON.stringify(targetValue)).digest('hex');
|
||||
rowHashes.push(rowHash);
|
||||
}
|
||||
|
||||
// store compressed values
|
||||
await context.store.put(`${sheetId}`, rowHashes);
|
||||
|
||||
// create file watch notification
|
||||
const fileNotificationRes = await createFileNotification(
|
||||
context.auth,
|
||||
spreadsheetId,
|
||||
context.webhookUrl,
|
||||
context.propsValue.includeTeamDrives,
|
||||
);
|
||||
|
||||
await context.store.put<WebhookInformation>(
|
||||
'google-sheets-new-or-updated-row',
|
||||
fileNotificationRes.data,
|
||||
);
|
||||
},
|
||||
|
||||
async onDisable(context) {
|
||||
const webhook = await context.store.get<WebhookInformation>('google-sheets-new-or-updated-row');
|
||||
|
||||
if (webhook != null && webhook.id != null && webhook.resourceId != null) {
|
||||
try
|
||||
{
|
||||
await deleteFileNotification(context.auth, webhook.id, webhook.resourceId);
|
||||
}
|
||||
catch(err){
|
||||
console.debug("deleteFileNotification failed :",JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async run(context) {
|
||||
if (isSyncMessage(context.payload.headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isChangeContentMessage(context.payload.headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inputSpreadsheetId = context.propsValue.spreadsheetId;
|
||||
const inputSheetId = context.propsValue.sheetId;
|
||||
const triggerColumn = context.propsValue.trigger_column ?? ALL_COLUMNS;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
|
||||
const oldValuesHashes = (await context.store.get(`${sheetId}`)) as any[];
|
||||
|
||||
/* Fetch rows values with all columns as this will be used on returning updated/new row data
|
||||
*/
|
||||
const currentValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
|
||||
const headers = currentValues[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
|
||||
// const rowCount = Math.max(oldValuesHashes.length, currentValues.length);
|
||||
|
||||
const changedValues = [];
|
||||
const newRowHashes = [];
|
||||
|
||||
for (let row = 0; row < currentValues.length; row++) {
|
||||
const currentRowValue = currentValues[row];
|
||||
|
||||
/**
|
||||
* This variable store value based on trigger column.
|
||||
* If trigger column is all_columns then store entry row as target value, else store only column value.
|
||||
*/
|
||||
let targetValue;
|
||||
if (triggerColumn === ALL_COLUMNS) {
|
||||
targetValue = currentRowValue;
|
||||
} else {
|
||||
const currentTriggerColumnValue = currentRowValue[labelToColumn(triggerColumn)];
|
||||
|
||||
targetValue =
|
||||
currentTriggerColumnValue !== undefined && currentTriggerColumnValue !== ''
|
||||
? [currentTriggerColumnValue]
|
||||
: [];
|
||||
}
|
||||
|
||||
// create hash for new row values
|
||||
const currentRowHash = crypto
|
||||
.createHash('md5')
|
||||
.update(JSON.stringify(targetValue))
|
||||
.digest('hex');
|
||||
newRowHashes.push(currentRowHash);
|
||||
|
||||
// If row is empty then skip
|
||||
if (currentRowValue === undefined || currentRowValue.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldRowHash =
|
||||
!isNil(oldValuesHashes) && row < oldValuesHashes.length ? oldValuesHashes[row] : undefined;
|
||||
|
||||
if (oldRowHash === undefined || oldRowHash != currentRowHash) {
|
||||
const formattedValues: any = {};
|
||||
|
||||
for (let column = 0; column < headerCount; column++) {
|
||||
formattedValues[columnToLabel(column)] = currentValues[row][column] ?? '';
|
||||
}
|
||||
|
||||
changedValues.push({
|
||||
row: row + 1,
|
||||
values: formattedValues,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// update the row hashes
|
||||
await context.store.put(`${sheetId}`, newRowHashes);
|
||||
|
||||
return changedValues.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
[DEDUPE_KEY_PROPERTY]: hashObject(row),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
async test(context) {
|
||||
const inputSpreadsheetId = context.propsValue.spreadsheetId;
|
||||
const inputSheetId = context.propsValue.sheetId;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
const currentSheetValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
|
||||
const headers = currentSheetValues[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
|
||||
const transformedRowValues = mapRowsToColumnLabels(currentSheetValues, 0, headerCount)
|
||||
.slice(-5)
|
||||
.reverse();
|
||||
|
||||
return transformedRowValues;
|
||||
},
|
||||
|
||||
async onRenew(context) {
|
||||
// get current channel ID & resource ID
|
||||
const webhook = await context.store.get<WebhookInformation>(`google-sheets-new-or-updated-row`);
|
||||
if (webhook != null && webhook.id != null && webhook.resourceId != null) {
|
||||
// delete current channel
|
||||
await deleteFileNotification(context.auth, webhook.id, webhook.resourceId);
|
||||
const fileNotificationRes = await createFileNotification(
|
||||
context.auth,
|
||||
context.propsValue.spreadsheetId!,
|
||||
context.webhookUrl,
|
||||
context.propsValue.includeTeamDrives,
|
||||
);
|
||||
// store channel response
|
||||
await context.store.put<WebhookInformation>(
|
||||
'google-sheets-new-or-updated-row',
|
||||
fileNotificationRes.data,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
sampleData: {},
|
||||
});
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
DEDUPE_KEY_PROPERTY,
|
||||
Property,
|
||||
TriggerStrategy,
|
||||
WebhookRenewStrategy,
|
||||
createTrigger,
|
||||
} from '@activepieces/pieces-framework';
|
||||
|
||||
import {
|
||||
createFileNotification,
|
||||
deleteFileNotification,
|
||||
getWorkSheetName,
|
||||
getWorkSheetValues,
|
||||
hashObject,
|
||||
isChangeContentMessage,
|
||||
isSyncMessage,
|
||||
mapRowsToColumnLabels,
|
||||
WebhookInformation,
|
||||
} from './helpers';
|
||||
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { commonProps } from '../common/props';
|
||||
import { areSheetIdsValid, } from '../common/common';
|
||||
|
||||
export const newRowAddedTrigger = createTrigger({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'googlesheets_new_row_added',
|
||||
displayName: 'New Row Added',
|
||||
description: 'Triggers when a new row is added to bottom of a spreadsheet.',
|
||||
props: {
|
||||
info: Property.MarkDown({
|
||||
value:
|
||||
'Please note that there might be a delay of up to 3 minutes for the trigger to be fired, due to a delay from Google.',
|
||||
}),
|
||||
...commonProps,
|
||||
},
|
||||
renewConfiguration: {
|
||||
strategy: WebhookRenewStrategy.CRON,
|
||||
cronExpression: '0 */12 * * *',
|
||||
},
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
async onEnable(context) {
|
||||
const { spreadsheetId:inputSpreadsheetId, sheetId:inputSheetId } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
const currentSheetValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
|
||||
await context.store.put(`${sheetId}`, currentSheetValues.length);
|
||||
|
||||
const fileNotificationRes = await createFileNotification(
|
||||
context.auth,
|
||||
spreadsheetId,
|
||||
context.webhookUrl,
|
||||
context.propsValue.includeTeamDrives,
|
||||
);
|
||||
|
||||
await context.store.put<WebhookInformation>(
|
||||
'googlesheets_new_row_added',
|
||||
fileNotificationRes.data,
|
||||
);
|
||||
},
|
||||
async onDisable(context) {
|
||||
const webhook = await context.store.get<WebhookInformation>(`googlesheets_new_row_added`);
|
||||
if (webhook != null && webhook.id != null && webhook.resourceId != null) {
|
||||
try
|
||||
{
|
||||
await deleteFileNotification(context.auth, webhook.id, webhook.resourceId);
|
||||
}
|
||||
catch(err){
|
||||
console.debug("deleteFileNotification failed :",JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
},
|
||||
async run(context) {
|
||||
if (isSyncMessage(context.payload.headers)) {
|
||||
return [];
|
||||
}
|
||||
if (!isChangeContentMessage(context.payload.headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { spreadsheetId:inputSpreadsheetId, sheetId:inputSheetId } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const oldRowCount = (await context.store.get(`${sheetId}`)) as number;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
const currentRowValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
const currentRowCount = currentRowValues.length;
|
||||
|
||||
const headers = currentRowValues[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
|
||||
if (oldRowCount >= currentRowCount) {
|
||||
if (oldRowCount > currentRowCount) {
|
||||
await context.store.put(`${sheetId}`, currentRowCount);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// create A1 notation range for new rows
|
||||
const range = `${sheetName}!${oldRowCount + 1}:${currentRowCount}`;
|
||||
|
||||
const newRowValues = await getWorkSheetValues(
|
||||
context.auth,
|
||||
spreadsheetId,
|
||||
range,
|
||||
);
|
||||
|
||||
await context.store.put(`${sheetId}`, currentRowCount);
|
||||
|
||||
const transformedRowValues = mapRowsToColumnLabels(newRowValues, oldRowCount,headerCount);
|
||||
return transformedRowValues.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
[DEDUPE_KEY_PROPERTY]: hashObject(row),
|
||||
};
|
||||
});
|
||||
},
|
||||
async onRenew(context) {
|
||||
// get current channel ID & resource ID
|
||||
const webhook = await context.store.get<WebhookInformation>(`googlesheets_new_row_added`);
|
||||
|
||||
const { spreadsheetId:inputSpreadsheetId, sheetId:inputSheetId } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
if (webhook != null && webhook.id != null && webhook.resourceId != null) {
|
||||
await deleteFileNotification(context.auth, webhook.id, webhook.resourceId);
|
||||
const fileNotificationRes = await createFileNotification(
|
||||
context.auth,
|
||||
spreadsheetId,
|
||||
context.webhookUrl,
|
||||
context.propsValue.includeTeamDrives,
|
||||
);
|
||||
await context.store.put<WebhookInformation>(
|
||||
'googlesheets_new_row_added',
|
||||
fileNotificationRes.data,
|
||||
);
|
||||
}
|
||||
},
|
||||
async test(context) {
|
||||
const { spreadsheetId:inputSpreadsheetId, sheetId:inputSheetId } = context.propsValue;
|
||||
|
||||
if (!areSheetIdsValid(inputSpreadsheetId, inputSheetId)) {
|
||||
throw new Error('Please select a spreadsheet and sheet first.');
|
||||
}
|
||||
|
||||
const sheetId = Number(inputSheetId);
|
||||
const spreadsheetId = inputSpreadsheetId as string;
|
||||
|
||||
const sheetName = await getWorkSheetName(context.auth, spreadsheetId, sheetId);
|
||||
const currentSheetValues = await getWorkSheetValues(context.auth, spreadsheetId, sheetName);
|
||||
|
||||
const headers = currentSheetValues[0] ?? [];
|
||||
const headerCount = headers.length;
|
||||
|
||||
const transformedRowValues = mapRowsToColumnLabels(currentSheetValues, 0,headerCount)
|
||||
.slice(-5)
|
||||
.reverse();
|
||||
|
||||
return transformedRowValues;
|
||||
},
|
||||
sampleData: {},
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { AppConnectionValueForAuthProperty, createTrigger,PiecePropValueSchema,TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { DedupeStrategy, Polling, pollingHelper } from '@activepieces/pieces-common';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { google, drive_v3 } from 'googleapis';
|
||||
import { includeTeamDrivesProp } from '../common/props';
|
||||
import { createGoogleClient, GoogleSheetsAuthValue } from '../common/common';
|
||||
type Props = {
|
||||
includeTeamDrives?: boolean;
|
||||
};
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof googleSheetsAuth>, Props> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
async items({ auth, propsValue, lastFetchEpochMS }) {
|
||||
const authValue = auth as GoogleSheetsAuthValue;
|
||||
const q = ["mimeType='application/vnd.google-apps.spreadsheet'",'trashed = false'];
|
||||
if (lastFetchEpochMS) {
|
||||
q.push(`createdTime > '${dayjs(lastFetchEpochMS).toISOString()}'`);
|
||||
}
|
||||
const authClient = await createGoogleClient(authValue);
|
||||
const drive = google.drive({ version: 'v3', auth: authClient });
|
||||
let nextPageToken;
|
||||
const items = [];
|
||||
do {
|
||||
const response: any = await drive.files.list({
|
||||
q: q.join(' and '),
|
||||
fields: '*',
|
||||
orderBy: 'createdTime desc',
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: propsValue.includeTeamDrives,
|
||||
pageToken: nextPageToken,
|
||||
});
|
||||
const fileList: drive_v3.Schema$FileList = response.data;
|
||||
if (fileList.files) {
|
||||
items.push(...fileList.files);
|
||||
}
|
||||
if (lastFetchEpochMS === 0) break;
|
||||
nextPageToken = response.data.nextPageToken;
|
||||
} while (nextPageToken);
|
||||
return items.map((item) => ({
|
||||
epochMilliSeconds: dayjs(item.createdTime).valueOf(),
|
||||
data: item,
|
||||
}));
|
||||
},
|
||||
};
|
||||
export const newSpreadsheetTrigger = createTrigger({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'new-spreadsheet',
|
||||
displayName: 'New Spreadsheet',
|
||||
description: 'Triggers when a new spreadsheet is created.',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp()
|
||||
},
|
||||
async onEnable(context) {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async onDisable(context) {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
async test(context) {
|
||||
return await pollingHelper.test(polling, context);
|
||||
},
|
||||
async run(context) {
|
||||
return await pollingHelper.poll(polling, context);
|
||||
},
|
||||
sampleData:{
|
||||
kind: 'drive#file',
|
||||
mimeType: 'application/vnd.google-apps.spreadsheet',
|
||||
webViewLink:
|
||||
'https://docs.google.com/document/d/1_9xjsrYFgHVvgqYwAJ8KcsDcNU/edit?usp=drivesdk',
|
||||
id: '1_9xjsrYFgHVvgqYwAJ8KcsDcN3AzPelsux',
|
||||
name: 'Test Document',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { googleSheetsAuth } from '../common/common';
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { google } from 'googleapis';
|
||||
import { OAuth2Client } from 'googleapis-common';
|
||||
import { isNil } from '@activepieces/shared';
|
||||
import { includeTeamDrivesProp, spreadsheetIdProp } from '../common/props';
|
||||
import { createGoogleClient } from '../common/common';
|
||||
|
||||
export const newWorksheetTrigger = createTrigger({
|
||||
auth: googleSheetsAuth,
|
||||
name: 'new-worksheet',
|
||||
displayName: 'New Worksheet',
|
||||
description: 'Triggers when a worksheet is created in a spreadsheet.',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
includeTeamDrives: includeTeamDrivesProp(),
|
||||
spreadsheetId: spreadsheetIdProp('Spreadsheet', '',true),
|
||||
},
|
||||
async onEnable(context) {
|
||||
const ids: number[] = [];
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
const response = await sheets.spreadsheets.get({
|
||||
spreadsheetId: context.propsValue.spreadsheetId as string,
|
||||
});
|
||||
if (response.data.sheets) {
|
||||
for (const sheet of response.data.sheets) {
|
||||
const sheetId = sheet.properties?.sheetId;
|
||||
if (sheetId) {
|
||||
ids.push(sheetId);
|
||||
}
|
||||
}
|
||||
}
|
||||
await context.store.put('worksheets', JSON.stringify(ids));
|
||||
},
|
||||
async onDisable(context) {
|
||||
await context.store.delete('worksheets');
|
||||
},
|
||||
async test(context) {
|
||||
const worksheets = [];
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
const response = await sheets.spreadsheets.get({
|
||||
spreadsheetId: context.propsValue.spreadsheetId as string,
|
||||
});
|
||||
|
||||
if (response.data.sheets) {
|
||||
for (const sheet of response.data.sheets) {
|
||||
worksheets.push(sheet);
|
||||
}
|
||||
}
|
||||
return worksheets;
|
||||
},
|
||||
async run(context) {
|
||||
const existingIds = (await context.store.get<string>('worksheets')) ?? '[]';
|
||||
const parsedExistingIds = JSON.parse(existingIds) as number[];
|
||||
|
||||
const authClient = await createGoogleClient(context.auth);
|
||||
|
||||
const sheets = google.sheets({ version: 'v4', auth: authClient });
|
||||
|
||||
const response = await sheets.spreadsheets.get({
|
||||
spreadsheetId: context.propsValue.spreadsheetId as string,
|
||||
});
|
||||
if (isNil(response.data.sheets) || response.data.sheets.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// Filter valid worksheetss
|
||||
const newWorksheets = response.data.sheets.filter((sheet) => {
|
||||
const sheetId = sheet.properties?.sheetId ?? undefined;
|
||||
return sheetId !== undefined && !parsedExistingIds.includes(sheetId);
|
||||
});
|
||||
|
||||
const newIds = newWorksheets
|
||||
.map((sheet) => sheet.properties?.sheetId ?? undefined)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
|
||||
if (newIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
// Store new IDs
|
||||
await context.store.put('worksheets', JSON.stringify([...newIds, ...parsedExistingIds]));
|
||||
return newWorksheets;
|
||||
},
|
||||
sampleData: {
|
||||
properties: {
|
||||
sheetId: 2077270595,
|
||||
title: 'Sheet5',
|
||||
index: 1,
|
||||
sheetType: 'GRID',
|
||||
gridProperties: {
|
||||
rowCount: 1000,
|
||||
columnCount: 26,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user