Files
erika/db/schema/form.ts
T
2026-07-31 21:14:02 +07:00

121 lines
3.7 KiB
TypeScript

import {
pgSchema,
pgTable,
text,
timestamp,
boolean,
integer,
jsonb,
index,
customType,
} from "drizzle-orm/pg-core";
import { uuidv7 } from "uuidv7";
export const formSchema = pgSchema("form");
export const bytea = customType<{
data: Buffer;
driverData: Buffer;
}>({
dataType() {
return "bytea";
},
fromDriver(value) {
return Buffer.isBuffer(value) ? value : Buffer.from(value);
},
});
export const forms = formSchema.table("form", {
id: text("id")
.primaryKey()
.$defaultFn(() => uuidv7()),
title: text("title").notNull(),
description: text("description"),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
isOpen: boolean("is_open").notNull().default(true),
closesAt: timestamp("closes_at", { mode: "date", withTimezone: true }),
discordWebhookUrl: text("discord_webhook_url"),
discordWebhookTemplate: text("discord_webhook_template"),
discordWebhookUpdateTemplate: text("discord_webhook_update_template"),
allowedRoles: jsonb("allowed_roles").$type<string[]>().default([]),
deniedRoles: jsonb("denied_roles").$type<string[]>().default([]),
allowMultipleSubmissions: boolean("allow_multiple_submissions").notNull().default(false),
});
export type QuestionType = "text" | "textarea" | "radio" | "checkbox";
export type ImageAnswerMode = "none" | "optional" | "required";
export const questions = formSchema.table("question", {
id: text("id")
.primaryKey()
.$defaultFn(() => uuidv7()),
formId: text("form_id")
.notNull()
.references(() => forms.id, { onDelete: "cascade" }),
type: text("type").$type<QuestionType>().notNull().default("text"),
label: text("label").notNull(),
imageUrl: text("image_url"),
displayOrder: integer("display_order").notNull().default(0),
required: boolean("required").notNull().default(false),
options: jsonb("options").$type<string[]>().default([]),
allowOther: boolean("allow_other").notNull().default(false),
imageAnswerMode: text("image_answer_mode")
.$type<ImageAnswerMode>()
.notNull()
.default("none"),
});
export const cdn = pgTable("cdn", {
id: text("id")
.primaryKey()
.$defaultFn(() => uuidv7()),
data: bytea("data").notNull(),
name: text("name"),
type: text("type").notNull(),
size: integer("size").notNull(),
ownerDiscordId: text("owner_discord_id"),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
});
export const submissions = formSchema.table(
"submission",
{
id: text("id")
.primaryKey()
.$defaultFn(() => uuidv7()),
formId: text("form_id")
.notNull()
.references(() => forms.id, { onDelete: "cascade" }),
userDiscordId: text("user_discord_id"),
userName: text("user_name"),
adminNote: text("admin_note"),
submittedAt: timestamp("submitted_at", { mode: "date" }).defaultNow().notNull(),
editHistory: jsonb("edit_history")
.$type<{ editedAt: string; oldAnswers: Record<string, { value: string; imageCdnId: string | null }> }[]>()
.default([]),
},
(submission) => [
index("submission_form_submitted_at_id_idx").on(
submission.formId,
submission.submittedAt,
submission.id
),
]
);
export const answers = formSchema.table("answer", {
id: text("id")
.primaryKey()
.$defaultFn(() => uuidv7()),
submissionId: text("submission_id")
.notNull()
.references(() => submissions.id, { onDelete: "cascade" }),
questionId: text("question_id")
.notNull()
.references(() => questions.id, { onDelete: "cascade" }),
value: text("value").notNull().default(""),
imageCdnId: text("image_cdn_id").references(() => cdn.id, {
onUpdate: "cascade",
onDelete: "set null",
}),
});