From ea1a72bc00f9267efabd643caf1f675b8b728bb7 Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Mon, 13 Jun 2022 23:09:33 +0900 Subject: [PATCH] use event structure for submissionSessions --- components/results/Submission.tsx | 10 ++-- lib/apiEvents.ts | 58 +++++++++++++++++++ lib/types.ts | 45 ++++++++++++++ package.json | 4 +- pages/api/forms/[id]/{schema.ts => event.ts} | 19 +++--- .../submissions/index.ts | 39 ------------- .../forms/[id]/submissionSessions/index.ts | 5 +- .../migration.sql | 13 +++-- prisma/schema.prisma | 23 +++++--- yarn.lock | 36 ++++++------ 10 files changed, 167 insertions(+), 85 deletions(-) create mode 100644 lib/apiEvents.ts rename pages/api/forms/[id]/{schema.ts => event.ts} (65%) delete mode 100644 pages/api/forms/[id]/submissionSessions/[submissionSessionId]/submissions/index.ts rename prisma/migrations/{20220609063823_initial => 20220613140315_init}/migration.sql (82%) diff --git a/components/results/Submission.tsx b/components/results/Submission.tsx index aa4c1b878a..cbb6090c41 100644 --- a/components/results/Submission.tsx +++ b/components/results/Submission.tsx @@ -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]; } } } diff --git a/lib/apiEvents.ts b/lib/apiEvents.ts new file mode 100644 index 0000000000..dc25c6928a --- /dev/null +++ b/lib/apiEvents.ts @@ -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)}` + ); + } +}; diff --git a/lib/types.ts b/lib/types.ts index df28c7939a..f09efb6535 100644 --- a/lib/types.ts +++ b/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 }; diff --git a/package.json b/package.json index f31f6f62db..87e82ce50b 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pages/api/forms/[id]/schema.ts b/pages/api/forms/[id]/event.ts similarity index 65% rename from pages/api/forms/[id]/schema.ts rename to pages/api/forms/[id]/event.ts index f64b8684be..0d3764cf08 100644 --- a/pages/api/forms/[id]/schema.ts +++ b/pages/api/forms/[id]/event.ts @@ -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 { diff --git a/pages/api/forms/[id]/submissionSessions/[submissionSessionId]/submissions/index.ts b/pages/api/forms/[id]/submissionSessions/[submissionSessionId]/submissions/index.ts deleted file mode 100644 index 7d0ef06327..0000000000 --- a/pages/api/forms/[id]/submissionSessions/[submissionSessionId]/submissions/index.ts +++ /dev/null @@ -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.` - ); - } -} diff --git a/pages/api/forms/[id]/submissionSessions/index.ts b/pages/api/forms/[id]/submissionSessions/index.ts index b74a323e0c..f996d56b51 100644 --- a/pages/api/forms/[id]/submissionSessions/index.ts +++ b/pages/api/forms/[id]/submissionSessions/index.ts @@ -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); } diff --git a/prisma/migrations/20220609063823_initial/migration.sql b/prisma/migrations/20220613140315_init/migration.sql similarity index 82% rename from prisma/migrations/20220609063823_initial/migration.sql rename to prisma/migrations/20220613140315_init/migration.sql index 472498c498..166f4d3b51 100644 --- a/prisma/migrations/20220609063823_initial/migration.sql +++ b/prisma/migrations/20220613140315_init/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bdff606c75..4aa242aec4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 } diff --git a/yarn.lock b/yarn.lock index 5f8c70d78b..dee5184ba4 100644 --- a/yarn.lock +++ b/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"