diff --git a/Dockerfile b/Dockerfile index 655c1cd..ee61310 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ FROM node:22-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN npx prisma generate && npm run build +RUN npm run build FROM node:22-alpine AS runner WORKDIR /app @@ -17,8 +17,8 @@ COPY --from=builder /app/package*.json ./ COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public -COPY --from=builder /app/prisma ./prisma -COPY --from=builder /app/prisma.config.ts ./prisma.config.ts +COPY --from=builder /app/drizzle.config.ts ./drizzle.config.ts +COPY --from=builder /app/lib/db ./lib/db COPY --from=builder /app/next.config.mjs ./next.config.mjs EXPOSE 3000 diff --git a/Makefile b/Makefile index ad646ac..8f424bf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: start stop restart deploy logs build ps port health clean-orphans app-shell db-shell db-init db-migrate db-seed prisma-generate prisma-migrate test test-watch help +.PHONY: start stop restart deploy logs build ps port health clean-orphans app-shell db-shell db-init db-migrate db-generate test test-watch help MIGRATION_NAME ?= init @@ -38,13 +38,10 @@ db-shell: docker compose exec db psql -U postgres -d moh_sass db-init: - docker compose exec app sh -lc "npx prisma generate && npx prisma migrate deploy && npx prisma db seed" + docker compose exec app npm run db:migrate db-migrate: - docker compose exec app npx prisma migrate deploy - -db-seed: - docker compose exec app npx prisma db seed + docker compose exec app npm run db:migrate test: @node scripts/test-summary.mjs @@ -52,11 +49,8 @@ test: test-watch: npm run test:watch -prisma-generate: - docker compose exec app npx prisma generate - -prisma-migrate: - docker compose exec app npx prisma migrate dev --name $(MIGRATION_NAME) +db-generate: + docker compose exec app npm run db:generate health: curl -sS https://mohfarawati.de/api/health @@ -74,11 +68,9 @@ help: @echo " make clean-orphans Remove orphaned old containers" @echo " make app-shell Open shell in app container" @echo " make db-shell Open PostgreSQL shell" - @echo " make db-init Generate client, apply migrations, run seed" - @echo " make db-migrate Apply prisma migrations" - @echo " make db-seed Seed database data" - @echo " make prisma-generate Run prisma generate" - @echo " make prisma-migrate Create/apply dev migration" + @echo " make db-init Apply Drizzle migrations to the DB" + @echo " make db-migrate Apply Drizzle migrations (drizzle-kit migrate)" + @echo " make db-generate Generate a migration from schema changes (drizzle-kit generate)" @echo " make health Check app health endpoint via public domain" @echo " make test Run the whole test suite + print a copy-paste summary" @echo " make test-watch Run the test suite in watch mode" diff --git a/app/_admin/media/actions.ts b/app/_admin/media/actions.ts index b299b9a..9f03e47 100644 --- a/app/_admin/media/actions.ts +++ b/app/_admin/media/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { MediaKind } from "@prisma/client"; +import { MediaKind } from "@/lib/db/enums"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -11,7 +11,10 @@ import { withFlash } from "@/lib/admin-feedback"; import { countMediaUsageReferences, getMediaAssetById } from "@/lib/media"; import { createStandaloneMediaAsset, deleteMediaAssetAndFile } from "@/lib/media-service"; import { isManagedMediaFilePath } from "@/lib/media-storage"; -import { prisma } from "@/lib/prisma"; +import { eq } from "drizzle-orm"; + +import { db } from "@/lib/db"; +import { mediaAsset } from "@/lib/db/schema"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { @@ -71,11 +74,7 @@ export async function deleteMediaAssetAction(formData: FormData) { redirect(withFlash(getAdminAppPath("/media"), { error: "Datei wird noch verwendet." })); } - await prisma.mediaAsset.delete({ - where: { - id: asset.id, - }, - }); + await db.delete(mediaAsset).where(eq(mediaAsset.id, asset.id)); if (isManagedMediaFilePath(asset.url)) { await deleteMediaAssetAndFile({ diff --git a/app/_admin/portfolio/actions.ts b/app/_admin/portfolio/actions.ts index 3342bd1..e8c1599 100644 --- a/app/_admin/portfolio/actions.ts +++ b/app/_admin/portfolio/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { MediaUsageType, Prisma } from "@prisma/client"; +import { and, eq, inArray } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -15,7 +15,16 @@ import { resolveMediaSelection } from "@/lib/media-service"; import { getLocalizedPath } from "@/lib/locale"; import { removeManagedMediaFile } from "@/lib/media-storage"; import { mediaFieldInputSchema } from "@/lib/media-validation"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { + category, + mediaAsset, + mediaUsage, + portfolioAsset, + portfolioProject, + portfolioSection, +} from "@/lib/db/schema"; +import { MediaUsageType } from "@/lib/db/enums"; import { isCheckedFormValue } from "@/lib/form-data"; import { getSiteSettings } from "@/lib/app-config"; import { @@ -81,6 +90,20 @@ function parseZodError(error: ZodError) { return error.issues[0]?.message ?? "Validierung fehlgeschlagen."; } +// Postgres unique-violation (code 23505, was Prisma's "P2002"). The error shape +// differs between drivers (postgres.js exposes `.code`; PGlite in tests nests it +// or only in the message), so check code, cause.code, and the message text. +function isUniqueViolation(error: unknown): boolean { + if (typeof error !== "object" || error === null) { + return false; + } + const e = error as { code?: string; cause?: { code?: string }; message?: string }; + if (e.code === "23505" || e.cause?.code === "23505") { + return true; + } + return typeof e.message === "string" && /23505|duplicate key|unique constraint/i.test(e.message); +} + async function revalidatePortfolioPages() { revalidatePath(toInternalAdminPath("/")); revalidatePath(toInternalAdminPath("/media")); @@ -121,16 +144,9 @@ export async function upsertCategoryAction(formData: FormData) { }); if (parsed.id) { - await prisma.category.update({ - where: { - id: parsed.id, - }, - data: parsed, - }); + await db.update(category).set(parsed).where(eq(category.id, parsed.id)); } else { - await prisma.category.create({ - data: parsed, - }); + await db.insert(category).values(parsed); } await revalidatePortfolioPages(); @@ -143,7 +159,7 @@ export async function upsertCategoryAction(formData: FormData) { const message = error instanceof ZodError ? parseZodError(error) - : error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" + : isUniqueViolation(error) ? "Kategorie Slug muss eindeutig sein." : "Kategorie konnte nicht gespeichert werden."; @@ -158,21 +174,13 @@ export async function deleteCategoryAction(formData: FormData) { const id = String(formData.get("id") ?? ""); try { - const projectCount = await prisma.portfolioProject.count({ - where: { - categoryId: id, - }, - }); + const projectCount = await db.$count(portfolioProject, eq(portfolioProject.categoryId, id)); if (projectCount > 0) { redirect(withFlash(redirectPath, { error: "Kategorie mit Projekten kann nicht geloescht werden." })); } - await prisma.category.delete({ - where: { - id, - }, - }); + await db.delete(category).where(eq(category.id, id)); await revalidatePortfolioPages(); redirect(withFlash(redirectPath, { success: "Kategorie geloescht." })); @@ -241,15 +249,16 @@ export async function saveProjectAction(formData: FormData) { }); const existingProject = parsed.id - ? await prisma.portfolioProject.findUnique({ - where: { - id: parsed.id, - }, - select: { - isPublished: true, - publishedAt: true, - }, - }) + ? ( + await db + .select({ + isPublished: portfolioProject.isPublished, + publishedAt: portfolioProject.publishedAt, + }) + .from(portfolioProject) + .where(eq(portfolioProject.id, parsed.id)) + .limit(1) + )[0] ?? null : null; const shouldPublishNow = parsed.isPublished && !existingProject?.publishedAt; @@ -359,81 +368,56 @@ export async function saveProjectAction(formData: FormData) { }); } - const projectResult = await prisma.$transaction(async (tx) => { - const currentProject = parsed.id - ? await tx.portfolioProject.update({ - where: { - id: parsed.id, - }, - data: { - categoryId: parsed.categoryId, - slug: parsed.slug, - viewMode: parsed.viewMode, - titleAr: parsed.titleAr, - titleEn: parsed.titleEn, - titleDe: parsed.titleDe, - summaryAr: parsed.summaryAr, - summaryEn: parsed.summaryEn, - summaryDe: parsed.summaryDe, - clientName: parsed.clientName, - projectYear: parsed.projectYear, - serviceLabelAr: parsed.serviceLabelAr, - serviceLabelEn: parsed.serviceLabelEn, - serviceLabelDe: parsed.serviceLabelDe, - previewUrl: parsed.previewUrl || null, - coverImagePath: coverSelection.url || null, - isFeatured: parsed.isFeatured, - isPublished: parsed.isPublished, + const projectResult = await db.transaction(async (tx) => { + const projectValues = { + categoryId: parsed.categoryId, + slug: parsed.slug, + viewMode: parsed.viewMode, + titleAr: parsed.titleAr, + titleEn: parsed.titleEn, + titleDe: parsed.titleDe, + summaryAr: parsed.summaryAr, + summaryEn: parsed.summaryEn, + summaryDe: parsed.summaryDe, + clientName: parsed.clientName, + projectYear: parsed.projectYear, + serviceLabelAr: parsed.serviceLabelAr, + serviceLabelEn: parsed.serviceLabelEn, + serviceLabelDe: parsed.serviceLabelDe, + previewUrl: parsed.previewUrl || null, + coverImagePath: coverSelection.url || null, + isFeatured: parsed.isFeatured, + isPublished: parsed.isPublished, + sortOrder: parsed.sortOrder, + }; + + const [currentProject] = parsed.id + ? await tx + .update(portfolioProject) + .set({ + ...projectValues, publishedAt: parsed.isPublished ? shouldPublishNow ? new Date() : existingProject?.publishedAt ?? new Date() : null, - sortOrder: parsed.sortOrder, - }, - }) - : await tx.portfolioProject.create({ - data: { - categoryId: parsed.categoryId, - slug: parsed.slug, - viewMode: parsed.viewMode, - titleAr: parsed.titleAr, - titleEn: parsed.titleEn, - titleDe: parsed.titleDe, - summaryAr: parsed.summaryAr, - summaryEn: parsed.summaryEn, - summaryDe: parsed.summaryDe, - clientName: parsed.clientName, - projectYear: parsed.projectYear, - serviceLabelAr: parsed.serviceLabelAr, - serviceLabelEn: parsed.serviceLabelEn, - serviceLabelDe: parsed.serviceLabelDe, - previewUrl: parsed.previewUrl || null, - coverImagePath: coverSelection.url || null, - isFeatured: parsed.isFeatured, - isPublished: parsed.isPublished, - publishedAt: parsed.isPublished ? new Date() : null, - sortOrder: parsed.sortOrder, - }, - }); + }) + .where(eq(portfolioProject.id, parsed.id)) + .returning() + : await tx + .insert(portfolioProject) + .values({ ...projectValues, publishedAt: parsed.isPublished ? new Date() : null }) + .returning(); - await tx.portfolioSection.deleteMany({ - where: { - projectId: currentProject.id, - }, - }); - - await tx.portfolioAsset.deleteMany({ - where: { - projectId: currentProject.id, - }, - }); + await tx.delete(portfolioSection).where(eq(portfolioSection.projectId, currentProject.id)); + await tx.delete(portfolioAsset).where(eq(portfolioAsset.projectId, currentProject.id)); const createdSections = []; for (const section of sectionRows) { - const createdSection = await tx.portfolioSection.create({ - data: { + const [createdSection] = await tx + .insert(portfolioSection) + .values({ projectId: currentProject.id, type: section.type, titleAr: section.titleAr, @@ -445,8 +429,8 @@ export async function saveProjectAction(formData: FormData) { imagePath: section.imagePath || null, linkUrl: section.linkUrl || null, sortOrder: section.sortOrder, - }, - }); + }) + .returning(); createdSections.push(createdSection); } @@ -454,8 +438,9 @@ export async function saveProjectAction(formData: FormData) { const createdAssets = []; for (const asset of assetRows) { - const createdAsset = await tx.portfolioAsset.create({ - data: { + const [createdAsset] = await tx + .insert(portfolioAsset) + .values({ projectId: currentProject.id, kind: asset.kind, filePath: asset.filePath, @@ -463,8 +448,8 @@ export async function saveProjectAction(formData: FormData) { altEn: asset.altEn, altDe: asset.altDe, sortOrder: asset.sortOrder, - }, - }); + }) + .returning(); createdAssets.push(createdAsset); } @@ -536,7 +521,7 @@ export async function saveProjectAction(formData: FormData) { const message = error instanceof ZodError ? parseZodError(error) - : error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002" + : isUniqueViolation(error) ? "Projekt Slug muss eindeutig sein." : error instanceof Error ? error.message @@ -544,20 +529,8 @@ export async function saveProjectAction(formData: FormData) { await removeManagedPaths(uploadedPaths); if (createdMediaAssetIds.length > 0) { - await prisma.mediaUsage.deleteMany({ - where: { - assetId: { - in: createdMediaAssetIds, - }, - }, - }); - await prisma.mediaAsset.deleteMany({ - where: { - id: { - in: createdMediaAssetIds, - }, - }, - }); + await db.delete(mediaUsage).where(inArray(mediaUsage.assetId, createdMediaAssetIds)); + await db.delete(mediaAsset).where(inArray(mediaAsset.id, createdMediaAssetIds)); } redirect(withFlash(redirectPath, { error: message })); } @@ -569,24 +542,17 @@ export async function deleteProjectAction(formData: FormData) { const id = String(formData.get("id") ?? ""); try { - const project = await prisma.portfolioProject.findUnique({ - where: { - id, - }, - select: { - slug: true, - }, - }); + const [project] = await db + .select({ slug: portfolioProject.slug }) + .from(portfolioProject) + .where(eq(portfolioProject.id, id)) + .limit(1); if (!project) { redirect(withFlash(getAdminAppPath("/portfolio"), { error: "Projekt nicht gefunden." })); } - await prisma.portfolioProject.delete({ - where: { - id, - }, - }); + await db.delete(portfolioProject).where(eq(portfolioProject.id, id)); await deleteEntityMediaUsages("portfolio-project", id); await revalidatePortfolioPages(); diff --git a/app/_admin/site-settings/actions.ts b/app/_admin/site-settings/actions.ts index c395f3d..eb6a311 100644 --- a/app/_admin/site-settings/actions.ts +++ b/app/_admin/site-settings/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { MediaUsageType } from "@prisma/client"; +import { MediaUsageType } from "@/lib/db/enums"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { isRedirectError } from "next/dist/client/components/redirect-error"; @@ -30,7 +30,10 @@ import { routing } from "@/i18n/routing"; import { getLocalizedPath } from "@/lib/locale"; import { removeManagedMediaFile } from "@/lib/media-storage"; import { mediaFieldInputSchema } from "@/lib/media-validation"; -import { prisma } from "@/lib/prisma"; +import { inArray } from "drizzle-orm"; + +import { db } from "@/lib/db"; +import { mediaAsset } from "@/lib/db/schema"; async function ensureAdmin() { if (!(await isAdminAuthenticated())) { @@ -60,13 +63,7 @@ function parseJsonObject(rawValue: FormDataEntryValue | null, key: string) { async function cleanupCreatedMedia(assetIds: string[], uploadedPaths: string[]) { if (assetIds.length > 0) { - await prisma.mediaAsset.deleteMany({ - where: { - id: { - in: Array.from(new Set(assetIds)), - }, - }, - }); + await db.delete(mediaAsset).where(inArray(mediaAsset.id, Array.from(new Set(assetIds)))); } for (const filePath of Array.from(new Set(uploadedPaths.filter(Boolean)))) { diff --git a/app/_admin/site-settings/brand/page.tsx b/app/_admin/site-settings/brand/page.tsx index c3937b2..c09bbd6 100644 --- a/app/_admin/site-settings/brand/page.tsx +++ b/app/_admin/site-settings/brand/page.tsx @@ -1,4 +1,4 @@ -import { MediaKind } from "@prisma/client"; +import { MediaKind } from "@/lib/db/enums"; import { redirect } from "next/navigation"; import { MotionFade } from "@/components/motion-fade"; diff --git a/app/api/health/route.ts b/app/api/health/route.ts index 6c802de..fba9e74 100644 --- a/app/api/health/route.ts +++ b/app/api/health/route.ts @@ -1,6 +1,7 @@ +import { sql } from "drizzle-orm"; import { NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; export const dynamic = "force-dynamic"; @@ -8,7 +9,7 @@ export async function GET() { const timestamp = new Date().toISOString(); try { - await prisma.$queryRaw`SELECT 1`; + await db.execute(sql`SELECT 1`); return NextResponse.json( { diff --git a/components/admin/media-field-picker.tsx b/components/admin/media-field-picker.tsx index 35e2290..63908d9 100644 --- a/components/admin/media-field-picker.tsx +++ b/components/admin/media-field-picker.tsx @@ -2,7 +2,7 @@ /* eslint-disable @next/next/no-img-element */ -import type { MediaKind } from "@prisma/client"; +import type { MediaKind } from "@/lib/db/enums"; import { Check, ImageIcon, Search, Trash2 } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; diff --git a/components/admin/media-library-manager.tsx b/components/admin/media-library-manager.tsx index 445a6aa..09a718d 100644 --- a/components/admin/media-library-manager.tsx +++ b/components/admin/media-library-manager.tsx @@ -2,7 +2,7 @@ /* eslint-disable @next/next/no-img-element */ -import type { MediaKind } from "@prisma/client"; +import type { MediaKind } from "@/lib/db/enums"; import { FileType2, Grid2x2, ImageIcon, LayoutList, LoaderCircle, Trash2, Upload } from "lucide-react"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; diff --git a/components/admin/portfolio-project-form.tsx b/components/admin/portfolio-project-form.tsx index d13d635..9c88fa6 100644 --- a/components/admin/portfolio-project-form.tsx +++ b/components/admin/portfolio-project-form.tsx @@ -1,6 +1,6 @@ "use client"; -import type { PortfolioProjectViewMode, PortfolioSectionType } from "@prisma/client"; +import type { PortfolioProjectViewMode, PortfolioSectionType } from "@/lib/db/enums"; import { ArrowDown, ArrowUp, diff --git a/components/admin/site-settings-form.tsx b/components/admin/site-settings-form.tsx index 134c28f..c85d71d 100644 --- a/components/admin/site-settings-form.tsx +++ b/components/admin/site-settings-form.tsx @@ -2,7 +2,7 @@ /* eslint-disable @next/next/no-img-element */ -import { MediaKind } from "@prisma/client"; +import { MediaKind } from "@/lib/db/enums"; import { Check, FileText, diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..70cac7d --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./lib/db/schema.ts", + out: "./lib/db/migrations", + dialect: "postgresql", + dbCredentials: { + url: + process.env.DATABASE_URL ?? + "postgresql://postgres:postgres@localhost:5432/moh_sass?schema=public", + }, +}); diff --git a/lib/admin-auth.ts b/lib/admin-auth.ts index 4e66165..6ff827e 100644 --- a/lib/admin-auth.ts +++ b/lib/admin-auth.ts @@ -2,7 +2,10 @@ import { createHash, createHmac, timingSafeEqual } from "crypto"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; -import { prisma } from "./prisma"; +import { and, eq, like, lt } from "drizzle-orm"; + +import { db } from "./db"; +import { appConfig } from "./db/schema"; import { getAdminAppPath } from "./admin-routing"; export const ADMIN_SESSION_COOKIE = "moh_admin_session"; @@ -137,11 +140,9 @@ function getLockoutKey(ip: string): string { async function cleanupExpiredLockouts(): Promise { try { const cutoff = new Date(Date.now() - LOCKOUT_SECONDS * 2 * 1000); - await prisma.$executeRaw` - DELETE FROM "AppConfig" - WHERE key LIKE ${`${ADMIN_LOCKOUT_KEY_PREFIX}:%`} - AND "updatedAt" < ${cutoff} - `; + await db + .delete(appConfig) + .where(and(like(appConfig.key, `${ADMIN_LOCKOUT_KEY_PREFIX}:%`), lt(appConfig.updatedAt, cutoff))); } catch { // Non-critical — ignore cleanup errors. } @@ -216,10 +217,11 @@ export async function getAdminLockState(): Promise<{ locked: boolean; remainingS try { const ip = await getClientIp(); const key = getLockoutKey(ip); - const config = await prisma.appConfig.findUnique({ - where: { key }, - select: { value: true }, - }); + const [config] = await db + .select({ value: appConfig.value }) + .from(appConfig) + .where(eq(appConfig.key, key)) + .limit(1); const state = parseFailState(config?.value); const now = Date.now(); @@ -244,10 +246,11 @@ export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; r await cleanupExpiredLockouts(); - const config = await prisma.appConfig.findUnique({ - where: { key }, - select: { value: true }, - }); + const [config] = await db + .select({ value: appConfig.value }) + .from(appConfig) + .where(eq(appConfig.key, key)) + .limit(1); const current = parseFailState(config?.value); // If a previous lockout has expired, reset the counter. @@ -256,11 +259,13 @@ export async function registerFailedAdminAttempt(): Promise<{ locked: boolean; r const locked = attempts >= MAX_FAILED_ATTEMPTS; const lockUntil = locked ? now + LOCKOUT_SECONDS * 1000 : 0; - await prisma.appConfig.upsert({ - where: { key }, - update: { value: JSON.stringify({ attempts, lockUntil }) }, - create: { key, value: JSON.stringify({ attempts, lockUntil }) }, - }); + await db + .insert(appConfig) + .values({ key, value: JSON.stringify({ attempts, lockUntil }) }) + .onConflictDoUpdate({ + target: appConfig.key, + set: { value: JSON.stringify({ attempts, lockUntil }) }, + }); return { locked, @@ -276,7 +281,7 @@ export async function resetAdminFailedAttempts(): Promise { try { const ip = await getClientIp(); const key = getLockoutKey(ip); - await prisma.appConfig.deleteMany({ where: { key } }); + await db.delete(appConfig).where(eq(appConfig.key, key)); } catch { // Non-critical — ignore. } diff --git a/lib/app-config.ts b/lib/app-config.ts index 8760aaf..0a6e9d2 100644 --- a/lib/app-config.ts +++ b/lib/app-config.ts @@ -1,4 +1,7 @@ -import { prisma } from "./prisma"; +import { and, eq, inArray } from "drizzle-orm"; + +import { db } from "./db"; +import { appConfig, mediaAsset, mediaUsage } from "./db/schema"; export const MAINTENANCE_MODE_KEY = "maintenance_mode"; export { SITE_NAME_KEY, @@ -65,45 +68,42 @@ import { type MarqueeSettings, } from "./marquee-settings"; +// Small helpers over the app_config key/value table (Drizzle). +async function readConfigValue(key: string): Promise { + const [row] = await db + .select({ value: appConfig.value }) + .from(appConfig) + .where(eq(appConfig.key, key)) + .limit(1); + + return row?.value; +} + +async function upsertConfig(key: string, value: string): Promise { + await db + .insert(appConfig) + .values({ key, value }) + .onConflictDoUpdate({ target: appConfig.key, set: { value } }); +} + export async function getMaintenanceMode(): Promise { try { - const config = await prisma.appConfig.findUnique({ - where: { key: MAINTENANCE_MODE_KEY }, - select: { value: true }, - }); - - return config?.value === "true"; + return (await readConfigValue(MAINTENANCE_MODE_KEY)) === "true"; } catch { return false; } } export async function setMaintenanceMode(enabled: boolean): Promise { - await prisma.appConfig.upsert({ - where: { key: MAINTENANCE_MODE_KEY }, - update: { - value: enabled ? "true" : "false", - }, - create: { - key: MAINTENANCE_MODE_KEY, - value: enabled ? "true" : "false", - }, - }); + await upsertConfig(MAINTENANCE_MODE_KEY, enabled ? "true" : "false"); } export async function getSiteSettings(): Promise { try { - const configs = await prisma.appConfig.findMany({ - where: { - key: { - in: [SITE_SETTINGS_KEY, SITE_NAME_KEY], - }, - }, - select: { - key: true, - value: true, - }, - }); + const configs = await db + .select({ key: appConfig.key, value: appConfig.value }) + .from(appConfig) + .where(inArray(appConfig.key, [SITE_SETTINGS_KEY, SITE_NAME_KEY])); const configMap = new Map(configs.map((config) => [config.key, config.value])); const fallbackName = configMap.get(SITE_NAME_KEY) ?? DEFAULT_SITE_NAME; @@ -115,26 +115,12 @@ export async function getSiteSettings(): Promise { } export async function updateSiteSettings(settings: SiteSettings): Promise { - await prisma.appConfig.upsert({ - where: { key: SITE_SETTINGS_KEY }, - update: { - value: JSON.stringify(settings), - }, - create: { - key: SITE_SETTINGS_KEY, - value: JSON.stringify(settings), - }, - }); + await upsertConfig(SITE_SETTINGS_KEY, JSON.stringify(settings)); } export async function getMailSettings(): Promise { try { - const config = await prisma.appConfig.findUnique({ - where: { key: MAIL_SETTINGS_KEY }, - select: { value: true }, - }); - - return parseMailSettingsValue(config?.value); + return parseMailSettingsValue(await readConfigValue(MAIL_SETTINGS_KEY)); } catch { return buildDefaultMailSettings(); } @@ -147,26 +133,12 @@ export async function getMailSettingsFormValues(): Promise { - await prisma.appConfig.upsert({ - where: { key: MAIL_SETTINGS_KEY }, - update: { - value: JSON.stringify(settings), - }, - create: { - key: MAIL_SETTINGS_KEY, - value: JSON.stringify(settings), - }, - }); + await upsertConfig(MAIL_SETTINGS_KEY, JSON.stringify(settings)); } export async function getMarqueeSettings(): Promise { try { - const config = await prisma.appConfig.findUnique({ - where: { key: MARQUEE_SETTINGS_KEY }, - select: { value: true }, - }); - - return parseMarqueeSettingsValue(config?.value); + return parseMarqueeSettingsValue(await readConfigValue(MARQUEE_SETTINGS_KEY)); } catch { return buildDefaultMarqueeSettings(); } @@ -175,75 +147,52 @@ export async function getMarqueeSettings(): Promise { export async function updateMarqueeSettings(settings: MarqueeSettings): Promise { const normalizedSettings = syncMarqueeSettingsToGermanSource(settings); - await prisma.appConfig.upsert({ - where: { key: MARQUEE_SETTINGS_KEY }, - update: { - value: JSON.stringify(normalizedSettings), - }, - create: { - key: MARQUEE_SETTINGS_KEY, - value: JSON.stringify(normalizedSettings), - }, - }); + await upsertConfig(MARQUEE_SETTINGS_KEY, JSON.stringify(normalizedSettings)); } export async function getSiteSettingsMediaBindings(): Promise { try { - const usages = await prisma.mediaUsage.findMany({ - where: { - entityType: SITE_SETTINGS_ENTITY_TYPE, - entityId: SITE_SETTINGS_ENTITY_ID, - }, - select: { - fieldKey: true, - updatedAt: true, - asset: { - select: { - id: true, - url: true, - }, - }, - }, - }); + const usages = await db + .select({ + fieldKey: mediaUsage.fieldKey, + updatedAt: mediaUsage.updatedAt, + assetId: mediaAsset.id, + assetUrl: mediaAsset.url, + }) + .from(mediaUsage) + .innerJoin(mediaAsset, eq(mediaAsset.id, mediaUsage.assetId)) + .where( + and( + eq(mediaUsage.entityType, SITE_SETTINGS_ENTITY_TYPE), + eq(mediaUsage.entityId, SITE_SETTINGS_ENTITY_ID), + ), + ); - return usages.reduce( - (result, usage) => { - if (usage.fieldKey === SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY) { - result.siteLogoLight = { - assetId: usage.asset.id, - url: usage.asset.url, - version: usage.updatedAt.toISOString(), - }; - } + return usages.reduce((result, usage) => { + const binding = { + assetId: usage.assetId, + url: usage.assetUrl, + version: usage.updatedAt.toISOString(), + }; - if (usage.fieldKey === SITE_SETTINGS_LOGO_DARK_FIELD_KEY) { - result.siteLogoDark = { - assetId: usage.asset.id, - url: usage.asset.url, - version: usage.updatedAt.toISOString(), - }; - } + if (usage.fieldKey === SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY) { + result.siteLogoLight = binding; + } - if (usage.fieldKey === SITE_SETTINGS_FAVICON_FIELD_KEY) { - result.favicon = { - assetId: usage.asset.id, - url: usage.asset.url, - version: usage.updatedAt.toISOString(), - }; - } + if (usage.fieldKey === SITE_SETTINGS_LOGO_DARK_FIELD_KEY) { + result.siteLogoDark = binding; + } - if (usage.fieldKey === SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY) { - result.defaultOgImage = { - assetId: usage.asset.id, - url: usage.asset.url, - version: usage.updatedAt.toISOString(), - }; - } + if (usage.fieldKey === SITE_SETTINGS_FAVICON_FIELD_KEY) { + result.favicon = binding; + } - return result; - }, - getDefaultSiteSettingsMediaBindings(), - ); + if (usage.fieldKey === SITE_SETTINGS_DEFAULT_OG_IMAGE_FIELD_KEY) { + result.defaultOgImage = binding; + } + + return result; + }, getDefaultSiteSettingsMediaBindings()); } catch { return getDefaultSiteSettingsMediaBindings(); } diff --git a/lib/db/enums.ts b/lib/db/enums.ts new file mode 100644 index 0000000..fea96b1 --- /dev/null +++ b/lib/db/enums.ts @@ -0,0 +1,35 @@ +import { + mediaKind, + mediaSource, + mediaUsageType, + portfolioAssetKind, + portfolioProjectViewMode, + portfolioSectionType, +} from "./schema"; + +/** + * Prisma-compatible enum objects + types, derived from the Drizzle pgEnums, so + * existing consumers can keep writing `MediaKind.IMAGE` (value) and `: MediaKind` + * (type) — only the import path changes from `@prisma/client` to `@/lib/db/enums`. + */ +function asEnum(values: readonly T[]): { [K in T]: K } { + return Object.fromEntries(values.map((v) => [v, v])) as { [K in T]: K }; +} + +export const MediaKind = asEnum(mediaKind.enumValues); +export type MediaKind = (typeof mediaKind.enumValues)[number]; + +export const MediaSource = asEnum(mediaSource.enumValues); +export type MediaSource = (typeof mediaSource.enumValues)[number]; + +export const MediaUsageType = asEnum(mediaUsageType.enumValues); +export type MediaUsageType = (typeof mediaUsageType.enumValues)[number]; + +export const PortfolioAssetKind = asEnum(portfolioAssetKind.enumValues); +export type PortfolioAssetKind = (typeof portfolioAssetKind.enumValues)[number]; + +export const PortfolioProjectViewMode = asEnum(portfolioProjectViewMode.enumValues); +export type PortfolioProjectViewMode = (typeof portfolioProjectViewMode.enumValues)[number]; + +export const PortfolioSectionType = asEnum(portfolioSectionType.enumValues); +export type PortfolioSectionType = (typeof portfolioSectionType.enumValues)[number]; diff --git a/lib/db/index.ts b/lib/db/index.ts new file mode 100644 index 0000000..c7302da --- /dev/null +++ b/lib/db/index.ts @@ -0,0 +1,26 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +import * as schema from "./schema"; + +/** + * The Drizzle database client (postgres.js driver), matching the house standard + * used by the other projects. Replaces the old Prisma client (`lib/prisma.ts`). + * A single connection is reused across hot reloads in dev. + */ +const connectionString = + process.env.DATABASE_URL ?? + "postgresql://postgres:postgres@localhost:5432/moh_sass?schema=public"; + +const globalForDb = globalThis as unknown as { + dbClient: ReturnType | undefined; +}; + +const client = globalForDb.dbClient ?? postgres(connectionString); + +if (process.env.NODE_ENV !== "production") { + globalForDb.dbClient = client; +} + +export const db = drizzle(client, { schema }); +export { schema }; diff --git a/lib/db/migrations/0000_fixed_venom.sql b/lib/db/migrations/0000_fixed_venom.sql new file mode 100644 index 0000000..49dda32 --- /dev/null +++ b/lib/db/migrations/0000_fixed_venom.sql @@ -0,0 +1,125 @@ +CREATE TYPE "public"."media_kind" AS ENUM('IMAGE', 'DOCUMENT');--> statement-breakpoint +CREATE TYPE "public"."media_source" AS ENUM('UPLOAD', 'EXTERNAL');--> statement-breakpoint +CREATE TYPE "public"."media_usage_type" AS ENUM('PORTFOLIO_COVER', 'PORTFOLIO_SECTION', 'PORTFOLIO_ASSET', 'GENERIC');--> statement-breakpoint +CREATE TYPE "public"."portfolio_asset_kind" AS ENUM('IMAGE', 'DOCUMENT');--> statement-breakpoint +CREATE TYPE "public"."portfolio_project_view_mode" AS ENUM('GRID', 'STORY', 'CASE_STUDY');--> statement-breakpoint +CREATE TYPE "public"."portfolio_section_type" AS ENUM('RICH_TEXT', 'GALLERY', 'STATS', 'DELIVERABLES', 'LINK');--> statement-breakpoint +CREATE TABLE "app_config" ( + "id" text PRIMARY KEY NOT NULL, + "key" text NOT NULL, + "value" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "app_config_key_unique" UNIQUE("key") +); +--> statement-breakpoint +CREATE TABLE "category" ( + "id" text PRIMARY KEY NOT NULL, + "slug" text NOT NULL, + "name_ar" text NOT NULL, + "name_en" text NOT NULL, + "name_de" text NOT NULL, + "description_ar" text NOT NULL, + "description_en" text NOT NULL, + "description_de" text NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "category_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "media_asset" ( + "id" text PRIMARY KEY NOT NULL, + "source" "media_source" NOT NULL, + "kind" "media_kind" NOT NULL, + "url" text NOT NULL, + "file_name" text NOT NULL, + "label" text NOT NULL, + "alt_text" text, + "mime_type" text, + "size" integer, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "media_usage" ( + "id" text PRIMARY KEY NOT NULL, + "asset_id" text NOT NULL, + "usage_type" "media_usage_type" NOT NULL, + "entity_type" text NOT NULL, + "entity_id" text NOT NULL, + "field_key" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "portfolio_asset" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "kind" "portfolio_asset_kind" NOT NULL, + "file_path" text NOT NULL, + "alt_ar" text NOT NULL, + "alt_en" text NOT NULL, + "alt_de" text NOT NULL, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "portfolio_project" ( + "id" text PRIMARY KEY NOT NULL, + "category_id" text NOT NULL, + "slug" text NOT NULL, + "view_mode" "portfolio_project_view_mode" DEFAULT 'GRID' NOT NULL, + "title_ar" text NOT NULL, + "title_en" text NOT NULL, + "title_de" text NOT NULL, + "summary_ar" text NOT NULL, + "summary_en" text NOT NULL, + "summary_de" text NOT NULL, + "client_name" text NOT NULL, + "project_year" integer NOT NULL, + "service_label_ar" text NOT NULL, + "service_label_en" text NOT NULL, + "service_label_de" text NOT NULL, + "preview_url" text, + "cover_image_path" text, + "is_featured" boolean DEFAULT false NOT NULL, + "is_published" boolean DEFAULT false NOT NULL, + "published_at" timestamp with time zone, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "portfolio_project_slug_unique" UNIQUE("slug") +); +--> statement-breakpoint +CREATE TABLE "portfolio_section" ( + "id" text PRIMARY KEY NOT NULL, + "project_id" text NOT NULL, + "type" "portfolio_section_type" NOT NULL, + "title_ar" text NOT NULL, + "title_en" text NOT NULL, + "title_de" text NOT NULL, + "body_ar" text NOT NULL, + "body_en" text NOT NULL, + "body_de" text NOT NULL, + "image_path" text, + "link_url" text, + "sort_order" integer DEFAULT 0 NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "media_usage" ADD CONSTRAINT "media_usage_asset_id_media_asset_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."media_asset"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "portfolio_asset" ADD CONSTRAINT "portfolio_asset_project_id_portfolio_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."portfolio_project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "portfolio_project" ADD CONSTRAINT "portfolio_project_category_id_category_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."category"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "portfolio_section" ADD CONSTRAINT "portfolio_section_project_id_portfolio_project_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."portfolio_project"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "media_asset_kind_created_idx" ON "media_asset" USING btree ("kind","created_at");--> statement-breakpoint +CREATE UNIQUE INDEX "media_usage_unique_slot" ON "media_usage" USING btree ("usage_type","entity_type","entity_id","field_key");--> statement-breakpoint +CREATE INDEX "media_usage_asset_idx" ON "media_usage" USING btree ("asset_id");--> statement-breakpoint +CREATE INDEX "media_usage_entity_idx" ON "media_usage" USING btree ("entity_type","entity_id");--> statement-breakpoint +CREATE INDEX "portfolio_asset_project_sort_idx" ON "portfolio_asset" USING btree ("project_id","sort_order");--> statement-breakpoint +CREATE INDEX "portfolio_project_category_published_sort_idx" ON "portfolio_project" USING btree ("category_id","is_published","sort_order");--> statement-breakpoint +CREATE INDEX "portfolio_project_published_sort_idx" ON "portfolio_project" USING btree ("is_published","sort_order");--> statement-breakpoint +CREATE INDEX "portfolio_section_project_sort_idx" ON "portfolio_section" USING btree ("project_id","sort_order"); \ No newline at end of file diff --git a/lib/db/migrations/meta/0000_snapshot.json b/lib/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..b3551fd --- /dev/null +++ b/lib/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,956 @@ +{ + "id": "a001b9c1-c931-4e9f-b21c-50cdfbffb6a6", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_config": { + "name": "app_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "app_config_key_unique": { + "name": "app_config_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.category": { + "name": "category", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_ar": { + "name": "name_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_en": { + "name": "name_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name_de": { + "name": "name_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_ar": { + "name": "description_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_en": { + "name": "description_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description_de": { + "name": "description_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "category_slug_unique": { + "name": "category_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media_asset": { + "name": "media_asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "source": { + "name": "source", + "type": "media_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "media_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_text": { + "name": "alt_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_asset_kind_created_idx": { + "name": "media_asset_kind_created_idx", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.media_usage": { + "name": "media_usage", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_type": { + "name": "usage_type", + "type": "media_usage_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_key": { + "name": "field_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "media_usage_unique_slot": { + "name": "media_usage_unique_slot", + "columns": [ + { + "expression": "usage_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "field_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_usage_asset_idx": { + "name": "media_usage_asset_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_usage_entity_idx": { + "name": "media_usage_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_usage_asset_id_media_asset_id_fk": { + "name": "media_usage_asset_id_media_asset_id_fk", + "tableFrom": "media_usage", + "tableTo": "media_asset", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_asset": { + "name": "portfolio_asset", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "portfolio_asset_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_ar": { + "name": "alt_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_en": { + "name": "alt_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alt_de": { + "name": "alt_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "portfolio_asset_project_sort_idx": { + "name": "portfolio_asset_project_sort_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "portfolio_asset_project_id_portfolio_project_id_fk": { + "name": "portfolio_asset_project_id_portfolio_project_id_fk", + "tableFrom": "portfolio_asset", + "tableTo": "portfolio_project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_project": { + "name": "portfolio_project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "category_id": { + "name": "category_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "view_mode": { + "name": "view_mode", + "type": "portfolio_project_view_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GRID'" + }, + "title_ar": { + "name": "title_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_de": { + "name": "title_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_ar": { + "name": "summary_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_en": { + "name": "summary_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary_de": { + "name": "summary_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_year": { + "name": "project_year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "service_label_ar": { + "name": "service_label_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_label_en": { + "name": "service_label_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "service_label_de": { + "name": "service_label_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview_url": { + "name": "preview_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image_path": { + "name": "cover_image_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_featured": { + "name": "is_featured", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "portfolio_project_category_published_sort_idx": { + "name": "portfolio_project_category_published_sort_idx", + "columns": [ + { + "expression": "category_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_published", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "portfolio_project_published_sort_idx": { + "name": "portfolio_project_published_sort_idx", + "columns": [ + { + "expression": "is_published", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "portfolio_project_category_id_category_id_fk": { + "name": "portfolio_project_category_id_category_id_fk", + "tableFrom": "portfolio_project", + "tableTo": "category", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "portfolio_project_slug_unique": { + "name": "portfolio_project_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.portfolio_section": { + "name": "portfolio_section", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "portfolio_section_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "title_ar": { + "name": "title_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_en": { + "name": "title_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title_de": { + "name": "title_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_ar": { + "name": "body_ar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_en": { + "name": "body_en", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_de": { + "name": "body_de", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_path": { + "name": "image_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_url": { + "name": "link_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "portfolio_section_project_sort_idx": { + "name": "portfolio_section_project_sort_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "portfolio_section_project_id_portfolio_project_id_fk": { + "name": "portfolio_section_project_id_portfolio_project_id_fk", + "tableFrom": "portfolio_section", + "tableTo": "portfolio_project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.media_kind": { + "name": "media_kind", + "schema": "public", + "values": [ + "IMAGE", + "DOCUMENT" + ] + }, + "public.media_source": { + "name": "media_source", + "schema": "public", + "values": [ + "UPLOAD", + "EXTERNAL" + ] + }, + "public.media_usage_type": { + "name": "media_usage_type", + "schema": "public", + "values": [ + "PORTFOLIO_COVER", + "PORTFOLIO_SECTION", + "PORTFOLIO_ASSET", + "GENERIC" + ] + }, + "public.portfolio_asset_kind": { + "name": "portfolio_asset_kind", + "schema": "public", + "values": [ + "IMAGE", + "DOCUMENT" + ] + }, + "public.portfolio_project_view_mode": { + "name": "portfolio_project_view_mode", + "schema": "public", + "values": [ + "GRID", + "STORY", + "CASE_STUDY" + ] + }, + "public.portfolio_section_type": { + "name": "portfolio_section_type", + "schema": "public", + "values": [ + "RICH_TEXT", + "GALLERY", + "STATS", + "DELIVERABLES", + "LINK" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json new file mode 100644 index 0000000..ab09565 --- /dev/null +++ b/lib/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1786049545718, + "tag": "0000_fixed_venom", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/lib/db/schema.ts b/lib/db/schema.ts new file mode 100644 index 0000000..fe3f3b4 --- /dev/null +++ b/lib/db/schema.ts @@ -0,0 +1,241 @@ +import { relations } from "drizzle-orm"; +import { + boolean, + index, + integer, + pgEnum, + pgTable, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; + +/** + * Drizzle schema — the single source of truth for the database, replacing the + * old Prisma schema (see docs). The database is Postgres; migrations are + * generated with `drizzle-kit generate`. IDs are app-generated opaque strings + * (was Prisma `cuid()`), timestamps default in the DB and bump on update. + */ + +// `crypto.randomUUID()` is a global in Node 20+ and browsers (no node: import), +// so the schema stays safe to pull into a client bundle via lib/db/enums. +const id = () => + text("id") + .primaryKey() + .$defaultFn(() => crypto.randomUUID()); + +const createdAt = timestamp("created_at", { withTimezone: true }).notNull().defaultNow(); +const updatedAt = timestamp("updated_at", { withTimezone: true }) + .notNull() + .defaultNow() + .$onUpdate(() => new Date()); + +// --- Enums ------------------------------------------------------------------ + +export const portfolioSectionType = pgEnum("portfolio_section_type", [ + "RICH_TEXT", + "GALLERY", + "STATS", + "DELIVERABLES", + "LINK", +]); + +export const portfolioAssetKind = pgEnum("portfolio_asset_kind", ["IMAGE", "DOCUMENT"]); + +export const portfolioProjectViewMode = pgEnum("portfolio_project_view_mode", [ + "GRID", + "STORY", + "CASE_STUDY", +]); + +export const mediaSource = pgEnum("media_source", ["UPLOAD", "EXTERNAL"]); + +export const mediaKind = pgEnum("media_kind", ["IMAGE", "DOCUMENT"]); + +export const mediaUsageType = pgEnum("media_usage_type", [ + "PORTFOLIO_COVER", + "PORTFOLIO_SECTION", + "PORTFOLIO_ASSET", + "GENERIC", +]); + +// --- Tables ----------------------------------------------------------------- + +export const appConfig = pgTable("app_config", { + id: id(), + key: text("key").notNull().unique(), + value: text("value").notNull(), + createdAt, + updatedAt, +}); + +export const category = pgTable("category", { + id: id(), + slug: text("slug").notNull().unique(), + nameAr: text("name_ar").notNull(), + nameEn: text("name_en").notNull(), + nameDe: text("name_de").notNull(), + descriptionAr: text("description_ar").notNull(), + descriptionEn: text("description_en").notNull(), + descriptionDe: text("description_de").notNull(), + sortOrder: integer("sort_order").notNull().default(0), + isActive: boolean("is_active").notNull().default(true), + createdAt, + updatedAt, +}); + +export const portfolioProject = pgTable( + "portfolio_project", + { + id: id(), + categoryId: text("category_id") + .notNull() + .references(() => category.id, { onDelete: "restrict" }), + slug: text("slug").notNull().unique(), + viewMode: portfolioProjectViewMode("view_mode").notNull().default("GRID"), + titleAr: text("title_ar").notNull(), + titleEn: text("title_en").notNull(), + titleDe: text("title_de").notNull(), + summaryAr: text("summary_ar").notNull(), + summaryEn: text("summary_en").notNull(), + summaryDe: text("summary_de").notNull(), + clientName: text("client_name").notNull(), + projectYear: integer("project_year").notNull(), + serviceLabelAr: text("service_label_ar").notNull(), + serviceLabelEn: text("service_label_en").notNull(), + serviceLabelDe: text("service_label_de").notNull(), + previewUrl: text("preview_url"), + coverImagePath: text("cover_image_path"), + isFeatured: boolean("is_featured").notNull().default(false), + isPublished: boolean("is_published").notNull().default(false), + publishedAt: timestamp("published_at", { withTimezone: true }), + sortOrder: integer("sort_order").notNull().default(0), + createdAt, + updatedAt, + }, + (t) => [ + index("portfolio_project_category_published_sort_idx").on(t.categoryId, t.isPublished, t.sortOrder), + index("portfolio_project_published_sort_idx").on(t.isPublished, t.sortOrder), + ], +); + +export const portfolioSection = pgTable( + "portfolio_section", + { + id: id(), + projectId: text("project_id") + .notNull() + .references(() => portfolioProject.id, { onDelete: "cascade" }), + type: portfolioSectionType("type").notNull(), + titleAr: text("title_ar").notNull(), + titleEn: text("title_en").notNull(), + titleDe: text("title_de").notNull(), + bodyAr: text("body_ar").notNull(), + bodyEn: text("body_en").notNull(), + bodyDe: text("body_de").notNull(), + imagePath: text("image_path"), + linkUrl: text("link_url"), + sortOrder: integer("sort_order").notNull().default(0), + createdAt, + updatedAt, + }, + (t) => [index("portfolio_section_project_sort_idx").on(t.projectId, t.sortOrder)], +); + +export const portfolioAsset = pgTable( + "portfolio_asset", + { + id: id(), + projectId: text("project_id") + .notNull() + .references(() => portfolioProject.id, { onDelete: "cascade" }), + kind: portfolioAssetKind("kind").notNull(), + filePath: text("file_path").notNull(), + altAr: text("alt_ar").notNull(), + altEn: text("alt_en").notNull(), + altDe: text("alt_de").notNull(), + sortOrder: integer("sort_order").notNull().default(0), + createdAt, + updatedAt, + }, + (t) => [index("portfolio_asset_project_sort_idx").on(t.projectId, t.sortOrder)], +); + +export const mediaAsset = pgTable( + "media_asset", + { + id: id(), + source: mediaSource("source").notNull(), + kind: mediaKind("kind").notNull(), + url: text("url").notNull(), + fileName: text("file_name").notNull(), + label: text("label").notNull(), + altText: text("alt_text"), + mimeType: text("mime_type"), + size: integer("size"), + createdAt, + updatedAt, + }, + (t) => [index("media_asset_kind_created_idx").on(t.kind, t.createdAt)], +); + +export const mediaUsage = pgTable( + "media_usage", + { + id: id(), + assetId: text("asset_id") + .notNull() + .references(() => mediaAsset.id, { onDelete: "cascade" }), + usageType: mediaUsageType("usage_type").notNull(), + entityType: text("entity_type").notNull(), + entityId: text("entity_id").notNull(), + fieldKey: text("field_key").notNull(), + createdAt, + updatedAt, + }, + (t) => [ + uniqueIndex("media_usage_unique_slot").on(t.usageType, t.entityType, t.entityId, t.fieldKey), + index("media_usage_asset_idx").on(t.assetId), + index("media_usage_entity_idx").on(t.entityType, t.entityId), + ], +); + +// --- Relations (for the relational query API: db.query.*.findMany({ with })) -- + +export const categoryRelations = relations(category, ({ many }) => ({ + projects: many(portfolioProject), +})); + +export const portfolioProjectRelations = relations(portfolioProject, ({ one, many }) => ({ + category: one(category, { + fields: [portfolioProject.categoryId], + references: [category.id], + }), + sections: many(portfolioSection), + assets: many(portfolioAsset), +})); + +export const portfolioSectionRelations = relations(portfolioSection, ({ one }) => ({ + project: one(portfolioProject, { + fields: [portfolioSection.projectId], + references: [portfolioProject.id], + }), +})); + +export const portfolioAssetRelations = relations(portfolioAsset, ({ one }) => ({ + project: one(portfolioProject, { + fields: [portfolioAsset.projectId], + references: [portfolioProject.id], + }), +})); + +export const mediaAssetRelations = relations(mediaAsset, ({ many }) => ({ + usages: many(mediaUsage), +})); + +export const mediaUsageRelations = relations(mediaUsage, ({ one }) => ({ + asset: one(mediaAsset, { + fields: [mediaUsage.assetId], + references: [mediaAsset.id], + }), +})); diff --git a/lib/media-service.ts b/lib/media-service.ts index e23b386..65eacf1 100644 --- a/lib/media-service.ts +++ b/lib/media-service.ts @@ -1,6 +1,6 @@ import path from "path"; -import { MediaKind, MediaSource } from "@prisma/client"; +import { MediaKind, MediaSource } from "@/lib/db/enums"; import { createMediaAsset, getMediaAssetById } from "@/lib/media"; import { getExtensionForMimeType, removeManagedMediaFile, saveMediaUpload } from "@/lib/media-storage"; diff --git a/lib/media-validation.ts b/lib/media-validation.ts index 974ba29..d5e978e 100644 --- a/lib/media-validation.ts +++ b/lib/media-validation.ts @@ -1,4 +1,4 @@ -import { MediaKind } from "@prisma/client"; +import { MediaKind } from "@/lib/db/enums"; import { z } from "zod"; const mediaModeSchema = z.enum(["library", "external", "upload"]); diff --git a/lib/media.ts b/lib/media.ts index 0a22426..c7d4879 100644 --- a/lib/media.ts +++ b/lib/media.ts @@ -1,20 +1,17 @@ -import type { - MediaAsset, - MediaKind, - MediaSource, - MediaUsage, - MediaUsageType, -} from "@prisma/client"; +import { and, desc, eq } from "drizzle-orm"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { mediaAsset, mediaUsage } from "@/lib/db/schema"; +import type { MediaKind, MediaSource, MediaUsageType } from "@/lib/db/enums"; + +type MediaAssetRow = typeof mediaAsset.$inferSelect; +type MediaUsageRow = typeof mediaUsage.$inferSelect; export type MediaAssetView = Pick< - MediaAsset, + MediaAssetRow, "id" | "source" | "kind" | "url" | "fileName" | "label" | "altText" | "mimeType" | "size" | "createdAt" > & { - usages: Array< - Pick - >; + usages: Array>; }; export type MediaOption = Pick; @@ -25,11 +22,7 @@ export type PortfolioMediaBindings = { assetIds: Record; }; -function mapMediaAsset( - asset: MediaAsset & { - usages: MediaUsage[]; - }, -): MediaAssetView { +function mapMediaAsset(asset: MediaAssetRow & { usages: MediaUsageRow[] }): MediaAssetView { return { id: asset.id, source: asset.source, @@ -52,40 +45,32 @@ function mapMediaAsset( } export async function getAdminMediaAssets() { - const assets = await prisma.mediaAsset.findMany({ - include: { - usages: { - orderBy: [{ createdAt: "desc" }], - }, - }, - orderBy: [{ createdAt: "desc" }], + const assets = await db.query.mediaAsset.findMany({ + with: { usages: { orderBy: [desc(mediaUsage.createdAt)] } }, + orderBy: [desc(mediaAsset.createdAt)], }); return assets.map(mapMediaAsset); } -export async function getMediaOptions(filters?: { kind?: MediaKind }) { - const assets = await prisma.mediaAsset.findMany({ - where: filters?.kind ? { kind: filters.kind } : undefined, - orderBy: [{ createdAt: "desc" }], - select: { - id: true, - kind: true, - url: true, - label: true, - source: true, - }, - }); - - return assets; +export async function getMediaOptions(filters?: { kind?: MediaKind }): Promise { + return db + .select({ + id: mediaAsset.id, + kind: mediaAsset.kind, + url: mediaAsset.url, + label: mediaAsset.label, + source: mediaAsset.source, + }) + .from(mediaAsset) + .where(filters?.kind ? eq(mediaAsset.kind, filters.kind) : undefined) + .orderBy(desc(mediaAsset.createdAt)); } export async function getMediaAssetById(id: string) { - const asset = await prisma.mediaAsset.findUnique({ - where: { id }, - include: { - usages: true, - }, + const asset = await db.query.mediaAsset.findFirst({ + where: eq(mediaAsset.id, id), + with: { usages: true }, }); return asset ? mapMediaAsset(asset) : null; @@ -101,8 +86,9 @@ export async function createMediaAsset(input: { mimeType?: string | null; size?: number | null; }) { - return prisma.mediaAsset.create({ - data: { + const [created] = await db + .insert(mediaAsset) + .values({ source: input.source, kind: input.kind, url: input.url, @@ -111,8 +97,10 @@ export async function createMediaAsset(input: { altText: input.altText ?? null, mimeType: input.mimeType ?? null, size: input.size ?? null, - }, - }); + }) + .returning(); + + return created; } export async function replaceEntityMediaUsages(input: { @@ -124,51 +112,42 @@ export async function replaceEntityMediaUsages(input: { fieldKey: string; }>; }) { - await prisma.$transaction(async (tx) => { - await tx.mediaUsage.deleteMany({ - where: { - entityType: input.entityType, - entityId: input.entityId, - }, - }); + await db.transaction(async (tx) => { + await tx + .delete(mediaUsage) + .where(and(eq(mediaUsage.entityType, input.entityType), eq(mediaUsage.entityId, input.entityId))); if (input.usages.length === 0) { return; } - await tx.mediaUsage.createMany({ - data: input.usages.map((usage) => ({ + await tx.insert(mediaUsage).values( + input.usages.map((usage) => ({ assetId: usage.assetId, usageType: usage.usageType, entityType: input.entityType, entityId: input.entityId, fieldKey: usage.fieldKey, })), - }); + ); }); } export async function deleteEntityMediaUsages(entityType: string, entityId: string) { - await prisma.mediaUsage.deleteMany({ - where: { - entityType, - entityId, - }, - }); + await db + .delete(mediaUsage) + .where(and(eq(mediaUsage.entityType, entityType), eq(mediaUsage.entityId, entityId))); } export async function getPortfolioMediaBindings(projectId: string): Promise { - const usages = await prisma.mediaUsage.findMany({ - where: { - entityType: "portfolio-project", - entityId: projectId, - }, - select: { - assetId: true, - usageType: true, - fieldKey: true, - }, - }); + const usages = await db + .select({ + assetId: mediaUsage.assetId, + usageType: mediaUsage.usageType, + fieldKey: mediaUsage.fieldKey, + }) + .from(mediaUsage) + .where(and(eq(mediaUsage.entityType, "portfolio-project"), eq(mediaUsage.entityId, projectId))); return usages.reduce( (result, usage) => { @@ -195,9 +174,5 @@ export async function getPortfolioMediaBindings(projectId: string): Promise; - -type SectionRecord = Pick< - PortfolioSection, - | "id" - | "type" - | "titleAr" - | "titleEn" - | "titleDe" - | "bodyAr" - | "bodyEn" - | "bodyDe" - | "imagePath" - | "linkUrl" - | "sortOrder" ->; - -type AssetRecord = Pick< - PortfolioAsset, - "id" | "kind" | "filePath" | "altAr" | "altEn" | "altDe" | "sortOrder" ->; - -type ProjectRecord = Pick< - PortfolioProject, - | "id" - | "slug" - | "viewMode" - | "titleAr" - | "titleEn" - | "titleDe" - | "summaryAr" - | "summaryEn" - | "summaryDe" - | "clientName" - | "projectYear" - | "serviceLabelAr" - | "serviceLabelEn" - | "serviceLabelDe" - | "previewUrl" - | "coverImagePath" - | "isFeatured" - | "isPublished" - | "publishedAt" - | "sortOrder" ->; +type CategoryRecord = typeof categoryTable.$inferSelect; +type SectionRecord = typeof portfolioSection.$inferSelect; +type AssetRecord = typeof portfolioAsset.$inferSelect; +type ProjectRecord = typeof portfolioProject.$inferSelect; export type LocalizedContent = { ar: string; @@ -240,40 +188,29 @@ export function getLocalizedValue( } export async function getAdminPortfolioCategories() { - const categories = await prisma.category.findMany({ - include: { - _count: { - select: { - projects: true, - }, - }, - }, - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + const categories = await db.query.category.findMany({ + orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)], + with: { projects: { columns: { id: true } } }, }); return categories.map((category) => ({ ...mapCategory(category), - projectCount: category._count.projects, + projectCount: category.projects.length, })); } export async function getActivePortfolioCategories() { - const categories = await prisma.category.findMany({ - where: { - isActive: true, - }, - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], + const categories = await db.query.category.findMany({ + where: eq(categoryTable.isActive, true), + orderBy: [asc(categoryTable.sortOrder), asc(categoryTable.createdAt)], }); return categories.map(mapCategory); } export async function getActivePortfolioCategoryBySlug(slug: string) { - const category = await prisma.category.findFirst({ - where: { - slug, - isActive: true, - }, + const category = await db.query.category.findFirst({ + where: and(eq(categoryTable.slug, slug), eq(categoryTable.isActive, true)), }); return category ? mapCategory(category) : null; @@ -283,90 +220,78 @@ export async function getAdminPortfolioProjects(filters?: { categoryId?: string; status?: "all" | "draft" | "published"; }) { - const projects = await prisma.portfolioProject.findMany({ - where: { - ...(filters?.categoryId ? { categoryId: filters.categoryId } : {}), - ...(filters?.status === "draft" - ? { isPublished: false } - : filters?.status === "published" - ? { isPublished: true } - : {}), - }, - include: { + const conditions = [ + ...(filters?.categoryId ? [eq(portfolioProject.categoryId, filters.categoryId)] : []), + ...(filters?.status === "draft" + ? [eq(portfolioProject.isPublished, false)] + : filters?.status === "published" + ? [eq(portfolioProject.isPublished, true)] + : []), + ]; + + const projects = await db.query.portfolioProject.findMany({ + where: conditions.length ? and(...conditions) : undefined, + with: { category: true, - sections: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, - assets: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, + sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, + assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, - orderBy: [{ sortOrder: "asc" }, { createdAt: "desc" }], + orderBy: [asc(portfolioProject.sortOrder), desc(portfolioProject.createdAt)], }); return projects.map((project) => mapProject(project)); } export async function getPublishedPortfolioProjects(filters?: { categorySlug?: string }) { - const projects = await prisma.portfolioProject.findMany({ - where: { - isPublished: true, - category: { - isActive: true, - ...(filters?.categorySlug ? { slug: filters.categorySlug } : {}), - }, - }, - include: { + const projects = await db.query.portfolioProject.findMany({ + where: eq(portfolioProject.isPublished, true), + with: { category: true, - sections: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, - assets: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, + sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, + assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, - orderBy: [{ sortOrder: "asc" }, { publishedAt: "desc" }, { createdAt: "desc" }], + orderBy: [ + asc(portfolioProject.sortOrder), + desc(portfolioProject.publishedAt), + desc(portfolioProject.createdAt), + ], }); - return projects.map((project) => mapProject(project)); + // Prisma filtered on the related category (active + optional slug); the + // relational query filters the main table only, so narrow here. + return projects + .filter( + (project) => + project.category.isActive && + (!filters?.categorySlug || project.category.slug === filters.categorySlug), + ) + .map((project) => mapProject(project)); } export const getPublishedPortfolioProjectBySlug = cache(async function (slug: string) { - const project = await prisma.portfolioProject.findFirst({ - where: { - slug, - isPublished: true, - category: { - isActive: true, - }, - }, - include: { + const project = await db.query.portfolioProject.findFirst({ + where: and(eq(portfolioProject.slug, slug), eq(portfolioProject.isPublished, true)), + with: { category: true, - sections: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, - assets: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, + sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, + assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, }); - return project ? mapProject(project) : null; + if (!project || !project.category.isActive) { + return null; + } + + return mapProject(project); }); export async function getAdminPortfolioProjectById(id: string) { - const project = await prisma.portfolioProject.findUnique({ - where: { - id, - }, - include: { + const project = await db.query.portfolioProject.findFirst({ + where: eq(portfolioProject.id, id), + with: { category: true, - sections: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, - assets: { - orderBy: [{ sortOrder: "asc" }, { createdAt: "asc" }], - }, + sections: { orderBy: [asc(portfolioSection.sortOrder), asc(portfolioSection.createdAt)] }, + assets: { orderBy: [asc(portfolioAsset.sortOrder), asc(portfolioAsset.createdAt)] }, }, }); diff --git a/lib/prisma.ts b/lib/prisma.ts deleted file mode 100644 index 5c4d2bf..0000000 --- a/lib/prisma.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { PrismaPg } from "@prisma/adapter-pg"; -import { PrismaClient } from "@prisma/client"; -import { Pool } from "pg"; - -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; - prismaPool: Pool | undefined; -}; - -const connectionString = - process.env.DATABASE_URL ?? - "postgresql://postgres:postgres@localhost:5432/moh_sass?schema=public"; - -const pool = - globalForPrisma.prismaPool ?? - new Pool({ - connectionString, - }); - -const adapter = new PrismaPg(pool); - -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - adapter, - log: ["warn", "error"], - }); - -if (process.env.NODE_ENV !== "production") { - globalForPrisma.prismaPool = pool; - globalForPrisma.prisma = prisma; -} diff --git a/package-lock.json b/package-lock.json index 83550fa..ba64118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,6 @@ "name": "moh-sass", "version": "0.1.0", "dependencies": { - "@prisma/adapter-pg": "^7.4.2", - "@prisma/client": "^7.4.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -19,6 +17,7 @@ "@types/nodemailer": "^7.0.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "framer-motion": "^12.35.0", "gsap": "^3.15.0", "lucide-react": "^0.577.0", @@ -27,6 +26,7 @@ "next-themes": "^0.4.6", "nodemailer": "^8.0.1", "pg": "^8.20.0", + "postgres": "^3.4.9", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", @@ -35,7 +35,6 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.5.4", - "@electric-sql/pglite-socket": "^0.2.7", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", @@ -44,12 +43,11 @@ "@types/pg": "^8.18.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.4", "eslint-config-next": "^16.1.6", "jsdom": "^30.0.1", - "pglite-prisma-adapter": "^0.7.2", "postcss": "^8", - "prisma": "^7.4.2", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "^5", @@ -429,8 +427,9 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", @@ -441,8 +440,9 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" @@ -452,15 +452,17 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@chevrotain/utils": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@csstools/color-helpers": { "version": "6.1.0", @@ -602,110 +604,20 @@ "node": ">=20.19.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@electric-sql/pglite": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.4.tgz", "integrity": "sha512-yYZUyyXrHU7tPlCjwZQJ6hIG9DscdCCn7Uk0mYKwC1FeHX286AbcmFveMiRBEak8e9iPupjsoVImN3yJZVed2g==", - "dev": true, + "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@electric-sql/pglite-age": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-age/-/pglite-age-0.0.5.tgz", - "integrity": "sha512-0PoZu8c5tkvqvVrS77Wcz3hK+nSGw8u8omX8cOWzrg7P95lvS3t4ie9tg5RUPSoFS3xkGJFMBwrqWm5BHTDjiQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pg_hashids": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_hashids/-/pglite-pg_hashids-0.0.5.tgz", - "integrity": "sha512-z8iR0ui18LiK8QBCQfq4vrzMIBEUROdYPWJSG5f91v8JHv0lbCc5B/qw5pSKnoht1pXlH0yPcvLF/+GuMYSRdQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pg_ivm": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_ivm/-/pglite-pg_ivm-0.0.5.tgz", - "integrity": "sha512-xx9IjXcRmbHivVLB9DbaA9MsMx8vJGk+cqN2N592oCoKqe+k7SXcUuRh/PSglS7jaHPXM/KvE7KDOh+lip3LfA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pg_textsearch": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_textsearch/-/pglite-pg_textsearch-0.0.6.tgz", - "integrity": "sha512-n5lTjivQn/ZvVFdMoW3Rn9u30S0LeAh/sOmjKptnF0nsTa1zIZxa0mAuynzIKrMoUQ0wOFHyB3mj1lIK2UWHUA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pg_uuidv7": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pg_uuidv7/-/pglite-pg_uuidv7-0.0.5.tgz", - "integrity": "sha512-uAatun9hKF6TKYZBc83SaU+snWsg6v2cRatzmty+3e6ai4yDpcOMJBjmA+I73ec0Dk6tykEDsgcsJ0+Eg8af+Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pgtap": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pgtap/-/pglite-pgtap-0.0.5.tgz", - "integrity": "sha512-Bf2L2mcsN8AjqZJVXKoUOIaXljtrapclfgrgx4MN2UMLxJtzoPju0KR9aD89mi09eJEMZuKIDsKrtEdXUPuzLQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-pgvector": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-pgvector/-/pglite-pgvector-0.0.5.tgz", - "integrity": "sha512-KEPAkD/3FN6WlDM6XPi2Ko1Xk0ecjOU9OfzCoclVkiRoEVNRYwKo1iO+tlkelMgp07SszmSC4JdSBueH2p7uOQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4" - } - }, - "node_modules/@electric-sql/pglite-socket": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.2.7.tgz", - "integrity": "sha512-lhYx1OOVueiWJRa2Uj1QiIjCWmpRnYLNs5rRH/g47EqrYXP/Qt3VvcXGGum4uIouKKMVlPlkXb6E5Uw0aHRymw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "pglite-server": "dist/scripts/server.js" - }, - "peerDependencies": { - "@electric-sql/pglite": "0.5.4", - "@electric-sql/pglite-age": "0.0.5", - "@electric-sql/pglite-pg_hashids": "0.0.5", - "@electric-sql/pglite-pg_ivm": "0.0.5", - "@electric-sql/pglite-pg_textsearch": "0.0.6", - "@electric-sql/pglite-pg_uuidv7": "0.0.5", - "@electric-sql/pglite-pgtap": "0.0.5", - "@electric-sql/pglite-pgvector": "0.0.5" - } - }, "node_modules/@emnapi/core": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", @@ -739,6 +651,442 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.hirok.io", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", @@ -1499,8 +1847,9 @@ "version": "1.19.9", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18.14.1" }, @@ -2080,8 +2429,9 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" @@ -2094,8 +2444,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" } @@ -2642,22 +2993,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@prisma/adapter-pg": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.4.2.tgz", - "integrity": "sha512-oUo2Zhe9Tf6YwVL8kLPuOLTK1Z2pwi/Ua77t2PuGyBan2w7shRKqHvYK+3XXmRH9RWhPJ4SMtHZKpNo6Ax/4bQ==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/driver-adapter-utils": "7.4.2", - "pg": "^8.16.3", - "postgres-array": "3.0.4" - } - }, "node_modules/@prisma/client": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.2.tgz", "integrity": "sha512-ts2mu+cQHriAhSxngO3StcYubBGTWDtu/4juZhXCUKOwgh26l+s4KD3vT2kMUzFyrYnll9u/3qWrtzRv9CGWzA==", "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/client-runtime-utils": "7.4.2" }, @@ -2681,14 +3023,17 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/client-runtime-utils/-/client-runtime-utils-7.4.2.tgz", "integrity": "sha512-cID+rzOEb38VyMsx5LwJMEY4NGIrWCNpKu/0ImbeooQ2Px7TI+kOt7cm0NelxUzF2V41UVVXAmYjANZQtCu1/Q==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/config": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.2.tgz", "integrity": "sha512-CftBjWxav99lzY1Z4oDgomdb1gh9BJFAOmWF6P2v1xRfXqQb56DfBub+QKcERRdNoAzCb3HXy3Zii8Vb4AsXhg==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", @@ -2700,14 +3045,17 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.2.tgz", "integrity": "sha512-aP7qzu+g/JnbF6U69LMwHoUkELiserKmWsE2shYuEpNUJ4GrtxBCvZwCyCBHFSH2kLTF2l1goBlBh4wuvRq62w==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/dev": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", @@ -2732,15 +3080,17 @@ "version": "0.3.15", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/dev/node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "bin": { "pglite-server": "dist/scripts/server.js" }, @@ -2752,28 +3102,21 @@ "version": "0.2.20", "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, - "node_modules/@prisma/driver-adapter-utils": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.4.2.tgz", - "integrity": "sha512-REdjFpT/ye9KdDs+CXAXPIbMQkVLhne9G5Pe97sNY4Ovx4r2DAbWM9hOFvvB1Oq8H8bOCdu0Ri3AoGALquQqVw==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.4.2" - } - }, "node_modules/@prisma/engines": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.2.tgz", "integrity": "sha512-B+ZZhI4rXlzjVqRw/93AothEKOU5/x4oVyJFGo9RpHPnBwaPwk4Pi0Q4iGXipKxeXPs/dqljgNBjK0m8nocOJA==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", @@ -2785,15 +3128,17 @@ "version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919.tgz", "integrity": "sha512-5FIKY3KoYQlBuZC2yc16EXfVRQ8HY+fLqgxkYfWCtKhRb3ajCRzP/rPeoSx11+NueJDANdh4hjY36mdmrTcGSg==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "7.4.2" } @@ -2802,8 +3147,9 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.2.tgz", "integrity": "sha512-f/c/MwYpdJO7taLETU8rahEstLeXfYgQGlz5fycG7Fbmva3iPdzGmjiSWHeSWIgNnlXnelUdCJqyZnFocurZuA==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "7.4.2", "@prisma/engines-version": "7.5.0-10.94a226be1cf2967af2541cca5529f0f7ba866919", @@ -2814,8 +3160,9 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.2.tgz", "integrity": "sha512-UTnChXRwiauzl/8wT4hhe7Xmixja9WE28oCnGpBtRejaHhvekx5kudr3R4Y9mLSA0kqGnAMeyTiKwDVMjaEVsw==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "7.4.2" } @@ -2824,8 +3171,9 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/debug": "7.2.0" } @@ -2834,22 +3182,25 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/query-plan-executor": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/@prisma/studio-core": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", @@ -4060,8 +4411,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@swc/core-darwin-arm64": { "version": "1.15.18", @@ -4428,7 +4780,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -5413,8 +5765,9 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 6.0.0" } @@ -5544,12 +5897,20 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", @@ -5577,8 +5938,9 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -5593,8 +5955,9 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -5603,8 +5966,9 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -5761,8 +6125,9 @@ "version": "10.5.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", @@ -5814,8 +6179,9 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "consola": "^3.2.3" } @@ -5888,15 +6254,17 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^14.18.0 || >=16.10.0" } @@ -6099,8 +6467,9 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "devOptional": true, "license": "BSD-3-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=16.0.0" } @@ -6145,15 +6514,17 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=0.10" } @@ -6172,8 +6543,9 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/detect-libc": { "version": "2.1.2", @@ -6228,8 +6600,9 @@ "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, "license": "BSD-2-Clause", + "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -6237,6 +6610,631 @@ "url": "https://dotenvx.com" } }, + "node_modules/drizzle-kit": { + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "tsx": "^4.21.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz", + "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6256,8 +7254,9 @@ "version": "3.18.4", "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" @@ -6281,8 +7280,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=14" } @@ -7089,14 +8089,14 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "devOptional": true, "funding": [ { "type": "individual", @@ -7108,6 +8108,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "pure-rand": "^6.1.0" }, @@ -7260,8 +8262,9 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" @@ -7360,8 +8363,9 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "is-property": "^1.0.2" } @@ -7424,8 +8428,9 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/get-proto": { "version": "1.0.1", @@ -7476,8 +8481,9 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -7550,22 +8556,25 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/grammex": { "version": "3.1.12", "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/graphmatch": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/gsap": { "version": "3.15.0", @@ -7688,8 +8697,9 @@ "version": "4.11.4", "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=16.9.0" } @@ -7711,15 +8721,17 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8099,8 +9111,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-regex": { "version": "1.2.1", @@ -8507,8 +9520,9 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -8521,8 +9535,9 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "devOptional": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/loose-envify": { "version": "1.4.0", @@ -8558,8 +9573,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "bun": ">=1.0.0", "deno": ">=1.30.0", @@ -8702,8 +9718,9 @@ "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", @@ -8735,8 +9752,9 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "lru.min": "^1.1.0" }, @@ -8923,6 +9941,17 @@ } } }, + "node_modules/next-intl/node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.8.0" + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -9000,8 +10029,9 @@ "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/node-releases": { "version": "2.0.36", @@ -9033,8 +10063,9 @@ "version": "0.6.5", "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", @@ -9051,8 +10082,9 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/object-assign": { "version": "4.1.1", @@ -9191,8 +10223,9 @@ "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/optionator": { "version": "0.9.4", @@ -9336,8 +10369,9 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/pg": { "version": "8.20.0", @@ -9428,38 +10462,6 @@ "node": ">=4" } }, - "node_modules/pglite-prisma-adapter": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/pglite-prisma-adapter/-/pglite-prisma-adapter-0.7.2.tgz", - "integrity": "sha512-NqLXIUguFbgEHpIoyCvAbpEurbK+AUukiv96gIogVnNYWBdhAU0idfTXcJrsS7OdWenYa8QxEQM2ND1cTQrU4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@prisma/driver-adapter-utils": "7.2.0", - "postgres-array": "^3.0.4" - }, - "peerDependencies": { - "@electric-sql/pglite": ">=0.2.0", - "@prisma/client": ">= 7.1.0" - } - }, - "node_modules/pglite-prisma-adapter/node_modules/@prisma/debug": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", - "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/pglite-prisma-adapter/node_modules/@prisma/driver-adapter-utils": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.2.0.tgz", - "integrity": "sha512-gzrUcbI9VmHS24Uf+0+7DNzdIw7keglJsD5m/MHxQOU68OhGVzlphQRobLiDMn8CHNA2XN8uugwKjudVtnfMVQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.2.0" - } - }, "node_modules/pgpass": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", @@ -9512,8 +10514,9 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", @@ -9700,10 +10703,9 @@ "license": "MIT" }, "node_modules/postgres": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", - "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.9.tgz", + "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==", "license": "Unlicense", "engines": { "node": ">=12" @@ -9713,15 +10715,6 @@ "url": "https://github.com/sponsors/porsager" } }, - "node_modules/postgres-array": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", - "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/postgres-bytea": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", @@ -9801,9 +10794,10 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.2.tgz", "integrity": "sha512-2bP8Ruww3Q95Z2eH4Yqh4KAENRsj/SxbdknIVBfd6DmjPwmpsC4OVFMLOeHt6tM3Amh8ebjvstrUz3V/hOe1dA==", - "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@prisma/config": "7.4.2", "@prisma/dev": "0.20.0", @@ -9831,6 +10825,21 @@ } } }, + "node_modules/prisma/node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -9847,8 +10856,9 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", @@ -9859,8 +10869,9 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, - "license": "ISC" + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/punycode": { "version": "2.3.1", @@ -9876,7 +10887,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -9887,7 +10897,9 @@ "url": "https://opencollective.com/fast-check" } ], - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -9914,8 +10926,9 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" @@ -10098,8 +11111,9 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", @@ -10126,8 +11140,9 @@ "version": "2.33.4", "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/remeda" } @@ -10187,8 +11202,9 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 4" } @@ -10332,8 +11348,9 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/saxes": { "version": "6.0.0", @@ -10371,7 +11388,8 @@ "version": "0.0.5", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true + "optional": true, + "peer": true }, "node_modules/set-function-length": { "version": "1.2.2", @@ -10577,8 +11595,9 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=14" }, @@ -10586,6 +11605,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -10595,6 +11624,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -10608,8 +11648,9 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.6" } @@ -10989,8 +12030,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=18" } @@ -11171,6 +12213,509 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.9", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.9.tgz", + "integrity": "sha512-6q8uTORRGauQVjqMQnKUucLFoeXZAfw6zKvG35GLbdKWbLdeOtZ3H4mhyA5mxuUd2o2cRTskhj59nLLQseUvUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11486,8 +13031,9 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "typescript": ">=5" }, @@ -11949,8 +13495,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" diff --git a/package.json b/package.json index 07a0228..ea3f4a6 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,12 @@ "start": "next start", "lint": "eslint .", "test": "vitest run", - "prisma:generate": "prisma generate", - "db:migrate": "prisma migrate deploy", - "db:migrate:dev": "prisma migrate dev", - "db:seed": "prisma db seed" - }, - "prisma": { - "seed": "node prisma/seed.js" + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" }, "dependencies": { - "@prisma/adapter-pg": "^7.4.2", - "@prisma/client": "^7.4.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -28,6 +23,7 @@ "@types/nodemailer": "^7.0.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "drizzle-orm": "^0.45.2", "framer-motion": "^12.35.0", "gsap": "^3.15.0", "lucide-react": "^0.577.0", @@ -36,6 +32,7 @@ "next-themes": "^0.4.6", "nodemailer": "^8.0.1", "pg": "^8.20.0", + "postgres": "^3.4.9", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", @@ -44,7 +41,6 @@ }, "devDependencies": { "@electric-sql/pglite": "^0.5.4", - "@electric-sql/pglite-socket": "^0.2.7", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", @@ -53,12 +49,11 @@ "@types/pg": "^8.18.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "drizzle-kit": "^0.31.10", "eslint": "^9.39.4", "eslint-config-next": "^16.1.6", "jsdom": "^30.0.1", - "pglite-prisma-adapter": "^0.7.2", "postcss": "^8", - "prisma": "^7.4.2", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", "typescript": "^5", diff --git a/prisma.config.ts b/prisma.config.ts deleted file mode 100644 index 970093f..0000000 --- a/prisma.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import "dotenv/config"; -import { defineConfig } from "prisma/config"; - -export default defineConfig({ - schema: "prisma/schema.prisma", - migrations: { - path: "prisma/migrations", - seed: "node prisma/seed.js", - }, - datasource: { - url: process.env["DATABASE_URL"] ?? "", - }, -}); diff --git a/prisma/migrations/20260306090522_init_app_config/migration.sql b/prisma/migrations/20260306090522_init_app_config/migration.sql deleted file mode 100644 index 31f0801..0000000 --- a/prisma/migrations/20260306090522_init_app_config/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- CreateTable -CREATE TABLE "AppConfig" ( - "id" TEXT NOT NULL, - "key" TEXT NOT NULL, - "value" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "AppConfig_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "AppConfig_key_key" ON "AppConfig"("key"); diff --git a/prisma/migrations/20260307152159_add_portfolio_management/migration.sql b/prisma/migrations/20260307152159_add_portfolio_management/migration.sql deleted file mode 100644 index 86100f4..0000000 --- a/prisma/migrations/20260307152159_add_portfolio_management/migration.sql +++ /dev/null @@ -1,112 +0,0 @@ -CREATE TYPE "PortfolioSectionType" AS ENUM ( - 'RICH_TEXT', - 'GALLERY', - 'STATS', - 'DELIVERABLES', - 'LINK' -); - -CREATE TYPE "PortfolioAssetKind" AS ENUM ( - 'IMAGE', - 'DOCUMENT' -); - -CREATE TABLE "Category" ( - "id" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "nameAr" TEXT NOT NULL, - "nameEn" TEXT NOT NULL, - "nameDe" TEXT NOT NULL, - "descriptionAr" TEXT NOT NULL, - "descriptionEn" TEXT NOT NULL, - "descriptionDe" TEXT NOT NULL, - "sortOrder" INTEGER NOT NULL DEFAULT 0, - "isActive" BOOLEAN NOT NULL DEFAULT true, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Category_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "PortfolioProject" ( - "id" TEXT NOT NULL, - "categoryId" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "titleAr" TEXT NOT NULL, - "titleEn" TEXT NOT NULL, - "titleDe" TEXT NOT NULL, - "summaryAr" TEXT NOT NULL, - "summaryEn" TEXT NOT NULL, - "summaryDe" TEXT NOT NULL, - "clientName" TEXT NOT NULL, - "projectYear" INTEGER NOT NULL, - "serviceLabelAr" TEXT NOT NULL, - "serviceLabelEn" TEXT NOT NULL, - "serviceLabelDe" TEXT NOT NULL, - "previewUrl" TEXT, - "coverImagePath" TEXT, - "isFeatured" BOOLEAN NOT NULL DEFAULT false, - "isPublished" BOOLEAN NOT NULL DEFAULT false, - "publishedAt" TIMESTAMP(3), - "sortOrder" INTEGER NOT NULL DEFAULT 0, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "PortfolioProject_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "PortfolioSection" ( - "id" TEXT NOT NULL, - "projectId" TEXT NOT NULL, - "type" "PortfolioSectionType" NOT NULL, - "titleAr" TEXT NOT NULL, - "titleEn" TEXT NOT NULL, - "titleDe" TEXT NOT NULL, - "bodyAr" TEXT NOT NULL, - "bodyEn" TEXT NOT NULL, - "bodyDe" TEXT NOT NULL, - "imagePath" TEXT, - "linkUrl" TEXT, - "sortOrder" INTEGER NOT NULL DEFAULT 0, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "PortfolioSection_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "PortfolioAsset" ( - "id" TEXT NOT NULL, - "projectId" TEXT NOT NULL, - "kind" "PortfolioAssetKind" NOT NULL, - "filePath" TEXT NOT NULL, - "altAr" TEXT NOT NULL, - "altEn" TEXT NOT NULL, - "altDe" TEXT NOT NULL, - "sortOrder" INTEGER NOT NULL DEFAULT 0, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "PortfolioAsset_pkey" PRIMARY KEY ("id") -); - -CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug"); -CREATE UNIQUE INDEX "PortfolioProject_slug_key" ON "PortfolioProject"("slug"); -CREATE INDEX "PortfolioProject_categoryId_isPublished_sortOrder_idx" ON "PortfolioProject"("categoryId", "isPublished", "sortOrder"); -CREATE INDEX "PortfolioProject_isPublished_sortOrder_idx" ON "PortfolioProject"("isPublished", "sortOrder"); -CREATE INDEX "PortfolioSection_projectId_sortOrder_idx" ON "PortfolioSection"("projectId", "sortOrder"); -CREATE INDEX "PortfolioAsset_projectId_sortOrder_idx" ON "PortfolioAsset"("projectId", "sortOrder"); - -ALTER TABLE "PortfolioProject" -ADD CONSTRAINT "PortfolioProject_categoryId_fkey" -FOREIGN KEY ("categoryId") REFERENCES "Category"("id") -ON DELETE RESTRICT ON UPDATE CASCADE; - -ALTER TABLE "PortfolioSection" -ADD CONSTRAINT "PortfolioSection_projectId_fkey" -FOREIGN KEY ("projectId") REFERENCES "PortfolioProject"("id") -ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE "PortfolioAsset" -ADD CONSTRAINT "PortfolioAsset_projectId_fkey" -FOREIGN KEY ("projectId") REFERENCES "PortfolioProject"("id") -ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260307183000_add_media_library/migration.sql b/prisma/migrations/20260307183000_add_media_library/migration.sql deleted file mode 100644 index ad9139d..0000000 --- a/prisma/migrations/20260307183000_add_media_library/migration.sql +++ /dev/null @@ -1,44 +0,0 @@ -CREATE TYPE "MediaSource" AS ENUM ('UPLOAD', 'EXTERNAL'); - -CREATE TYPE "MediaKind" AS ENUM ('IMAGE', 'DOCUMENT'); - -CREATE TYPE "MediaUsageType" AS ENUM ('PORTFOLIO_COVER', 'PORTFOLIO_SECTION', 'PORTFOLIO_ASSET', 'GENERIC'); - -CREATE TABLE "MediaAsset" ( - "id" TEXT NOT NULL, - "source" "MediaSource" NOT NULL, - "kind" "MediaKind" NOT NULL, - "url" TEXT NOT NULL, - "fileName" TEXT NOT NULL, - "label" TEXT NOT NULL, - "altText" TEXT, - "mimeType" TEXT, - "size" INTEGER, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "MediaAsset_pkey" PRIMARY KEY ("id") -); - -CREATE TABLE "MediaUsage" ( - "id" TEXT NOT NULL, - "assetId" TEXT NOT NULL, - "usageType" "MediaUsageType" NOT NULL, - "entityType" TEXT NOT NULL, - "entityId" TEXT NOT NULL, - "fieldKey" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "MediaUsage_pkey" PRIMARY KEY ("id") -); - -CREATE INDEX "MediaAsset_kind_createdAt_idx" ON "MediaAsset"("kind", "createdAt"); - -CREATE INDEX "MediaUsage_assetId_idx" ON "MediaUsage"("assetId"); - -CREATE INDEX "MediaUsage_entityType_entityId_idx" ON "MediaUsage"("entityType", "entityId"); - -CREATE UNIQUE INDEX "MediaUsage_usageType_entityType_entityId_fieldKey_key" ON "MediaUsage"("usageType", "entityType", "entityId", "fieldKey"); - -ALTER TABLE "MediaUsage" ADD CONSTRAINT "MediaUsage_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "MediaAsset"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20260311120000_add_portfolio_project_view_mode/migration.sql b/prisma/migrations/20260311120000_add_portfolio_project_view_mode/migration.sql deleted file mode 100644 index a4ff63d..0000000 --- a/prisma/migrations/20260311120000_add_portfolio_project_view_mode/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TYPE "PortfolioProjectViewMode" AS ENUM ('GRID', 'STORY', 'CASE_STUDY'); - -ALTER TABLE "PortfolioProject" -ADD COLUMN "viewMode" "PortfolioProjectViewMode" NOT NULL DEFAULT 'GRID'; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index 044d57c..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index ddc5624..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,168 +0,0 @@ -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" -} - -model AppConfig { - id String @id @default(cuid()) - key String @unique - value String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -enum PortfolioSectionType { - RICH_TEXT - GALLERY - STATS - DELIVERABLES - LINK -} - -enum PortfolioAssetKind { - IMAGE - DOCUMENT -} - -enum PortfolioProjectViewMode { - GRID - STORY - CASE_STUDY -} - -enum MediaSource { - UPLOAD - EXTERNAL -} - -enum MediaKind { - IMAGE - DOCUMENT -} - -enum MediaUsageType { - PORTFOLIO_COVER - PORTFOLIO_SECTION - PORTFOLIO_ASSET - GENERIC -} - -model Category { - id String @id @default(cuid()) - slug String @unique - nameAr String - nameEn String - nameDe String - descriptionAr String - descriptionEn String - descriptionDe String - sortOrder Int @default(0) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - projects PortfolioProject[] -} - -model PortfolioProject { - id String @id @default(cuid()) - categoryId String - slug String @unique - viewMode PortfolioProjectViewMode @default(GRID) - titleAr String - titleEn String - titleDe String - summaryAr String - summaryEn String - summaryDe String - clientName String - projectYear Int - serviceLabelAr String - serviceLabelEn String - serviceLabelDe String - previewUrl String? - coverImagePath String? - isFeatured Boolean @default(false) - isPublished Boolean @default(false) - publishedAt DateTime? - sortOrder Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict) - sections PortfolioSection[] - assets PortfolioAsset[] - - @@index([categoryId, isPublished, sortOrder]) - @@index([isPublished, sortOrder]) -} - -model PortfolioSection { - id String @id @default(cuid()) - projectId String - type PortfolioSectionType - titleAr String - titleEn String - titleDe String - bodyAr String - bodyEn String - bodyDe String - imagePath String? - linkUrl String? - sortOrder Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - project PortfolioProject @relation(fields: [projectId], references: [id], onDelete: Cascade) - - @@index([projectId, sortOrder]) -} - -model PortfolioAsset { - id String @id @default(cuid()) - projectId String - kind PortfolioAssetKind - filePath String - altAr String - altEn String - altDe String - sortOrder Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - project PortfolioProject @relation(fields: [projectId], references: [id], onDelete: Cascade) - - @@index([projectId, sortOrder]) -} - -model MediaAsset { - id String @id @default(cuid()) - source MediaSource - kind MediaKind - url String - fileName String - label String - altText String? - mimeType String? - size Int? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - usages MediaUsage[] - - @@index([kind, createdAt]) -} - -model MediaUsage { - id String @id @default(cuid()) - assetId String - usageType MediaUsageType - entityType String - entityId String - fieldKey String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade) - - @@unique([usageType, entityType, entityId, fieldKey]) - @@index([assetId]) - @@index([entityType, entityId]) -} diff --git a/prisma/seed.js b/scripts/legacy-prisma-seed.cjs similarity index 100% rename from prisma/seed.js rename to scripts/legacy-prisma-seed.cjs diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts index 2f9b737..92b20c7 100644 --- a/tests/helpers/factories.ts +++ b/tests/helpers/factories.ts @@ -1,6 +1,13 @@ -import { MediaKind, MediaSource, MediaUsageType, PortfolioSectionType } from "@prisma/client"; - -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { + category, + mediaAsset, + mediaUsage, + portfolioAsset, + portfolioProject, + portfolioSection, +} from "@/lib/db/schema"; +import { MediaKind, MediaSource, MediaUsageType, PortfolioSectionType } from "@/lib/db/enums"; let counter = 0; function uniq(prefix: string) { @@ -8,11 +15,11 @@ function uniq(prefix: string) { return `${prefix}-${counter}`; } -export function createCategory(overrides: Record = {}) { - const slug = (overrides.slug as string) ?? uniq("cat"); - return prisma.category.create({ - data: { - slug, +export async function createCategory(overrides: Record = {}) { + const [row] = await db + .insert(category) + .values({ + slug: (overrides.slug as string) ?? uniq("cat"), nameAr: "الاسم", nameEn: "Name", nameDe: "Name", @@ -22,16 +29,19 @@ export function createCategory(overrides: Record = {}) { sortOrder: 0, isActive: true, ...overrides, - }, - }); + } as typeof category.$inferInsert) + .returning(); + + return row; } export async function createProject(overrides: Record = {}) { const categoryId = (overrides.categoryId as string) ?? (await createCategory()).id; const isPublished = (overrides.isPublished as boolean) ?? true; const slug = (overrides.slug as string) ?? uniq("proj"); - return prisma.portfolioProject.create({ - data: { + const [row] = await db + .insert(portfolioProject) + .values({ categoryId, slug, viewMode: "GRID", @@ -51,13 +61,16 @@ export async function createProject(overrides: Record = {}) { ...overrides, isPublished, publishedAt: isPublished ? new Date() : null, - }, - }); + } as typeof portfolioProject.$inferInsert) + .returning(); + + return row; } -export function createSection(projectId: string, overrides: Record = {}) { - return prisma.portfolioSection.create({ - data: { +export async function createSection(projectId: string, overrides: Record = {}) { + const [row] = await db + .insert(portfolioSection) + .values({ projectId, type: PortfolioSectionType.RICH_TEXT, titleAr: "ع", @@ -68,13 +81,16 @@ export function createSection(projectId: string, overrides: Record = {}) { - return prisma.portfolioAsset.create({ - data: { +export async function createAsset(projectId: string, overrides: Record = {}) { + const [row] = await db + .insert(portfolioAsset) + .values({ projectId, kind: "IMAGE", filePath: "/uploads/media/assets/x.svg", @@ -83,35 +99,40 @@ export function createAsset(projectId: string, overrides: Record = {}) { - return prisma.mediaAsset.create({ - data: { +export async function createMediaAsset(overrides: Record = {}) { + const [row] = await db + .insert(mediaAsset) + .values({ source: MediaSource.EXTERNAL, kind: MediaKind.IMAGE, url: (overrides.url as string) ?? `https://cdn.example.com/${uniq("img")}.png`, fileName: "img.png", label: "Image", ...overrides, - }, - }); + } as typeof mediaAsset.$inferInsert) + .returning(); + + return row; } -export function createMediaUsage( - assetId: string, - overrides: Record = {}, -) { - return prisma.mediaUsage.create({ - data: { +export async function createMediaUsage(assetId: string, overrides: Record = {}) { + const [row] = await db + .insert(mediaUsage) + .values({ assetId, usageType: MediaUsageType.GENERIC, entityType: "test-entity", entityId: "e1", fieldKey: uniq("field"), ...overrides, - }, - }); + } as typeof mediaUsage.$inferInsert) + .returning(); + + return row; } diff --git a/tests/helpers/global-db-setup.ts b/tests/helpers/global-db-setup.ts index 8e29cf3..f20fd16 100644 --- a/tests/helpers/global-db-setup.ts +++ b/tests/helpers/global-db-setup.ts @@ -5,12 +5,12 @@ import path from "path"; * Global integration setup. * * Only needed when running against a real Postgres via TEST_DATABASE_URL: reset the - * schema and apply every migration once before the workers start. When - * TEST_DATABASE_URL is not set, each worker spins up its own in-process PGlite database - * (see tests/helpers/integration-setup.ts) and this is a no-op. + * schema and apply every Drizzle migration once before the workers start. When + * TEST_DATABASE_URL is not set, each worker spins up its own in-process PGlite + * database (see tests/helpers/integration-setup.ts) and this is a no-op. */ -const MIGRATIONS_DIR = path.resolve(process.cwd(), "prisma", "migrations"); +const MIGRATIONS_DIR = path.resolve(process.cwd(), "lib", "db", "migrations"); export default async function setup() { const connectionString = process.env.TEST_DATABASE_URL?.trim(); @@ -23,11 +23,11 @@ export default async function setup() { await client.connect(); try { await client.query("DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;"); - const dirs = readdirSync(MIGRATIONS_DIR) - .filter((entry) => /^\d/.test(entry)) + const files = readdirSync(MIGRATIONS_DIR) + .filter((entry) => entry.endsWith(".sql")) .sort(); - for (const dir of dirs) { - await client.query(readFileSync(path.join(MIGRATIONS_DIR, dir, "migration.sql"), "utf8")); + for (const file of files) { + await client.query(readFileSync(path.join(MIGRATIONS_DIR, file), "utf8")); } } finally { await client.end(); diff --git a/tests/helpers/integration-setup.ts b/tests/helpers/integration-setup.ts index a2cdd48..f059ee2 100644 --- a/tests/helpers/integration-setup.ts +++ b/tests/helpers/integration-setup.ts @@ -1,16 +1,19 @@ import { readFileSync, readdirSync } from "fs"; import path from "path"; +import { sql } from "drizzle-orm"; import { afterAll, beforeEach, vi } from "vitest"; +import * as schema from "@/lib/db/schema"; + /** - * Integration database wiring. + * Integration database wiring (Drizzle). * - * - If TEST_DATABASE_URL is set, the real `lib/prisma` singleton is used unchanged, - * pointed at that Postgres (e.g. the Docker instance). Migrations are applied once - * by the global setup; files run serially and truncate between tests. + * - If TEST_DATABASE_URL is set, the real `@/lib/db` singleton is used unchanged, + * pointed at that Postgres. Migrations are applied once by the global setup; + * files run serially and truncate between tests. * - * - Otherwise, `lib/prisma` is mocked with a Prisma client backed by an in-process + * - Otherwise, `@/lib/db` is mocked with a Drizzle client backed by an in-process * PGlite database (Postgres compiled to WASM) — real Postgres semantics, fully * isolated per worker, no external server. Production code is never modified. */ @@ -20,44 +23,45 @@ if (realDbUrl) { process.env.DATABASE_URL = realDbUrl; } -vi.mock("@/lib/prisma", async () => { +vi.mock("@/lib/db", async () => { if (process.env.TEST_DATABASE_URL?.trim()) { - return await vi.importActual("@/lib/prisma"); + return await vi.importActual("@/lib/db"); } const { PGlite } = await import("@electric-sql/pglite"); - const { PrismaPGlite } = await import("pglite-prisma-adapter"); - const { PrismaClient } = await import("@prisma/client"); + const { drizzle } = await import("drizzle-orm/pglite"); - const db = await PGlite.create(); - const migrationsDir = path.resolve(process.cwd(), "prisma", "migrations"); - const dirs = readdirSync(migrationsDir) - .filter((entry) => /^\d/.test(entry)) + const client = new PGlite(); + const migrationsDir = path.resolve(process.cwd(), "lib", "db", "migrations"); + const files = readdirSync(migrationsDir) + .filter((entry) => entry.endsWith(".sql")) .sort(); - for (const dir of dirs) { - await db.exec(readFileSync(path.join(migrationsDir, dir, "migration.sql"), "utf8")); + for (const file of files) { + await client.exec(readFileSync(path.join(migrationsDir, file), "utf8")); } - const prisma = new PrismaClient({ adapter: new PrismaPGlite(db) }); - return { prisma }; + const db = drizzle(client, { schema }); + return { db, schema }; }); -const { prisma } = await import("@/lib/prisma"); +const { db } = await import("@/lib/db"); + +export { db }; // Truncated in dependency order (children first) between every test for isolation. const TABLES = [ - "MediaUsage", - "MediaAsset", - "PortfolioAsset", - "PortfolioSection", - "PortfolioProject", - "Category", - "AppConfig", + "media_usage", + "media_asset", + "portfolio_asset", + "portfolio_section", + "portfolio_project", + "category", + "app_config", ]; export async function resetDb() { const list = TABLES.map((table) => `"${table}"`).join(", "); - await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`); + await db.execute(sql.raw(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`)); } beforeEach(async () => { @@ -65,5 +69,6 @@ beforeEach(async () => { }); afterAll(async () => { - await prisma.$disconnect(); + // PGlite is in-process and torn down with the worker; the real postgres.js + // client is a shared singleton and is left open on purpose. }); diff --git a/tests/integration/_smoke.test.ts b/tests/integration/_smoke.test.ts index ce67e53..ccac2e7 100644 --- a/tests/integration/_smoke.test.ts +++ b/tests/integration/_smoke.test.ts @@ -1,11 +1,14 @@ +import { eq } from "drizzle-orm"; import { describe, expect, it } from "vitest"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { appConfig, category } from "@/lib/db/schema"; describe("integration harness smoke test", () => { it("connects to the migrated test database and performs CRUD", async () => { - const created = await prisma.category.create({ - data: { + const [created] = await db + .insert(category) + .values({ slug: "smoke", nameAr: "a", nameEn: "b", @@ -13,28 +16,27 @@ describe("integration harness smoke test", () => { descriptionAr: "a", descriptionEn: "b", descriptionDe: "c", - }, - }); + }) + .returning(); expect(created.id).toBeTruthy(); expect(created.isActive).toBe(true); - const found = await prisma.category.findUnique({ where: { slug: "smoke" } }); + const found = await db.query.category.findFirst({ where: eq(category.slug, "smoke") }); expect(found?.nameEn).toBe("b"); }); it("resets the database between tests", async () => { - const count = await prisma.category.count(); + const count = await db.$count(category); expect(count).toBe(0); }); it("supports enums and appconfig upsert", async () => { - await prisma.appConfig.upsert({ - where: { key: "k" }, - update: { value: "v2" }, - create: { key: "k", value: "v1" }, - }); - const row = await prisma.appConfig.findUnique({ where: { key: "k" } }); + await db + .insert(appConfig) + .values({ key: "k", value: "v1" }) + .onConflictDoUpdate({ target: appConfig.key, set: { value: "v2" } }); + const row = await db.query.appConfig.findFirst({ where: eq(appConfig.key, "k") }); expect(row?.value).toBe("v1"); }); }); diff --git a/tests/integration/actions-media.test.ts b/tests/integration/actions-media.test.ts index de8371c..42cdbae 100644 --- a/tests/integration/actions-media.test.ts +++ b/tests/integration/actions-media.test.ts @@ -10,8 +10,11 @@ vi.mock("@/lib/admin-auth", async () => { return { isAdminAuthenticated: m.isAdminAuthenticated, clearAdminSessionCookie: m.clearAdminSessionCookie }; }); +import { eq } from "drizzle-orm"; + import { createMediaAssetAction, deleteMediaAssetAction } from "@/app/_admin/media/actions"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { mediaAsset } from "@/lib/db/schema"; import { removeManagedMediaFile } from "@/lib/media-storage"; import { createMediaAsset, createMediaUsage } from "@/tests/helpers/factories"; import { canManageUploads } from "@/tests/helpers/fs-capability"; @@ -25,7 +28,7 @@ describe("createMediaAssetAction", () => { it("errors when no file is provided", async () => { const url = await captureRedirect(() => createMediaAssetAction(formDataFrom({ kind: "IMAGE", label: "L" }))); expect(url).toContain("error="); - expect(await prisma.mediaAsset.count()).toBe(0); + expect(await db.$count(mediaAsset)).toBe(0); }); it.skipIf(!canManageUploads)("creates an asset from an uploaded file", async () => { @@ -34,7 +37,7 @@ describe("createMediaAssetAction", () => { createMediaAssetAction(formDataFrom({ kind: "IMAGE", label: "Pic", file })), ); expect(url).toContain("success="); - const assets = await prisma.mediaAsset.findMany(); + const assets = await db.select().from(mediaAsset); expect(assets.length).toBe(1); expect(assets[0].source).toBe("UPLOAD"); await removeManagedMediaFile(assets[0].url); @@ -58,14 +61,14 @@ describe("deleteMediaAssetAction", () => { await createMediaUsage(asset.id); const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: asset.id }))); expect(url).toContain("error="); - expect(await prisma.mediaAsset.findUnique({ where: { id: asset.id } })).not.toBeNull(); + expect((await db.query.mediaAsset.findFirst({ where: eq(mediaAsset.id, asset.id) })) ?? null).not.toBeNull(); }); it("deletes an unused external asset", async () => { const asset = await createMediaAsset({ url: "https://cdn/external.png" }); const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: asset.id }))); expect(url).toContain("success="); - expect(await prisma.mediaAsset.findUnique({ where: { id: asset.id } })).toBeNull(); + expect((await db.query.mediaAsset.findFirst({ where: eq(mediaAsset.id, asset.id) })) ?? null).toBeNull(); }); it("redirects unauthenticated callers to the admin root", async () => { diff --git a/tests/integration/actions-portfolio.test.ts b/tests/integration/actions-portfolio.test.ts index 9df9db4..51f7ddb 100644 --- a/tests/integration/actions-portfolio.test.ts +++ b/tests/integration/actions-portfolio.test.ts @@ -16,7 +16,15 @@ import { saveProjectAction, upsertCategoryAction, } from "@/app/_admin/portfolio/actions"; -import { prisma } from "@/lib/prisma"; +import { and, eq } from "drizzle-orm"; + +import { db } from "@/lib/db"; +import { + category as categoryTable, + mediaUsage as mediaUsageTable, + portfolioAsset as portfolioAssetTable, + portfolioProject as portfolioProjectTable, +} from "@/lib/db/schema"; import { createCategory, createProject } from "@/tests/helpers/factories"; import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks"; @@ -81,7 +89,7 @@ describe("upsertCategoryAction", () => { it("creates a category", async () => { const url = await captureRedirect(() => upsertCategoryAction(categoryForm())); expect(url).toContain("success="); - const category = await prisma.category.findUnique({ where: { slug: "branding" } }); + const category = await db.query.category.findFirst({ where: eq(categoryTable.slug, "branding") }); expect(category?.nameEn).toBe("Branding"); expect(category?.isActive).toBe(true); }); @@ -92,7 +100,7 @@ describe("upsertCategoryAction", () => { upsertCategoryAction(categoryForm({ id: existing.id, slug: "old", nameEn: "Renamed" })), ); expect(url).toContain("success="); - const category = await prisma.category.findUnique({ where: { id: existing.id } }); + const category = await db.query.category.findFirst({ where: eq(categoryTable.id, existing.id) }); expect(category?.nameEn).toBe("Renamed"); }); @@ -121,14 +129,14 @@ describe("deleteCategoryAction", () => { await createProject({ categoryId: category.id }); const url = await captureRedirect(() => deleteCategoryAction(formDataFrom({ id: category.id }))); expect(url).toContain("error="); - expect(await prisma.category.findUnique({ where: { id: category.id } })).not.toBeNull(); + expect((await db.query.category.findFirst({ where: eq(categoryTable.id, category.id) })) ?? null).not.toBeNull(); }); it("deletes an empty category", async () => { const category = await createCategory(); const url = await captureRedirect(() => deleteCategoryAction(formDataFrom({ id: category.id }))); expect(url).toContain("success="); - expect(await prisma.category.findUnique({ where: { id: category.id } })).toBeNull(); + expect((await db.query.category.findFirst({ where: eq(categoryTable.id, category.id) })) ?? null).toBeNull(); }); }); @@ -138,16 +146,14 @@ describe("saveProjectAction", () => { const url = await captureRedirect(() => saveProjectAction(projectForm(category.id))); expect(url).toContain("success="); - const project = await prisma.portfolioProject.findUnique({ where: { slug: "case-study" } }); + const project = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); expect(project).not.toBeNull(); expect(project?.isPublished).toBe(true); expect(project?.publishedAt).not.toBeNull(); expect(project?.coverImagePath).toBe("https://cdn/cover.png"); - expect(await prisma.portfolioAsset.count({ where: { projectId: project!.id } })).toBe(1); - const usages = await prisma.mediaUsage.findMany({ - where: { entityType: "portfolio-project", entityId: project!.id }, - }); + expect(await db.$count(portfolioAssetTable, eq(portfolioAssetTable.projectId, project!.id))).toBe(1); + const usages = await db.select().from(mediaUsageTable).where(and(eq(mediaUsageTable.entityType, "portfolio-project"), eq(mediaUsageTable.entityId, project!.id))); const usageTypes = usages.map((u) => u.usageType).sort(); expect(usageTypes).toEqual(["PORTFOLIO_ASSET", "PORTFOLIO_COVER"]); }); @@ -156,26 +162,26 @@ describe("saveProjectAction", () => { const category = await createCategory(); const created = await captureRedirect(() => saveProjectAction(projectForm(category.id))); void created; - const project = await prisma.portfolioProject.findUnique({ where: { slug: "case-study" } }); + const project = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id, { id: project!.id, titleEn: "Updated Title" })), ); expect(url).toContain("success="); - const updated = await prisma.portfolioProject.findUnique({ where: { id: project!.id } }); + const updated = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, project!.id) }); expect(updated?.titleEn).toBe("Updated Title"); // assets are replaced, not duplicated - expect(await prisma.portfolioAsset.count({ where: { projectId: project!.id } })).toBe(1); + expect(await db.$count(portfolioAssetTable, eq(portfolioAssetTable.projectId, project!.id))).toBe(1); }); it("keeps the original publishedAt when re-saving an already published project", async () => { const category = await createCategory(); await captureRedirect(() => saveProjectAction(projectForm(category.id))); - const first = await prisma.portfolioProject.findUnique({ where: { slug: "case-study" } }); + const first = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); const originalPublishedAt = first!.publishedAt; await captureRedirect(() => saveProjectAction(projectForm(category.id, { id: first!.id }))); - const second = await prisma.portfolioProject.findUnique({ where: { id: first!.id } }); + const second = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, first!.id) }); expect(second?.publishedAt?.toISOString()).toBe(originalPublishedAt?.toISOString()); }); @@ -183,7 +189,7 @@ describe("saveProjectAction", () => { const category = await createCategory(); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id, { titleEn: "" }))); expect(url).toContain("error="); - expect(await prisma.portfolioProject.count()).toBe(0); + expect(await db.$count(portfolioProjectTable)).toBe(0); }); it("reports a unique-constraint violation on duplicate slugs", async () => { @@ -206,7 +212,7 @@ describe("deleteProjectAction", () => { const project = await createProject(); const url = await captureRedirect(() => deleteProjectAction(formDataFrom({ id: project.id }))); expect(url).toContain("success="); - expect(await prisma.portfolioProject.findUnique({ where: { id: project.id } })).toBeNull(); + expect((await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, project.id) })) ?? null).toBeNull(); }); it("errors when the project does not exist", async () => { diff --git a/tests/integration/api-routes.test.ts b/tests/integration/api-routes.test.ts index a06b6aa..9459c70 100644 --- a/tests/integration/api-routes.test.ts +++ b/tests/integration/api-routes.test.ts @@ -3,7 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { GET as healthGet } from "@/app/api/health/route"; import { GET as defaultLocaleGet } from "@/app/api/site/default-locale/route"; import { setMaintenanceMode, updateSiteSettings, getSiteSettings } from "@/lib/app-config"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; afterEach(() => { vi.restoreAllMocks(); @@ -20,7 +20,7 @@ describe("GET /api/health", () => { }); it("reports degraded (503) when the database query throws", async () => { - vi.spyOn(prisma, "$queryRaw").mockRejectedValueOnce(new Error("db down")); + vi.spyOn(db, "execute").mockRejectedValueOnce(new Error("db down")); const response = await healthGet(); expect(response.status).toBe(503); const body = await response.json(); diff --git a/tests/integration/app-config.test.ts b/tests/integration/app-config.test.ts index 2fcf6fd..bb6fa05 100644 --- a/tests/integration/app-config.test.ts +++ b/tests/integration/app-config.test.ts @@ -20,7 +20,10 @@ import { updateMarqueeSettings, updateSiteSettings, } from "@/lib/app-config"; -import { prisma } from "@/lib/prisma"; +import { eq } from "drizzle-orm"; + +import { db } from "@/lib/db"; +import { appConfig, mediaUsage } from "@/lib/db/schema"; import { createMediaAsset } from "@/tests/helpers/factories"; describe("maintenance mode", () => { @@ -31,7 +34,7 @@ describe("maintenance mode", () => { it("persists and reads back the enabled flag", async () => { await setMaintenanceMode(true); expect(await getMaintenanceMode()).toBe(true); - const row = await prisma.appConfig.findUnique({ where: { key: MAINTENANCE_MODE_KEY } }); + const row = await db.query.appConfig.findFirst({ where: eq(appConfig.key, MAINTENANCE_MODE_KEY) }); expect(row?.value).toBe("true"); await setMaintenanceMode(false); expect(await getMaintenanceMode()).toBe(false); @@ -46,7 +49,7 @@ describe("site settings", () => { }); it("uses the stored siteName key as the fallback name", async () => { - await prisma.appConfig.create({ data: { key: SITE_NAME_KEY, value: "My Studio" } }); + await db.insert(appConfig).values({ key: SITE_NAME_KEY, value: "My Studio" }); const settings = await getSiteSettings(); expect(settings.locales.ar.siteName).toBe("My Studio"); }); @@ -113,24 +116,20 @@ describe("getSiteSettingsMediaBindings", () => { it("maps media usages to their field bindings", async () => { const logo = await createMediaAsset({ url: "https://cdn/logo.png" }); const favicon = await createMediaAsset({ url: "https://cdn/favicon.svg" }); - await prisma.mediaUsage.create({ - data: { + await db.insert(mediaUsage).values({ assetId: logo.id, usageType: "GENERIC", entityType: SITE_SETTINGS_ENTITY_TYPE, entityId: SITE_SETTINGS_ENTITY_ID, fieldKey: SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY, - }, - }); - await prisma.mediaUsage.create({ - data: { + }); + await db.insert(mediaUsage).values({ assetId: favicon.id, usageType: "GENERIC", entityType: SITE_SETTINGS_ENTITY_TYPE, entityId: SITE_SETTINGS_ENTITY_ID, fieldKey: SITE_SETTINGS_FAVICON_FIELD_KEY, - }, - }); + }); const bindings = await getSiteSettingsMediaBindings(); expect(bindings.siteLogoLight?.assetId).toBe(logo.id); diff --git a/tests/integration/media-service.test.ts b/tests/integration/media-service.test.ts index 1ed8d63..5db574e 100644 --- a/tests/integration/media-service.test.ts +++ b/tests/integration/media-service.test.ts @@ -2,9 +2,12 @@ import { readFile } from "fs/promises"; import { describe, expect, it } from "vitest"; +import { eq } from "drizzle-orm"; + import { resolveMediaSelection } from "@/lib/media-service"; import { resolveMediaUploadPath } from "@/lib/media-storage"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { mediaAsset } from "@/lib/db/schema"; import { createMediaAsset } from "@/tests/helpers/factories"; import { canManageUploads } from "@/tests/helpers/fs-capability"; @@ -47,7 +50,7 @@ describe("resolveMediaSelection — external mode", () => { expect(result.createdAssetId).toBeTruthy(); expect(result.url).toBe("https://cdn/new/photo.png"); - const stored = await prisma.mediaAsset.findUnique({ where: { id: result.assetId! } }); + const stored = await db.query.mediaAsset.findFirst({ where: eq(mediaAsset.id, result.assetId!) }); expect(stored?.source).toBe("EXTERNAL"); expect(stored?.fileName).toBe("photo.png"); expect(stored?.label).toBe("Photo"); @@ -108,7 +111,7 @@ describe("resolveMediaSelection — upload mode (filesystem)", () => { required: true, }); expect(result.uploadedUrl).toBeTruthy(); - const stored = await prisma.mediaAsset.findUnique({ where: { id: result.assetId! } }); + const stored = await db.query.mediaAsset.findFirst({ where: eq(mediaAsset.id, result.assetId!) }); expect(stored?.source).toBe("UPLOAD"); // File actually written to disk const bytes = await readFile(resolveMediaUploadPath(result.url)); diff --git a/tests/integration/media.test.ts b/tests/integration/media.test.ts index 83afa1b..1bafa42 100644 --- a/tests/integration/media.test.ts +++ b/tests/integration/media.test.ts @@ -10,7 +10,8 @@ import { getPortfolioMediaBindings, replaceEntityMediaUsages, } from "@/lib/media"; -import { prisma } from "@/lib/prisma"; +import { db } from "@/lib/db"; +import { mediaUsage } from "@/lib/db/schema"; import { createMediaAsset as seedAsset } from "@/tests/helpers/factories"; describe("createMediaAsset / getMediaAssetById", () => { @@ -110,13 +111,11 @@ describe("getPortfolioMediaBindings", () => { const section = await seedAsset(); const asset = await seedAsset(); - await prisma.mediaUsage.createMany({ - data: [ + await db.insert(mediaUsage).values([ { assetId: cover.id, usageType: "PORTFOLIO_COVER", entityType: "portfolio-project", entityId: "proj", fieldKey: "cover" }, { assetId: section.id, usageType: "PORTFOLIO_SECTION", entityType: "portfolio-project", entityId: "proj", fieldKey: "sec_1" }, { assetId: asset.id, usageType: "PORTFOLIO_ASSET", entityType: "portfolio-project", entityId: "proj", fieldKey: "ast_1" }, - ], - }); + ]); const bindings = await getPortfolioMediaBindings("proj"); expect(bindings.coverAssetId).toBe(cover.id); @@ -131,7 +130,5 @@ describe("getPortfolioMediaBindings", () => { }); async function createMediaUsageFor(assetId: string) { - await prisma.mediaUsage.create({ - data: { assetId, usageType: "GENERIC", entityType: "e", entityId: "1", fieldKey: "f" }, - }); + await db.insert(mediaUsage).values({ assetId, usageType: "GENERIC", entityType: "e", entityId: "1", fieldKey: "f" }); } diff --git a/tests/integration/portfolio.test.ts b/tests/integration/portfolio.test.ts index 32ba97e..fdd8081 100644 --- a/tests/integration/portfolio.test.ts +++ b/tests/integration/portfolio.test.ts @@ -9,7 +9,10 @@ import { getPublishedPortfolioProjectBySlug, getPublishedPortfolioProjects, } from "@/lib/portfolio"; -import { prisma } from "@/lib/prisma"; +import { eq } from "drizzle-orm"; + +import { db } from "@/lib/db"; +import { category, mediaUsage, portfolioAsset, portfolioProject, portfolioSection } from "@/lib/db/schema"; import { createAsset, createCategory, @@ -75,15 +78,13 @@ describe("admin projects", () => { it("attaches media bindings to a project fetched by id", async () => { const project = await createProject(); const cover = await createMediaAsset(); - await prisma.mediaUsage.create({ - data: { + await db.insert(mediaUsage).values({ assetId: cover.id, usageType: "PORTFOLIO_COVER", entityType: "portfolio-project", entityId: project.id, fieldKey: "cover", - }, - }); + }); const detail = await getAdminPortfolioProjectById(project.id); expect(detail?.coverMediaAssetId).toBe(cover.id); }); @@ -127,15 +128,15 @@ describe("referential integrity", () => { it("restricts deleting a category that still has projects", async () => { const cat = await createCategory(); await createProject({ categoryId: cat.id }); - await expect(prisma.category.delete({ where: { id: cat.id } })).rejects.toThrow(); + await expect(db.delete(category).where(eq(category.id, cat.id))).rejects.toThrow(); }); it("cascades section and asset deletion when a project is removed", async () => { const project = await createProject(); await createSection(project.id); await createAsset(project.id); - await prisma.portfolioProject.delete({ where: { id: project.id } }); - expect(await prisma.portfolioSection.count()).toBe(0); - expect(await prisma.portfolioAsset.count()).toBe(0); + await db.delete(portfolioProject).where(eq(portfolioProject.id, project.id)); + expect(await db.$count(portfolioSection)).toBe(0); + expect(await db.$count(portfolioAsset)).toBe(0); }); });