2
0

🐛 (webhook) Fix parent linked typebot data parsing in webhook

This commit is contained in:
Baptiste Arnaud
2022-12-22 11:49:46 +01:00
parent d1b5b6ebe6
commit c3985b0d50
15 changed files with 166 additions and 75 deletions

View File

@@ -1,4 +1,3 @@
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api'
import { ExecuteIntegrationResponse } from '@/features/chat' import { ExecuteIntegrationResponse } from '@/features/chat'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api' import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { parseVariables } from '@/features/variables' import { parseVariables } from '@/features/variables'
@@ -183,13 +182,12 @@ const getEmailBody = async ({
where: { typebotId }, where: { typebotId },
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const linkedTypebots = await getLinkedTypebots(typebot)
const resultValues = (await prisma.result.findUnique({ const resultValues = (await prisma.result.findUnique({
where: { id: resultId }, where: { id: resultId },
include: { answers: true }, include: { answers: true },
})) as ResultValues | null })) as ResultValues | null
if (!resultValues) return if (!resultValues) return
const answers = parseAnswers(typebot, linkedTypebots)(resultValues) const answers = parseAnswers(typebot, [])(resultValues)
return { return {
html: render( html: render(
<DefaultBotNotificationEmail <DefaultBotNotificationEmail

View File

@@ -24,7 +24,6 @@ import { stringify } from 'qs'
import { byId, omit, parseAnswers } from 'utils' import { byId, omit, parseAnswers } from 'utils'
import got, { Method, Headers, HTTPError } from 'got' import got, { Method, Headers, HTTPError } from 'got'
import { getResultValues } from '@/features/results/api' import { getResultValues } from '@/features/results/api'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api'
import { parseSampleResult } from './parseSampleResult' import { parseSampleResult } from './parseSampleResult'
export const executeWebhookBlock = async ( export const executeWebhookBlock = async (
@@ -45,7 +44,7 @@ export const executeWebhookBlock = async (
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const resultValues = await getResultValues(result.id) const resultValues = await getResultValues(result.id)
if (!resultValues) return { outgoingEdgeId: block.outgoingEdgeId } if (!resultValues) return { outgoingEdgeId: block.outgoingEdgeId }
const webhookResponse = await executeWebhook(typebot)( const webhookResponse = await executeWebhook({ typebot })(
preparedWebhook, preparedWebhook,
typebot.variables, typebot.variables,
block.groupId, block.groupId,
@@ -112,7 +111,7 @@ const prepareWebhookAttributes = (
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body) const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook = export const executeWebhook =
(typebot: SessionState['typebot']) => ({ typebot }: Pick<SessionState, 'typebot'>) =>
async ( async (
webhook: Webhook, webhook: Webhook,
variables: Variable[], variables: Variable[],
@@ -148,11 +147,10 @@ export const executeWebhook =
convertKeyValueTableToObject(webhook.queryParams, variables) convertKeyValueTableToObject(webhook.queryParams, variables)
) )
const contentType = headers ? headers['Content-Type'] : undefined const contentType = headers ? headers['Content-Type'] : undefined
const linkedTypebots = await getLinkedTypebots(typebot)
const bodyContent = await getBodyContent( const bodyContent = await getBodyContent(
typebot, typebot,
linkedTypebots []
)({ )({
body: webhook.body, body: webhook.body,
resultValues, resultValues,

View File

@@ -1,41 +1,20 @@
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/utils/api/dbRules' import { PublicTypebot, Typebot } from 'models'
import { User } from 'db'
import {
LogicBlockType,
PublicTypebot,
Typebot,
TypebotLinkBlock,
} from 'models'
import { isDefined } from 'utils'
export const getLinkedTypebots = async ( type Props = {
typebot: Pick<PublicTypebot, 'groups'>, isPreview: boolean
user?: User typebotIds: string[]
): Promise<(Typebot | PublicTypebot)[]> => { }
const linkedTypebotIds = (
typebot.groups export const getLinkedTypebots = async ({ isPreview, typebotIds }: Props) => {
.flatMap((g) => g.blocks) const linkedTypebots = (
.filter( isPreview
(s) => ? await prisma.typebot.findMany({
s.type === LogicBlockType.TYPEBOT_LINK && where: { id: { in: typebotIds } },
isDefined(s.options.typebotId) })
) as TypebotLinkBlock[] : await prisma.publicTypebot.findMany({
).map((s) => s.options.typebotId as string) where: { id: { in: typebotIds } },
if (linkedTypebotIds.length === 0) return [] })
const typebots = (await ('typebotId' in typebot ) as (Typebot | PublicTypebot)[]
? prisma.publicTypebot.findMany({ return linkedTypebots
where: { id: { in: linkedTypebotIds } },
})
: prisma.typebot.findMany({
where: user
? {
AND: [
{ id: { in: linkedTypebotIds } },
canReadTypebots(linkedTypebotIds, user as User),
],
}
: { id: { in: linkedTypebotIds } },
}))) as unknown as (Typebot | PublicTypebot)[]
return typebots
} }

View File

@@ -0,0 +1,63 @@
import prisma from '@/lib/prisma'
import { canReadTypebots } from '@/utils/api/dbRules'
import { User } from 'db'
import {
LogicBlockType,
PublicTypebot,
Typebot,
TypebotLinkBlock,
} from 'models'
import { isDefined } from 'utils'
type Props = {
typebots: Pick<PublicTypebot, 'groups'>[]
user?: User
isPreview?: boolean
}
export const getLinkedTypebotsChildren =
({ typebots, user, isPreview }: Props) =>
async (
capturedLinkedBots: (Typebot | PublicTypebot)[]
): Promise<(Typebot | PublicTypebot)[]> => {
const linkedTypebotIds = typebots
.flatMap((typebot) =>
(
typebot.groups
.flatMap((group) => group.blocks)
.filter(
(block) =>
block.type === LogicBlockType.TYPEBOT_LINK &&
isDefined(block.options.typebotId) &&
!capturedLinkedBots.some(
(bot) =>
('typebotId' in bot ? bot.typebotId : bot.id) ===
block.options.typebotId
)
) as TypebotLinkBlock[]
).map((s) => s.options.typebotId)
)
.filter(isDefined)
if (linkedTypebotIds.length === 0) return capturedLinkedBots
const linkedTypebots = (
isPreview
? await prisma.typebot.findMany({
where: user
? {
AND: [
{ id: { in: linkedTypebotIds } },
canReadTypebots(linkedTypebotIds, user as User),
],
}
: { id: { in: linkedTypebotIds } },
})
: await prisma.publicTypebot.findMany({
where: { id: { in: linkedTypebotIds } },
})
) as (Typebot | PublicTypebot)[]
return getLinkedTypebotsChildren({
typebots: linkedTypebots,
user,
isPreview,
})([...capturedLinkedBots, ...linkedTypebots])
}

View File

@@ -1,2 +1,3 @@
export * from './executeTypebotLink' export * from './executeTypebotLink'
export * from './getLinkedTypebots' export * from './getLinkedTypebots'
export * from './getLinkedTypebotsChildren'

View File

@@ -21,7 +21,10 @@ import Cors from 'cors'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { saveErrorLog, saveSuccessLog } from '@/features/logs/api' import { saveErrorLog, saveSuccessLog } from '@/features/logs/api'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/api' import { parseSampleResult } from '@/features/blocks/integrations/webhook/api'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api' import {
getLinkedTypebots,
getLinkedTypebotsChildren,
} from '@/features/blocks/logic/typebotLink/api'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
@@ -31,11 +34,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const typebotId = req.query.typebotId as string const typebotId = req.query.typebotId as string
const blockId = req.query.blockId as string const blockId = req.query.blockId as string
const resultId = req.query.resultId as string | undefined const resultId = req.query.resultId as string | undefined
const { resultValues, variables } = ( const { resultValues, variables, parentTypebotIds } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as { ) as {
resultValues: ResultValues | undefined resultValues: ResultValues | undefined
variables: Variable[] variables: Variable[]
parentTypebotIds: string[]
} }
const typebot = (await prisma.typebot.findUnique({ const typebot = (await prisma.typebot.findUnique({
where: { id: typebotId }, where: { id: typebotId },
@@ -51,13 +55,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
.status(404) .status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) .send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const result = await executeWebhook(typebot)( const result = await executeWebhook(typebot)({
preparedWebhook, webhook: preparedWebhook,
variables, variables,
block.groupId, groupId: block.groupId,
resultValues, resultValues,
resultId resultId,
) parentTypebotIds,
})
return res.status(200).send(result) return res.status(200).send(result)
} }
return methodNotAllowed(res) return methodNotAllowed(res)
@@ -79,13 +84,21 @@ const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
export const executeWebhook = export const executeWebhook =
(typebot: Typebot) => (typebot: Typebot) =>
async ( async ({
webhook: Webhook, webhook,
variables: Variable[], variables,
groupId: string, groupId,
resultValues?: ResultValues, resultValues,
resultId,
parentTypebotIds = [],
}: {
webhook: Webhook
variables: Variable[]
groupId: string
resultValues?: ResultValues
resultId?: string resultId?: string
): Promise<WebhookResponse> => { parentTypebotIds: string[]
}): Promise<WebhookResponse> => {
if (!webhook.url || !webhook.method) if (!webhook.url || !webhook.method)
return { return {
statusCode: 400, statusCode: 400,
@@ -114,11 +127,18 @@ export const executeWebhook =
convertKeyValueTableToObject(webhook.queryParams, variables) convertKeyValueTableToObject(webhook.queryParams, variables)
) )
const contentType = headers ? headers['Content-Type'] : undefined const contentType = headers ? headers['Content-Type'] : undefined
const linkedTypebots = await getLinkedTypebots(typebot) const linkedTypebotsParents = await getLinkedTypebots({
const bodyContent = await getBodyContent( isPreview: !('typebotId' in typebot),
typebot, typebotIds: parentTypebotIds,
linkedTypebots })
)({ const linkedTypebotsChildren = await getLinkedTypebotsChildren({
isPreview: !('typebotId' in typebot),
typebots: [typebot],
})([])
const bodyContent = await getBodyContent(typebot, [
...linkedTypebotsParents,
...linkedTypebotsChildren,
])({
body: webhook.body, body: webhook.body,
resultValues, resultValues,
groupId, groupId,

View File

@@ -1,5 +1,5 @@
import { authenticateUser } from '@/features/auth/api' import { authenticateUser } from '@/features/auth/api'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api' import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/api'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/api' import { parseSampleResult } from '@/features/blocks/integrations/webhook/api'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Typebot } from 'models' import { Typebot } from 'models'
@@ -23,7 +23,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
.flatMap((g) => g.blocks) .flatMap((g) => g.blocks)
.find((s) => s.id === blockId) .find((s) => s.id === blockId)
if (!block) return res.status(404).send({ message: 'Group not found' }) if (!block) return res.status(404).send({ message: 'Group not found' })
const linkedTypebots = await getLinkedTypebots(typebot, user) const linkedTypebots = await getLinkedTypebotsChildren({
isPreview: true,
typebots: [typebot],
user,
})([])
return res.send( return res.send(
await parseSampleResult(typebot, linkedTypebots)(block.groupId) await parseSampleResult(typebot, linkedTypebots)(block.groupId)
) )

View File

@@ -23,11 +23,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const groupId = req.query.groupId as string const groupId = req.query.groupId as string
const blockId = req.query.blockId as string const blockId = req.query.blockId as string
const resultId = req.query.resultId as string | undefined const resultId = req.query.resultId as string | undefined
const { resultValues, variables } = ( const { resultValues, variables, parentTypebotIds } = (
typeof req.body === 'string' ? JSON.parse(req.body) : req.body typeof req.body === 'string' ? JSON.parse(req.body) : req.body
) as { ) as {
resultValues: ResultValues resultValues: ResultValues
variables: Variable[] variables: Variable[]
parentTypebotIds: string[]
} }
const typebot = (await prisma.typebot.findUnique({ const typebot = (await prisma.typebot.findUnique({
where: { id: typebotId }, where: { id: typebotId },
@@ -43,13 +44,14 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
.status(404) .status(404)
.send({ statusCode: 404, data: { message: `Couldn't find webhook` } }) .send({ statusCode: 404, data: { message: `Couldn't find webhook` } })
const preparedWebhook = prepareWebhookAttributes(webhook, block.options) const preparedWebhook = prepareWebhookAttributes(webhook, block.options)
const result = await executeWebhook(typebot)( const result = await executeWebhook(typebot)({
preparedWebhook, webhook: preparedWebhook,
variables, variables,
groupId, groupId,
resultValues, resultValues,
resultId resultId,
) parentTypebotIds,
})
return res.status(200).send(result) return res.status(200).send(result)
} }
return methodNotAllowed(res) return methodNotAllowed(res)

View File

@@ -1,5 +1,5 @@
import { authenticateUser } from '@/features/auth/api' import { authenticateUser } from '@/features/auth/api'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api' import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/api'
import { parseSampleResult } from '@/features/blocks/integrations/webhook/api' import { parseSampleResult } from '@/features/blocks/integrations/webhook/api'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { Typebot } from 'models' import { Typebot } from 'models'
@@ -19,7 +19,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}, },
})) as unknown as Typebot | undefined })) as unknown as Typebot | undefined
if (!typebot) return res.status(400).send({ message: 'Typebot not found' }) if (!typebot) return res.status(400).send({ message: 'Typebot not found' })
const linkedTypebots = await getLinkedTypebots(typebot, user) const linkedTypebots = await getLinkedTypebotsChildren({
isPreview: true,
typebots: [typebot],
user,
})([])
return res.send(await parseSampleResult(typebot, linkedTypebots)(groupId)) return res.send(await parseSampleResult(typebot, linkedTypebots)(groupId))
} }
methodNotAllowed(res) methodNotAllowed(res)

View File

@@ -15,7 +15,7 @@ import Mail from 'nodemailer/lib/mailer'
import { DefaultBotNotificationEmail } from 'emails' import { DefaultBotNotificationEmail } from 'emails'
import { render } from '@faire/mjml-react/dist/src/utils/render' import { render } from '@faire/mjml-react/dist/src/utils/render'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { getLinkedTypebots } from '@/features/blocks/logic/typebotLink/api' import { getLinkedTypebotsChildren } from '@/features/blocks/logic/typebotLink/api'
const cors = initMiddleware(Cors()) const cors = initMiddleware(Cors())
@@ -189,7 +189,9 @@ const getEmailBody = async ({
where: { typebotId }, where: { typebotId },
})) as unknown as PublicTypebot })) as unknown as PublicTypebot
if (!typebot) return if (!typebot) return
const linkedTypebots = await getLinkedTypebots(typebot) const linkedTypebots = await getLinkedTypebotsChildren({
typebots: [typebot],
})([])
const answers = parseAnswers(typebot, linkedTypebots)(resultValues) const answers = parseAnswers(typebot, linkedTypebots)(resultValues)
return { return {
html: render( html: render(

View File

@@ -60,11 +60,13 @@ export const ChatGroup = ({
createEdge, createEdge,
apiHost, apiHost,
isPreview, isPreview,
parentTypebotIds,
onNewLog, onNewLog,
injectLinkedTypebot, injectLinkedTypebot,
linkedTypebots, linkedTypebots,
setCurrentTypebotId, setCurrentTypebotId,
pushEdgeIdInLinkedTypebotQueue, pushEdgeIdInLinkedTypebotQueue,
pushParentTypebotId,
} = useTypebot() } = useTypebot()
const { resultValues, updateVariables, resultId } = useAnswers() const { resultValues, updateVariables, resultId } = useAnswers()
const { scroll } = useChat() const { scroll } = useChat()
@@ -131,6 +133,7 @@ export const ChatGroup = ({
setCurrentTypebotId, setCurrentTypebotId,
pushEdgeIdInLinkedTypebotQueue, pushEdgeIdInLinkedTypebotQueue,
currentTypebotId, currentTypebotId,
pushParentTypebotId,
}) })
const isRedirecting = const isRedirecting =
currentBlock.type === LogicBlockType.REDIRECT && currentBlock.type === LogicBlockType.REDIRECT &&
@@ -156,6 +159,7 @@ export const ChatGroup = ({
groups: typebot.groups, groups: typebot.groups,
onNewLog, onNewLog,
resultId, resultId,
parentTypebotIds,
}, },
}) })
nextEdgeId ? onGroupEnd({ edgeId: nextEdgeId }) : displayNextBlock() nextEdgeId ? onGroupEnd({ edgeId: nextEdgeId }) : displayNextBlock()

View File

@@ -22,6 +22,7 @@ export const executeWebhook = async (
resultValues, resultValues,
onNewLog, onNewLog,
resultId, resultId,
parentTypebotIds,
}: IntegrationState }: IntegrationState
) => { ) => {
const params = stringify({ resultId }) const params = stringify({ resultId })
@@ -31,6 +32,7 @@ export const executeWebhook = async (
body: { body: {
variables, variables,
resultValues, resultValues,
parentTypebotIds,
}, },
}) })
const statusCode = ( const statusCode = (

View File

@@ -18,6 +18,7 @@ export const executeTypebotLink = async (
createEdge, createEdge,
setCurrentTypebotId, setCurrentTypebotId,
pushEdgeIdInLinkedTypebotQueue, pushEdgeIdInLinkedTypebotQueue,
pushParentTypebotId,
currentTypebotId, currentTypebotId,
} = context } = context
const linkedTypebot = ( const linkedTypebot = (
@@ -42,6 +43,7 @@ export const executeTypebotLink = async (
edgeId: block.outgoingEdgeId, edgeId: block.outgoingEdgeId,
typebotId: currentTypebotId, typebotId: currentTypebotId,
}) })
pushParentTypebotId(currentTypebotId)
setCurrentTypebotId( setCurrentTypebotId(
'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id 'typebotId' in linkedTypebot ? linkedTypebot.typebotId : linkedTypebot.id
) )

View File

@@ -30,10 +30,12 @@ const typebotContext = createContext<{
isPreview: boolean isPreview: boolean
linkedBotQueue: LinkedTypebotQueue linkedBotQueue: LinkedTypebotQueue
isLoading: boolean isLoading: boolean
parentTypebotIds: string[]
setCurrentTypebotId: (id: string) => void setCurrentTypebotId: (id: string) => void
updateVariableValue: (variableId: string, value: unknown) => void updateVariableValue: (variableId: string, value: unknown) => void
createEdge: (edge: Edge) => void createEdge: (edge: Edge) => void
injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot injectLinkedTypebot: (typebot: Typebot | PublicTypebot) => LinkedTypebot
pushParentTypebotId: (typebotId: string) => void
popEdgeIdFromLinkedTypebotQueue: () => void popEdgeIdFromLinkedTypebotQueue: () => void
pushEdgeIdInLinkedTypebotQueue: (bot: { pushEdgeIdInLinkedTypebotQueue: (bot: {
typebotId: string typebotId: string
@@ -63,6 +65,7 @@ export const TypebotProvider = ({
const [linkedTypebots, setLinkedTypebots] = useState<LinkedTypebot[]>([]) const [linkedTypebots, setLinkedTypebots] = useState<LinkedTypebot[]>([])
const [currentTypebotId, setCurrentTypebotId] = useState(typebot.typebotId) const [currentTypebotId, setCurrentTypebotId] = useState(typebot.typebotId)
const [linkedBotQueue, setLinkedBotQueue] = useState<LinkedTypebotQueue>([]) const [linkedBotQueue, setLinkedBotQueue] = useState<LinkedTypebotQueue>([])
const [parentTypebotIds, setParentTypebotIds] = useState<string[]>([])
useEffect(() => { useEffect(() => {
setLocalTypebot((localTypebot) => ({ setLocalTypebot((localTypebot) => ({
@@ -149,6 +152,10 @@ export const TypebotProvider = ({
} }
}) })
const pushParentTypebotId = (typebotId: string) => {
setParentTypebotIds((ids) => [...ids, typebotId])
}
const pushEdgeIdInLinkedTypebotQueue = (bot: { const pushEdgeIdInLinkedTypebotQueue = (bot: {
typebotId: string typebotId: string
edgeId: string edgeId: string
@@ -156,6 +163,7 @@ export const TypebotProvider = ({
const popEdgeIdFromLinkedTypebotQueue = () => { const popEdgeIdFromLinkedTypebotQueue = () => {
setLinkedBotQueue((queue) => queue.slice(1)) setLinkedBotQueue((queue) => queue.slice(1))
setParentTypebotIds((ids) => ids.slice(1))
setCurrentTypebotId(linkedBotQueue[0].typebotId) setCurrentTypebotId(linkedBotQueue[0].typebotId)
} }
@@ -172,6 +180,8 @@ export const TypebotProvider = ({
onNewLog, onNewLog,
linkedBotQueue, linkedBotQueue,
isLoading, isLoading,
parentTypebotIds,
pushParentTypebotId,
pushEdgeIdInLinkedTypebotQueue, pushEdgeIdInLinkedTypebotQueue,
popEdgeIdFromLinkedTypebotQueue, popEdgeIdFromLinkedTypebotQueue,
currentTypebotId, currentTypebotId,

View File

@@ -25,6 +25,7 @@ export type LogicState = {
typebot: TypebotViewerProps['typebot'] typebot: TypebotViewerProps['typebot']
linkedTypebots: LinkedTypebot[] linkedTypebots: LinkedTypebot[]
currentTypebotId: string currentTypebotId: string
pushParentTypebotId: (id: string) => void
pushEdgeIdInLinkedTypebotQueue: (bot: { pushEdgeIdInLinkedTypebotQueue: (bot: {
edgeId: string edgeId: string
typebotId: string typebotId: string
@@ -47,6 +48,7 @@ export type IntegrationState = {
resultValues: ResultValues resultValues: ResultValues
groups: Group[] groups: Group[]
resultId?: string resultId?: string
parentTypebotIds: string[]
updateVariables: (variables: VariableWithUnknowValue[]) => void updateVariables: (variables: VariableWithUnknowValue[]) => void
updateVariableValue: (variableId: string, value: unknown) => void updateVariableValue: (variableId: string, value: unknown) => void
onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void onNewLog: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void