feat(bot): ⚡️ Add custom file upload size limit
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { FormLabel, Stack } from '@chakra-ui/react'
|
import { FormLabel, Stack } from '@chakra-ui/react'
|
||||||
import { CodeEditor } from 'components/shared/CodeEditor'
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
|
import { SmartNumberInput } from 'components/shared/SmartNumberInput'
|
||||||
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||||
import { Input } from 'components/shared/Textbox'
|
import { Input } from 'components/shared/Textbox'
|
||||||
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
import { VariableSearchInput } from 'components/shared/VariableSearchInput'
|
||||||
@@ -20,7 +21,8 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
onOptionsChange({ ...options, isMultipleAllowed })
|
onOptionsChange({ ...options, isMultipleAllowed })
|
||||||
const handleVariableChange = (variable?: Variable) =>
|
const handleVariableChange = (variable?: Variable) =>
|
||||||
onOptionsChange({ ...options, variableId: variable?.id })
|
onOptionsChange({ ...options, variableId: variable?.id })
|
||||||
|
const handleSizeLimitChange = (sizeLimit?: number) =>
|
||||||
|
onOptionsChange({ ...options, sizeLimit })
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<SwitchWithLabel
|
<SwitchWithLabel
|
||||||
@@ -29,6 +31,16 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
initialValue={options.isMultipleAllowed}
|
initialValue={options.isMultipleAllowed}
|
||||||
onCheckChange={handleLongChange}
|
onCheckChange={handleLongChange}
|
||||||
/>
|
/>
|
||||||
|
<Stack>
|
||||||
|
<FormLabel mb="0" htmlFor="limit">
|
||||||
|
Size limit (MB):
|
||||||
|
</FormLabel>
|
||||||
|
<SmartNumberInput
|
||||||
|
id="limit"
|
||||||
|
value={options.sizeLimit ?? 10}
|
||||||
|
onValueChange={handleSizeLimitChange}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<Stack>
|
<Stack>
|
||||||
<FormLabel mb="0">Placeholder:</FormLabel>
|
<FormLabel mb="0">Placeholder:</FormLabel>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ test('options should work', async ({ page }) => {
|
|||||||
await page.click('text="Allow multiple files?"')
|
await page.click('text="Allow multiple files?"')
|
||||||
await page.fill('div[contenteditable=true]', '<strong>Upload now!!</strong>')
|
await page.fill('div[contenteditable=true]', '<strong>Upload now!!</strong>')
|
||||||
await page.fill('[value="Upload"]', 'Go')
|
await page.fill('[value="Upload"]', 'Go')
|
||||||
|
await page.fill('input[value="10"]', '20')
|
||||||
await page.click('text="Restart"')
|
await page.click('text="Restart"')
|
||||||
await expect(typebotViewer(page).locator(`text="Upload now!!"`)).toBeVisible()
|
await expect(typebotViewer(page).locator(`text="Upload now!!"`)).toBeVisible()
|
||||||
await typebotViewer(page)
|
await typebotViewer(page)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const TypebotPage = ({
|
|||||||
const [variableUpdateQueue, setVariableUpdateQueue] = useState<
|
const [variableUpdateQueue, setVariableUpdateQueue] = useState<
|
||||||
VariableWithValue[][]
|
VariableWithValue[][]
|
||||||
>([])
|
>([])
|
||||||
|
const [chatStarted, setChatStarted] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowTypebot(true)
|
setShowTypebot(true)
|
||||||
@@ -94,10 +95,16 @@ export const TypebotPage = ({
|
|||||||
if (error) setError(error)
|
if (error) setError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNewAnswer = async (answer: Answer) => {
|
const handleNewAnswer = async (
|
||||||
|
answer: Answer & { uploadedFiles: boolean }
|
||||||
|
) => {
|
||||||
if (!resultId) return setError(new Error('Result was not created'))
|
if (!resultId) return setError(new Error('Result was not created'))
|
||||||
const { error } = await upsertAnswer({ ...answer, resultId })
|
const { error } = await upsertAnswer({ ...answer, resultId })
|
||||||
if (error) setError(error)
|
if (error) setError(error)
|
||||||
|
if (chatStarted) return
|
||||||
|
updateResult(resultId, {
|
||||||
|
hasStarted: true,
|
||||||
|
}).then(({ error }) => (error ? setError(error) : setChatStarted(true)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCompleted = async () => {
|
const handleCompleted = async () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
|
import prisma from 'libs/prisma'
|
||||||
|
import { InputBlockType, PublicTypebot } from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { badRequest, generatePresignedUrl, methodNotAllowed } from 'utils'
|
import { badRequest, generatePresignedUrl, methodNotAllowed, byId } from 'utils'
|
||||||
|
|
||||||
const handler = async (
|
const handler = async (
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
@@ -19,8 +21,25 @@ const handler = async (
|
|||||||
)
|
)
|
||||||
const filePath = req.query.filePath as string | undefined
|
const filePath = req.query.filePath as string | undefined
|
||||||
const fileType = req.query.fileType as string | undefined
|
const fileType = req.query.fileType as string | undefined
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
|
const blockId = req.query.blockId as string
|
||||||
if (!filePath || !fileType) return badRequest(res)
|
if (!filePath || !fileType) return badRequest(res)
|
||||||
const presignedUrl = generatePresignedUrl({ fileType, filePath })
|
const typebot = (await prisma.publicTypebot.findFirst({
|
||||||
|
where: { typebotId },
|
||||||
|
})) as unknown as PublicTypebot
|
||||||
|
const fileUploadBlock = typebot.groups
|
||||||
|
.flatMap((g) => g.blocks)
|
||||||
|
.find(byId(blockId))
|
||||||
|
if (fileUploadBlock?.type !== InputBlockType.FILE) return badRequest(res)
|
||||||
|
const sizeLimit = fileUploadBlock.options.sizeLimit
|
||||||
|
? Math.min(fileUploadBlock.options.sizeLimit, 500)
|
||||||
|
: 10
|
||||||
|
|
||||||
|
const presignedUrl = generatePresignedUrl({
|
||||||
|
fileType,
|
||||||
|
filePath,
|
||||||
|
sizeLimit: sizeLimit * 1024 * 1024,
|
||||||
|
})
|
||||||
|
|
||||||
return res.status(200).send({ presignedUrl })
|
return res.status(200).send({ presignedUrl })
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const typebotId = req.query.typebotId as string
|
const typebotId = req.query.typebotId as string
|
||||||
const result = await prisma.result.create({
|
const result = await prisma.result.create({
|
||||||
data: { typebotId, isCompleted: false },
|
data: {
|
||||||
|
typebotId,
|
||||||
|
isCompleted: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return res.send(result)
|
return res.send(result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { Answer } from 'db'
|
import { Answer } from 'db'
|
||||||
|
import { got } from 'got'
|
||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { methodNotAllowed } from 'utils'
|
import { isNotDefined, methodNotAllowed } from 'utils'
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
if (req.method === 'PUT') {
|
if (req.method === 'PUT') {
|
||||||
const answer = (
|
const { uploadedFiles, ...answer } = (
|
||||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
) as Answer
|
) as Answer & { uploadedFiles?: boolean }
|
||||||
|
let storageUsed = 0
|
||||||
|
if (uploadedFiles && answer.content.includes('http')) {
|
||||||
|
const fileUrls = answer.content.split(', ')
|
||||||
|
for (const url of fileUrls) {
|
||||||
|
const { headers } = await got(url)
|
||||||
|
const size = headers['content-length']
|
||||||
|
if (isNotDefined(size)) return
|
||||||
|
storageUsed += parseInt(size, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
const result = await prisma.answer.upsert({
|
const result = await prisma.answer.upsert({
|
||||||
where: {
|
where: {
|
||||||
resultId_blockId_groupId: {
|
resultId_blockId_groupId: {
|
||||||
@@ -17,8 +28,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
blockId: answer.blockId,
|
blockId: answer.blockId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: answer,
|
create: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||||
update: answer,
|
update: { ...answer, storageUsed: storageUsed > 0 ? storageUsed : null },
|
||||||
})
|
})
|
||||||
return res.send(result)
|
return res.send(result)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Answer } from 'models'
|
import { Answer } from 'models'
|
||||||
import { sendRequest } from 'utils'
|
import { sendRequest } from 'utils'
|
||||||
|
|
||||||
export const upsertAnswer = async (answer: Answer & { resultId: string }) =>
|
export const upsertAnswer = async (
|
||||||
|
answer: Answer & { resultId: string } & { uploadedFiles?: boolean }
|
||||||
|
) =>
|
||||||
sendRequest<Answer>({
|
sendRequest<Answer>({
|
||||||
url: `/api/typebots/t/results/r/answers`,
|
url: `/api/typebots/t/results/r/answers`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const InputChatBlock = ({
|
|||||||
groupId: block.groupId,
|
groupId: block.groupId,
|
||||||
content: value,
|
content: value,
|
||||||
variableId: variableId ?? null,
|
variableId: variableId ?? null,
|
||||||
|
uploadedFiles: block.type === InputBlockType.FILE,
|
||||||
})
|
})
|
||||||
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
|
if (!isEditting) onTransitionEnd({ label, value, itemId }, isRetry)
|
||||||
setIsEditting(false)
|
setIsEditting(false)
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ type Props = {
|
|||||||
onSubmit: (url: InputSubmitContent) => void
|
onSubmit: (url: InputSubmitContent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenMB = 10 * 1024 * 1024
|
|
||||||
export const FileUploadForm = ({
|
export const FileUploadForm = ({
|
||||||
block: {
|
block: {
|
||||||
id,
|
id,
|
||||||
options: { isMultipleAllowed, labels },
|
options: { isMultipleAllowed, labels, sizeLimit },
|
||||||
},
|
},
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { isPreview } = useTypebot()
|
const {
|
||||||
|
isPreview,
|
||||||
|
typebot: { typebotId },
|
||||||
|
} = useTypebot()
|
||||||
const { resultId } = useAnswers()
|
const { resultId } = useAnswers()
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([])
|
||||||
const [isUploading, setIsUploading] = useState(false)
|
const [isUploading, setIsUploading] = useState(false)
|
||||||
@@ -35,8 +37,8 @@ export const FileUploadForm = ({
|
|||||||
const onNewFiles = (files: FileList) => {
|
const onNewFiles = (files: FileList) => {
|
||||||
setErrorMessage(undefined)
|
setErrorMessage(undefined)
|
||||||
const newFiles = Array.from(files)
|
const newFiles = Array.from(files)
|
||||||
if (newFiles.some((file) => file.size > tenMB))
|
if (newFiles.some((file) => file.size > (sizeLimit ?? 10) * 1024 * 1024))
|
||||||
return setErrorMessage('A file is larger than 10MB')
|
return setErrorMessage(`A file is larger than ${sizeLimit ?? 10}MB`)
|
||||||
if (!isMultipleAllowed && files) return startSingleFileUpload(newFiles[0])
|
if (!isMultipleAllowed && files) return startSingleFileUpload(newFiles[0])
|
||||||
setSelectedFiles([...selectedFiles, ...newFiles])
|
setSelectedFiles([...selectedFiles, ...newFiles])
|
||||||
}
|
}
|
||||||
@@ -55,6 +57,7 @@ export const FileUploadForm = ({
|
|||||||
})
|
})
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
|
basePath: `/api/typebots/${typebotId}/blocks/${id}`,
|
||||||
files: [
|
files: [
|
||||||
{
|
{
|
||||||
file,
|
file,
|
||||||
@@ -76,6 +79,7 @@ export const FileUploadForm = ({
|
|||||||
})
|
})
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
const urls = await uploadFiles({
|
const urls = await uploadFiles({
|
||||||
|
basePath: `/api/typebots/${typebotId}/blocks/${id}`,
|
||||||
files: files.map((file) => ({
|
files: files.map((file) => ({
|
||||||
file: file,
|
file: file,
|
||||||
path: `public/results/${resultId}/${id}/${file.name}`,
|
path: `public/results/${resultId}/${id}/${file.name}`,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type TypebotViewerProps = {
|
|||||||
startGroupId?: string
|
startGroupId?: string
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
onNewGroupVisible?: (edge: Edge) => void
|
onNewGroupVisible?: (edge: Edge) => void
|
||||||
onNewAnswer?: (answer: Answer) => Promise<void>
|
onNewAnswer?: (answer: Answer & { uploadedFiles: boolean }) => Promise<void>
|
||||||
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
onNewLog?: (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) => void
|
||||||
onCompleted?: () => void
|
onCompleted?: () => void
|
||||||
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
||||||
@@ -64,7 +64,8 @@ export const TypebotViewer = ({
|
|||||||
const handleNewGroupVisible = (edge: Edge) =>
|
const handleNewGroupVisible = (edge: Edge) =>
|
||||||
onNewGroupVisible && onNewGroupVisible(edge)
|
onNewGroupVisible && onNewGroupVisible(edge)
|
||||||
|
|
||||||
const handleNewAnswer = (answer: Answer) => onNewAnswer && onNewAnswer(answer)
|
const handleNewAnswer = (answer: Answer & { uploadedFiles: boolean }) =>
|
||||||
|
onNewAnswer && onNewAnswer(answer)
|
||||||
|
|
||||||
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
const handleNewLog = (log: Omit<Log, 'id' | 'createdAt' | 'resultId'>) =>
|
||||||
onNewLog && onNewLog(log)
|
onNewLog && onNewLog(log)
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import React, { createContext, ReactNode, useContext, useState } from 'react'
|
|||||||
const answersContext = createContext<{
|
const answersContext = createContext<{
|
||||||
resultId?: string
|
resultId?: string
|
||||||
resultValues: ResultValues
|
resultValues: ResultValues
|
||||||
addAnswer: (answer: Answer) => Promise<void> | undefined
|
addAnswer: (
|
||||||
|
answer: Answer & { uploadedFiles: boolean }
|
||||||
|
) => Promise<void> | undefined
|
||||||
updateVariables: (variables: VariableWithValue[]) => void
|
updateVariables: (variables: VariableWithValue[]) => void
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -17,7 +19,9 @@ export const AnswersContext = ({
|
|||||||
onVariablesUpdated,
|
onVariablesUpdated,
|
||||||
}: {
|
}: {
|
||||||
resultId?: string
|
resultId?: string
|
||||||
onNewAnswer: (answer: Answer) => Promise<void> | undefined
|
onNewAnswer: (
|
||||||
|
answer: Answer & { uploadedFiles: boolean }
|
||||||
|
) => Promise<void> | undefined
|
||||||
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
onVariablesUpdated?: (variables: VariableWithValue[]) => void
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
@@ -27,7 +31,7 @@ export const AnswersContext = ({
|
|||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const addAnswer = (answer: Answer) => {
|
const addAnswer = (answer: Answer & { uploadedFiles: boolean }) => {
|
||||||
setResultValues((resultValues) => ({
|
setResultValues((resultValues) => ({
|
||||||
...resultValues,
|
...resultValues,
|
||||||
answers: [...resultValues.answers, answer],
|
answers: [...resultValues.answers, answer],
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `groups` on table `PublicTypebot` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `edges` on table `PublicTypebot` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `groups` on table `Typebot` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
- Made the column `edges` on table `Typebot` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Answer" ADD COLUMN "storageUsed" INTEGER;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "PublicTypebot" ALTER COLUMN "groups" SET NOT NULL,
|
||||||
|
ALTER COLUMN "edges" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Result" ADD COLUMN "hasStarted" BOOLEAN;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ALTER COLUMN "groups" SET NOT NULL,
|
||||||
|
ALTER COLUMN "edges" SET NOT NULL;
|
||||||
@@ -112,15 +112,15 @@ enum GraphNavigation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model CustomDomain {
|
model CustomDomain {
|
||||||
name String @id
|
name String @id
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
workspaceId String
|
workspaceId String
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Credentials {
|
model Credentials {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
workspaceId String
|
workspaceId String
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
data String // Encrypted data
|
data String // Encrypted data
|
||||||
@@ -155,7 +155,7 @@ model DashboardFolder {
|
|||||||
childrenFolder DashboardFolder[] @relation("ParentChild")
|
childrenFolder DashboardFolder[] @relation("ParentChild")
|
||||||
typebots Typebot[]
|
typebots Typebot[]
|
||||||
workspaceId String
|
workspaceId String
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Typebot {
|
model Typebot {
|
||||||
@@ -180,7 +180,7 @@ model Typebot {
|
|||||||
invitations Invitation[]
|
invitations Invitation[]
|
||||||
webhooks Webhook[]
|
webhooks Webhook[]
|
||||||
workspaceId String
|
workspaceId String
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Invitation {
|
model Invitation {
|
||||||
@@ -210,16 +210,16 @@ enum CollaborationType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model PublicTypebot {
|
model PublicTypebot {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
typebotId String @unique
|
typebotId String @unique
|
||||||
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
typebot Typebot @relation(fields: [typebotId], references: [id], onDelete: Cascade)
|
||||||
groups Json
|
groups Json
|
||||||
variables Json[]
|
variables Json[]
|
||||||
edges Json
|
edges Json
|
||||||
theme Json
|
theme Json
|
||||||
settings Json
|
settings Json
|
||||||
}
|
}
|
||||||
|
|
||||||
model Result {
|
model Result {
|
||||||
@@ -231,6 +231,7 @@ model Result {
|
|||||||
answers Answer[]
|
answers Answer[]
|
||||||
variables Json[]
|
variables Json[]
|
||||||
isCompleted Boolean
|
isCompleted Boolean
|
||||||
|
hasStarted Boolean?
|
||||||
logs Log[]
|
logs Log[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,10 +249,11 @@ model Answer {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
resultId String
|
resultId String
|
||||||
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
result Result @relation(fields: [resultId], references: [id], onDelete: Cascade)
|
||||||
blockId String
|
blockId String
|
||||||
groupId String
|
groupId String
|
||||||
variableId String?
|
variableId String?
|
||||||
content String
|
content String
|
||||||
|
storageUsed Int?
|
||||||
|
|
||||||
@@unique([resultId, blockId, groupId])
|
@@unique([resultId, blockId, groupId])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Answer as AnswerFromPrisma } from 'db'
|
import { Answer as AnswerFromPrisma } from 'db'
|
||||||
|
|
||||||
export type Answer = Omit<AnswerFromPrisma, 'resultId' | 'createdAt'>
|
export type Answer = Omit<
|
||||||
|
AnswerFromPrisma,
|
||||||
|
'resultId' | 'createdAt' | 'storageUsed'
|
||||||
|
> & { storageUsed?: number }
|
||||||
|
|
||||||
export type Stats = {
|
export type Stats = {
|
||||||
totalViews: number
|
totalViews: number
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const fileInputOptionsSchema = optionBaseSchema.and(
|
|||||||
placeholder: z.string(),
|
placeholder: z.string(),
|
||||||
button: z.string(),
|
button: z.string(),
|
||||||
}),
|
}),
|
||||||
|
sizeLimit: z.number().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { config, Endpoint, S3 } from 'aws-sdk'
|
|||||||
type GeneratePresignedUrlProps = {
|
type GeneratePresignedUrlProps = {
|
||||||
filePath: string
|
filePath: string
|
||||||
fileType: string
|
fileType: string
|
||||||
|
sizeLimit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const tenMB = 10485760
|
const tenMB = 10 * 1024 * 1024
|
||||||
const oneHundredAndTwentySeconds = 120
|
const oneHundredAndTwentySeconds = 120
|
||||||
|
|
||||||
export const generatePresignedUrl = ({
|
export const generatePresignedUrl = ({
|
||||||
filePath,
|
filePath,
|
||||||
fileType,
|
fileType,
|
||||||
|
sizeLimit = tenMB,
|
||||||
}: GeneratePresignedUrlProps): S3.PresignedPost => {
|
}: GeneratePresignedUrlProps): S3.PresignedPost => {
|
||||||
if (
|
if (
|
||||||
!process.env.S3_ENDPOINT ||
|
!process.env.S3_ENDPOINT ||
|
||||||
@@ -45,7 +47,7 @@ export const generatePresignedUrl = ({
|
|||||||
'Content-Type': fileType,
|
'Content-Type': fileType,
|
||||||
},
|
},
|
||||||
Expires: oneHundredAndTwentySeconds,
|
Expires: oneHundredAndTwentySeconds,
|
||||||
Conditions: [['content-length-range', 0, tenMB]],
|
Conditions: [['content-length-range', 0, sizeLimit]],
|
||||||
})
|
})
|
||||||
return presignedUrl
|
return presignedUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ export const generateId = (idDesiredLength: number): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UploadFileProps = {
|
type UploadFileProps = {
|
||||||
|
basePath?: string
|
||||||
files: {
|
files: {
|
||||||
file: File
|
file: File
|
||||||
path: string
|
path: string
|
||||||
@@ -202,6 +203,7 @@ type UploadFileProps = {
|
|||||||
type UrlList = string[]
|
type UrlList = string[]
|
||||||
|
|
||||||
export const uploadFiles = async ({
|
export const uploadFiles = async ({
|
||||||
|
basePath = '/api',
|
||||||
files,
|
files,
|
||||||
onUploadProgress,
|
onUploadProgress,
|
||||||
}: UploadFileProps): Promise<UrlList> => {
|
}: UploadFileProps): Promise<UrlList> => {
|
||||||
@@ -209,9 +211,9 @@ export const uploadFiles = async ({
|
|||||||
const { data } = await sendRequest<{
|
const { data } = await sendRequest<{
|
||||||
presignedUrl: { url: string; fields: any }
|
presignedUrl: { url: string; fields: any }
|
||||||
}>(
|
}>(
|
||||||
`/api/storage/upload-url?filePath=${encodeURIComponent(path)}&fileType=${
|
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
|
||||||
file.type
|
path
|
||||||
}`
|
)}&fileType=${file.type}`
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!data?.presignedUrl) return null
|
if (!data?.presignedUrl) return null
|
||||||
|
|||||||
Reference in New Issue
Block a user