use event structure for submissionSessions

This commit is contained in:
Matthias Nannt
2022-06-13 23:09:33 +09:00
parent c9257de0e0
commit ea1a72bc00
10 changed files with 167 additions and 85 deletions

View File

@@ -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
View 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)}`
);
}
};

View File

@@ -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 };

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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.`
);
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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"