2
0

♻️ Remove @typebot.io/schemas from @typebot.io/lib

This commit is contained in:
Baptiste Arnaud
2024-03-15 16:32:29 +01:00
parent b53242ce6a
commit 5073be2439
186 changed files with 809 additions and 581 deletions

View File

@ -0,0 +1,92 @@
import { Prisma, PrismaClient } from '@typebot.io/prisma'
import { Block, Typebot } from '@typebot.io/schemas'
import { deleteFilesFromBucket } from '@typebot.io/lib/s3/deleteFilesFromBucket'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
type ArchiveResultsProps = {
typebot: Pick<Typebot, 'groups'>
resultsFilter?: Omit<Prisma.ResultWhereInput, 'typebotId'> & {
typebotId: string
}
}
export const archiveResults =
(prisma: PrismaClient) =>
async ({ typebot, resultsFilter }: ArchiveResultsProps) => {
const batchSize = 100
const fileUploadBlockIds = typebot.groups
.flatMap<Block>((group) => group.blocks)
.filter((block) => block.type === InputBlockType.FILE)
.map((block) => block.id)
let currentTotalResults = 0
const resultsCount = await prisma.result.count({
where: {
...resultsFilter,
OR: [{ isArchived: false }, { isArchived: null }],
},
})
if (resultsCount === 0) return { success: true }
let progress = 0
do {
progress += batchSize
console.log(`Archiving ${progress} / ${resultsCount} results...`)
const resultsToDelete = await prisma.result.findMany({
where: {
...resultsFilter,
OR: [{ isArchived: false }, { isArchived: null }],
},
select: {
id: true,
},
take: batchSize,
})
if (resultsToDelete.length === 0) break
currentTotalResults = resultsToDelete.length
const resultIds = resultsToDelete.map((result) => result.id)
if (fileUploadBlockIds.length > 0) {
const filesToDelete = await prisma.answer.findMany({
where: {
resultId: { in: resultIds },
blockId: { in: fileUploadBlockIds },
},
})
if (filesToDelete.length > 0)
await deleteFilesFromBucket({
urls: filesToDelete.flatMap((a) => a.content.split(', ')),
})
}
await prisma.$transaction([
prisma.log.deleteMany({
where: {
resultId: { in: resultIds },
},
}),
prisma.answer.deleteMany({
where: {
resultId: { in: resultIds },
},
}),
prisma.result.updateMany({
where: {
id: { in: resultIds },
},
data: {
isArchived: true,
variables: [],
},
}),
])
} while (currentTotalResults >= batchSize)
return { success: true }
}

View File

@ -0,0 +1,88 @@
import {
ResultWithAnswers,
ResultHeaderCell,
VariableWithValue,
Answer,
TableData,
} from '@typebot.io/schemas'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { isDefined } from '../lib/utils'
type CellParser = (
content: VariableWithValue['value'],
blockType?: InputBlockType
) => { element?: React.JSX.Element; plainText: string }
const defaultCellParser: CellParser = (content, blockType) => {
if (!content) return { plainText: '' }
if (Array.isArray(content))
return {
plainText: content.join(', '),
}
return blockType === InputBlockType.FILE
? { plainText: content }
: { plainText: content.toString() }
}
export const convertResultsToTableData = (
results: ResultWithAnswers[] | undefined,
headerCells: ResultHeaderCell[],
cellParser: CellParser = defaultCellParser
): TableData[] =>
(results ?? []).map((result) => ({
id: { plainText: result.id },
date: {
plainText: convertDateToReadable(result.createdAt),
},
...[...result.answers, ...result.variables].reduce<{
[key: string]: { element?: JSX.Element; plainText: string }
}>((tableData, answerOrVariable) => {
if ('groupId' in answerOrVariable) {
const answer = answerOrVariable satisfies Answer
const header = answer.variableId
? headerCells.find((headerCell) =>
headerCell.variableIds?.includes(answer.variableId as string)
)
: headerCells.find((headerCell) =>
headerCell.blocks?.some((block) => block.id === answer.blockId)
)
if (!header || !header.blocks || !header.blockType) return tableData
const variableValue = result.variables.find(
(variable) => variable.id === answer.variableId
)?.value
const content = variableValue ?? answer.content
return {
...tableData,
[header.id]: cellParser(content, header.blockType),
}
}
const variable = answerOrVariable satisfies VariableWithValue
if (variable.value === null) return tableData
const headerId = headerCells.find((headerCell) =>
headerCell.variableIds?.includes(variable.id)
)?.id
if (!headerId) return tableData
if (isDefined(tableData[headerId])) return tableData
return {
...tableData,
[headerId]: cellParser(variable.value),
}
}, {}),
}))
const convertDateToReadable = (date: Date): string => {
const isThisYear = new Date().getFullYear() === date.getFullYear()
const dateString = date.toLocaleDateString('default', {
month: 'short',
day: 'numeric',
year: isThisYear ? undefined : 'numeric', // Only show the year if it's not the current year
})
const timeString = date.toLocaleTimeString('default', {
hour: '2-digit',
minute: '2-digit',
})
return `${dateString}, ${timeString}`
}

View File

@ -0,0 +1,17 @@
{
"name": "@typebot.io/results",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"@typebot.io/schemas": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*"
},
"devDependencies": {
"@typebot.io/tsconfig": "workspace:*"
}
}

View File

@ -0,0 +1,39 @@
import {
AnswerInSessionState,
Variable,
VariableWithValue,
} from '@typebot.io/schemas'
import { isDefined, isEmpty } from '../lib/utils'
export const parseAnswers = ({
answers,
variables: resultVariables,
}: {
answers: AnswerInSessionState[]
variables: Variable[]
}): {
[key: string]: string
} => {
const variablesWithValues = resultVariables.filter((variable) =>
isDefined(variable.value)
) as VariableWithValue[]
return {
submittedAt: new Date().toISOString(),
...[...answers, ...variablesWithValues].reduce<{
[key: string]: string
}>((o, answerOrVariable) => {
if ('id' in answerOrVariable) {
const variable = answerOrVariable
if (variable.value === null) return o
return { ...o, [variable.name]: variable.value.toString() }
}
const answer = answerOrVariable as AnswerInSessionState
if (isEmpty(answer.key)) return o
return {
...o,
[answer.key]: answer.value,
}
}, {}),
}
}

View File

@ -0,0 +1,15 @@
import { ResultHeaderCell } from '@typebot.io/schemas'
export const parseColumnsOrder = (
existingOrder: string[] | undefined,
resultHeader: ResultHeaderCell[]
) =>
existingOrder
? [
...existingOrder.slice(0, -1),
...resultHeader
.filter((header) => !existingOrder.includes(header.id))
.map((h) => h.id),
'logs',
]
: ['select', ...resultHeader.map((h) => h.id), 'logs']

View File

@ -0,0 +1,213 @@
import {
ResultWithAnswers,
ResultHeaderCell,
Group,
Variable,
InputBlock,
Typebot,
} from '@typebot.io/schemas'
import { isInputBlock } from '@typebot.io/schemas/helpers'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { byId, isNotEmpty } from '@typebot.io/lib/utils'
export const parseResultHeader = (
typebot: Pick<Typebot, 'groups' | 'variables'>,
linkedTypebots: Pick<Typebot, 'groups' | 'variables'>[] | undefined,
results?: ResultWithAnswers[]
): ResultHeaderCell[] => {
const parsedGroups = [
...typebot.groups,
...(linkedTypebots ?? []).flatMap(
(linkedTypebot) => linkedTypebot.groups as Group[]
),
]
const parsedVariables = [
...typebot.variables,
...(linkedTypebots ?? []).flatMap(
(linkedTypebot) => linkedTypebot.variables
),
]
const inputsResultHeader = parseInputsResultHeader({
groups: parsedGroups,
variables: parsedVariables,
})
return [
{ label: 'Submitted at', id: 'date' },
...inputsResultHeader,
...parseVariablesHeaders(parsedVariables, inputsResultHeader),
...parseResultsFromPreviousBotVersions(results ?? [], inputsResultHeader),
]
}
type ResultHeaderCellWithBlock = Omit<ResultHeaderCell, 'blocks'> & {
blocks: NonNullable<ResultHeaderCell['blocks']>
}
const parseInputsResultHeader = ({
groups,
variables,
}: {
groups: Group[]
variables: Variable[]
}): ResultHeaderCellWithBlock[] =>
(
groups
.flatMap((group) =>
group.blocks.map((block) => ({
...block,
groupTitle: group.title,
groupId: group.id,
}))
)
.filter((block) => isInputBlock(block)) as (InputBlock & {
groupId: string
groupTitle: string
})[]
).reduce<ResultHeaderCellWithBlock[]>((existingHeaders, inputBlock) => {
if (
existingHeaders.some(
(existingHeader) =>
inputBlock.options?.variableId &&
existingHeader.variableIds?.includes(inputBlock.options.variableId)
)
)
return existingHeaders
const matchedVariableName =
inputBlock.options?.variableId &&
variables.find(byId(inputBlock.options.variableId))?.name
let label = matchedVariableName ?? inputBlock.groupTitle
const headerWithSameLabel = existingHeaders.find((h) => h.label === label)
if (headerWithSameLabel) {
const shouldMerge = headerWithSameLabel.blocks?.some(
(block) => block.id === inputBlock.id
)
if (shouldMerge) {
const updatedHeaderCell: ResultHeaderCellWithBlock = {
...headerWithSameLabel,
variableIds:
headerWithSameLabel.variableIds && inputBlock.options?.variableId
? headerWithSameLabel.variableIds.concat([
inputBlock.options.variableId,
])
: undefined,
blocks: headerWithSameLabel.blocks.concat({
id: inputBlock.id,
groupId: inputBlock.groupId,
}),
}
return [
...existingHeaders.filter(
(existingHeader) => existingHeader.label !== label
),
updatedHeaderCell,
]
}
const totalPrevious = existingHeaders.filter((h) =>
h.label.includes(label)
).length
const newHeaderCell: ResultHeaderCellWithBlock = {
id: inputBlock.id,
label: label + ` (${totalPrevious})`,
blocks: [
{
id: inputBlock.id,
groupId: inputBlock.groupId,
},
],
blockType: inputBlock.type,
variableIds: inputBlock.options?.variableId
? [inputBlock.options.variableId]
: undefined,
}
return [...existingHeaders, newHeaderCell]
}
const newHeaderCell: ResultHeaderCellWithBlock = {
id: inputBlock.id,
label,
blocks: [
{
id: inputBlock.id,
groupId: inputBlock.groupId,
},
],
blockType: inputBlock.type,
variableIds: inputBlock.options?.variableId
? [inputBlock.options.variableId]
: undefined,
}
return [...existingHeaders, newHeaderCell]
}, [])
const parseVariablesHeaders = (
variables: Variable[],
existingInputResultHeaders: ResultHeaderCell[]
) =>
variables.reduce<ResultHeaderCell[]>((existingHeaders, variable) => {
if (
existingInputResultHeaders.some((existingInputResultHeader) =>
existingInputResultHeader.variableIds?.includes(variable.id)
)
)
return existingHeaders
const headerCellWithSameLabel = existingHeaders.find(
(existingHeader) => existingHeader.label === variable.name
)
if (headerCellWithSameLabel) {
const updatedHeaderCell: ResultHeaderCell = {
...headerCellWithSameLabel,
variableIds: headerCellWithSameLabel.variableIds?.concat([variable.id]),
}
return [
...existingHeaders.filter((h) => h.label !== variable.name),
updatedHeaderCell,
]
}
const newHeaderCell: ResultHeaderCell = {
id: variable.id,
label: variable.name,
variableIds: [variable.id],
}
return [...existingHeaders, newHeaderCell]
}, [])
const parseResultsFromPreviousBotVersions = (
results: ResultWithAnswers[],
existingInputResultHeaders: ResultHeaderCell[]
): ResultHeaderCell[] =>
results
.flatMap((result) => result.answers)
.filter(
(answer) =>
!answer.variableId &&
existingInputResultHeaders.every(
(header) => header.id !== answer.blockId
) &&
isNotEmpty(answer.content)
)
.reduce<ResultHeaderCell[]>((existingHeaders, answer) => {
if (
existingHeaders.some(
(existingHeader) => existingHeader.id === answer.blockId
)
)
return existingHeaders
return [
...existingHeaders,
{
id: answer.blockId,
label: `${answer.blockId} (deleted block)`,
blocks: [
{
id: answer.blockId,
groupId: answer.groupId,
},
],
blockType: InputBlockType.TEXT,
},
]
}, [])

View File

@ -0,0 +1,8 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["ES2021", "DOM"]
}
}