♻️ Export bot-engine code into its own package
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
GoogleSheetsBlock,
|
||||
GoogleSheetsAction,
|
||||
SessionState,
|
||||
} from '@typebot.io/schemas'
|
||||
import { insertRow } from './insertRow'
|
||||
import { updateRow } from './updateRow'
|
||||
import { getRow } from './getRow'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
|
||||
export const executeGoogleSheetBlock = async (
|
||||
state: SessionState,
|
||||
block: GoogleSheetsBlock
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const action = block.options.action
|
||||
if (!action) return { outgoingEdgeId: block.outgoingEdgeId }
|
||||
switch (action) {
|
||||
case GoogleSheetsAction.INSERT_ROW:
|
||||
return insertRow(state, {
|
||||
options: block.options,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
})
|
||||
case GoogleSheetsAction.UPDATE_ROW:
|
||||
return updateRow(state, {
|
||||
options: block.options,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
})
|
||||
case GoogleSheetsAction.GET:
|
||||
return getRow(state, {
|
||||
options: block.options,
|
||||
outgoingEdgeId: block.outgoingEdgeId,
|
||||
})
|
||||
}
|
||||
}
|
||||
108
packages/bot-engine/blocks/integrations/googleSheets/getRow.ts
Normal file
108
packages/bot-engine/blocks/integrations/googleSheets/getRow.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
SessionState,
|
||||
GoogleSheetsGetOptions,
|
||||
VariableWithValue,
|
||||
ReplyLog,
|
||||
} from '@typebot.io/schemas'
|
||||
import { isNotEmpty, byId } from '@typebot.io/lib'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
import { updateVariablesInSession } from '../../../variables/updateVariablesInSession'
|
||||
|
||||
export const getRow = async (
|
||||
state: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsGetOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const logs: ReplyLog[] = []
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const { sheetId, cellsToExtract, referenceCell, filter } =
|
||||
deepParseVariables(variables)(options)
|
||||
if (!sheetId) return { outgoingEdgeId }
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
spreadsheetId: options.spreadsheetId,
|
||||
})
|
||||
|
||||
try {
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[Number(sheetId)]
|
||||
const rows = await sheet.getRows()
|
||||
const filteredRows = getTotalRows(
|
||||
options.totalRowsToExtract,
|
||||
rows.filter((row) =>
|
||||
referenceCell
|
||||
? row.get(referenceCell.column as string) === referenceCell.value
|
||||
: matchFilter(row, filter)
|
||||
)
|
||||
)
|
||||
if (filteredRows.length === 0) {
|
||||
logs.push({
|
||||
status: 'info',
|
||||
description: `Couldn't find any rows matching the filter`,
|
||||
details: JSON.stringify(filter, null, 2),
|
||||
})
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
const extractingColumns = cellsToExtract
|
||||
.map((cell) => cell.column)
|
||||
.filter(isNotEmpty)
|
||||
const selectedRows = filteredRows.map((row) =>
|
||||
extractingColumns.reduce<{ [key: string]: string }>(
|
||||
(obj, column) => ({ ...obj, [column]: row.get(column) }),
|
||||
{}
|
||||
)
|
||||
)
|
||||
if (!selectedRows) return { outgoingEdgeId }
|
||||
|
||||
const newVariables = options.cellsToExtract.reduce<VariableWithValue[]>(
|
||||
(newVariables, cell) => {
|
||||
const existingVariable = variables.find(byId(cell.variableId))
|
||||
const value = selectedRows.map((row) => row[cell.column ?? ''])
|
||||
if (!existingVariable) return newVariables
|
||||
return [
|
||||
...newVariables,
|
||||
{
|
||||
...existingVariable,
|
||||
value: value.length === 1 ? value[0] : value,
|
||||
},
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
const newSessionState = updateVariablesInSession(state)(newVariables)
|
||||
return {
|
||||
outgoingEdgeId,
|
||||
newSessionState,
|
||||
}
|
||||
} catch (err) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occurred while fetching the spreadsheet data`,
|
||||
details: err,
|
||||
})
|
||||
}
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
const getTotalRows = <T>(
|
||||
totalRowsToExtract: GoogleSheetsGetOptions['totalRowsToExtract'],
|
||||
rows: T[]
|
||||
): T[] => {
|
||||
switch (totalRowsToExtract) {
|
||||
case 'All':
|
||||
case undefined:
|
||||
return rows
|
||||
case 'First':
|
||||
return rows.slice(0, 1)
|
||||
case 'Last':
|
||||
return rows.slice(-1)
|
||||
case 'Random':
|
||||
return [rows[Math.floor(Math.random() * rows.length)]]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { env } from '@typebot.io/env'
|
||||
import { decrypt, encrypt } from '@typebot.io/lib/api/encryption'
|
||||
import { isDefined } from '@typebot.io/lib/utils'
|
||||
import { GoogleSheetsCredentials } from '@typebot.io/schemas/features/blocks/integrations/googleSheets/schemas'
|
||||
import { Credentials as CredentialsFromDb } from '@typebot.io/prisma'
|
||||
import { GoogleSpreadsheet } from 'google-spreadsheet'
|
||||
import { OAuth2Client, Credentials } from 'google-auth-library'
|
||||
import prisma from '@typebot.io/lib/prisma'
|
||||
|
||||
export const getAuthenticatedGoogleDoc = async ({
|
||||
credentialsId,
|
||||
spreadsheetId,
|
||||
}: {
|
||||
credentialsId?: string
|
||||
spreadsheetId?: string
|
||||
}) => {
|
||||
if (!credentialsId || !spreadsheetId)
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Missing credentialsId or spreadsheetId',
|
||||
})
|
||||
const auth = await getAuthenticatedGoogleClient(credentialsId)
|
||||
if (!auth)
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: "Couldn't find credentials in database",
|
||||
})
|
||||
return new GoogleSpreadsheet(spreadsheetId, auth)
|
||||
}
|
||||
|
||||
const getAuthenticatedGoogleClient = async (
|
||||
credentialsId: string
|
||||
): Promise<OAuth2Client | undefined> => {
|
||||
const credentials = (await prisma.credentials.findFirst({
|
||||
where: { id: credentialsId },
|
||||
})) as CredentialsFromDb | undefined
|
||||
if (!credentials) return
|
||||
const data = (await decrypt(
|
||||
credentials.data,
|
||||
credentials.iv
|
||||
)) as GoogleSheetsCredentials['data']
|
||||
|
||||
const oauth2Client = new OAuth2Client(
|
||||
env.GOOGLE_CLIENT_ID,
|
||||
env.GOOGLE_CLIENT_SECRET,
|
||||
`${env.NEXTAUTH_URL}/api/credentials/google-sheets/callback`
|
||||
)
|
||||
oauth2Client.setCredentials(data)
|
||||
oauth2Client.on('tokens', updateTokens(credentialsId, data))
|
||||
return oauth2Client
|
||||
}
|
||||
|
||||
const updateTokens =
|
||||
(
|
||||
credentialsId: string,
|
||||
existingCredentials: GoogleSheetsCredentials['data']
|
||||
) =>
|
||||
async (credentials: Credentials) => {
|
||||
if (
|
||||
isDefined(existingCredentials.id_token) &&
|
||||
credentials.id_token !== existingCredentials.id_token
|
||||
)
|
||||
return
|
||||
const newCredentials: GoogleSheetsCredentials['data'] = {
|
||||
...existingCredentials,
|
||||
expiry_date: credentials.expiry_date,
|
||||
access_token: credentials.access_token,
|
||||
}
|
||||
const { encryptedData, iv } = await encrypt(newCredentials)
|
||||
await prisma.credentials.updateMany({
|
||||
where: { id: credentialsId },
|
||||
data: { data: encryptedData, iv },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import {
|
||||
GoogleSheetsGetOptions,
|
||||
LogicalOperator,
|
||||
ComparisonOperators,
|
||||
} from '@typebot.io/schemas'
|
||||
import { GoogleSpreadsheetRow } from 'google-spreadsheet'
|
||||
|
||||
export const matchFilter = (
|
||||
row: GoogleSpreadsheetRow,
|
||||
filter: GoogleSheetsGetOptions['filter']
|
||||
) => {
|
||||
if (!filter) return true
|
||||
return filter.logicalOperator === LogicalOperator.AND
|
||||
? filter.comparisons.every(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
row.get(comparison.column),
|
||||
comparison.comparisonOperator,
|
||||
comparison.value
|
||||
)
|
||||
)
|
||||
: filter.comparisons.some(
|
||||
(comparison) =>
|
||||
comparison.column &&
|
||||
matchComparison(
|
||||
row.get(comparison.column),
|
||||
comparison.comparisonOperator,
|
||||
comparison.value
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const matchComparison = (
|
||||
inputValue?: string,
|
||||
comparisonOperator?: ComparisonOperators,
|
||||
value?: string
|
||||
): boolean | undefined => {
|
||||
if (!comparisonOperator) return false
|
||||
switch (comparisonOperator) {
|
||||
case ComparisonOperators.CONTAINS: {
|
||||
if (!inputValue || !value) return false
|
||||
return inputValue
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.includes(value.toLowerCase().trim().normalize())
|
||||
}
|
||||
case ComparisonOperators.EQUAL: {
|
||||
return inputValue?.normalize() === value?.normalize()
|
||||
}
|
||||
case ComparisonOperators.NOT_EQUAL: {
|
||||
return inputValue?.normalize() !== value?.normalize()
|
||||
}
|
||||
case ComparisonOperators.GREATER: {
|
||||
if (!inputValue || !value) return false
|
||||
return parseFloat(inputValue) > parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.LESS: {
|
||||
if (!inputValue || !value) return false
|
||||
return parseFloat(inputValue) < parseFloat(value)
|
||||
}
|
||||
case ComparisonOperators.IS_SET: {
|
||||
return isDefined(inputValue) && inputValue.length > 0
|
||||
}
|
||||
case ComparisonOperators.IS_EMPTY: {
|
||||
return !isDefined(inputValue) || inputValue.length === 0
|
||||
}
|
||||
case ComparisonOperators.STARTS_WITH: {
|
||||
if (!inputValue || !value) return false
|
||||
return inputValue
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.startsWith(value.toLowerCase().trim().normalize())
|
||||
}
|
||||
case ComparisonOperators.ENDS_WITH: {
|
||||
if (!inputValue || !value) return false
|
||||
return inputValue
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.endsWith(value.toLowerCase().trim().normalize())
|
||||
}
|
||||
case ComparisonOperators.NOT_CONTAINS: {
|
||||
if (!inputValue || !value) return false
|
||||
return !inputValue
|
||||
?.toLowerCase()
|
||||
.trim()
|
||||
.normalize()
|
||||
.includes(value.toLowerCase().trim().normalize())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Variable, Cell } from '@typebot.io/schemas'
|
||||
import { parseVariables } from '../../../../variables/parseVariables'
|
||||
|
||||
export const parseCellValues =
|
||||
(variables: Variable[]) =>
|
||||
(cells: Cell[]): { [key: string]: string } =>
|
||||
cells.reduce((row, cell) => {
|
||||
return !cell.column || !cell.value
|
||||
? row
|
||||
: {
|
||||
...row,
|
||||
[cell.column]: parseVariables(variables)(cell.value),
|
||||
}
|
||||
}, {})
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
SessionState,
|
||||
GoogleSheetsInsertRowOptions,
|
||||
ReplyLog,
|
||||
} from '@typebot.io/schemas'
|
||||
import { parseCellValues } from './helpers/parseCellValues'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
|
||||
export const insertRow = async (
|
||||
state: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsInsertRowOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
if (!options.cellsToInsert || !options.sheetId) return { outgoingEdgeId }
|
||||
|
||||
const logs: ReplyLog[] = []
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
spreadsheetId: options.spreadsheetId,
|
||||
})
|
||||
|
||||
const parsedValues = parseCellValues(variables)(options.cellsToInsert)
|
||||
|
||||
try {
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[Number(options.sheetId)]
|
||||
await sheet.addRow(parsedValues)
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: `Succesfully inserted row in ${doc.title} > ${sheet.title}`,
|
||||
})
|
||||
} catch (err) {
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occured while inserting the row`,
|
||||
details: err,
|
||||
})
|
||||
}
|
||||
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
SessionState,
|
||||
GoogleSheetsUpdateRowOptions,
|
||||
ReplyLog,
|
||||
} from '@typebot.io/schemas'
|
||||
import { parseCellValues } from './helpers/parseCellValues'
|
||||
import { getAuthenticatedGoogleDoc } from './helpers/getAuthenticatedGoogleDoc'
|
||||
import { ExecuteIntegrationResponse } from '../../../types'
|
||||
import { matchFilter } from './helpers/matchFilter'
|
||||
import { deepParseVariables } from '../../../variables/deepParseVariables'
|
||||
|
||||
export const updateRow = async (
|
||||
state: SessionState,
|
||||
{
|
||||
outgoingEdgeId,
|
||||
options,
|
||||
}: { outgoingEdgeId?: string; options: GoogleSheetsUpdateRowOptions }
|
||||
): Promise<ExecuteIntegrationResponse> => {
|
||||
const { variables } = state.typebotsQueue[0].typebot
|
||||
const { sheetId, referenceCell, filter } =
|
||||
deepParseVariables(variables)(options)
|
||||
if (!options.cellsToUpsert || !sheetId || (!referenceCell && !filter))
|
||||
return { outgoingEdgeId }
|
||||
|
||||
const logs: ReplyLog[] = []
|
||||
|
||||
const doc = await getAuthenticatedGoogleDoc({
|
||||
credentialsId: options.credentialsId,
|
||||
spreadsheetId: options.spreadsheetId,
|
||||
})
|
||||
|
||||
const parsedValues = parseCellValues(variables)(options.cellsToUpsert)
|
||||
|
||||
await doc.loadInfo()
|
||||
const sheet = doc.sheetsById[Number(sheetId)]
|
||||
const rows = await sheet.getRows()
|
||||
const filteredRows = rows.filter((row) =>
|
||||
referenceCell
|
||||
? row.get(referenceCell.column as string) === referenceCell.value
|
||||
: matchFilter(row, filter as NonNullable<typeof filter>)
|
||||
)
|
||||
if (filteredRows.length === 0) {
|
||||
logs.push({
|
||||
status: 'info',
|
||||
description: `Could not find any row that matches the filter`,
|
||||
details: filter,
|
||||
})
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
|
||||
try {
|
||||
for (const filteredRow of filteredRows) {
|
||||
const rowIndex = filteredRow.rowNumber - 2 // -1 for 1-indexing, -1 for header row
|
||||
for (const key in parsedValues) {
|
||||
rows[rowIndex].set(key, parsedValues[key])
|
||||
}
|
||||
await rows[rowIndex].save()
|
||||
}
|
||||
|
||||
logs.push({
|
||||
status: 'success',
|
||||
description: `Succesfully updated matching rows`,
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
logs.push({
|
||||
status: 'error',
|
||||
description: `An error occured while updating the row`,
|
||||
details: err,
|
||||
})
|
||||
}
|
||||
return { outgoingEdgeId, logs }
|
||||
}
|
||||
Reference in New Issue
Block a user