mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-05-08 13:58:55 -04:00
feat: complete forms system overhaul
- Add new forms table with type (consent/survey/questionnaire) - Add formResponses table for submissions - Add forms API router with full CRUD: - list, get, create, update, delete - setActive, createVersion - getResponses, submitResponse - Add forms list page with card-based UI - Add form builder with field types (text, textarea, multiple_choice, checkbox, rating, yes_no, date, signature) - Add form viewer with edit mode and preview - Add responses viewing with participant info
This commit is contained in:
@@ -5,6 +5,7 @@ import { collaborationRouter } from "~/server/api/routers/collaboration";
|
||||
import { dashboardRouter } from "~/server/api/routers/dashboard";
|
||||
import { experimentsRouter } from "~/server/api/routers/experiments";
|
||||
import { filesRouter } from "~/server/api/routers/files";
|
||||
import { formsRouter } from "~/server/api/routers/forms";
|
||||
import { mediaRouter } from "~/server/api/routers/media";
|
||||
import { participantsRouter } from "~/server/api/routers/participants";
|
||||
import { pluginsRouter } from "~/server/api/routers/plugins";
|
||||
@@ -34,6 +35,7 @@ export const appRouter = createTRPCRouter({
|
||||
admin: adminRouter,
|
||||
dashboard: dashboardRouter,
|
||||
storage: storageRouter,
|
||||
forms: formsRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, count, desc, eq, ilike, or } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import {
|
||||
activityLogs,
|
||||
formResponses,
|
||||
formTypeEnum,
|
||||
forms,
|
||||
formFieldTypeEnum,
|
||||
participants,
|
||||
studyMembers,
|
||||
userSystemRoles,
|
||||
} from "~/server/db/schema";
|
||||
|
||||
const formTypes = formTypeEnum.enumValues;
|
||||
const fieldTypes = formFieldTypeEnum.enumValues;
|
||||
|
||||
async function checkStudyAccess(
|
||||
db: typeof import("~/server/db").db,
|
||||
userId: string,
|
||||
studyId: string,
|
||||
requiredRole?: string[],
|
||||
) {
|
||||
const adminRole = await db.query.userSystemRoles.findFirst({
|
||||
where: and(
|
||||
eq(userSystemRoles.userId, userId),
|
||||
eq(userSystemRoles.role, "administrator"),
|
||||
),
|
||||
});
|
||||
|
||||
if (adminRole) {
|
||||
return { role: "administrator", studyId, userId, joinedAt: new Date() };
|
||||
}
|
||||
|
||||
const membership = await db.query.studyMembers.findFirst({
|
||||
where: and(
|
||||
eq(studyMembers.studyId, studyId),
|
||||
eq(studyMembers.userId, userId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You don't have access to this study",
|
||||
});
|
||||
}
|
||||
|
||||
if (requiredRole && !requiredRole.includes(membership.role)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You don't have permission to perform this action",
|
||||
});
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
export const formsRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
studyId: z.string().uuid(),
|
||||
type: z.enum(formTypes).optional(),
|
||||
search: z.string().optional(),
|
||||
page: z.number().min(1).default(1),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { studyId, type, search, page, limit } = input;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, studyId);
|
||||
|
||||
const conditions = [eq(forms.studyId, studyId)];
|
||||
if (type) {
|
||||
conditions.push(eq(forms.type, type));
|
||||
}
|
||||
if (search) {
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(forms.title, `%${search}%`),
|
||||
ilike(forms.description, `%${search}%`),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
const [formsList, totalCount] = await Promise.all([
|
||||
ctx.db.query.forms.findMany({
|
||||
where: and(...conditions),
|
||||
with: {
|
||||
createdBy: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(forms.updatedAt)],
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
ctx.db
|
||||
.select({ count: count() })
|
||||
.from(forms)
|
||||
.where(and(...conditions)),
|
||||
]);
|
||||
|
||||
const formsWithCounts = await Promise.all(
|
||||
formsList.map(async (form) => {
|
||||
const responseCount = await ctx.db
|
||||
.select({ count: count() })
|
||||
.from(formResponses)
|
||||
.where(eq(formResponses.formId, form.id));
|
||||
return { ...form, _count: { responses: responseCount[0]?.count ?? 0 } };
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
forms: formsWithCounts,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: totalCount[0]?.count ?? 0,
|
||||
pages: Math.ceil((totalCount[0]?.count ?? 0) / limit),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
get: protectedProcedure
|
||||
.input(z.object({ id: z.string().uuid() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const form = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, input.id),
|
||||
with: {
|
||||
createdBy: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
with: {
|
||||
participant: {
|
||||
columns: {
|
||||
id: true,
|
||||
participantCode: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(formResponses.submittedAt)],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, form.studyId);
|
||||
|
||||
return form;
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
studyId: z.string().uuid(),
|
||||
type: z.enum(formTypes),
|
||||
title: z.string().min(1).max(255),
|
||||
description: z.string().optional(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
label: z.string(),
|
||||
required: z.boolean().default(false),
|
||||
options: z.array(z.string()).optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
).default([]),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, input.studyId, [
|
||||
"owner",
|
||||
"researcher",
|
||||
]);
|
||||
|
||||
const [newForm] = await ctx.db
|
||||
.insert(forms)
|
||||
.values({
|
||||
studyId: input.studyId,
|
||||
type: input.type,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
fields: input.fields,
|
||||
settings: input.settings ?? {},
|
||||
createdBy: ctx.session.user.id,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!newForm) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create form",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(activityLogs).values({
|
||||
studyId: input.studyId,
|
||||
userId: ctx.session.user.id,
|
||||
action: "form_created",
|
||||
description: `Created form "${newForm.title}"`,
|
||||
resourceType: "form",
|
||||
resourceId: newForm.id,
|
||||
});
|
||||
|
||||
return newForm;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
title: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
label: z.string(),
|
||||
required: z.boolean().default(false),
|
||||
options: z.array(z.string()).optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
).optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, ...updateData } = input;
|
||||
|
||||
const existingForm = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, id),
|
||||
});
|
||||
|
||||
if (!existingForm) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, existingForm.studyId, [
|
||||
"owner",
|
||||
"researcher",
|
||||
]);
|
||||
|
||||
const [updatedForm] = await ctx.db
|
||||
.update(forms)
|
||||
.set({
|
||||
...updateData,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(forms.id, id))
|
||||
.returning();
|
||||
|
||||
if (!updatedForm) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to update form",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(activityLogs).values({
|
||||
studyId: existingForm.studyId,
|
||||
userId: ctx.session.user.id,
|
||||
action: "form_updated",
|
||||
description: `Updated form "${updatedForm.title}"`,
|
||||
resourceType: "form",
|
||||
resourceId: id,
|
||||
});
|
||||
|
||||
return updatedForm;
|
||||
}),
|
||||
|
||||
delete: protectedProcedure
|
||||
.input(z.object({ id: z.string().uuid() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const form = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, input.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, form.studyId, [
|
||||
"owner",
|
||||
"researcher",
|
||||
]);
|
||||
|
||||
await ctx.db.delete(forms).where(eq(forms.id, input.id));
|
||||
|
||||
await ctx.db.insert(activityLogs).values({
|
||||
studyId: form.studyId,
|
||||
userId: ctx.session.user.id,
|
||||
action: "form_deleted",
|
||||
description: `Deleted form "${form.title}"`,
|
||||
resourceType: "form",
|
||||
resourceId: input.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
setActive: protectedProcedure
|
||||
.input(z.object({ id: z.string().uuid() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const form = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, input.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, form.studyId, [
|
||||
"owner",
|
||||
"researcher",
|
||||
]);
|
||||
|
||||
await ctx.db
|
||||
.update(forms)
|
||||
.set({ active: false })
|
||||
.where(eq(forms.studyId, form.studyId));
|
||||
|
||||
const [updatedForm] = await ctx.db
|
||||
.update(forms)
|
||||
.set({ active: true })
|
||||
.where(eq(forms.id, input.id))
|
||||
.returning();
|
||||
|
||||
return updatedForm;
|
||||
}),
|
||||
|
||||
createVersion: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
title: z.string().min(1).max(255).optional(),
|
||||
description: z.string().optional(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
label: z.string(),
|
||||
required: z.boolean().default(false),
|
||||
options: z.array(z.string()).optional(),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
),
|
||||
settings: z.record(z.string(), z.any()).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { id, ...updateData } = input;
|
||||
|
||||
const existingForm = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, id),
|
||||
});
|
||||
|
||||
if (!existingForm) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, existingForm.studyId, [
|
||||
"owner",
|
||||
"researcher",
|
||||
]);
|
||||
|
||||
const latestForm = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.studyId, existingForm.studyId),
|
||||
orderBy: [desc(forms.version)],
|
||||
});
|
||||
const newVersion = (latestForm?.version ?? 0) + 1;
|
||||
|
||||
const [newForm] = await ctx.db
|
||||
.insert(forms)
|
||||
.values({
|
||||
studyId: existingForm.studyId,
|
||||
type: existingForm.type,
|
||||
title: updateData.title ?? existingForm.title,
|
||||
description: updateData.description ?? existingForm.description,
|
||||
fields: updateData.fields ?? existingForm.fields,
|
||||
settings: updateData.settings ?? existingForm.settings,
|
||||
version: newVersion,
|
||||
active: false,
|
||||
createdBy: ctx.session.user.id,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!newForm) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to create form version",
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.db.insert(activityLogs).values({
|
||||
studyId: existingForm.studyId,
|
||||
userId: ctx.session.user.id,
|
||||
action: "form_version_created",
|
||||
description: `Created version ${newVersion} of form "${newForm.title}"`,
|
||||
resourceType: "form",
|
||||
resourceId: newForm.id,
|
||||
});
|
||||
|
||||
return newForm;
|
||||
}),
|
||||
|
||||
getResponses: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
formId: z.string().uuid(),
|
||||
page: z.number().min(1).default(1),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
status: z.enum(["pending", "completed", "rejected"]).optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { formId, page, limit, status } = input;
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const form = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, formId),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, form.studyId);
|
||||
|
||||
const conditions = [eq(formResponses.formId, formId)];
|
||||
if (status) {
|
||||
conditions.push(eq(formResponses.status, status));
|
||||
}
|
||||
|
||||
const [responses, totalCount] = await Promise.all([
|
||||
ctx.db.query.formResponses.findMany({
|
||||
where: and(...conditions),
|
||||
with: {
|
||||
participant: {
|
||||
columns: {
|
||||
id: true,
|
||||
participantCode: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(formResponses.submittedAt)],
|
||||
limit,
|
||||
offset,
|
||||
}),
|
||||
ctx.db
|
||||
.select({ count: count() })
|
||||
.from(formResponses)
|
||||
.where(and(...conditions)),
|
||||
]);
|
||||
|
||||
return {
|
||||
responses,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total: totalCount[0]?.count ?? 0,
|
||||
pages: Math.ceil((totalCount[0]?.count ?? 0) / limit),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
submitResponse: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
formId: z.string().uuid(),
|
||||
participantId: z.string().uuid(),
|
||||
responses: z.record(z.string(), z.any()),
|
||||
signatureData: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { formId, participantId, responses, signatureData } = input;
|
||||
|
||||
const form = await ctx.db.query.forms.findFirst({
|
||||
where: eq(forms.id, formId),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Form not found",
|
||||
});
|
||||
}
|
||||
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, form.studyId);
|
||||
|
||||
const existingResponse = await ctx.db.query.formResponses.findFirst({
|
||||
where: and(
|
||||
eq(formResponses.formId, formId),
|
||||
eq(formResponses.participantId, participantId),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingResponse) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Participant has already submitted this form",
|
||||
});
|
||||
}
|
||||
|
||||
const [newResponse] = await ctx.db
|
||||
.insert(formResponses)
|
||||
.values({
|
||||
formId,
|
||||
participantId,
|
||||
responses,
|
||||
signatureData,
|
||||
status: signatureData ? "completed" : "pending",
|
||||
signedAt: signatureData ? new Date() : null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newResponse;
|
||||
}),
|
||||
|
||||
listVersions: protectedProcedure
|
||||
.input(z.object({ studyId: z.string().uuid() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
await checkStudyAccess(ctx.db, ctx.session.user.id, input.studyId);
|
||||
|
||||
const formsList = await ctx.db.query.forms.findMany({
|
||||
where: eq(forms.studyId, input.studyId),
|
||||
with: {
|
||||
createdBy: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [desc(forms.version)],
|
||||
});
|
||||
|
||||
const formsWithCounts = await Promise.all(
|
||||
formsList.map(async (form) => {
|
||||
const responseCount = await ctx.db
|
||||
.select({ count: count() })
|
||||
.from(formResponses)
|
||||
.where(eq(formResponses.formId, form.id));
|
||||
return { ...form, _count: { responses: responseCount[0]?.count ?? 0 } };
|
||||
})
|
||||
);
|
||||
|
||||
return formsWithCounts;
|
||||
}),
|
||||
});
|
||||
@@ -68,6 +68,29 @@ export const stepTypeEnum = pgEnum("step_type", [
|
||||
"conditional",
|
||||
]);
|
||||
|
||||
export const formTypeEnum = pgEnum("form_type", [
|
||||
"consent",
|
||||
"survey",
|
||||
"questionnaire",
|
||||
]);
|
||||
|
||||
export const formFieldTypeEnum = pgEnum("form_field_type", [
|
||||
"text",
|
||||
"textarea",
|
||||
"multiple_choice",
|
||||
"checkbox",
|
||||
"rating",
|
||||
"yes_no",
|
||||
"date",
|
||||
"signature",
|
||||
]);
|
||||
|
||||
export const formResponseStatusEnum = pgEnum("form_response_status", [
|
||||
"pending",
|
||||
"completed",
|
||||
"rejected",
|
||||
]);
|
||||
|
||||
export const communicationProtocolEnum = pgEnum("communication_protocol", [
|
||||
"rest",
|
||||
"ros2",
|
||||
@@ -594,6 +617,64 @@ export const consentForms = createTable(
|
||||
}),
|
||||
);
|
||||
|
||||
// New unified forms table
|
||||
export const forms = createTable(
|
||||
"form",
|
||||
{
|
||||
id: uuid("id").notNull().primaryKey().defaultRandom(),
|
||||
studyId: uuid("study_id")
|
||||
.notNull()
|
||||
.references(() => studies.id, { onDelete: "cascade" }),
|
||||
type: formTypeEnum("type").notNull(),
|
||||
title: varchar("title", { length: 255 }).notNull(),
|
||||
description: text("description"),
|
||||
version: integer("version").default(1).notNull(),
|
||||
active: boolean("active").default(true).notNull(),
|
||||
fields: jsonb("fields").notNull().default([]),
|
||||
settings: jsonb("settings").default({}),
|
||||
createdBy: text("created_by")
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
studyVersionUnique: unique().on(table.studyId, table.version),
|
||||
}),
|
||||
);
|
||||
|
||||
// Form responses/submissions
|
||||
export const formResponses = createTable(
|
||||
"form_response",
|
||||
{
|
||||
id: uuid("id").notNull().primaryKey().defaultRandom(),
|
||||
formId: uuid("form_id")
|
||||
.notNull()
|
||||
.references(() => forms.id, { onDelete: "cascade" }),
|
||||
participantId: uuid("participant_id")
|
||||
.notNull()
|
||||
.references(() => participants.id, { onDelete: "cascade" }),
|
||||
responses: jsonb("responses").notNull().default({}),
|
||||
status: formResponseStatusEnum("status").default("pending"),
|
||||
signatureData: text("signature_data"),
|
||||
signedAt: timestamp("signed_at", { withTimezone: true }),
|
||||
ipAddress: inet("ip_address"),
|
||||
submittedAt: timestamp("submitted_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
formParticipantUnique: unique().on(table.formId, table.participantId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const participantConsents = createTable(
|
||||
"participant_consent",
|
||||
{
|
||||
@@ -1118,6 +1199,29 @@ export const participantConsentsRelations = relations(
|
||||
}),
|
||||
);
|
||||
|
||||
export const formsRelations = relations(forms, ({ one, many }) => ({
|
||||
study: one(studies, {
|
||||
fields: [forms.studyId],
|
||||
references: [studies.id],
|
||||
}),
|
||||
createdBy: one(users, {
|
||||
fields: [forms.createdBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
responses: many(formResponses),
|
||||
}));
|
||||
|
||||
export const formResponsesRelations = relations(formResponses, ({ one }) => ({
|
||||
form: one(forms, {
|
||||
fields: [formResponses.formId],
|
||||
references: [forms.id],
|
||||
}),
|
||||
participant: one(participants, {
|
||||
fields: [formResponses.participantId],
|
||||
references: [participants.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const robotsRelations = relations(robots, ({ many }) => ({
|
||||
experiments: many(experiments),
|
||||
plugins: many(plugins),
|
||||
|
||||
Reference in New Issue
Block a user