mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
use event structure for submissionSessions
This commit is contained in:
@@ -12,17 +12,17 @@ export default function Submission({ formId, submissionSession }) {
|
||||
const submission = JSON.parse(JSON.stringify(schema));
|
||||
submission.id = submissionSession.id;
|
||||
submission.createdAt = submissionSession.createdAt;
|
||||
if (submissionSession.submissions.length > 0) {
|
||||
if (submissionSession.events.length > 0) {
|
||||
for (const page of submission.pages) {
|
||||
if (page.type === "form") {
|
||||
const pageSubmission = submissionSession.submissions.find(
|
||||
(s) => s.pageName === page.name
|
||||
const pageSubmission = submissionSession.events.find(
|
||||
(s) => s.type === "pageSubmission" && s.data?.pageName === page.name
|
||||
);
|
||||
if (typeof pageSubmission !== "undefined") {
|
||||
for (const element of page.elements) {
|
||||
if (element.type !== "submit") {
|
||||
if (element.name in pageSubmission.data) {
|
||||
element.value = pageSubmission.data[element.name];
|
||||
if (element.name in pageSubmission.data?.submission) {
|
||||
element.value = pageSubmission.data.submission[element.name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
lib/apiEvents.ts
Normal file
58
lib/apiEvents.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { ApiEvent, pageSubmissionData } from "./types";
|
||||
|
||||
type validationError = {
|
||||
status: number;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const validateEvents = (
|
||||
events: ApiEvent[]
|
||||
): validationError | undefined => {
|
||||
if (!Array.isArray(events)) {
|
||||
return { status: 400, message: `"events" needs to be a list` };
|
||||
}
|
||||
for (const event of events) {
|
||||
if (
|
||||
![
|
||||
"createSubmissionSession",
|
||||
"pageSubmission",
|
||||
"submissionCompleted",
|
||||
"updateSchema",
|
||||
].includes(event.type)
|
||||
) {
|
||||
return {
|
||||
status: 400,
|
||||
message: `event type ${event.type} is not suppported`,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const processApiEvent = async (event: ApiEvent, formId) => {
|
||||
if (event.type === "pageSubmission") {
|
||||
const data = event.data;
|
||||
await prisma.sessionEvent.create({
|
||||
data: {
|
||||
type: "pageSubmission",
|
||||
data: {
|
||||
pageName: data.pageName,
|
||||
submission: data.submission,
|
||||
},
|
||||
submissionSession: { connect: { id: data.submissionSessionId } },
|
||||
},
|
||||
});
|
||||
} else if (event.type === "submissionCompleted") {
|
||||
// TODO
|
||||
} else if (event.type === "updateSchema") {
|
||||
const data = { schema: event.data, updatedAt: new Date() };
|
||||
await prisma.form.update({
|
||||
where: { id: formId },
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
throw Error(
|
||||
`apiEvents: unsupported event type in event ${JSON.stringify(event)}`
|
||||
);
|
||||
}
|
||||
};
|
||||
45
lib/types.ts
45
lib/types.ts
@@ -29,3 +29,48 @@ export type Answer = {
|
||||
export type AnswerData = {
|
||||
value: string | string[];
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
pages: SchemaPage[];
|
||||
};
|
||||
|
||||
export type SchemaPage = {
|
||||
type: "form" | "thankyou";
|
||||
name: string;
|
||||
elements: SchemaElement[];
|
||||
};
|
||||
|
||||
export type SchemaElement = {
|
||||
name: string;
|
||||
type: "checkbox" | "radio" | "text" | "textarea" | "submit";
|
||||
label?: string;
|
||||
options?: SchemaOption[];
|
||||
};
|
||||
|
||||
export type SchemaOption = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type pageSubmissionData = {
|
||||
type: "pageSubmission";
|
||||
data: {
|
||||
submissionSessionId: string;
|
||||
pageName: string;
|
||||
submission: { [key: string]: string };
|
||||
};
|
||||
};
|
||||
|
||||
export type submissionCompletedEvent = {
|
||||
type: "submissionCompleted";
|
||||
data: { [key: string]: string };
|
||||
};
|
||||
|
||||
export type updateSchemaEvent = { type: "updateSchema"; data: Schema };
|
||||
|
||||
export type ApiEvent =
|
||||
| pageSubmissionData
|
||||
| submissionCompletedEvent
|
||||
| updateSchemaEvent;
|
||||
|
||||
export type WebhookEvent = Event & { formId: string; timestamp: string };
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.6.1",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@prisma/client": "^3.15.0",
|
||||
"@prisma/client": "^3.15.1",
|
||||
"babel-plugin-superjson-next": "^0.4.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"date-fns": "^2.28.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"eslint": "8.15.0",
|
||||
"eslint-config-next": "12.1.6",
|
||||
"postcss": "^8.4.13",
|
||||
"prisma": "^3.15.0",
|
||||
"prisma": "^3.15.1",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"ts-node": "^10.7.0",
|
||||
"typescript": "4.6.4"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import NextCors from "nextjs-cors";
|
||||
import { processApiEvent, validateEvents } from "../../../../lib/apiEvents";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
///api/submissionSession
|
||||
@@ -21,13 +22,17 @@ export default async function handle(
|
||||
// Required fields in body: schema
|
||||
// Optional fields in body: -
|
||||
if (req.method === "POST") {
|
||||
const { schema } = req.body;
|
||||
const data = { schema, updatedAt: new Date() };
|
||||
await prisma.form.update({
|
||||
where: { id: formId },
|
||||
data,
|
||||
});
|
||||
return res.json({ success: true });
|
||||
const { events } = req.body;
|
||||
const error = validateEvents(events);
|
||||
if (error) {
|
||||
console.log(JSON.stringify(error, null, 2));
|
||||
const { status, message } = error;
|
||||
return res.status(status).json({ error: message });
|
||||
}
|
||||
res.json({ success: true });
|
||||
for (const event of events) {
|
||||
processApiEvent(event, formId);
|
||||
}
|
||||
}
|
||||
// Unknown HTTP Method
|
||||
else {
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { NextApiResponse, NextApiRequest } from "next";
|
||||
import NextCors from "nextjs-cors";
|
||||
import { prisma } from "../../../../../../../lib/prisma";
|
||||
|
||||
///api/submissionSession
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
const submissionSessionId = req.query.submissionSessionId.toString();
|
||||
// POST /api/forms/:id/submissionSessions/:submissionSessionId/submissions
|
||||
// Creates a new submission
|
||||
// Required fields in body: elementId, data
|
||||
// Optional fields in body: -
|
||||
|
||||
await NextCors(req, res, {
|
||||
// Options
|
||||
methods: ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"],
|
||||
origin: "*",
|
||||
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
|
||||
});
|
||||
if (req.method === "POST") {
|
||||
const { pageName, data } = req.body;
|
||||
const prismaRes = await prisma.submission.create({
|
||||
data: {
|
||||
data,
|
||||
pageName,
|
||||
submissionSession: { connect: { id: submissionSessionId } },
|
||||
},
|
||||
});
|
||||
return res.json(prismaRes);
|
||||
}
|
||||
// Unknown HTTP Method
|
||||
else {
|
||||
throw new Error(
|
||||
`The HTTP ${req.method} method is not supported by this route.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default async function handle(
|
||||
form: { id: formId },
|
||||
},
|
||||
include: {
|
||||
submissions: true,
|
||||
events: true,
|
||||
},
|
||||
});
|
||||
return res.json(submissionSessionsData);
|
||||
@@ -34,8 +34,9 @@ export default async function handle(
|
||||
// Required fields in body: -
|
||||
// Optional fields in body: -
|
||||
if (req.method === "POST") {
|
||||
const { userFingerprint } = req.body;
|
||||
const prismaRes = await prisma.submissionSession.create({
|
||||
data: { form: { connect: { id: formId } } },
|
||||
data: { userFingerprint, form: { connect: { id: formId } } },
|
||||
});
|
||||
return res.json(prismaRes);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "FormType" AS ENUM ('CODE', 'NOCODE');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Form" (
|
||||
"id" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"ownerId" INTEGER NOT NULL,
|
||||
"formType" "FormType" NOT NULL DEFAULT E'NOCODE',
|
||||
"name" TEXT NOT NULL,
|
||||
"published" BOOLEAN NOT NULL DEFAULT false,
|
||||
"finishedOnboarding" BOOLEAN NOT NULL DEFAULT false,
|
||||
@@ -18,20 +22,21 @@ CREATE TABLE "SubmissionSession" (
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"formId" TEXT NOT NULL,
|
||||
"userFingerprint" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "SubmissionSession_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Submission" (
|
||||
CREATE TABLE "SessionEvent" (
|
||||
"id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"submissionSessionId" TEXT NOT NULL,
|
||||
"pageName" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
|
||||
CONSTRAINT "SessionEvent_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
@@ -72,4 +77,4 @@ ALTER TABLE "Form" ADD CONSTRAINT "Form_ownerId_fkey" FOREIGN KEY ("ownerId") RE
|
||||
ALTER TABLE "SubmissionSession" ADD CONSTRAINT "SubmissionSession_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Submission" ADD CONSTRAINT "Submission_submissionSessionId_fkey" FOREIGN KEY ("submissionSessionId") REFERENCES "SubmissionSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
ALTER TABLE "SessionEvent" ADD CONSTRAINT "SessionEvent_submissionSessionId_fkey" FOREIGN KEY ("submissionSessionId") REFERENCES "SubmissionSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -7,17 +7,23 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
enum FormType {
|
||||
CODE
|
||||
NOCODE
|
||||
}
|
||||
|
||||
model Form {
|
||||
id String @id
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
ownerId Int
|
||||
formType FormType @default(NOCODE)
|
||||
name String
|
||||
published Boolean @default(false)
|
||||
finishedOnboarding Boolean @default(false)
|
||||
schema Json
|
||||
submissionSessions SubmissionSession[]
|
||||
schema Json
|
||||
submissionSessions SubmissionSession[]
|
||||
}
|
||||
|
||||
model SubmissionSession {
|
||||
@@ -26,16 +32,17 @@ model SubmissionSession {
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||
formId String
|
||||
submissions Submission[]
|
||||
userFingerprint String
|
||||
events SessionEvent[]
|
||||
}
|
||||
|
||||
model Submission {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
model SessionEvent {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
submissionSession SubmissionSession @relation(fields: [submissionSessionId], references: [id], onDelete: Cascade)
|
||||
submissionSessionId String
|
||||
pageName String
|
||||
type String
|
||||
data Json
|
||||
}
|
||||
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -191,22 +191,22 @@
|
||||
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.0.2.tgz#bab0f09d09de9fd83628220d496627681bc440d6"
|
||||
integrity sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==
|
||||
|
||||
"@prisma/client@^3.15.0":
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.0.tgz#d248b906c254806b10b95d9a2abfcf40d70a263a"
|
||||
integrity sha512-0bwpIXtE3qkkm8JV6kSHLPyEbEUQD+hFAO7NM5INr23vFBOoyKwKI1Q4V08A7jq3NdI7g2NlBuugCHE9//1ANw==
|
||||
"@prisma/client@^3.15.1":
|
||||
version "3.15.1"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.15.1.tgz#798e8bf0370abe686d599c822e93f7adb7c0c9c0"
|
||||
integrity sha512-Lsk7oupvO9g99mrIs07iE6BIMouHs46Yq/YY8itTsUQNKfecsPuZvVYvcKci0pqRQ0neOpvIvoA/ouZmIMBCrQ==
|
||||
dependencies:
|
||||
"@prisma/engines-version" "3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372"
|
||||
"@prisma/engines-version" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
|
||||
|
||||
"@prisma/engines-version@3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372":
|
||||
version "3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372.tgz#d6518de90c689666b18e28546d3d4649f7f8dcd0"
|
||||
integrity sha512-VRj1Svti6h4UnUxEbIpio+40zppFztGKogVbkgblndbEQfmNdLh2M5bzDD6kaO+r8LBxHUBzUG/zEmlpxTvwoA==
|
||||
"@prisma/engines-version@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
|
||||
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#bf5e2373ca68ce7556b967cb4965a7095e93fe53"
|
||||
integrity sha512-e3k2Vd606efd1ZYy2NQKkT4C/pn31nehyLhVug6To/q8JT8FpiMrDy7zmm3KLF0L98NOQQcutaVtAPhzKhzn9w==
|
||||
|
||||
"@prisma/engines@3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372":
|
||||
version "3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372.tgz#c3dda39b42e9f62f910675818e4d813a16a3a76d"
|
||||
integrity sha512-S5T0NuVF2+NoKoxY5bN6O/7avv4ifcjqqk5+JFZ6C4g+P7JMM3nY0wGBPI+46A8yGIDsyyFmvFTYiIDsEUwUeQ==
|
||||
"@prisma/engines@3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e":
|
||||
version "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
|
||||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e.tgz#f691893df506b93e3cb1ccc15ec6e5ac64e8e570"
|
||||
integrity sha512-NHlojO1DFTsSi3FtEleL9QWXeSF/UjhCW0fgpi7bumnNZ4wj/eQ+BJJ5n2pgoOliTOGv9nX2qXvmHap7rJMNmg==
|
||||
|
||||
"@rushstack/eslint-patch@^1.1.3":
|
||||
version "1.1.3"
|
||||
@@ -1881,12 +1881,12 @@ pretty-format@^3.8.0:
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
|
||||
integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U=
|
||||
|
||||
prisma@^3.15.0:
|
||||
version "3.15.0"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.0.tgz#3c80e8ca0814f1e90668b883ceb0c9ce3f8c5538"
|
||||
integrity sha512-HL1kGDuFi9PdhbVLYsQO9vAH9uRFAHk7ifUOQxSrzUDs7R4VfLtg45VvXz8VHwcgLY5h0OrDVe1TdEX7a4o9tg==
|
||||
prisma@^3.15.1:
|
||||
version "3.15.1"
|
||||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.15.1.tgz#5f7e22012775af3861ce0ff517cba581c9bd7350"
|
||||
integrity sha512-MLO3JUGJpe5+EVisA/i47+zlyF8Ug0ivvGYG4B9oSXQcPiUHB1ccmnpxqR7o0Up5SQgmxkBiEU//HgR6UuIKOw==
|
||||
dependencies:
|
||||
"@prisma/engines" "3.15.0-29.b9297dc3a59307060c1c39d7e4f5765066f38372"
|
||||
"@prisma/engines" "3.15.1-1.461d6a05159055555eb7dfb337c9fb271cbd4d7e"
|
||||
|
||||
prop-types@^15.5.10, prop-types@^15.7.1, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
version "15.8.1"
|
||||
|
||||
Reference in New Issue
Block a user