From 59972418b90587d90eb912cde2965bd2f3a93c7e Mon Sep 17 00:00:00 2001 From: buddywinte Date: Thu, 9 Jul 2026 16:40:51 -0500 Subject: [PATCH 01/12] remove form apis --- pages/api/workspace/[id]/forms/helpers.ts | 49 --------------- pages/api/workspace/[id]/forms/index.ts | 73 ----------------------- 2 files changed, 122 deletions(-) delete mode 100644 pages/api/workspace/[id]/forms/helpers.ts delete mode 100644 pages/api/workspace/[id]/forms/index.ts diff --git a/pages/api/workspace/[id]/forms/helpers.ts b/pages/api/workspace/[id]/forms/helpers.ts deleted file mode 100644 index 58cda7c6..00000000 --- a/pages/api/workspace/[id]/forms/helpers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Orbit Forms - * Licensed under GPL-3.0 (see LICENSE for details) - * - * Helpers to make life easier in the other scripts - * - * @author BuddyWinte - * @since 2.1.10-beta20 - */ - -type Permission = string; -interface Role { - permissions: Permission[]; -} -interface UserWithRoles { - roles: Role[]; -} - -/** - * Checks if a user has a given permission. - * - * Supports: - * - Exact matches: "Form.View" - * - Wildcards: "Form.*" - * - * @returns true if the user has permissions - * @returns false if the user doesn't have permissions - * @readonly - */ -export function hasPerms( - user: UserWithRoles, - permission: string -): boolean { - if (!user?.roles?.length) return false; - for (const role of user.roles) { - if (!role?.permissions?.length) continue; - for (const perm of role.permissions) { - if (perm === permission) return true; - if (perm.endsWith(".*")) { - const prefix = perm.slice(0,-2); - if (permission.startsWith(prefix + ".")) { - return true; - } - } - } - } - - return false; -} \ No newline at end of file diff --git a/pages/api/workspace/[id]/forms/index.ts b/pages/api/workspace/[id]/forms/index.ts deleted file mode 100644 index 3c21c65a..00000000 --- a/pages/api/workspace/[id]/forms/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import prisma from "@/utils/database"; -type Data = { - success: boolean; - error?: string; - forms?: unknown[]; -}; -import { withAuth, AuthenticatedRequest } from "@/lib/withAuth"; -import { hasPerms } from "./helpers"; - -export default withAuth(handler); - -// GET /api/workspace/[id]/forms - Returns a list of forms a specific user can see -// POST /api/workspace/[id]/forms - Create a new form -export async function handler( - req: AuthenticatedRequest, - res: NextApiResponse, -) { - const user = await prisma.user.findUnique({ - where: { - userid: req.auth.userId, - }, - include: { - roles: { - select: { - id: true, - name: true, - permissions: true, - }, - }, - workspaceMemberships: { - where: { - workspaceGroupId: Number(req.query.id), - }, - include: { - workspace: true, - }, - }, - }, - }); - - if (!user) { - return res.status(404).json({ - success: false, - error: "User not found", - }); - } - - if (req.method === "GET") { - if (!hasPerms(user, "Form.View")) { - return res.status(403).json({ - success: false, - error: "Missing permission: Form.View", - }); - } - - const forms = await prisma.form.findMany({ - where: { - workspaceGroupId: workspaceId, - }, - }); - - return res.status(200).json({ - success: true, - forms, - }); - } - - return res.status(405).json({ - success: false, - error: "Method Not Allowed" - }) -} From f0b72981a981e0b049e0a342f8c1b11723219190 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Thu, 9 Jul 2026 17:03:36 -0500 Subject: [PATCH 02/12] update readme --- README.md | 1 + package-lock.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba89bd99..77130a5a 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Orbit ships with a comprehensive set of management tools out of the box: | **Documentation** | Host your group's docs natively inside Orbit | | **Policies** | Create and assign policy documents for members to review and sign | | **Sessions** | Schedule and host sessions with minimal overhead | +| **Forms** | Create your own forms, manage there analytics. All in one dashboard. | --- diff --git a/package-lock.json b/package-lock.json index 9ca56f1e..49a51440 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "orbit", - "version": "2.1.10beta20", + "version": "2.1.10beta21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "orbit", - "version": "2.1.10beta20", + "version": "2.1.10beta21", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", From 3ac48c57e7e4dbfb1c78725f8c19f591934eb34b Mon Sep 17 00:00:00 2001 From: buddywinte Date: Thu, 9 Jul 2026 17:04:32 -0500 Subject: [PATCH 03/12] update readme x2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77130a5a..1ebcebb4 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Orbit ships with a comprehensive set of management tools out of the box: | **Documentation** | Host your group's docs natively inside Orbit | | **Policies** | Create and assign policy documents for members to review and sign | | **Sessions** | Schedule and host sessions with minimal overhead | -| **Forms** | Create your own forms, manage there analytics. All in one dashboard. | +| **Forms** | Build custom forms, manage submissions, and track analytics from a single dashboard. | --- From 4bb4a1d037ad155417ea87e9b663b8d68a3e3a31 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Thu, 9 Jul 2026 17:05:33 -0500 Subject: [PATCH 04/12] re-enable the toggle --- components/settings/general/form.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/components/settings/general/form.tsx b/components/settings/general/form.tsx index b6246acf..e94cf17c 100644 --- a/components/settings/general/form.tsx +++ b/components/settings/general/form.tsx @@ -26,14 +26,8 @@ const Forms: FC = (props) => {

Create, customize, and manage workspace forms for collecting structured data, submissions, and user input across your workspace (Coming Soon)

- {/**/} From 71bdcd2319706cb993eb374e34992e6b0c85b2ba Mon Sep 17 00:00:00 2001 From: buddywinte Date: Tue, 14 Jul 2026 08:36:22 -0500 Subject: [PATCH 05/12] database done? --- prisma/schema.prisma | 212 ++++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b4d16557..748b0874 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -37,7 +37,7 @@ model workspace { policyLinks PolicyShareableLink[] stickyAnnouncements StickyAnnouncement[] staffResignations staffResignation[] - forms form[] + forms Form[] } model config { @@ -101,7 +101,9 @@ model user { quotaCustomCompletions QuotaCustomCompletion[] @relation("QuotaCompletionUser") quotaCompletionsReviewed QuotaCustomCompletion[] @relation("QuotaCompletionReviewer") quotaUsers QuotaUser[] - forms form[] + createdForms Form[] + formResponses FormResponse[] + formReviews FormReview[] } model AuthSession { @@ -742,120 +744,120 @@ model PolicyShareableLink { @@index([createdById]) } -model form { - id String @id @default(uuid()) @db.Uuid +model Form { + id String @id @default(uuid()) @db.Uuid workspaceGroupId Int + createdById BigInt name String description String? - isEnabled Boolean - settings Json - visibility Json? - createdById BigInt - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - workspace workspace @relation(fields: [workspaceGroupId], references: [groupId]) - createdBy user @relation(fields: [createdById], references: [userid]) - questions formQuestion[] - submissions formSubmission[] - auditLogs formAuditLog[] + slug String? + enabled Boolean @default(true) + archived Boolean @default(false) + settings Json //"allowAnonymous": false, "allowMutiple": false, "successMessage": null + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + workspace workspace @relation(fields: [workspaceGroupId], references: [groupId]) + createdBy user @relation(fields: [createdById], references: [userid]) + formPages FormPage[] + formResponses FormResponse[] + @@unique([workspaceGroupId, slug]) @@index([workspaceGroupId]) } -model formQuestion { - id String @id @default(uuid()) @db.Uuid - formId String @db.Uuid - title String - description String? - type String - required Boolean @default(false) - position Int - settings Json? - visibilityRules Json? - createdAt DateTime @default(now()) - updatedat DateTime @updatedAt - form form @relation(fields: [formId], references: [id], onDelete: Cascade) - answers formAnswer[] +model FormPage { + id String @id @default(uuid()) @db.Uuid + formId String @db.Uuid + name String + description String? + order Int + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) + questions FormQuestion[] @@index([formId]) } -model formSubmission { - id String @id @default(uuid()) @db.Uuid - formId String @db.Uuid - workspaceGroupId Int - userId BigInt? - robloxUserId BigInt? - discordUserId String? - status String @default("SUBMITTED") - metadata Json? - submittedAt DateTime @default(now()) - updatedAt DateTime @updatedAt - form form @relation(fields: [formId], references: [id], onDelete: Cascade) - answers formAnswer[] - reviews formReview[] - comments formComment[] - auditLogs formAuditLog[] +enum FormQuestionType { + SHORT_TEXT + LONG_TEXT + NUMBER + BOOLEAN + DATE + TIME + DATETIME + EMAIL + URL + SINGLE_CHOICE + MULTIPLE_CHOICE + ROBLOX_USER + DISCORD_USER +} + +model FormQuestion { + id String @id @default(uuid()) @db.Uuid + pageId String @db.Uuid + label String + description String? + type FormQuestionType + required Boolean @default(false) + placeholder String? + order Int + settings Json? + createdAt DateTime @default(now()) + page FormPage @relation(fields: [pageId], references: [id], onDelete: Cascade) + answers FormAnswer[] + + @@index([pageId]) +} + +enum FormResponseStatus { + PENDING + IN_REVIEW + APPROVED + DENIED + WITHDRAWN +} + +model FormResponse { + id String @id @default(uuid()) @db.Uuid + formId String @db.Uuid + submuttedById BigInt? + status FormResponseStatus @default(PENDING) + submittedAt DateTime @default(now()) + updatedAt DateTime @updatedAt + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) + submittedBy user? @relation(fields: [submuttedById], references: [userid]) + answers FormAnswer[] + reviews FormReview[] @@index([formId]) - @@index([workspaceGroupId]) - @@index([status]) -} - -model formAnswer { - id String @id @default(uuid()) @db.Uuid - submissionId String @db.Uuid - questionId String @db.Uuid - value Json - createdAt DateTime @default(now()) - submission formSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) - question formQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) - - @@index([submissionId]) - @@index([questionId]) -} - -model formReview { - id String @id @default(uuid()) @db.Uuid - submissionId String @db.Uuid - reviewerId BigInt - vote String? - score Int? - notes String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - submission formSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) - - @@index([submissionId]) - @@index([reviewerId]) -} - -model formComment { - id String @id @default(uuid()) @db.Uuid - submissionId String @db.Uuid - authorId BigInt - content String - internal Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - submission formSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) - - @@index([submissionId]) -} - -model formAuditLog { - id String @id @default(uuid()) @db.Uuid - workspaceGroupId Int? - formId String? @db.Uuid - submissionId String? @db.Uuid - actorId BigInt? - action String - details Json? - createdAt DateTime @default(now()) - form form? @relation(fields: [formId], references: [id]) - submission formSubmission? @relation(fields: [submissionId], references: [id]) + @@index([submuttedById]) +} - @@index([formId]) - @@index([submissionId]) - @@index([workspaceGroupId]) -} \ No newline at end of file +model FormAnswer { + id String @id @default(uuid()) @db.Uuid + questionId String @db.Uuid + responseId String @db.Uuid + value Json + response FormResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) + question FormQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + + @@unique([responseId, questionId]) +} + +enum FormReviewDecision { + APPROVED + DENIED + COMMENT +} + +model FormReview { + id String @id @default(uuid()) @db.Uuid + responseId String @db.Uuid + reviewerId BigInt + decision FormReviewDecision + comment String? + createdAt DateTime @default(now()) + response FormResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) + reviewer user @relation(fields: [reviewerId], references: [userid]) +} From ff0440c7472b2216d9ac2087029a023e9acb47f6 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Tue, 14 Jul 2026 09:35:51 -0500 Subject: [PATCH 06/12] woah a few APIs are (planned) --- pages/api/workspace/[id]/forms/README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pages/api/workspace/[id]/forms/README.md diff --git a/pages/api/workspace/[id]/forms/README.md b/pages/api/workspace/[id]/forms/README.md new file mode 100644 index 00000000..e4b9120a --- /dev/null +++ b/pages/api/workspace/[id]/forms/README.md @@ -0,0 +1,4 @@ +# Forms API +> Designed by BuddyWinte with the assistance of Pawesome Contributers + + database done? From efa8a76165bce1dfdbb29fb3c1612e3a154aee1c Mon Sep 17 00:00:00 2001 From: buddywinte Date: Tue, 14 Jul 2026 16:11:25 -0500 Subject: [PATCH 07/12] updated APIs --- pages/api/workspace/[id]/forms/README.md | 157 ++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/pages/api/workspace/[id]/forms/README.md b/pages/api/workspace/[id]/forms/README.md index e4b9120a..9b03f2b6 100644 --- a/pages/api/workspace/[id]/forms/README.md +++ b/pages/api/workspace/[id]/forms/README.md @@ -1,4 +1,159 @@ # Forms API > Designed by BuddyWinte with the assistance of Pawesome Contributers - database done? +[PR #166](https://github.com/PlanetaryOrbit/orbit/pull/166) is the RFC for the Forms. Please feel free to read there about the Forms. + +Base Url: `/api/workspace/[id]/forms`, any routes with `/` are relative to this base url. + +**Design is not final. It is very likely to change as we expand on the Forms functionality.** + +--- + +# Forms + +## Create + +`POST /` + +Creates a new form. + +### Request Body +```json +{ + "name": "Developer Application", + "description": "Apply to become a developer.", + "slug": "developer-application", + "settings": { + "allowAnonymous": false, + "allowMultiple": false + } +} +``` + +### Response +```json +{ + "success": true, + "data": { + "id": "uuid", + "name": "Developer Application", + "createdAt": "2026-07-14T00:00:00Z" + } +} +``` + +## List +`GET /` + +Returns all forms in the workspace. + +### Query Params +``` +?archived=false +?enabled=true +?page=1 +?limit=20 +?search=developer +``` + +### Response +```json +{ + "success": true, + "data": { + "forms": [ + { + "id": "uuid", + "name": "Developer Application", + "description": "...", + "enabled": true, + "archived": false, + "createdAt": "..." + } + ], + "pagination": { + "page": 1, + "limit": 20, + "total": 50 + } + } +} +``` + +## Get Form +`GET /[id]` + +Returns a complete form schema. + +### Response +```json +{ + "success": true, + "data": { + "id": "uuid", + "name": "Developer Application", + "description": "...", + "settings": { + "allowAnonymous": false, + "allowMultiple": false + }, + "pages": [ + { + "id": "uuid", + "name": "General", + "questions": [] + } + ] + } +} +``` + +## Update +`PUT /[id]` + +Updates a form. +> Only send fields that need changing. + +### Request Body +```json +{ + "name": "Updated Application", + "description": "Updated description" +} +``` + +### Response +```json +{ + "success": true, + "data": { + "id": "uuid", + "updatedAt": "..." + } +} +``` + +## Delete +Deleting is not currently supported, we are still trying to plan this API. + +## Duplicate +`POST /[id]/duplicate` + +Creates a copy of a form. +> **Responses are not copied**. +> The word "**Copy**" is appended to the name of the copied form if `name` isn't given in the request body. + +### Request Body +```json +{ + "name": "Developer Application Copy" +} +``` + +### Response +```json +{ + "id": "new-uuid", + "name": "Developer Application Copy" +} +``` From 99fc9df05e6abf7d595ec414955220c7947a57f4 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Wed, 15 Jul 2026 13:25:31 -0500 Subject: [PATCH 08/12] better API..still have more progreess --- pages/api/{ => public/v1}/random/music.ts | 0 pages/api/workspace/[id]/forms/README.md | 252 ++++++++++++++++++++++ pages/api/workspace/[id]/forms/helpers.ts | 30 +++ pages/api/workspace/[id]/forms/index.ts | 0 4 files changed, 282 insertions(+) rename pages/api/{ => public/v1}/random/music.ts (100%) create mode 100644 pages/api/workspace/[id]/forms/helpers.ts create mode 100644 pages/api/workspace/[id]/forms/index.ts diff --git a/pages/api/random/music.ts b/pages/api/public/v1/random/music.ts similarity index 100% rename from pages/api/random/music.ts rename to pages/api/public/v1/random/music.ts diff --git a/pages/api/workspace/[id]/forms/README.md b/pages/api/workspace/[id]/forms/README.md index 9b03f2b6..aa16ef23 100644 --- a/pages/api/workspace/[id]/forms/README.md +++ b/pages/api/workspace/[id]/forms/README.md @@ -157,3 +157,255 @@ Creates a copy of a form. "name": "Developer Application Copy" } ``` + +# Pages + +## List Pages +`GET /[id]/pages` + +### Response +```json +{ + "success": true, + "data": [ + { + "id": "uuid", + "name": "General Information", + "order": 0 + } + ] +} +``` + + +## Create +`POST /[id]/pages` + +### Request Body +```json +{ + "name": "Experience", + "order": 1 +} +``` + +### Response +```json +{ + "success": true, +} +``` + +## Update +`PATCH /[id]/pages/[pageId]` + +### Request Body +```json +{ + "name": "Updated Experience", + "order": 2 +} +``` + +## Delete +`DELETE /[id]/pages/[pageId]` + +# Questions +## List Questions +`GET /[id]/questions` + +## Create +`POST /[id]/questions` + +### Request Body +```json +{ + "label": "Why do you want to join?", + "description": "Explain your experience.", + "type": "LONG_TEXT", + "required": true, + "order": 1, + "settings": {} +} +``` + +## Update +`PATCH /[id]/questions/[questionId]` +> Only send fields that need to be updated. + +### Request Body +```json +{ + "label": "Updated question", + "required": false +} +``` + +## Delete +`DELETE /[id]/questions/[questionId]` + +## Duplicate +`POST /[id]/questions/[questionId]/duplicate` + +### Response Body +```json +{ + "success": true, + "data": { + "id": "new-question-id" + } +} +``` + +## Reorder +`PATCH /[id]/questions/reorder` +> All questions must be included in the request or a 400 BAD_REQUEST will be returned. +> Order starts at 0, and must be sequential. **Questions cannot have the same order value.** + +### Request Body +```json +[ + { + "id": "question-1", + "order": 0 + }, + { + "id": "question-2", + "order": 1 + } +] +``` + +# Responses + +## Submit +`POST /[id]/responses` + +### Request Body +```json +{ + "answers": [ + { + "questionId": "question-id", + "value": "BuddyWinte" + }, + { + "questionId": "question-id-2", + "value": true + } + ] +} +``` + +### Response Body +```json +{ + "success": true, + "data": { + "id": "response-id", + "status": "PENDING", + "submittedAt": "2026-06-01T12:00:00Z" + } +} +``` + +## List Responses +`GET /[id]/responses` + +### Query Parameters +``` +?status=PENDING +?page=1 +?limit=50 +?search=text +``` + +### Response Body +```json +{ + "success": true, + "data": { + "Responses": [ + { + "id": "response-id", + "status": "PENDING", + "submittedAt": "2026-06-01T12:00:00Z" + } + ], + "pagination": {} + } +} +``` + +## Get Response +`GET /[id]/responses/[responseId]` + +### Response Body +```json +{ + "success": true, + "data": { + "id": "uuid", + "status": "PENDING", + "submittedBy": { + "id": 123, + "username": "BuddyWinte" + }, + "answers": [ + { + "questionId": "uuid", + "value": "Example" + } + ] + } +} +``` + +## Get My Response +`GET /[id]/responses/me` +> **Authorization is required** +Returns the user's response for the form. Only returns minimal data, meaning you will need to fetch the full information through the ID for everything. + +### Response Body +```json +{ + "success": true, + "data": { + "id": "response-id", + "status": "PENDING", + "submittedAt": "2026-06-01T12:00:00Z" + } +} +``` + +## Withdraw Response +> Can only be done if a user is logged in, requires `CanWithdrawResponses` to be enabled in the forms settings. +`POST /[id]/responses/[responseId]/withdraw` + +### Response Body +```json +{ + "success": true, +} +``` + +# Reviews API coming soons + +# Miscellaneous + +## Form Statistics +`GET /[id]/stats` + +### Response Body +```json +{ + "success": true, + "data": { + "views": 500, + "responses": 120, + "pending": 20, + "approved": 80, + "denied": 20 + } +} +``` diff --git a/pages/api/workspace/[id]/forms/helpers.ts b/pages/api/workspace/[id]/forms/helpers.ts new file mode 100644 index 00000000..379a1295 --- /dev/null +++ b/pages/api/workspace/[id]/forms/helpers.ts @@ -0,0 +1,30 @@ +/* + * Forms API - Build custom forms, manage submissions, and track analytics + * from a single dashboard. + * + * This module contains shared helper utilities used throughout the Forms API. + * + * @file Helpers for the Forms API. + * @module Pages/API/Workspace/[id]/Forms/Helpers + * @since 2.1.10-beta21 + * @author BuddyWinte + */ + + export interface ErrorBody { + code: string; + message: string; + details?: unknown; + } + + export type RequestResponse = + | { + success: true; + data: T; + } + | { + success: false; + error: ErrorBody; + }; + +import { withPermissionCheck } from "@/utils/permissionsManager"; +import { withAuth } from "@/lib/withAuth"; diff --git a/pages/api/workspace/[id]/forms/index.ts b/pages/api/workspace/[id]/forms/index.ts new file mode 100644 index 00000000..e69de29b From 5147ca095da28b0d3855b296b1bffeb46d286fa9 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Wed, 15 Jul 2026 16:09:15 -0500 Subject: [PATCH 09/12] typos fixed --- prisma/schema.prisma | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 748b0874..399652c5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -773,8 +773,10 @@ model FormPage { order Int form Form @relation(fields: [formId], references: [id], onDelete: Cascade) questions FormQuestion[] - + updatedAt DateTime @updatedAt @@index([formId]) + @@unique([formId, order]) + @@unique([pageId, order]) } enum FormQuestionType { @@ -804,6 +806,7 @@ model FormQuestion { order Int settings Json? createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt page FormPage @relation(fields: [pageId], references: [id], onDelete: Cascade) answers FormAnswer[] @@ -821,12 +824,12 @@ enum FormResponseStatus { model FormResponse { id String @id @default(uuid()) @db.Uuid formId String @db.Uuid - submuttedById BigInt? + submittedById BigInt? status FormResponseStatus @default(PENDING) submittedAt DateTime @default(now()) updatedAt DateTime @updatedAt form Form @relation(fields: [formId], references: [id], onDelete: Cascade) - submittedBy user? @relation(fields: [submuttedById], references: [userid]) + submittedBy user? @relation(fields: [submittedById], references: [userid]) answers FormAnswer[] reviews FormReview[] From b4aab98c122c4975bc3bcf965208d711596d1613 Mon Sep 17 00:00:00 2001 From: buddywinte Date: Wed, 15 Jul 2026 16:14:24 -0500 Subject: [PATCH 10/12] Added permission overides to DB --- prisma/schema.prisma | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 399652c5..5586eabb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -864,3 +864,21 @@ model FormReview { response FormResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) reviewer user @relation(fields: [reviewerId], references: [userid]) } + +model FormPermission { + id String @id @default(uuid()) @db.Uuid + formId String @db.Uuid + roleId String? @db.Uuid + userId BigInt? + allow String[] + deny String[] + form Form @relation(fields: [formId], references: [id], onDelete: Cascade) + role Role? @relation(fields: [roleId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [userId], onDelete: Cascade) + + @@index([formId]) + @@index([roleId]) + @@index([userId]) + @@unique([formId, roleId]) + @@unique([formId, userId]) +} From 99e76afc7a7a1d6147a362bbd3aa91d3875147cd Mon Sep 17 00:00:00 2001 From: buddywinte Date: Wed, 15 Jul 2026 16:17:53 -0500 Subject: [PATCH 11/12] fixed a few typos --- prisma/schema.prisma | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5586eabb..7aa4de81 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -758,8 +758,9 @@ model Form { updatedAt DateTime @updatedAt workspace workspace @relation(fields: [workspaceGroupId], references: [groupId]) createdBy user @relation(fields: [createdById], references: [userid]) - formPages FormPage[] - formResponses FormResponse[] + pages FormPage[] + responses FormResponse[] + permissions FormPermission[] @@unique([workspaceGroupId, slug]) @@index([workspaceGroupId]) @@ -776,7 +777,6 @@ model FormPage { updatedAt DateTime @updatedAt @@index([formId]) @@unique([formId, order]) - @@unique([pageId, order]) } enum FormQuestionType { @@ -811,6 +811,7 @@ model FormQuestion { answers FormAnswer[] @@index([pageId]) + @@unique([pageId, order]) } enum FormResponseStatus { @@ -834,7 +835,7 @@ model FormResponse { reviews FormReview[] @@index([formId]) - @@index([submuttedById]) + @@index([submittedById]) } model FormAnswer { @@ -861,6 +862,7 @@ model FormReview { decision FormReviewDecision comment String? createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt response FormResponse @relation(fields: [responseId], references: [id], onDelete: Cascade) reviewer user @relation(fields: [reviewerId], references: [userid]) } @@ -873,8 +875,8 @@ model FormPermission { allow String[] deny String[] form Form @relation(fields: [formId], references: [id], onDelete: Cascade) - role Role? @relation(fields: [roleId], references: [id], onDelete: Cascade) - user User? @relation(fields: [userId], references: [userId], onDelete: Cascade) + role role? @relation(fields: [roleId], references: [id], onDelete: Cascade) + user user? @relation(fields: [userId], references: [userId], onDelete: Cascade) @@index([formId]) @@index([roleId]) From 56386339125b7ba19ed1cb337156c12d9653707a Mon Sep 17 00:00:00 2001 From: buddywinte Date: Wed, 15 Jul 2026 16:30:09 -0500 Subject: [PATCH 12/12] added formPermission --- prisma/schema.prisma | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7aa4de81..f24ffa71 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -758,9 +758,9 @@ model Form { updatedAt DateTime @updatedAt workspace workspace @relation(fields: [workspaceGroupId], references: [groupId]) createdBy user @relation(fields: [createdById], references: [userid]) - pages FormPage[] + pages FormPage[] responses FormResponse[] - permissions FormPermission[] + permissions FormPermission[] @@unique([workspaceGroupId, slug]) @@index([workspaceGroupId]) @@ -867,13 +867,33 @@ model FormReview { reviewer user @relation(fields: [reviewerId], references: [userid]) } +enum FormPermissionType { + ManagePages + ManageQuestions + ManageQuestionSettings + ManageSettings + ManagePermissions + ViewResponses + ViewOwnResponses + SubmitResponses + WithdrawResponses + DeleteResponses + ReviewResponses + ApproveResponses + DenyResponses + AddReviewComments + ManageReviews + ExportResponses + ViewStatistics +} + model FormPermission { id String @id @default(uuid()) @db.Uuid formId String @db.Uuid roleId String? @db.Uuid userId BigInt? - allow String[] - deny String[] + allow FormPermissionType[] + deny FormPermissionType[] form Form @relation(fields: [formId], references: [id], onDelete: Cascade) role role? @relation(fields: [roleId], references: [id], onDelete: Cascade) user user? @relation(fields: [userId], references: [userId], onDelete: Cascade)