From 99ad52107a01e667627d8e17c0b7505d89021080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lipt=C3=A1k=20P=C3=A9ter?= Date: Sun, 6 Sep 2026 20:08:21 +0200 Subject: [PATCH 1/6] feat: add configurable applications, private CV uploads and exhibition settings --- package.json | 1 + src/app/(payload)/admin/importMap.js | 2 + src/app/api/karrier/jelentkezes/route.ts | 154 +- src/collections/ApplicationCVs.ts | 27 + src/collections/CareerSettings.ts | 79 +- src/collections/Contact.ts | 90 +- src/collections/JobApplications.ts | 61 +- .../payload/FormQuestionRowLabel.tsx | 8 + src/fields/career-form-fields.ts | 125 + src/lib/application-upload.ts | 89 + src/lib/career-form.ts | 270 + src/lib/google-sheets.ts | 28 +- src/lib/karrier-sheet-sync.ts | 66 +- .../20260906_154658_contact_baseline.ts | 25 + ...0260906_154659_career_form_cv_contact.json | 4506 +++++++++++++++++ .../20260906_154659_career_form_cv_contact.ts | 164 + src/migrations/index.ts | 10 +- src/payload-types.ts | 117 +- src/payload.config.ts | 2 + yarn.lock | 29 + 20 files changed, 5735 insertions(+), 118 deletions(-) create mode 100644 src/collections/ApplicationCVs.ts create mode 100644 src/components/payload/FormQuestionRowLabel.tsx create mode 100644 src/fields/career-form-fields.ts create mode 100644 src/lib/application-upload.ts create mode 100644 src/lib/career-form.ts create mode 100644 src/migrations/20260906_154658_contact_baseline.ts create mode 100644 src/migrations/20260906_154659_career_form_cv_contact.json create mode 100644 src/migrations/20260906_154659_career_form_cv_contact.ts diff --git a/package.json b/package.json index 2ced8c0..ce29272 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "lucide-react": "^0.511.0", "next": "15.5.9", "payload": "^3.80.0", + "pdf-lib": "^1.17.1", "react": "19.2.1", "react-dom": "19.2.1", "tailwind-merge": "^3.3.0", diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 76f43f8..f91df03 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -24,6 +24,7 @@ import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0 import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BulkUpload as BulkUpload_f78d2ec679640e7d1f31ce1bfa4679ca } from '../../../components/payload/BulkUpload' import { ResyncSheetButton as ResyncSheetButton_fc506d5d7c6da31bb9047d09d1630842 } from '../../../components/payload/ResyncSheetButton' +import { FormQuestionRowLabel as FormQuestionRowLabel_a7bcd82cc9f2793c5e59ebf5fa65fa20 } from '../../../components/payload/FormQuestionRowLabel' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' export const importMap = { @@ -53,5 +54,6 @@ export const importMap = { "@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "/components/payload/BulkUpload#BulkUpload": BulkUpload_f78d2ec679640e7d1f31ce1bfa4679ca, "/components/payload/ResyncSheetButton#ResyncSheetButton": ResyncSheetButton_fc506d5d7c6da31bb9047d09d1630842, + "/components/payload/FormQuestionRowLabel#FormQuestionRowLabel": FormQuestionRowLabel_a7bcd82cc9f2793c5e59ebf5fa65fa20, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/src/app/api/karrier/jelentkezes/route.ts b/src/app/api/karrier/jelentkezes/route.ts index ee58d39..5e75fff 100644 --- a/src/app/api/karrier/jelentkezes/route.ts +++ b/src/app/api/karrier/jelentkezes/route.ts @@ -1,59 +1,137 @@ -import { parseApplicationInput } from "@/lib/karrier-application"; +import { randomUUID } from "node:crypto"; +import { + ApplicationBodyError, + readApplicationBody, + validateCV, +} from "@/lib/application-upload"; +import { + resolveFormConfig, + validateAnswers, + summarizeAnswers, + type CoreKey, +} from "@/lib/career-form"; +import { syncApplicationToSheet } from "@/lib/karrier-sheet-sync"; import { clientIp, createRateLimiter } from "@/lib/rate-limit"; import config from "@payload-config"; import { NextResponse } from "next/server"; import { getPayload } from "payload"; -/** - * A Karrier oldal beépített jelentkezési űrlapjának végpontja. - * - * A job-applications kollekció create jogosultsága zárt, ezért a beszúrás - * innen, overrideAccess-szel történik — így a nyilvános Payload REST végponton - * nem lehet közvetlenül jelentkezést létrehozni. - */ - +export const runtime = "nodejs"; const isRateLimited = createRateLimiter({ windowMs: 60_000, max: 5 }); - -const errorResponse = (error: string, status: number) => NextResponse.json({ error }, { status }); +const errorResponse = (error: string, status: number) => + NextResponse.json({ error }, { status }); export async function POST(request: Request) { - if (isRateLimited(clientIp(request))) { + if (isRateLimited(clientIp(request))) return errorResponse("TOO_MANY_REQUESTS", 429); - } - - let body: Record; - try { - body = await request.json(); - } catch { - return errorResponse("INVALID_BODY", 400); - } - - const parsed = parseApplicationInput(body); - - if (!parsed.ok) { - // A botnak sikeres választ adunk, hogy ne tudja kitalálni a csapdát. - return parsed.error === "HONEYPOT" - ? NextResponse.json({ ok: true }) - : errorResponse(parsed.error, 400); - } - + let cvId: number | undefined; + const payload = await getPayload({ config }); try { - const payload = await getPayload({ config }); - + const { body, cv } = await readApplicationBody(request); + if (typeof body.website === "string" && body.website.trim()) + return NextResponse.json({ ok: true }); const settings = await payload.findGlobal({ slug: "career-settings" }); - if (settings?.applicationsOpen === false) { + if ( + settings.applicationsOpen === false || + settings.applicationMode === "googleForm" + ) return errorResponse("APPLICATIONS_CLOSED", 403); + const formConfig = resolveFormConfig(settings); + if ( + body.formVersion !== undefined && + body.formVersion !== formConfig.version + ) + return NextResponse.json( + { error: "FORM_CHANGED", config: formConfig }, + { status: 409 }, + ); + const input = body.answers ?? body; + if (!input || typeof input !== "object" || Array.isArray(input)) + return errorResponse("INVALID_BODY", 400); + const parsed = validateAnswers( + input as Record, + formConfig, + ); + if (!parsed.ok) + return NextResponse.json( + { error: parsed.error, field: parsed.field }, + { status: 400 }, + ); + const language = body.language === "en" ? "en" : "hu"; + if (cv) { + const bytes = await validateCV(cv); + const uploaded = await payload.create({ + collection: "application-cvs", + overrideAccess: true, + data: {}, + file: { + data: bytes, + name: `${randomUUID()}.pdf`, + mimetype: "application/pdf", + size: bytes.length, + }, + }); + cvId = uploaded.id; } - - await payload.create({ + const textValue = (key: CoreKey) => + typeof parsed.values[key] === "string" + ? (parsed.values[key] as string) + : ""; + const doc = await payload.create({ collection: "job-applications", overrideAccess: true, - data: parsed.data, + context: { skipSheetSync: true }, + data: { + name: textValue("name"), + email: textValue("email"), + phone: textValue("phone"), + university: textValue("university"), + major: textValue("major"), + semester: textValue("semester"), + groupName: textValue("groupName"), + positionName: textValue("positionName"), + motivation: textValue("motivation"), + consent: true, + cv: cvId, + language, + formVersion: formConfig.version, + answerSnapshot: parsed.snapshots, + answerSummary: summarizeAnswers( + parsed.snapshots, + language === "en", + true, + ), + }, }); - + // The application and file are committed before contacting the external spreadsheet. + cvId = undefined; + await syncApplicationToSheet(payload, doc); return NextResponse.json({ ok: true }); } catch (error) { - console.error("Jelentkezés mentése sikertelen:", error); + if (cvId) { + try { + await payload.delete({ + collection: "application-cvs", + id: cvId, + overrideAccess: true, + }); + } catch (cleanupError) { + payload.logger.error( + { + errorType: + cleanupError instanceof Error ? cleanupError.name : "unknown", + cvId, + }, + "Árva önéletrajz takarítása sikertelen", + ); + } + } + if (error instanceof ApplicationBodyError) + return errorResponse(error.code, error.status); + payload.logger.error( + { errorType: error instanceof Error ? error.name : "unknown" }, + "Jelentkezés mentése sikertelen", + ); return errorResponse("SERVER_ERROR", 500); } } diff --git a/src/collections/ApplicationCVs.ts b/src/collections/ApplicationCVs.ts new file mode 100644 index 0000000..672744e --- /dev/null +++ b/src/collections/ApplicationCVs.ts @@ -0,0 +1,27 @@ +import path from "node:path" +import type { CollectionConfig } from "payload" + +export const ApplicationCVs: CollectionConfig = { + slug: "application-cvs", + labels: { singular: "Önéletrajz", plural: "Önéletrajzok" }, + admin: { hidden: true }, + access: { + create: () => false, + read: ({ req }) => !!req.user, + update: () => false, + delete: () => false, + }, + upload: { + staticDir: + process.env.CV_STORAGE_DIR || + path.resolve(process.cwd(), "private/cvs"), + mimeTypes: ["application/pdf"], + modifyResponseHeaders: ({ headers }) => { + headers.set("Cache-Control", "private, no-store") + headers.set("X-Content-Type-Options", "nosniff") + headers.set("Content-Disposition", "attachment") + return headers + }, + }, + fields: [], +} diff --git a/src/collections/CareerSettings.ts b/src/collections/CareerSettings.ts index e218fef..b9db03a 100644 --- a/src/collections/CareerSettings.ts +++ b/src/collections/CareerSettings.ts @@ -1,13 +1,20 @@ -import { checkSpreadsheetAccess, serviceAccountEmail } from "@/lib/google-sheets"; -import { FixedToolbarFeature, lexicalEditor } from "@payloadcms/richtext-lexical"; -import type { GlobalConfig } from "payload"; +import { + checkSpreadsheetAccess, + serviceAccountEmail, +} from "@/lib/google-sheets"; +import { + FixedToolbarFeature, + lexicalEditor, +} from "@payloadcms/richtext-lexical"; +import { ValidationError, type GlobalConfig } from "payload"; +import { careerFormFields } from "../fields/career-form-fields"; const spreadsheetHelp = () => { const email = serviceAccountEmail(); return email - ? `Illeszd be a Google táblázat linkjét, és oszd meg a táblázatot Szerkesztő jogosultsággal ezzel a címmel: ${email} — a fejlécsort automatikusan létrehozzuk. Üresen hagyva a jelentkezések csak ide, az adminba érkeznek.` - : "A táblázatba írás nincs beállítva a szerveren (hiányzik a Google szolgáltatásfiók). Szólj a fejlesztőknek, addig a jelentkezések csak ide, az adminba érkeznek."; + ? `Illeszd be a Google táblázat linkjét, és oszd meg a táblázatot Szerkesztő jogosultsággal ezzel a címmel: ${email} — a fejlécsort automatikusan létrehozzuk. Üresen hagyva a jelentkezések csak ide, az adminba érkeznek.` + : "A táblázatba írás nincs beállítva a szerveren (hiányzik a Google szolgáltatásfiók). Szólj a fejlesztőknek, addig a jelentkezések csak ide, az adminba érkeznek."; }; export const CareerSettings: GlobalConfig = { @@ -18,16 +25,56 @@ export const CareerSettings: GlobalConfig = { }, admin: { description: - "A Karrier oldal bevezetője és a jelentkezés módjának beállítása.", + "A Karrier oldal bevezetője és a jelentkezés módjának beállítása.", }, access: { read: () => true, update: ({ req: { user } }) => !!user, }, + hooks: { + beforeValidate: [ + ({ data, originalDoc }) => { + // A label edit must never silently reassign historical answers to another key. + for (const question of data?.formQuestions ?? []) { + const previous = originalDoc?.formQuestions?.find( + (q: { id?: string }) => q.id && q.id === question.id, + ); + if (previous && previous.key !== question.key) + throw new ValidationError({ + errors: [ + { + path: "formQuestions", + message: "A kérdés állandó azonosítója nem módosítható.", + }, + ], + }); + for (const option of question.options ?? []) { + const old = previous?.options?.find( + (o: { id?: string }) => o.id && o.id === option.id, + ); + if (old && old.key !== option.key) + throw new ValidationError({ + errors: [ + { + path: "formQuestions", + message: "A válaszlehetőség azonosítója nem módosítható.", + }, + ], + }); + } + } + return data; + }, + ], + }, fields: [ { type: "tabs", tabs: [ + { + label: { hu: "Űrlap kérdései", en: "Form questions" }, + fields: careerFormFields, + }, { label: { en: "Intro", @@ -40,7 +87,8 @@ export const CareerSettings: GlobalConfig = { required: false, label: "Általános leírás", admin: { - description: "Az oldal tetején, a csoportok listája fölött jelenik meg.", + description: + "Az oldal tetején, a csoportok listája fölött jelenik meg.", }, editor: lexicalEditor({ features: ({ defaultFeatures }) => [ @@ -81,7 +129,7 @@ export const CareerSettings: GlobalConfig = { ], admin: { description: - "Beépített űrlap esetén a jelentkezések a Payload adminba érkeznek (és a Google táblázatba, ha be van állítva a webhook).", + "Beépített űrlap esetén a jelentkezések a Payload adminba érkeznek (és a Google táblázatba, ha be van állítva a táblázat).", }, }, { @@ -90,7 +138,8 @@ export const CareerSettings: GlobalConfig = { required: false, label: "Google űrlap linkje", admin: { - description: "Csak a „Külső Google űrlap” mód esetén használjuk.", + description: + "Csak a „Külső Google űrlap” mód esetén használjuk.", condition: (data) => data?.applicationMode === "googleForm", }, }, @@ -105,15 +154,15 @@ export const CareerSettings: GlobalConfig = { }, // Mentéskor rögtön kiderül, ha a táblázat nincs megosztva velünk. validate: async (value: string | null | undefined) => { - if (!value?.trim()) return true + if (!value?.trim()) return true; - const result = await checkSpreadsheetAccess(value) + const result = await checkSpreadsheetAccess(value); if (result.status === "error") { - return `A táblázat nem érhető el: ${result.message}. Ellenőrizd a linket, és hogy megosztottad-e Szerkesztő jogosultsággal.` + return `A táblázat nem érhető el: ${result.message}. Ellenőrizd a linket, és hogy megosztottad-e Szerkesztő jogosultsággal.`; } - return true + return true; }, }, { @@ -123,7 +172,7 @@ export const CareerSettings: GlobalConfig = { label: "Jelentkezés nyitva", admin: { description: - "Kikapcsolva az oldal továbbra is elérhető, de a jelentkezési űrlap és a „Jelentkezz” gomb helyett egy tájékoztató szöveg jelenik meg.", + "Kikapcsolva az oldal továbbra is elérhető, de a jelentkezési űrlap és a „Jelentkezz” gomb helyett egy tájékoztató szöveg jelenik meg.", }, }, { @@ -163,7 +212,7 @@ export const CareerSettings: GlobalConfig = { }, admin: { description: - "A kérdések ebben a sorrendben jelennek meg a Karrier oldal alján. Üresen hagyva a GYIK szekció nem jelenik meg.", + "A kérdések ebben a sorrendben jelennek meg a csoportok után, a jelentkezés előtt. Üresen hagyva a GYIK szekció nem jelenik meg.", }, fields: [ { diff --git a/src/collections/Contact.ts b/src/collections/Contact.ts index 7fabd84..c8a1071 100644 --- a/src/collections/Contact.ts +++ b/src/collections/Contact.ts @@ -1,66 +1,98 @@ -import { CollectionConfig, getPayload } from 'payload'; -import config from "@payload-config"; -import { FixedToolbarFeature, lexicalEditor } from "@payloadcms/richtext-lexical"; +import { CollectionConfig } from "payload"; +import { + FixedToolbarFeature, + lexicalEditor, +} from "@payloadcms/richtext-lexical"; const Contact: CollectionConfig = { - slug: 'contact', + slug: "contact", admin: { - useAsTitle: 'title', - description: 'A kapcsolat oldal szerkeszthető tartalma', + useAsTitle: "title", + description: "A kapcsolat oldal szerkeszthető tartalma", }, labels: { - singular: 'Kapcsolat', - plural: 'Kapcsolat', + singular: "Kapcsolat", + plural: "Kapcsolat", }, access: { - create: async ({}) => { - const payload = await getPayload({ config }) - const docs = await payload.find({ collection: 'contact', limit: 1 }); + create: async ({ req: { user, payload } }) => { + if (!user) return false; + const docs = await payload.find({ collection: "contact", limit: 1 }); return docs.totalDocs === 0; }, - update: () => true, + update: ({ req: { user } }) => !!user, delete: () => false, read: () => true, }, fields: [ { - name: 'title', - type: 'text', - label: 'Cím', + name: "exhibitionUrl", + type: "text", + label: "Kiállítási igény – űrlap linkje", + defaultValue: + "https://docs.google.com/forms/d/e/1FAIpQLSdrA3VX3YpxQB3uZ1ojXov8-J3LPawAt4ip_Aj4k8FNbNOCrA/viewform?usp=dialog", + admin: { description: "Üresen hagyva a gomb nem jelenik meg." }, + validate: (value: string | null | undefined) => { + if (!value?.trim()) return true; + try { + const url = new URL(value); + return url.protocol === "https:" && !url.username && !url.password + ? true + : "HTTPS link szükséges."; + } catch { + return "Érvényes HTTPS link szükséges."; + } + }, + }, + { + name: "exhibitionLabel", + type: "text", + label: "Kiállítási gomb felirata", + defaultValue: "Rendezvényen való kiállítási igény", + }, + { + name: "exhibitionLabelEng", + type: "text", + label: "Kiállítási gomb felirata (angol)", + defaultValue: "Event Exhibition Request", + }, + { + name: "title", + type: "text", + label: "Cím", required: true, }, { - name: 'title_en', - type: 'text', - label: 'Cím (angol)', + name: "title_en", + type: "text", + label: "Cím (angol)", required: true, }, { - name: 'content', - type: 'richText', - label: 'Tartalom', + name: "content", + type: "richText", + label: "Tartalom", required: true, editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), - ] - }) + ], + }), }, { - name: 'content_en', - type: 'richText', - label: 'Tartalom (angol)', + name: "content_en", + type: "richText", + label: "Tartalom (angol)", required: true, editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), - ] - }) + ], + }), }, ], }; export default Contact; - diff --git a/src/collections/JobApplications.ts b/src/collections/JobApplications.ts index 24fcbb3..8827756 100644 --- a/src/collections/JobApplications.ts +++ b/src/collections/JobApplications.ts @@ -1,8 +1,17 @@ import { syncApplicationToSheet } from "@/lib/karrier-sheet-sync"; -import type { CollectionAfterChangeHook, CollectionConfig, PayloadHandler } from "payload"; +import type { + CollectionAfterChangeHook, + CollectionConfig, + PayloadHandler, +} from "payload"; /** Új jelentkezés automatikus kiírása a beállított Google táblázatba. */ -const syncOnCreate: CollectionAfterChangeHook = async ({ doc, operation, req, context }) => { +const syncOnCreate: CollectionAfterChangeHook = async ({ + doc, + operation, + req, + context, +}) => { if (operation !== "create" || context?.skipSheetSync) { return doc; } @@ -33,7 +42,9 @@ const resyncHandler: PayloadHandler = async (req) => { const result = await syncApplicationToSheet(req.payload, doc); - return Response.json(result, { status: result.status === "error" ? 502 : 200 }); + return Response.json(result, { + status: result.status === "error" ? 502 : 200, + }); }; export const JobApplications: CollectionConfig = { @@ -56,6 +67,38 @@ export const JobApplications: CollectionConfig = { delete: ({ req: { user } }) => !!user, }, fields: [ + { + name: "cv", + type: "upload", + relationTo: "application-cvs", + label: "Önéletrajz (PDF)", + admin: { readOnly: true }, + }, + { + name: "formVersion", + type: "text", + label: "Űrlap verziója", + admin: { readOnly: true }, + }, + { + name: "language", + type: "select", + options: ["hu", "en"], + label: "Beküldés nyelve", + admin: { readOnly: true }, + }, + { + name: "answerSummary", + type: "textarea", + label: "Beküldött válaszok", + admin: { readOnly: true }, + }, + { + name: "answerSnapshot", + type: "json", + label: "Kérdések és válaszok a beküldéskor", + admin: { readOnly: true }, + }, { name: "name", type: "text", @@ -165,5 +208,17 @@ export const JobApplications: CollectionConfig = { ], hooks: { afterChange: [syncOnCreate], + afterDelete: [ + async ({ doc, req }) => { + const id = typeof doc.cv === "object" ? doc.cv?.id : doc.cv; + if (id) + await req.payload.delete({ + collection: "application-cvs", + id, + overrideAccess: true, + req, + }); + }, + ], }, }; diff --git a/src/components/payload/FormQuestionRowLabel.tsx b/src/components/payload/FormQuestionRowLabel.tsx new file mode 100644 index 0000000..2b31004 --- /dev/null +++ b/src/components/payload/FormQuestionRowLabel.tsx @@ -0,0 +1,8 @@ +"use client" + +import { useRowLabel } from "@payloadcms/ui" + +export function FormQuestionRowLabel() { + const { data, rowNumber } = useRowLabel<{ label?: string }>() + return {data?.label || `Kérdés ${(rowNumber ?? 0) + 1}`} +} diff --git a/src/fields/career-form-fields.ts b/src/fields/career-form-fields.ts new file mode 100644 index 0000000..d8ab3e3 --- /dev/null +++ b/src/fields/career-form-fields.ts @@ -0,0 +1,125 @@ +import { randomUUID } from "node:crypto" +import type { Field } from "payload" +import { + DEFAULT_QUESTIONS, + validateQuestions, + type FormQuestion, +} from "../lib/career-form" + +export const careerFormFields: Field[] = [ + { + name: "formQuestions", + type: "array", + label: "Űrlap kérdései", + maxRows: 50, + defaultValue: DEFAULT_QUESTIONS, + labels: { singular: "Kérdés", plural: "Kérdések" }, + admin: { + components: { + RowLabel: + "/components/payload/FormQuestionRowLabel#FormQuestionRowLabel", + }, + initCollapsed: true, + description: + "Sorrend: húzd a kérdéseket a helyükre. A hozzájárulás mindig a CV után jelenik meg. A név, e-mail és hozzájárulás kötelező. A csoport és pozíció az automatikus kitöltés miatt nem törölhető.", + }, + validate: (value) => + validateQuestions((value ?? DEFAULT_QUESTIONS) as FormQuestion[]), + fields: [ + { + name: "key", + type: "text", + required: true, + defaultValue: () => randomUUID(), + label: "Állandó azonosító", + admin: { readOnly: true, hidden: true }, + }, + { + name: "label", + type: "text", + required: true, + maxLength: 500, + label: "Kérdés (magyar)", + }, + { + name: "labelEng", + type: "text", + required: true, + maxLength: 500, + label: "Kérdés (angol)", + }, + { + name: "hint", + type: "text", + maxLength: 1000, + label: "Súgó (magyar)", + }, + { + name: "hintEng", + type: "text", + maxLength: 1000, + label: "Súgó (angol)", + }, + { + name: "type", + type: "select", + required: true, + defaultValue: "text", + label: "Válasz típusa", + options: [ + { label: "Rövid szöveg", value: "text" }, + { label: "Hosszú szöveg", value: "textarea" }, + { label: "Egy választás", value: "select" }, + { label: "Több választás", value: "multiselect" }, + { label: "Jelölőnégyzet", value: "checkbox" }, + ], + }, + { + name: "required", + type: "checkbox", + label: "Kötelező", + defaultValue: false, + }, + { + name: "hidden", + type: "checkbox", + label: "Elrejtve", + defaultValue: false, + }, + { + name: "options", + type: "array", + label: "Válaszlehetőségek", + maxRows: 30, + admin: { + condition: (_, data) => + ["select", "multiselect"].includes(data?.type), + }, + fields: [ + { + name: "key", + type: "text", + required: true, + defaultValue: () => randomUUID(), + label: "Állandó azonosító", + admin: { readOnly: true, hidden: true }, + }, + { + name: "label", + type: "text", + required: true, + maxLength: 300, + label: "Felirat (magyar)", + }, + { + name: "labelEng", + type: "text", + required: true, + maxLength: 300, + label: "Felirat (angol)", + }, + ], + }, + ], + }, +] diff --git a/src/lib/application-upload.ts b/src/lib/application-upload.ts new file mode 100644 index 0000000..f50f3d5 --- /dev/null +++ b/src/lib/application-upload.ts @@ -0,0 +1,89 @@ +import { PDFDocument } from "pdf-lib" +import { MAX_CV_SIZE } from "./career-form" + +export const MAX_APPLICATION_BODY = MAX_CV_SIZE + 256 * 1024 +export class ApplicationBodyError extends Error { + constructor( + public code: string, + public status = 400, + ) { + super(code) + } +} +/** Bound actual streamed bytes as well as declared Content-Length before multipart parsing. */ +export async function readApplicationBody( + request: Request, +): Promise<{ body: Record; cv: File | null }> { + if (Number(request.headers.get("content-length")) > MAX_APPLICATION_BODY) + throw new ApplicationBodyError("BODY_TOO_LARGE", 413) + const reader = request.body?.getReader() + if (!reader) throw new ApplicationBodyError("INVALID_BODY") + const chunks: Uint8Array[] = [] + let size = 0 + try { + while (true) { + const { value, done } = await reader.read() + if (done) break + size += value.byteLength + if (size > MAX_APPLICATION_BODY) { + await reader.cancel() + throw new ApplicationBodyError("BODY_TOO_LARGE", 413) + } + chunks.push(value) + } + } finally { + reader.releaseLock() + } + const bytes = Buffer.concat(chunks) + const contentType = request.headers.get("content-type") ?? "" + try { + let body: unknown + let cv: File | null = null + if (contentType.startsWith("multipart/form-data")) { + const data = await new Response(bytes, { + headers: { "Content-Type": contentType }, + }).formData() + const files = data.getAll("cv") + if (files.length > 1 || (files[0] && typeof files[0] === "string")) + throw new ApplicationBodyError("INVALID_CV") + cv = files[0] instanceof File && files[0].size ? files[0] : null + const json = data.get("data") + if (typeof json !== "string") + throw new ApplicationBodyError("INVALID_BODY") + body = JSON.parse(json) + } else if (contentType.startsWith("application/json")) + body = JSON.parse(bytes.toString("utf8")) + else throw new ApplicationBodyError("INVALID_BODY", 415) + if (!body || typeof body !== "object" || Array.isArray(body)) + throw new ApplicationBodyError("INVALID_BODY") + return { body: body as Record, cv } + } catch (error) { + if (error instanceof ApplicationBodyError) throw error + throw new ApplicationBodyError("INVALID_BODY") + } +} +export async function validateCV(file: File): Promise { + if (file.size > MAX_CV_SIZE) + throw new ApplicationBodyError("CV_TOO_LARGE", 413) + if ( + !/\.pdf$/i.test(file.name) || + (file.type && file.type !== "application/pdf") + ) + throw new ApplicationBodyError("INVALID_CV") + const bytes = Buffer.from(await file.arrayBuffer()) + if ( + bytes.subarray(0, 5).toString() !== "%PDF-" || + !bytes.subarray(-1024).includes(Buffer.from("%%EOF")) + ) + throw new ApplicationBodyError("INVALID_CV") + try { + const pdf = await PDFDocument.load(bytes, { + updateMetadata: false, + throwOnInvalidObject: true, + }) + if (pdf.getPageCount() === 0) throw new Error("Empty PDF") + } catch { + throw new ApplicationBodyError("INVALID_CV") + } + return bytes +} diff --git a/src/lib/career-form.ts b/src/lib/career-form.ts new file mode 100644 index 0000000..3a24c58 --- /dev/null +++ b/src/lib/career-form.ts @@ -0,0 +1,270 @@ +/** Shared browser/server form contract. Keys identify answers independently of labels. */ +export const CORE_KEYS = [ + "name", + "email", + "phone", + "university", + "major", + "semester", + "groupName", + "positionName", + "motivation", + "consent", +] as const +export type CoreKey = (typeof CORE_KEYS)[number] +export type QuestionType = + | "text" + | "textarea" + | "select" + | "multiselect" + | "checkbox" +export type Answer = string | string[] | boolean +export interface QuestionOption { + key: string + label: string + labelEng: string +} +export interface FormQuestion { + key: string + label: string + labelEng: string + hint?: string | null + hintEng?: string | null + type: QuestionType + required?: boolean | null + hidden?: boolean | null + options?: QuestionOption[] | null +} +export interface FormConfig { + version: string + questions: FormQuestion[] +} +export const MAX_CV_SIZE = 5 * 1024 * 1024 +export const DEFAULT_QUESTIONS: FormQuestion[] = [ + { + key: "name", + label: "Név", + labelEng: "Name", + type: "text", + required: true, + }, + { + key: "email", + label: "E-mail cím", + labelEng: "Email address", + type: "text", + required: true, + }, + { + key: "phone", + label: "Telefonszám", + labelEng: "Phone number", + type: "text", + }, + { + key: "semester", + label: "Hányadik félév", + labelEng: "Semester", + type: "text", + }, + { + key: "university", + label: "Egyetem / kar", + labelEng: "University / faculty", + type: "text", + }, + { key: "major", label: "Szak", labelEng: "Major", type: "text" }, + { key: "groupName", label: "Csoport", labelEng: "Group", type: "text" }, + { + key: "positionName", + label: "Pozíció", + labelEng: "Position", + type: "text", + }, + { + key: "motivation", + label: "Motiváció / üzenet", + labelEng: "Motivation / message", + type: "textarea", + }, + { + key: "consent", + label: "Hozzájárulok a személyes adataim kezeléséhez a jelentkezés elbírálása céljából.", + labelEng: + "I consent to the processing of my personal data for the purpose of the application.", + type: "checkbox", + required: true, + }, +] +export function resolveFormConfig( + settings?: { + formQuestions?: FormQuestion[] | null + updatedAt?: string | null + } | null, +): FormConfig { + return { + version: settings?.updatedAt ?? "initial", + questions: settings?.formQuestions?.length + ? settings.formQuestions + : DEFAULT_QUESTIONS, + } +} +export function validateQuestions(questions: FormQuestion[]): true | string { + if (!questions.length || questions.length > 50) + return "Az űrlap 1–50 kérdést tartalmazhat." + const keys = new Set() + for (const q of questions) { + if ( + !q.key || + !/^[a-zA-Z0-9_-]{1,64}$/.test(q.key) || + keys.has(q.key) || + ["__proto__", "constructor", "prototype"].includes(q.key) + ) + return "A kérdések azonosítói legyenek egyediek." + keys.add(q.key) + if (!q.label?.trim() || !q.labelEng?.trim()) + return "Minden kérdés magyar és angol felirata kötelező." + const core = DEFAULT_QUESTIONS.find((item) => item.key === q.key) + if (core && q.type !== core.type) + return `Az alapmező típusa nem változtatható: ${core.label}.` + if (q.type === "select" || q.type === "multiselect") { + if (!q.options?.length || q.options.length > 30) + return "A választós kérdésekhez 1–30 lehetőség szükséges." + const options = new Set() + for (const option of q.options) { + if ( + !option.key || + options.has(option.key) || + !option.label?.trim() || + !option.labelEng?.trim() + ) + return "A lehetőségek azonosítója legyen egyedi, mindkét felirat kötelező." + options.add(option.key) + } + } + } + for (const key of ["name", "email", "consent"]) { + const q = questions.find((item) => item.key === key) + if (!q || q.hidden || !q.required) + return "A név, e-mail és hozzájárulás kötelező, látható alapmező." + } + for (const key of ["groupName", "positionName"]) { + if (!questions.some((item) => item.key === key)) + return "A csoport és pozíció alapmező nem törölhető (elrejthető)." + } + return true +} +export interface AnswerSnapshot { + key: string + label: string + labelEng: string + type: QuestionType + options: QuestionOption[] + value: Answer +} +const LIMITS: Record = { + name: 120, + email: 200, + phone: 40, + university: 120, + major: 120, + semester: 40, + groupName: 200, + positionName: 200, + motivation: 5000, +} +export function validateAnswers( + input: Record, + config: FormConfig, +): + | { ok: true; values: Record; snapshots: AnswerSnapshot[] } + | { ok: false; error: string; field?: string } { + const values: Record = {} + const snapshots: AnswerSnapshot[] = [] + for (const q of config.questions) { + // Hidden group/position still carry the position button's selection. + if (q.hidden && q.key !== "groupName" && q.key !== "positionName") + continue + const raw = input[q.key] + let value: Answer + if (q.type === "checkbox") { + if (raw !== undefined && typeof raw !== "boolean") + return { ok: false, error: "INVALID_ANSWER", field: q.key } + value = raw === true + } else if (q.type === "multiselect") { + if ( + raw !== undefined && + (!Array.isArray(raw) || raw.some((v) => typeof v !== "string")) + ) + return { ok: false, error: "INVALID_ANSWER", field: q.key } + value = [...new Set((raw ?? []) as string[])] + if (value.some((v) => !q.options?.some((o) => o.key === v))) + return { ok: false, error: "INVALID_ANSWER", field: q.key } + } else { + if (raw !== undefined && raw !== null && typeof raw !== "string") + return { ok: false, error: "INVALID_ANSWER", field: q.key } + value = typeof raw === "string" ? raw.trim() : "" + if ( + value.length > + (LIMITS[q.key] ?? (q.type === "textarea" ? 5000 : 500)) + ) + return { ok: false, error: "ANSWER_TOO_LONG", field: q.key } + if ( + q.type === "select" && + value && + !q.options?.some((o) => o.key === value) + ) + return { ok: false, error: "INVALID_ANSWER", field: q.key } + } + if ( + q.required && + !q.hidden && + (value === false || + (typeof value !== "boolean" && value.length === 0)) + ) + return { ok: false, error: "MISSING_FIELDS", field: q.key } + if ( + q.key === "email" && + !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value)) + ) + return { ok: false, error: "INVALID_EMAIL", field: q.key } + values[q.key] = value + snapshots.push({ + key: q.key, + label: q.label, + labelEng: q.labelEng, + type: q.type, + options: q.options ?? [], + value, + }) + } + return { ok: true, values, snapshots } +} +export function summarizeAnswers( + snapshots: AnswerSnapshot[], + isEn: boolean, + includeCore = false, +): string { + return snapshots + .filter((q) => includeCore || !CORE_KEYS.includes(q.key as CoreKey)) + .map((q) => { + const label = isEn ? q.labelEng : q.label + const format = (v: string) => { + const option = q.options.find((o) => o.key === v) + return option ? (isEn ? option.labelEng : option.label) : v + } + const value = Array.isArray(q.value) + ? q.value.map(format).join(", ") + : typeof q.value === "boolean" + ? q.value + ? isEn + ? "Yes" + : "Igen" + : isEn + ? "No" + : "Nem" + : format(q.value) + return `${label}: ${value}` + }) + .join("\n") +} diff --git a/src/lib/google-sheets.ts b/src/lib/google-sheets.ts index 20871b4..1e6cafb 100644 --- a/src/lib/google-sheets.ts +++ b/src/lib/google-sheets.ts @@ -29,6 +29,8 @@ export const SHEET_COLUMNS = [ "Csoport", "Pozíció", "Motiváció", + "További válaszok", + "Jelentkezés az adminban", ] as const; export type ApplicationRow = { @@ -42,6 +44,8 @@ export type ApplicationRow = { group: string; position: string; motivation: string; + additionalAnswers: string; + adminUrl: string; }; export type SheetResult = { status: "ok" } | { status: "skipped"; reason: string } | { status: "error"; message: string }; @@ -156,17 +160,19 @@ export async function checkSpreadsheetAccess(url: string): Promise } } -/** Ha a táblázat első sora üres, kiírjuk a fejlécet. */ +/** Extend only a known legacy header; never overwrite an editor's custom columns. */ +export function headerUpdate(existing: string[]): { range: string; values: string[] } | null { + if (existing.every(value => !value)) return { range: "A1:L1", values: [...SHEET_COLUMNS] }; + if (SHEET_COLUMNS.slice(0, 10).some((label, i) => existing[i] !== label)) throw new Error("A táblázat első tíz oszlopa eltér a jelentkezési fejléctől. A fejlécet nem írtuk felül."); + if (SHEET_COLUMNS.slice(10).some((label, i) => existing[i + 10] && existing[i + 10] !== label)) throw new Error("A K–L oszlopok foglaltak. Kérjük, helyezd át őket a két új jelentkezési oszlop számára."); + return existing[10] && existing[11] ? null : { range: "K1:L1", values: SHEET_COLUMNS.slice(10) }; +} async function ensureHeaderRow(spreadsheetId: string) { - const range = `A1:${String.fromCharCode(64 + SHEET_COLUMNS.length)}1`; - const existing = await sheetsRequest(`/${spreadsheetId}/values/${range}`); - - if (!existing.values || existing.values.length === 0) { - await sheetsRequest(`/${spreadsheetId}/values/${range}?valueInputOption=RAW`, { - method: "PUT", - body: JSON.stringify({ values: [SHEET_COLUMNS] }), - }); - } + const existing = await sheetsRequest(`/${spreadsheetId}/values/A1:L1`); + const update = headerUpdate(existing.values?.[0] ?? []); + if (update) await sheetsRequest(`/${spreadsheetId}/values/${update.range}?valueInputOption=RAW`, { + method: "PUT", body: JSON.stringify({ values: [update.values] }), + }); } /** @@ -205,6 +211,8 @@ export async function appendApplicationRow(spreadsheetUrl: string | null | undef row.group, row.position, row.motivation, + row.additionalAnswers, + row.adminUrl, ], ], }), diff --git a/src/lib/karrier-sheet-sync.ts b/src/lib/karrier-sheet-sync.ts index bc4ab2e..c6869a5 100644 --- a/src/lib/karrier-sheet-sync.ts +++ b/src/lib/karrier-sheet-sync.ts @@ -1,3 +1,4 @@ +import { summarizeAnswers, type AnswerSnapshot } from "./career-form"; import type { JobApplication } from "@/payload-types"; import type { Payload, PayloadRequest } from "payload"; import { appendApplicationRow, type SheetResult } from "./google-sheets"; @@ -10,30 +11,53 @@ import { appendApplicationRow, type SheetResult } from "./google-sheets"; * táblázat épp nem elérhető — a státusz az adminban látszik. */ export async function syncApplicationToSheet( - payload: Payload, - doc: JobApplication, - req?: PayloadRequest, + payload: Payload, + doc: JobApplication, + req?: PayloadRequest, ): Promise { - const settings = await payload.findGlobal({ slug: "career-settings" }); - - const result = await appendApplicationRow(settings?.spreadsheetUrl, { - submittedAt: doc.createdAt, - name: doc.name, - email: doc.email, - phone: doc.phone ?? "", - university: doc.university ?? "", - major: doc.major ?? "", - semester: doc.semester ?? "", - group: doc.groupName ?? "", - position: doc.positionName ?? "", - motivation: doc.motivation ?? "", - }); + let result: SheetResult; + try { + const settings = await payload.findGlobal({ slug: "career-settings", req }); + const origin = process.env.NEXT_PUBLIC_SERVER_URL?.replace(/\/$/, ""); + result = await appendApplicationRow(settings?.spreadsheetUrl, { + submittedAt: doc.createdAt, + name: doc.name, + email: doc.email, + phone: doc.phone ?? "", + university: doc.university ?? "", + major: doc.major ?? "", + semester: doc.semester ?? "", + group: doc.groupName ?? "", + position: doc.positionName ?? "", + motivation: doc.motivation ?? "", + additionalAnswers: summarizeAnswers( + (doc.answerSnapshot ?? []) as unknown as AnswerSnapshot[], + doc.language === "en", + ), + adminUrl: `${origin ?? ""}/admin/collections/job-applications/${doc.id}`, + }); + } catch (error) { + result = { + status: "error", + message: + error instanceof Error + ? error.message + : "A táblázat szinkronizálása sikertelen.", + }; + } if (result.status === "error") { - payload.logger.error(`A(z) #${doc.id} jelentkezés kiírása a táblázatba sikertelen: ${result.message}`); + payload.logger.error( + `A(z) #${doc.id} jelentkezés kiírása a táblázatba sikertelen: ${result.message}`, + ); } - const detail = result.status === "error" ? result.message : result.status === "skipped" ? result.reason : null; + const detail = + result.status === "error" + ? result.message + : result.status === "skipped" + ? result.reason + : null; try { await payload.update({ @@ -46,7 +70,9 @@ export async function syncApplicationToSheet( context: { skipSheetSync: true }, }); } catch (error) { - payload.logger.error(`A(z) #${doc.id} jelentkezés szinkron-státuszának mentése sikertelen: ${error}`); + payload.logger.error( + `A(z) #${doc.id} jelentkezés szinkron-státuszának mentése sikertelen: ${error}`, + ); } return result; diff --git a/src/migrations/20260906_154658_contact_baseline.ts b/src/migrations/20260906_154658_contact_baseline.ts new file mode 100644 index 0000000..8754c3e --- /dev/null +++ b/src/migrations/20260906_154658_contact_baseline.ts @@ -0,0 +1,25 @@ +import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres' + +/** Older snapshots included Contact, but the executable migration chain did not. */ +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TABLE IF NOT EXISTS contact ( + id serial PRIMARY KEY NOT NULL, + title varchar NOT NULL, title_en varchar NOT NULL, + content jsonb NOT NULL, content_en jsonb NOT NULL, + updated_at timestamp(3) with time zone DEFAULT now() NOT NULL, + created_at timestamp(3) with time zone DEFAULT now() NOT NULL + ); + CREATE INDEX IF NOT EXISTS contact_updated_at_idx ON contact (updated_at); + CREATE INDEX IF NOT EXISTS contact_created_at_idx ON contact (created_at); + ALTER TABLE payload_locked_documents_rels ADD COLUMN IF NOT EXISTS contact_id integer; + CREATE INDEX IF NOT EXISTS payload_locked_documents_rels_contact_id_idx ON payload_locked_documents_rels (contact_id); + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'payload_locked_documents_rels_contact_fk') THEN + ALTER TABLE payload_locked_documents_rels ADD CONSTRAINT payload_locked_documents_rels_contact_fk FOREIGN KEY (contact_id) REFERENCES contact(id) ON DELETE CASCADE; + END IF; + END $$; + `) +} +// Contact may predate this repair on an existing installation: preserve its data on rollback. +export async function down(): Promise {} diff --git a/src/migrations/20260906_154659_career_form_cv_contact.json b/src/migrations/20260906_154659_career_form_cv_contact.json new file mode 100644 index 0000000..d458829 --- /dev/null +++ b/src/migrations/20260906_154659_career_form_cv_contact.json @@ -0,0 +1,4506 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles_gallery": { + "name": "articles_gallery", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "articles_gallery_order_idx": { + "name": "articles_gallery_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_gallery_parent_id_idx": { + "name": "articles_gallery_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_gallery_image_idx": { + "name": "articles_gallery_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "articles_gallery_image_id_media_id_fk": { + "name": "articles_gallery_image_id_media_id_fk", + "tableFrom": "articles_gallery", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "articles_gallery_parent_id_fk": { + "name": "articles_gallery_parent_id_fk", + "tableFrom": "articles_gallery", + "tableTo": "articles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_eng": { + "name": "title_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "short_description": { + "name": "short_description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "short_description_eng": { + "name": "short_description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_eng": { + "name": "content_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "published_date": { + "name": "published_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "featured_image_id": { + "name": "featured_image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "category_eng": { + "name": "category_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "articles_featured_image_idx": { + "name": "articles_featured_image_idx", + "columns": [ + { + "expression": "featured_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_slug_idx": { + "name": "articles_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_updated_at_idx": { + "name": "articles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "articles_created_at_idx": { + "name": "articles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "articles_featured_image_id_media_id_fk": { + "name": "articles_featured_image_id_media_id_fk", + "tableFrom": "articles", + "tableTo": "media", + "columnsFrom": [ + "featured_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.events": { + "name": "events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_eng": { + "name": "title_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_eng": { + "name": "description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "facebook_event_link": { + "name": "facebook_event_link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link_to_picture_from_event_id": { + "name": "link_to_picture_from_event_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "events_image_idx": { + "name": "events_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_link_to_picture_from_event_idx": { + "name": "events_link_to_picture_from_event_idx", + "columns": [ + { + "expression": "link_to_picture_from_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_slug_idx": { + "name": "events_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_updated_at_idx": { + "name": "events_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "events_created_at_idx": { + "name": "events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "events_image_id_media_id_fk": { + "name": "events_image_id_media_id_fk", + "tableFrom": "events", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "events_link_to_picture_from_event_id_gallery_id_fk": { + "name": "events_link_to_picture_from_event_id_gallery_id_fk", + "tableFrom": "events", + "tableTo": "gallery", + "columnsFrom": [ + "link_to_picture_from_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cars_gallery": { + "name": "cars_gallery", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "cars_gallery_order_idx": { + "name": "cars_gallery_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cars_gallery_parent_id_idx": { + "name": "cars_gallery_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cars_gallery_image_idx": { + "name": "cars_gallery_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cars_gallery_image_id_media_id_fk": { + "name": "cars_gallery_image_id_media_id_fk", + "tableFrom": "cars_gallery", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cars_gallery_parent_id_fk": { + "name": "cars_gallery_parent_id_fk", + "tableFrom": "cars_gallery", + "tableTo": "cars", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cars": { + "name": "cars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_eng": { + "name": "description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "interactive_model_id": { + "name": "interactive_model_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cars_image_idx": { + "name": "cars_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cars_interactive_model_idx": { + "name": "cars_interactive_model_idx", + "columns": [ + { + "expression": "interactive_model_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cars_updated_at_idx": { + "name": "cars_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cars_created_at_idx": { + "name": "cars_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cars_image_id_media_id_fk": { + "name": "cars_image_id_media_id_fk", + "tableFrom": "cars", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cars_interactive_model_id_media_id_fk": { + "name": "cars_interactive_model_id_media_id_fk", + "tableFrom": "cars", + "tableTo": "media", + "columnsFrom": [ + "interactive_model_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gallery_images": { + "name": "gallery_images", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "gallery_images_order_idx": { + "name": "gallery_images_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gallery_images_parent_id_idx": { + "name": "gallery_images_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gallery_images_image_idx": { + "name": "gallery_images_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "gallery_images_image_id_media_id_fk": { + "name": "gallery_images_image_id_media_id_fk", + "tableFrom": "gallery_images", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "gallery_images_parent_id_fk": { + "name": "gallery_images_parent_id_fk", + "tableFrom": "gallery_images", + "tableTo": "gallery", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.gallery": { + "name": "gallery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_eng": { + "name": "title_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "date": { + "name": "date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "gallery_slug_idx": { + "name": "gallery_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gallery_updated_at_idx": { + "name": "gallery_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "gallery_created_at_idx": { + "name": "gallery_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.publications": { + "name": "publications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_eng": { + "name": "title_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author": { + "name": "author", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_eng": { + "name": "description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "publications_updated_at_idx": { + "name": "publications_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "publications_created_at_idx": { + "name": "publications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.members": { + "name": "members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "position_en": { + "name": "position_en", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "picture_id": { + "name": "picture_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "linkedin": { + "name": "linkedin", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "members_group_idx": { + "name": "members_group_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_picture_idx": { + "name": "members_picture_idx", + "columns": [ + { + "expression": "picture_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_updated_at_idx": { + "name": "members_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_created_at_idx": { + "name": "members_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_group_id_groups_id_fk": { + "name": "members_group_id_groups_id_fk", + "tableFrom": "members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "members_picture_id_media_id_fk": { + "name": "members_picture_id_media_id_fk", + "tableFrom": "members", + "tableTo": "media", + "columnsFrom": [ + "picture_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sponsors": { + "name": "sponsors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "logo_id": { + "name": "logo_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tier": { + "name": "tier", + "type": "enum_sponsors_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sponsors_logo_idx": { + "name": "sponsors_logo_idx", + "columns": [ + { + "expression": "logo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sponsors_updated_at_idx": { + "name": "sponsors_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sponsors_created_at_idx": { + "name": "sponsors_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sponsors_logo_id_media_id_fk": { + "name": "sponsors_logo_id_media_id_fk", + "tableFrom": "sponsors", + "tableTo": "media", + "columnsFrom": [ + "logo_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruitment_positions_sections": { + "name": "recruitment_positions_sections", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "section_title": { + "name": "section_title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "section_title_eng": { + "name": "section_title_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "section_content": { + "name": "section_content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "section_content_eng": { + "name": "section_content_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "recruitment_positions_sections_order_idx": { + "name": "recruitment_positions_sections_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recruitment_positions_sections_parent_id_idx": { + "name": "recruitment_positions_sections_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recruitment_positions_sections_parent_id_fk": { + "name": "recruitment_positions_sections_parent_id_fk", + "tableFrom": "recruitment_positions_sections", + "tableTo": "recruitment_positions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruitment_positions": { + "name": "recruitment_positions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "position_name": { + "name": "position_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "position_name_eng": { + "name": "position_name_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "position_open": { + "name": "position_open", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "position_description": { + "name": "position_description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "position_description_eng": { + "name": "position_description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "highlights_joining": { + "name": "highlights_joining", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "highlights_joining_eng": { + "name": "highlights_joining_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "highlights_time_commitment": { + "name": "highlights_time_commitment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "highlights_time_commitment_eng": { + "name": "highlights_time_commitment_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "highlights_subsystem": { + "name": "highlights_subsystem", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "highlights_subsystem_eng": { + "name": "highlights_subsystem_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "recruitment_positions_order_idx": { + "name": "recruitment_positions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recruitment_positions_parent_id_idx": { + "name": "recruitment_positions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recruitment_positions_parent_id_fk": { + "name": "recruitment_positions_parent_id_fk", + "tableFrom": "recruitment_positions", + "tableTo": "recruitment", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.recruitment": { + "name": "recruitment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "group_name_eng": { + "name": "group_name_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_eng": { + "name": "description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "recruitment_image_idx": { + "name": "recruitment_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recruitment_updated_at_idx": { + "name": "recruitment_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "recruitment_created_at_idx": { + "name": "recruitment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "recruitment_image_id_media_id_fk": { + "name": "recruitment_image_id_media_id_fk", + "tableFrom": "recruitment", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_applications": { + "name": "job_applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cv_id": { + "name": "cv_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "form_version": { + "name": "form_version", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "language": { + "name": "language", + "type": "enum_job_applications_language", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "answer_summary": { + "name": "answer_summary", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "answer_snapshot": { + "name": "answer_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "university": { + "name": "university", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "major": { + "name": "major", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "semester": { + "name": "semester", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "group_name": { + "name": "group_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "position_name": { + "name": "position_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "motivation": { + "name": "motivation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "consent": { + "name": "consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "sheet_sync_status": { + "name": "sheet_sync_status", + "type": "enum_job_applications_sheet_sync_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "sheet_sync_error": { + "name": "sheet_sync_error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_applications_cv_idx": { + "name": "job_applications_cv_idx", + "columns": [ + { + "expression": "cv_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_applications_updated_at_idx": { + "name": "job_applications_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_applications_created_at_idx": { + "name": "job_applications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_applications_cv_id_application_cvs_id_fk": { + "name": "job_applications_cv_id_application_cvs_id_fk", + "tableFrom": "job_applications", + "tableTo": "application_cvs", + "columnsFrom": [ + "cv_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.application_cvs": { + "name": "application_cvs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "application_cvs_updated_at_idx": { + "name": "application_cvs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "application_cvs_created_at_idx": { + "name": "application_cvs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "application_cvs_filename_idx": { + "name": "application_cvs_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "description_eng": { + "name": "description_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "groups_updated_at_idx": { + "name": "groups_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "groups_created_at_idx": { + "name": "groups_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.association": { + "name": "association", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_en": { + "name": "content_en", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "association_updated_at_idx": { + "name": "association_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "association_created_at_idx": { + "name": "association_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_us": { + "name": "support_us", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "button_text": { + "name": "button_text", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'Támogatom a csapatot'" + }, + "button_text_en": { + "name": "button_text_en", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'Support the team'" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_en": { + "name": "content_en", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_us_updated_at_idx": { + "name": "support_us_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_us_created_at_idx": { + "name": "support_us_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "exhibition_url": { + "name": "exhibition_url", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'https://docs.google.com/forms/d/e/1FAIpQLSdrA3VX3YpxQB3uZ1ojXov8-J3LPawAt4ip_Aj4k8FNbNOCrA/viewform?usp=dialog'" + }, + "exhibition_label": { + "name": "exhibition_label", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'Rendezvényen való kiállítási igény'" + }, + "exhibition_label_eng": { + "name": "exhibition_label_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'Event Exhibition Request'" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_en": { + "name": "content_en", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_updated_at_idx": { + "name": "contact_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_created_at_idx": { + "name": "contact_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.formula_student": { + "name": "formula_student", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "content_en": { + "name": "content_en", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "formula_student_updated_at_idx": { + "name": "formula_student_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "formula_student_created_at_idx": { + "name": "formula_student_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "articles_id": { + "name": "articles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "events_id": { + "name": "events_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cars_id": { + "name": "cars_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "gallery_id": { + "name": "gallery_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "publications_id": { + "name": "publications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "members_id": { + "name": "members_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sponsors_id": { + "name": "sponsors_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recruitment_id": { + "name": "recruitment_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "job_applications_id": { + "name": "job_applications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "application_cvs_id": { + "name": "application_cvs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "groups_id": { + "name": "groups_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "association_id": { + "name": "association_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "support_us_id": { + "name": "support_us_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "formula_student_id": { + "name": "formula_student_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_articles_id_idx": { + "name": "payload_locked_documents_rels_articles_id_idx", + "columns": [ + { + "expression": "articles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_events_id_idx": { + "name": "payload_locked_documents_rels_events_id_idx", + "columns": [ + { + "expression": "events_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_cars_id_idx": { + "name": "payload_locked_documents_rels_cars_id_idx", + "columns": [ + { + "expression": "cars_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_gallery_id_idx": { + "name": "payload_locked_documents_rels_gallery_id_idx", + "columns": [ + { + "expression": "gallery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_publications_id_idx": { + "name": "payload_locked_documents_rels_publications_id_idx", + "columns": [ + { + "expression": "publications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_members_id_idx": { + "name": "payload_locked_documents_rels_members_id_idx", + "columns": [ + { + "expression": "members_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_sponsors_id_idx": { + "name": "payload_locked_documents_rels_sponsors_id_idx", + "columns": [ + { + "expression": "sponsors_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_recruitment_id_idx": { + "name": "payload_locked_documents_rels_recruitment_id_idx", + "columns": [ + { + "expression": "recruitment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_job_applications_id_idx": { + "name": "payload_locked_documents_rels_job_applications_id_idx", + "columns": [ + { + "expression": "job_applications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_application_cvs_id_idx": { + "name": "payload_locked_documents_rels_application_cvs_id_idx", + "columns": [ + { + "expression": "application_cvs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_groups_id_idx": { + "name": "payload_locked_documents_rels_groups_id_idx", + "columns": [ + { + "expression": "groups_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_association_id_idx": { + "name": "payload_locked_documents_rels_association_id_idx", + "columns": [ + { + "expression": "association_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_support_us_id_idx": { + "name": "payload_locked_documents_rels_support_us_id_idx", + "columns": [ + { + "expression": "support_us_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_contact_id_idx": { + "name": "payload_locked_documents_rels_contact_id_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_formula_student_id_idx": { + "name": "payload_locked_documents_rels_formula_student_id_idx", + "columns": [ + { + "expression": "formula_student_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_articles_fk": { + "name": "payload_locked_documents_rels_articles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "articles", + "columnsFrom": [ + "articles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_events_fk": { + "name": "payload_locked_documents_rels_events_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "events", + "columnsFrom": [ + "events_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_cars_fk": { + "name": "payload_locked_documents_rels_cars_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "cars", + "columnsFrom": [ + "cars_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_gallery_fk": { + "name": "payload_locked_documents_rels_gallery_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "gallery", + "columnsFrom": [ + "gallery_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_publications_fk": { + "name": "payload_locked_documents_rels_publications_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "publications", + "columnsFrom": [ + "publications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_members_fk": { + "name": "payload_locked_documents_rels_members_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "members", + "columnsFrom": [ + "members_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_sponsors_fk": { + "name": "payload_locked_documents_rels_sponsors_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "sponsors", + "columnsFrom": [ + "sponsors_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_recruitment_fk": { + "name": "payload_locked_documents_rels_recruitment_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "recruitment", + "columnsFrom": [ + "recruitment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_job_applications_fk": { + "name": "payload_locked_documents_rels_job_applications_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "job_applications", + "columnsFrom": [ + "job_applications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_application_cvs_fk": { + "name": "payload_locked_documents_rels_application_cvs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "application_cvs", + "columnsFrom": [ + "application_cvs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_groups_fk": { + "name": "payload_locked_documents_rels_groups_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "groups", + "columnsFrom": [ + "groups_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_association_fk": { + "name": "payload_locked_documents_rels_association_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "association", + "columnsFrom": [ + "association_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_support_us_fk": { + "name": "payload_locked_documents_rels_support_us_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "support_us", + "columnsFrom": [ + "support_us_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_contact_fk": { + "name": "payload_locked_documents_rels_contact_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "contact", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_formula_student_fk": { + "name": "payload_locked_documents_rels_formula_student_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "formula_student", + "columnsFrom": [ + "formula_student_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.site_settings": { + "name": "site_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "show_association_page": { + "name": "show_association_page", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "show_recruitment_page": { + "name": "show_recruitment_page", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.career_settings_form_questions_options": { + "name": "career_settings_form_questions_options", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "label_eng": { + "name": "label_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "career_settings_form_questions_options_order_idx": { + "name": "career_settings_form_questions_options_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "career_settings_form_questions_options_parent_id_idx": { + "name": "career_settings_form_questions_options_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "career_settings_form_questions_options_parent_id_fk": { + "name": "career_settings_form_questions_options_parent_id_fk", + "tableFrom": "career_settings_form_questions_options", + "tableTo": "career_settings_form_questions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.career_settings_form_questions": { + "name": "career_settings_form_questions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "label_eng": { + "name": "label_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "hint": { + "name": "hint", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hint_eng": { + "name": "hint_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_career_settings_form_questions_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "career_settings_form_questions_order_idx": { + "name": "career_settings_form_questions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "career_settings_form_questions_parent_id_idx": { + "name": "career_settings_form_questions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "career_settings_form_questions_parent_id_fk": { + "name": "career_settings_form_questions_parent_id_fk", + "tableFrom": "career_settings_form_questions", + "tableTo": "career_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.career_settings_faqs": { + "name": "career_settings_faqs", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "question": { + "name": "question", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "question_eng": { + "name": "question_eng", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "answer": { + "name": "answer", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "answer_eng": { + "name": "answer_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "career_settings_faqs_order_idx": { + "name": "career_settings_faqs_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "career_settings_faqs_parent_id_idx": { + "name": "career_settings_faqs_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "career_settings_faqs_parent_id_fk": { + "name": "career_settings_faqs_parent_id_fk", + "tableFrom": "career_settings_faqs", + "tableTo": "career_settings", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.career_settings": { + "name": "career_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "intro": { + "name": "intro", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "intro_eng": { + "name": "intro_eng", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "application_mode": { + "name": "application_mode", + "type": "enum_career_settings_application_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'builtIn'" + }, + "google_form_url": { + "name": "google_form_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "spreadsheet_url": { + "name": "spreadsheet_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "applications_open": { + "name": "applications_open", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "applications_closed_text": { + "name": "applications_closed_text", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "applications_closed_text_eng": { + "name": "applications_closed_text_eng", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_sponsors_tier": { + "name": "enum_sponsors_tier", + "schema": "public", + "values": [ + "diamond", + "gold", + "silver", + "copper", + "other", + "bme" + ] + }, + "public.enum_job_applications_language": { + "name": "enum_job_applications_language", + "schema": "public", + "values": [ + "hu", + "en" + ] + }, + "public.enum_job_applications_sheet_sync_status": { + "name": "enum_job_applications_sheet_sync_status", + "schema": "public", + "values": [ + "ok", + "skipped", + "error" + ] + }, + "public.enum_career_settings_form_questions_type": { + "name": "enum_career_settings_form_questions_type", + "schema": "public", + "values": [ + "text", + "textarea", + "select", + "multiselect", + "checkbox" + ] + }, + "public.enum_career_settings_application_mode": { + "name": "enum_career_settings_application_mode", + "schema": "public", + "values": [ + "builtIn", + "googleForm" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "97528a1e-7acd-4b1a-a0f8-d3986f2c88d8", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260906_154659_career_form_cv_contact.ts b/src/migrations/20260906_154659_career_form_cv_contact.ts new file mode 100644 index 0000000..8589d49 --- /dev/null +++ b/src/migrations/20260906_154659_career_form_cv_contact.ts @@ -0,0 +1,164 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TYPE "public"."enum_job_applications_language" AS ENUM('hu', 'en'); + CREATE TYPE "public"."enum_career_settings_form_questions_type" AS ENUM('text', 'textarea', 'select', 'multiselect', 'checkbox'); + CREATE TABLE "application_cvs" ( + "id" serial PRIMARY KEY NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "url" varchar, + "thumbnail_u_r_l" varchar, + "filename" varchar, + "mime_type" varchar, + "filesize" numeric, + "width" numeric, + "height" numeric, + "focal_x" numeric, + "focal_y" numeric + ); + + CREATE TABLE "career_settings_form_questions_options" ( + "_order" integer NOT NULL, + "_parent_id" varchar NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "key" varchar, + "label" varchar, + "label_eng" varchar + ); + + CREATE TABLE "career_settings_form_questions" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "key" varchar NOT NULL, + "label" varchar NOT NULL, + "label_eng" varchar NOT NULL, + "hint" varchar, + "hint_eng" varchar, + "type" "enum_career_settings_form_questions_type" DEFAULT 'text' NOT NULL, + "required" boolean DEFAULT false, + "hidden" boolean DEFAULT false + ); + + ALTER TABLE "job_applications" ADD COLUMN "cv_id" integer; + ALTER TABLE "job_applications" ADD COLUMN "form_version" varchar; + ALTER TABLE "job_applications" ADD COLUMN "language" "enum_job_applications_language"; + ALTER TABLE "job_applications" ADD COLUMN "answer_summary" varchar; + ALTER TABLE "job_applications" ADD COLUMN "answer_snapshot" jsonb; + ALTER TABLE "contact" ADD COLUMN "exhibition_url" varchar DEFAULT 'https://docs.google.com/forms/d/e/1FAIpQLSdrA3VX3YpxQB3uZ1ojXov8-J3LPawAt4ip_Aj4k8FNbNOCrA/viewform?usp=dialog'; + ALTER TABLE "contact" ADD COLUMN "exhibition_label" varchar DEFAULT 'Rendezvényen való kiállítási igény'; + ALTER TABLE "contact" ADD COLUMN "exhibition_label_eng" varchar DEFAULT 'Event Exhibition Request'; + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "application_cvs_id" integer; + ALTER TABLE "career_settings_form_questions_options" ADD CONSTRAINT "career_settings_form_questions_options_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."career_settings_form_questions"("id") ON DELETE cascade ON UPDATE no action; + ALTER TABLE "career_settings_form_questions" ADD CONSTRAINT "career_settings_form_questions_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."career_settings"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "application_cvs_updated_at_idx" ON "application_cvs" USING btree ("updated_at"); + CREATE INDEX "application_cvs_created_at_idx" ON "application_cvs" USING btree ("created_at"); + CREATE UNIQUE INDEX "application_cvs_filename_idx" ON "application_cvs" USING btree ("filename"); + CREATE INDEX "career_settings_form_questions_options_order_idx" ON "career_settings_form_questions_options" USING btree ("_order"); + CREATE INDEX "career_settings_form_questions_options_parent_id_idx" ON "career_settings_form_questions_options" USING btree ("_parent_id"); + CREATE INDEX "career_settings_form_questions_order_idx" ON "career_settings_form_questions" USING btree ("_order"); + CREATE INDEX "career_settings_form_questions_parent_id_idx" ON "career_settings_form_questions" USING btree ("_parent_id"); + ALTER TABLE "job_applications" ADD CONSTRAINT "job_applications_cv_id_application_cvs_id_fk" FOREIGN KEY ("cv_id") REFERENCES "public"."application_cvs"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_application_cvs_fk" FOREIGN KEY ("application_cvs_id") REFERENCES "public"."application_cvs"("id") ON DELETE cascade ON UPDATE no action; + CREATE INDEX "job_applications_cv_idx" ON "job_applications" USING btree ("cv_id"); + CREATE INDEX "payload_locked_documents_rels_application_cvs_id_idx" ON "payload_locked_documents_rels" USING btree ("application_cvs_id");`) + // Freeze the initial form in this migration; later code changes must not alter it. + const questions = [ + { + "key": "name", + "label": "Név", + "labelEng": "Name", + "type": "text", + "required": true + }, + { + "key": "email", + "label": "E-mail cím", + "labelEng": "Email address", + "type": "text", + "required": true + }, + { + "key": "phone", + "label": "Telefonszám", + "labelEng": "Phone number", + "type": "text" + }, + { + "key": "semester", + "label": "Hányadik félév", + "labelEng": "Semester", + "type": "text" + }, + { + "key": "university", + "label": "Egyetem / kar", + "labelEng": "University / faculty", + "type": "text" + }, + { + "key": "major", + "label": "Szak", + "labelEng": "Major", + "type": "text" + }, + { + "key": "groupName", + "label": "Csoport", + "labelEng": "Group", + "type": "text" + }, + { + "key": "positionName", + "label": "Pozíció", + "labelEng": "Position", + "type": "text" + }, + { + "key": "motivation", + "label": "Motiváció / üzenet", + "labelEng": "Motivation / message", + "type": "textarea" + }, + { + "key": "consent", + "label": "Hozzájárulok a személyes adataim kezeléséhez a jelentkezés elbírálása céljából.", + "labelEng": "I consent to the processing of my personal data for the purpose of the application.", + "type": "checkbox", + "required": true + } +]; + for (const [order, question] of questions.entries()) { + await db.execute(sql`INSERT INTO career_settings_form_questions (_order, _parent_id, id, key, label, label_eng, type, required, hidden) + SELECT ${order + 1}, id, 'initial-' || id || '-' || ${question.key}, ${question.key}, ${question.label}, ${question.labelEng}, ${question.type}::enum_career_settings_form_questions_type, ${question.required ?? false}, false FROM career_settings`); + } +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "application_cvs" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "career_settings_form_questions_options" DISABLE ROW LEVEL SECURITY; + ALTER TABLE "career_settings_form_questions" DISABLE ROW LEVEL SECURITY; + DROP TABLE "application_cvs" CASCADE; + DROP TABLE "career_settings_form_questions_options" CASCADE; + DROP TABLE "career_settings_form_questions" CASCADE; + ALTER TABLE "job_applications" DROP CONSTRAINT IF EXISTS "job_applications_cv_id_application_cvs_id_fk"; + + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT IF EXISTS "payload_locked_documents_rels_application_cvs_fk"; + + DROP INDEX "job_applications_cv_idx"; + DROP INDEX "payload_locked_documents_rels_application_cvs_id_idx"; + ALTER TABLE "job_applications" DROP COLUMN "cv_id"; + ALTER TABLE "job_applications" DROP COLUMN "form_version"; + ALTER TABLE "job_applications" DROP COLUMN "language"; + ALTER TABLE "job_applications" DROP COLUMN "answer_summary"; + ALTER TABLE "job_applications" DROP COLUMN "answer_snapshot"; + ALTER TABLE "contact" DROP COLUMN "exhibition_url"; + ALTER TABLE "contact" DROP COLUMN "exhibition_label"; + ALTER TABLE "contact" DROP COLUMN "exhibition_label_eng"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "application_cvs_id"; + DROP TYPE "public"."enum_job_applications_language"; + DROP TYPE "public"."enum_career_settings_form_questions_type";`) +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 2e27f71..7fae80b 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -1,3 +1,4 @@ +import * as migration_contact_baseline from "./20260906_154658_contact_baseline"; import * as migration_20250907_085438 from './20250907_085438'; import * as migration_20251108_000000_add_support_us_button_text from './20251108_000000_add_support_us_button_text'; import * as migration_20251108_172023_add_events_facebook_event_link from './20251108_172023_add_events_facebook_event_link'; @@ -9,6 +10,7 @@ import * as migration_20251206_100136_update_payload_3_67 from './20251206_10013 import * as migration_20260729_182451_karrier_oldal from './20260729_182451_karrier_oldal'; import * as migration_20260730_204153_karrier_sheets_link from './20260730_204153_karrier_sheets_link'; import * as migration_20260803_170842_karrier_faq from './20260803_170842_karrier_faq'; +import * as migration_20260906_154659_career_form_cv_contact from './20260906_154659_career_form_cv_contact'; export const migrations = [ { @@ -64,6 +66,12 @@ export const migrations = [ { up: migration_20260803_170842_karrier_faq.up, down: migration_20260803_170842_karrier_faq.down, - name: '20260803_170842_karrier_faq' + name: '20260803_170842_karrier_faq', + }, + { up: migration_contact_baseline.up, down: migration_contact_baseline.down, name: "20260906_154658_contact_baseline" }, + { + up: migration_20260906_154659_career_form_cv_contact.up, + down: migration_20260906_154659_career_form_cv_contact.down, + name: '20260906_154659_career_form_cv_contact' }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index cf1319e..ff590b7 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -78,6 +78,7 @@ export interface Config { sponsors: Sponsor; recruitment: Recruitment; 'job-applications': JobApplication; + 'application-cvs': ApplicationCv; groups: Group; association: Association; 'support-us': SupportUs; @@ -101,6 +102,7 @@ export interface Config { sponsors: SponsorsSelect | SponsorsSelect; recruitment: RecruitmentSelect | RecruitmentSelect; 'job-applications': JobApplicationsSelect | JobApplicationsSelect; + 'application-cvs': ApplicationCvsSelect | ApplicationCvsSelect; groups: GroupsSelect | GroupsSelect; association: AssociationSelect | AssociationSelect; 'support-us': SupportUsSelect | SupportUsSelect; @@ -695,6 +697,19 @@ export interface Recruitment { */ export interface JobApplication { id: number; + cv?: (number | null) | ApplicationCv; + formVersion?: string | null; + language?: ('hu' | 'en') | null; + answerSummary?: string | null; + answerSnapshot?: + | { + [k: string]: unknown; + } + | unknown[] + | string + | number + | boolean + | null; name: string; email: string; phone?: string | null; @@ -710,6 +725,24 @@ export interface JobApplication { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "application-cvs". + */ +export interface ApplicationCv { + id: number; + updatedAt: string; + createdAt: string; + url?: string | null; + thumbnailURL?: string | null; + filename?: string | null; + mimeType?: string | null; + filesize?: number | null; + width?: number | null; + height?: number | null; + focalX?: number | null; + focalY?: number | null; +} /** * Az egyesület szerkeszthető tartalmai * @@ -806,6 +839,12 @@ export interface SupportUs { */ export interface Contact { id: number; + /** + * Üresen hagyva a gomb nem jelenik meg. + */ + exhibitionUrl?: string | null; + exhibitionLabel?: string | null; + exhibitionLabelEng?: string | null; title: string; title_en: string; content: { @@ -952,6 +991,10 @@ export interface PayloadLockedDocument { relationTo: 'job-applications'; value: number | JobApplication; } | null) + | ({ + relationTo: 'application-cvs'; + value: number | ApplicationCv; + } | null) | ({ relationTo: 'groups'; value: number | Group; @@ -1226,6 +1269,11 @@ export interface RecruitmentSelect { * via the `definition` "job-applications_select". */ export interface JobApplicationsSelect { + cv?: T; + formVersion?: T; + language?: T; + answerSummary?: T; + answerSnapshot?: T; name?: T; email?: T; phone?: T; @@ -1241,6 +1289,23 @@ export interface JobApplicationsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "application-cvs_select". + */ +export interface ApplicationCvsSelect { + updatedAt?: T; + createdAt?: T; + url?: T; + thumbnailURL?: T; + filename?: T; + mimeType?: T; + filesize?: T; + width?: T; + height?: T; + focalX?: T; + focalY?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "groups_select". @@ -1285,6 +1350,9 @@ export interface SupportUsSelect { * via the `definition` "contact_select". */ export interface ContactSelect { + exhibitionUrl?: T; + exhibitionLabel?: T; + exhibitionLabelEng?: T; title?: T; title_en?: T; content?: T; @@ -1369,6 +1437,30 @@ export interface SiteSetting { */ export interface CareerSetting { id: number; + /** + * Sorrend: húzd a kérdéseket a helyükre. A hozzájárulás mindig a CV után jelenik meg. A név, e-mail és hozzájárulás kötelező. A csoport és pozíció az automatikus kitöltés miatt nem törölhető. + */ + formQuestions?: + | { + key: string; + label: string; + labelEng: string; + hint?: string | null; + hintEng?: string | null; + type: 'text' | 'textarea' | 'select' | 'multiselect' | 'checkbox'; + required?: boolean | null; + hidden?: boolean | null; + options?: + | { + key: string; + label: string; + labelEng: string; + id?: string | null; + }[] + | null; + id?: string | null; + }[] + | null; /** * Az oldal tetején, a csoportok listája fölött jelenik meg. */ @@ -1403,7 +1495,7 @@ export interface CareerSetting { [k: string]: unknown; } | null; /** - * Beépített űrlap esetén a jelentkezések a Payload adminba érkeznek (és a Google táblázatba, ha be van állítva a webhook). + * Beépített űrlap esetén a jelentkezések a Payload adminba érkeznek (és a Google táblázatba, ha be van állítva a táblázat). */ applicationMode?: ('builtIn' | 'googleForm') | null; /** @@ -1418,7 +1510,7 @@ export interface CareerSetting { applicationsClosedText?: string | null; applicationsClosedTextEng?: string | null; /** - * A kérdések ebben a sorrendben jelennek meg a Karrier oldal alján. Üresen hagyva a GYIK szekció nem jelenik meg. + * A kérdések ebben a sorrendben jelennek meg a csoportok után, a jelentkezés előtt. Üresen hagyva a GYIK szekció nem jelenik meg. */ faqs?: | { @@ -1476,6 +1568,27 @@ export interface SiteSettingsSelect { * via the `definition` "career-settings_select". */ export interface CareerSettingsSelect { + formQuestions?: + | T + | { + key?: T; + label?: T; + labelEng?: T; + hint?: T; + hintEng?: T; + type?: T; + required?: T; + hidden?: T; + options?: + | T + | { + key?: T; + label?: T; + labelEng?: T; + id?: T; + }; + id?: T; + }; intro?: T; introEng?: T; applicationMode?: T; diff --git a/src/payload.config.ts b/src/payload.config.ts index 8ff2662..d108598 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -9,6 +9,7 @@ import { buildConfig } from "payload"; import sharp from "sharp"; import { fileURLToPath } from "url"; +import { ApplicationCVs } from "./collections/ApplicationCVs"; import { Articles } from "./collections/Articles"; import Association from "./collections/Association"; import { Cars } from "./collections/Cars"; @@ -43,6 +44,7 @@ const collections = [ Sponsors, Recruitment, JobApplications, + ApplicationCVs, Groups, Association, SupportUs, diff --git a/yarn.lock b/yarn.lock index 0c3527c..74a1e3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2336,6 +2336,20 @@ use-context-selector "2.0.0" uuid "10.0.0" +"@pdf-lib/standard-fonts@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f" + integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA== + dependencies: + pako "^1.0.6" + +"@pdf-lib/upng@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb" + integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ== + dependencies: + pako "^1.0.10" + "@pinojs/redact@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@pinojs/redact/-/redact-0.4.0.tgz#c3de060dd12640dcc838516aa2a6803cc7b2e9d6" @@ -6052,6 +6066,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +pako@^1.0.10, pako@^1.0.11, pako@^1.0.6: + version "1.0.11" + resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -6144,6 +6163,16 @@ payload@^3.80.0: uuid "10.0.0" ws "^8.16.0" +pdf-lib@^1.17.1: + version "1.17.1" + resolved "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f" + integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw== + dependencies: + "@pdf-lib/standard-fonts" "^1.0.0" + "@pdf-lib/upng" "^1.0.1" + pako "^1.0.11" + tslib "^1.11.1" + peek-readable@^5.1.4: version "5.4.2" resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.4.2.tgz#aff1e1ba27a7d6911ddb103f35252ffc1787af49" From d4db3f3fcbe54cade2c781e06b90b465d6055006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lipt=C3=A1k=20P=C3=A9ter?= Date: Sun, 6 Sep 2026 20:09:14 +0200 Subject: [PATCH 2/6] feat: improve career navigation and application experience --- src/app/(app)/kapcsolat/page.tsx | 8 +- src/app/(app)/karrier/career-page-client.tsx | 143 +++-- .../karrier/components/application-form.tsx | 507 +++++++++++++----- .../(app)/karrier/components/career-faq.tsx | 2 +- .../(app)/karrier/components/career-group.tsx | 2 +- .../karrier/components/career-sidebar.tsx | 66 ++- .../components/success-check.module.css | 43 ++ .../karrier/components/success-check.tsx | 37 ++ src/app/(app)/karrier/translations.ts | 13 +- src/app/(app)/karrier/use-career-page.ts | 5 +- src/config/navigation.ts | 12 +- 11 files changed, 610 insertions(+), 228 deletions(-) create mode 100644 src/app/(app)/karrier/components/success-check.module.css create mode 100644 src/app/(app)/karrier/components/success-check.tsx diff --git a/src/app/(app)/kapcsolat/page.tsx b/src/app/(app)/kapcsolat/page.tsx index 66622bc..30a5728 100644 --- a/src/app/(app)/kapcsolat/page.tsx +++ b/src/app/(app)/kapcsolat/page.tsx @@ -50,12 +50,12 @@ export default async function ContactPage (props: { searchParams?: Promise - - {lang === 'en' ? 'Event Exhibition Request' : 'Rendezvényen való kiállítási igény'} - + {lang === 'en' ? (contact.exhibitionLabelEng || 'Event Exhibition Request') : (contact.exhibitionLabel || 'Rendezvényen való kiállítási igény')} + }
diff --git a/src/app/(app)/karrier/career-page-client.tsx b/src/app/(app)/karrier/career-page-client.tsx index 14b3e0e..4404460 100644 --- a/src/app/(app)/karrier/career-page-client.tsx +++ b/src/app/(app)/karrier/career-page-client.tsx @@ -2,6 +2,7 @@ import type { CareerSetting, Recruitment } from "@/payload-types" import { RichText } from "@payloadcms/richtext-lexical/react" +import { resolveFormConfig } from "@/lib/career-form" import { resolveApplicationConfig } from "./career-config" import ApplicationForm from "./components/application-form" import ApplyButton from "./components/apply-button" @@ -17,12 +18,26 @@ interface CareerPageClientProps { lang: string } -export default function CareerPageClient({ recruitmentData, careerSettings, lang }: CareerPageClientProps) { +export default function CareerPageClient({ + recruitmentData, + careerSettings, + lang, +}: CareerPageClientProps) { const isEn = isEnglish(lang) const texts = careerTexts(isEn) - const application = resolveApplicationConfig(careerSettings, isEn, texts.applicationsClosed) + const application = resolveApplicationConfig( + careerSettings, + isEn, + texts.applicationsClosed, + ) const intro = isEn ? careerSettings?.introEng : careerSettings?.intro const faqs = careerSettings?.faqs ?? [] + const hasFaqs = faqs.some((item) => + Boolean( + (isEn ? item.questionEng : item.question)?.trim() && + (isEn ? item.answerEng : item.answer), + ), + ) const { formRef, @@ -37,70 +52,94 @@ export default function CareerPageClient({ recruitmentData, careerSettings, lang } = useCareerPage() return ( -
-
-
-

{texts.title}

+
+
+
+

+ {texts.title} +

{intro ? (
) : ( -

{texts.subtitle}

+

+ {texts.subtitle} +

)} +
+
+
+
+ {recruitmentData.map((group) => ( + toggleGroup(group.id)} + onTogglePosition={togglePosition} + onApply={applyForPosition} + /> + ))} +
-
- {recruitmentData.map((group) => ( - toggleGroup(group.id)} - onTogglePosition={togglePosition} - onApply={applyForPosition} - /> - ))} -
+ -
- {!application.isOpen ? ( -
-

{application.closedText}

-
- ) : application.useBuiltInForm ? ( - - ) : ( - application.googleFormUrl && ( +
+ {!application.isOpen ? (
-

{texts.interested}

- +

+ {application.closedText} +

- ) - )} + ) : application.useBuiltInForm ? ( + + ) : ( + application.googleFormUrl && ( +
+

+ {texts.interested} +

+ +
+ ) + )} +
- +
- -
-
+
) } diff --git a/src/app/(app)/karrier/components/application-form.tsx b/src/app/(app)/karrier/components/application-form.tsx index 8f2ad3e..0640217 100644 --- a/src/app/(app)/karrier/components/application-form.tsx +++ b/src/app/(app)/karrier/components/application-form.tsx @@ -1,193 +1,402 @@ "use client" -import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react" -import { useEffect, useState } from "react" +import { AlertCircle, Loader2, Paperclip, X } from "lucide-react" +import { useEffect, useRef, useState } from "react" +import { + MAX_CV_SIZE, + validateAnswers, + type Answer, + type FormConfig, + type FormQuestion, +} from "@/lib/career-form" import { applicationErrorText, applicationFormTexts } from "../translations" import type { SelectedPosition } from "../types" -import { CheckboxField, HoneypotField, TextAreaField, TextField } from "./form-field" +import { HoneypotField } from "./form-field" +import SuccessCheck from "./success-check" interface ApplicationFormProps { lang: string isEn: boolean selectedPosition: SelectedPosition | null + initialConfig: FormConfig } +const INPUT_CLASS = + "w-full rounded-lg border border-white/20 bg-black px-4 py-2.5 text-white focus:border-frtRed focus:outline-none focus:ring-1 focus:ring-frtRed" -type Status = "idle" | "submitting" | "success" | "error" - -const EMPTY_FORM = { - name: "", - email: "", - phone: "", - university: "", - major: "", - semester: "", - groupName: "", - positionName: "", - motivation: "", - consent: false, - website: "", +function Question({ + question: q, + value, + onChange, + isEn, + error, +}: { + question: FormQuestion + value?: Answer + onChange: (value: Answer) => void + isEn: boolean + error: boolean +}) { + const id = `application-${q.key}` + const label = isEn ? q.labelEng : q.label + const hint = isEn ? q.hintEng : q.hint + const labelContent = ( + <> + {label} + {q.required ? ( + " *" + ) : ( + + {" "} + ({isEn ? "optional" : "opcionális"}) + + )} + + ) + const common = { + id, + name: q.key, + "aria-invalid": error || undefined, + "aria-describedby": hint ? `${id}-hint` : undefined, + } + const onMultiChange = (key: string, checked: boolean) => { + const selected = Array.isArray(value) ? value : [] + onChange( + checked ? [...selected, key] : selected.filter((v) => v !== key), + ) + } + return ( +
+ {q.type === "multiselect" ? ( +
+ + {labelContent} + +
+ {q.options?.map((option) => ( + + ))} +
+
+ ) : q.type === "checkbox" ? ( + + ) : ( + <> + + {q.type === "textarea" ? ( +