Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
-- CreateEnum
CREATE TYPE "FieldType" AS ENUM ('TEXT', 'NUMBER', 'EMAIL', 'MULTIPLE_CHOICE', 'CHECKBOX', 'SINGLE_CHOICE', 'FILE', 'DATE', 'STAR_RATING', 'DROPDOWN');

-- CreateTable
CREATE TABLE "Form" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"slug" TEXT NOT NULL,
"isTemplate" BOOLEAN NOT NULL DEFAULT false,

CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "FormContributor" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "FormContributor_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "FormField" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"fieldType" "FieldType" NOT NULL,
"isRequired" BOOLEAN NOT NULL DEFAULT false,
"options" JSONB,
"position" INTEGER NOT NULL,
"placeholder" TEXT,
"helpText" TEXT,
"validation" JSONB,
"conditions" JSONB,

CONSTRAINT "FormField_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "FormResponse" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"submittedBy" TEXT,
"responderIp" TEXT,
"userAgent" TEXT,
"metadata" JSONB,

CONSTRAINT "FormResponse_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "FieldAnswer" (
"id" TEXT NOT NULL,
"responseId" TEXT NOT NULL,
"fieldId" TEXT NOT NULL,
"answerValue" TEXT,
"answerJson" JSONB,

CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Form_slug_key" ON "Form"("slug");

-- CreateIndex
CREATE UNIQUE INDEX "FormContributor_formId_userId_key" ON "FormContributor"("formId", "userId");

-- AddForeignKey
ALTER TABLE "Form" ADD CONSTRAINT "Form_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormContributor" ADD CONSTRAINT "FormContributor_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormContributor" ADD CONSTRAINT "FormContributor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormField" ADD CONSTRAINT "FormField_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormResponse" ADD CONSTRAINT "FormResponse_formId_fkey" FOREIGN KEY ("formId") REFERENCES "Form"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FormResponse" ADD CONSTRAINT "FormResponse_submittedBy_fkey" FOREIGN KEY ("submittedBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
Warnings:

- A unique constraint covering the columns `[formUrl]` on the table `Form` will be added. If there are existing duplicate values, this will fail.
- Added the required column `formUrl` to the `Form` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT NOT NULL;

-- CreateIndex
CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");
Comment on lines +8 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Risky NOT NULL add on non-empty table; backfill first, then enforce NOT NULL + UNIQUE.

As written, this fails if Form has rows or duplicates. Prefer a safe, two-step migration that backfills from an existing unique field (e.g., slug) before adding constraints.

--- BEFORE
 ALTER TABLE "Form" ADD COLUMN     "formUrl" TEXT NOT NULL;
 CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");

+++ AFTER (safer)
+-- 1) Add as nullable
+ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT;
+
+-- 2) Backfill (assuming "slug" exists and is unique enough)
+UPDATE "Form" SET "formUrl" = "slug" WHERE "formUrl" IS NULL;
+
+-- 3) Validate uniqueness (manually check/clean duplicates if any), then:
+CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");
+
+-- 4) Enforce NOT NULL
+ALTER TABLE "Form" ALTER COLUMN "formUrl" SET NOT NULL;

Please confirm Form.slug exists and is unique; if not, we should generate a stable code (e.g., shortid/uuid) and backfill instead. Also check for case-insensitive uniqueness needs; if required, consider a functional index on lower("formUrl").


I need to verify the safety of this migration by examining the codebase context. Let me gather information about the Form table structure and the migration context.






🏁 Script executed:

#!/bin/bash
# Find and examine the Prisma schema to understand Form model structure
find . -name "schema.prisma" -type f | head -5

Length of output: 30


🏁 Script executed:

#!/bin/bash
# Examine the migration file and surrounding context
find . -path "*prisma/migrations/20251018123047*" -type f

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Look for Prisma schema definition
find . -name "schema.prisma" -exec cat {} \;

Length of output: 10878


🏁 Script executed:

#!/bin/bash
# Search for Form model definition in the codebase
rg -n "model Form" --type ts --type js --type prisma -A 20

Length of output: 30


🏁 Script executed:

#!/bin/bash
# Check for any migration files that might have populated data before this one
find . -path "*/prisma/migrations/*" -name "*.sql" | sort | tail -10

Length of output: 797


I need to examine the earlier migration to understand the Form table evolution and determine if the backfill suggestion is valid.


---

🏁 Script executed:

```shell
#!/bin/bash
# Read the Form table creation migration
cat ./backend/prisma/migrations/20251018092621_add_forms_to_orbis/migration.sql

Length of output: 3384


🏁 Script executed:

#!/bin/bash
# Read the migration under review in full
cat ./backend/prisma/migrations/20251018123047_add_form_url/migration.sql

Length of output: 450


🏁 Script executed:

#!/bin/bash
# Check for any seed files or data population scripts
find . -name "seed*" -o -name "*seed*" -type f | grep -E "\.(ts|js|sql)$"

Length of output: 0


Migration adds NOT NULL constraint without safe backfill; use two-step approach.

The migration's risk is real: the Prisma warning confirms "This is not possible if the table is not empty". The Form table was created with a slug field that is unique, so backfilling from slug is a sound strategy. The two-step migration approach is appropriate: add nullable column, backfill from slug, create unique index, then enforce NOT NULL.

--- BEFORE
 ALTER TABLE "Form" ADD COLUMN     "formUrl" TEXT NOT NULL;
 CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");

+++ AFTER (safer)
+-- 1) Add as nullable
+ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT;
+
+-- 2) Backfill from slug (which is guaranteed unique)
+UPDATE "Form" SET "formUrl" = "slug" WHERE "formUrl" IS NULL;
+
+-- 3) Create unique index
+CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");
+
+-- 4) Enforce NOT NULL
+ALTER TABLE "Form" ALTER COLUMN "formUrl" SET NOT NULL;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- AlterTable
ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT NOT NULL;
-- CreateIndex
CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");
-- AlterTable
-- 1) Add as nullable
ALTER TABLE "Form" ADD COLUMN "formUrl" TEXT;
-- 2) Backfill from slug (which is guaranteed unique)
UPDATE "Form" SET "formUrl" = "slug" WHERE "formUrl" IS NULL;
-- 3) Create unique index
CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl");
-- 4) Enforce NOT NULL
ALTER TABLE "Form" ALTER COLUMN "formUrl" SET NOT NULL;
🤖 Prompt for AI Agents
In backend/prisma/migrations/20251018123047_add_form_url/migration.sql around
lines 8-12 the migration adds "formUrl" as NOT NULL directly which will fail for
non-empty tables; change to a two-step safe migration: 1) ADD COLUMN "formUrl"
TEXT NULL; 2) UPDATE "Form" SET "formUrl" = "slug" WHERE "formUrl" IS NULL; 3)
CREATE UNIQUE INDEX "Form_formUrl_key" ON "Form"("formUrl"); 4) ALTER TABLE
"Form" ALTER COLUMN "formUrl" SET NOT NULL. Ensure the backfill covers all
existing rows before setting NOT NULL and create the unique index after backfill
to avoid constraint violations.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Warnings:

- The primary key for the `FieldAnswer` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `FieldAnswer` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- You are about to drop the column `isTemplate` on the `Form` table. All the data in the column will be lost.
- You are about to drop the column `slug` on the `Form` table. All the data in the column will be lost.
- The primary key for the `FormContributor` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `FormContributor` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The primary key for the `FormField` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `FormField` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The primary key for the `FormResponse` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `FormResponse` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- Changed the type of `responseId` on the `FieldAnswer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
- Changed the type of `fieldId` on the `FieldAnswer` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.

*/
-- CreateEnum
CREATE TYPE "PermissionLevel" AS ENUM ('VIEW', 'EDIT');

-- DropForeignKey
ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_fieldId_fkey";

-- DropForeignKey
ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_responseId_fkey";

-- DropIndex
DROP INDEX "Form_slug_key";

-- AlterTable
ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_pkey",
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
DROP COLUMN "responseId",
ADD COLUMN "responseId" INTEGER NOT NULL,
DROP COLUMN "fieldId",
ADD COLUMN "fieldId" INTEGER NOT NULL,
ADD CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id");
Comment on lines +31 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

High risk of data loss by drop/recreate; prefer in-place type change or staged backfill.

Dropping responseId/fieldId and id can orphan or lose data in populated DBs.

Safer pattern:

-ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_pkey",
-DROP COLUMN "id",
-ADD COLUMN     "id" SERIAL NOT NULL,
-DROP COLUMN "responseId",
-ADD COLUMN     "responseId" INTEGER NOT NULL,
-DROP COLUMN "fieldId",
-ADD COLUMN     "fieldId" INTEGER NOT NULL,
-ADD CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id");
+-- Option A: in-place cast if prior types are castable
+ALTER TABLE "FieldAnswer" ALTER COLUMN "responseId" TYPE INTEGER USING ("responseId"::INTEGER);
+ALTER TABLE "FieldAnswer" ALTER COLUMN "fieldId"    TYPE INTEGER USING ("fieldId"::INTEGER);
+-- If changing PK strategy, add new column, backfill, then swap in a transaction with minimal lock.
+-- Example:
+-- ALTER TABLE "FieldAnswer" ADD COLUMN "id2" SERIAL;
+-- UPDATE "FieldAnswer" SET "id2" = nextval(pg_get_serial_sequence('"FieldAnswer"','id2')) WHERE "id2" IS NULL;
+-- ALTER TABLE "FieldAnswer" DROP CONSTRAINT "FieldAnswer_pkey";
+-- ALTER TABLE "FieldAnswer" RENAME COLUMN "id" TO "id_old";
+-- ALTER TABLE "FieldAnswer" RENAME COLUMN "id2" TO "id";
+-- ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_pkey" PRIMARY KEY ("id");

Schedule a maintenance window and take backups before applying.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
backend/prisma/migrations/20251018152641_add_permissions_to_forms/migration.sql
around lines 31 to 38, do not DROP and re-ADD the id, responseId, and fieldId
columns as that risks data loss; instead perform in-place changes or a staged
backfill: add new temporary columns of the desired type (e.g., new_id SERIAL,
new_responseId INTEGER, new_fieldId INTEGER), backfill them from the existing
columns or compute values as needed, verify integrity, set new columns NOT NULL,
create constraints/indexes on the new columns, then swap names (rename old to
backup_, rename new to original), and finally drop the old backup_ columns;
ensure this migration runs inside a maintenance window with a backup and add
transactional safety where supported.


-- AlterTable
ALTER TABLE "Form" DROP COLUMN "isTemplate",
DROP COLUMN "slug",
ADD COLUMN "isEditable" BOOLEAN NOT NULL DEFAULT false;

-- AlterTable
ALTER TABLE "FormContributor" DROP CONSTRAINT "FormContributor_pkey",
ADD COLUMN "permission" "PermissionLevel" NOT NULL DEFAULT 'VIEW',
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "FormContributor_pkey" PRIMARY KEY ("id");

-- AlterTable
ALTER TABLE "FormField" DROP CONSTRAINT "FormField_pkey",
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "FormField_pkey" PRIMARY KEY ("id");

-- AlterTable
ALTER TABLE "FormResponse" DROP CONSTRAINT "FormResponse_pkey",
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "FormResponse_pkey" PRIMARY KEY ("id");

-- AddForeignKey
ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Comment on lines +64 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use cascading deletes for child rows to prevent referential dead-ends.

RESTRICT will block deleting responses/fields during edits. Prefer ON DELETE CASCADE for FieldAnswer FKs.

-ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_responseId_fkey" FOREIGN KEY ("responseId") REFERENCES "FormResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "FieldAnswer" ADD CONSTRAINT "FieldAnswer_fieldId_fkey" FOREIGN KEY ("fieldId") REFERENCES "FormField"("id") ON DELETE CASCADE ON UPDATE CASCADE;

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Warnings:

- You are about to drop the column `metadata` on the `FormResponse` table. All the data in the column will be lost.
- You are about to drop the column `responderIp` on the `FormResponse` table. All the data in the column will be lost.
- You are about to drop the column `userAgent` on the `FormResponse` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "FormField" ADD COLUMN "allowMultiple" BOOLEAN NOT NULL DEFAULT false;

-- AlterTable
ALTER TABLE "FormResponse" DROP COLUMN "metadata",
DROP COLUMN "responderIp",
DROP COLUMN "userAgent",
ADD COLUMN "anonymousId" TEXT;
87 changes: 87 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ model User {
applications Application[]
createdEvents Event[]
teamMembers TeamMember[]
formsCreated Form[] @relation("UserForms")
contributions FormContributor[]
formResponses FormResponse[] @relation("UserFormResponses")
}

model UserProfile {
Expand Down Expand Up @@ -320,3 +323,87 @@ enum TeamMemberRole {
LEADER
MEMBER
}

model Form {
id String @id @default(cuid())
title String
description String?
createdBy String
formUrl String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
isEditable Boolean @default(false)
creator User @relation("UserForms", fields: [createdBy], references: [id])
contributors FormContributor[]
fields FormField[]
responses FormResponse[]
}

enum PermissionLevel {
VIEW
EDIT
}

model FormContributor {
id Int @id @default(autoincrement())
formId String
userId String
addedAt DateTime @default(now())
permission PermissionLevel @default(VIEW)
form Form @relation(fields: [formId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([formId, userId])
}

model FormField {
id Int @id @default(autoincrement())
formId String
label String
fieldType FieldType
isRequired Boolean @default(false)
allowMultiple Boolean @default(false)
options Json?
position Int
placeholder String?
helpText String?
validation Json?
conditions Json?
form Form @relation(fields: [formId], references: [id])
answers FieldAnswer[]
}

enum FieldType {
TEXT
NUMBER
EMAIL
MULTIPLE_CHOICE
CHECKBOX
SINGLE_CHOICE
FILE
DATE
STAR_RATING
DROPDOWN
}


model FormResponse {
id Int @id @default(autoincrement())
formId String
submittedAt DateTime @default(now())
submittedBy String?
anonymousId String?
form Form @relation(fields: [formId], references: [id])
user User? @relation("UserFormResponses", fields: [submittedBy], references: [id])
answers FieldAnswer[]
}

model FieldAnswer {
id Int @id @default(autoincrement())
responseId Int
fieldId Int
answerValue String?
answerJson Json?
response FormResponse @relation(fields: [responseId], references: [id])
field FormField @relation(fields: [fieldId], references: [id])
}
10 changes: 6 additions & 4 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import eventRoutes from './src/routes/events.js';
import teamRoutes from './src/routes/teams.js';
import projectRoutes from './src/routes/projects.js';
import profileRoutes from './src/routes/profiles.js';
import formRoutes from './src/routes/form.js';

const app = express();
const PORT = process.env.PORT || 4000;
Expand All @@ -26,18 +27,19 @@ app.use(cors({
app.use(express.json());
app.use(cookieParser());

// Mount routes
// Mount routes - IMPORTANT: Mount specific routes BEFORE generic ones
app.use('/api/auth', authRoutes);
app.use('/api/events', eventRoutes); // Remove checkJwt from public routes
app.use('/api/events', eventRoutes); // Public routes
app.use('/api/forms', formRoutes); // Form routes - mix of public and protected routes (MUST be before /api)
app.use('/api/teams', checkJwt, teamRoutes);
app.use('/api/projects', checkJwt, projectRoutes);
app.use('/api', checkJwt, profileRoutes); // Changed from '/api/profiles' to '/api' to match frontend calls
app.use('/api', checkJwt, profileRoutes); // Generic /api route with auth (MUST be last)

// Error handling
app.use(errorHandler);

// Start server
app.listen(PORT, () => {
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
console.log(`- Local: http://localhost:${PORT}`);
if (process.env.SERVER_URL) {
Expand Down
Loading