@usequark/quark-create-app 1.23.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myquark/db",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./umami": "./src/umami.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test 'src/**/*.test.js'",
|
|
13
|
+
"lint": "biome format --write && biome check --write",
|
|
14
|
+
"db:generate": "prisma generate",
|
|
15
|
+
"db:migrate": "prisma migrate dev",
|
|
16
|
+
"db:migrate:deploy": "prisma migrate deploy",
|
|
17
|
+
"db:push": "prisma db push",
|
|
18
|
+
"db:seed": "prisma db seed",
|
|
19
|
+
"db:studio": "prisma studio"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [],
|
|
22
|
+
"author": "",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"packageManager": "pnpm@10.12.1",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@faker-js/faker": "^10.6.0",
|
|
27
|
+
"@techstream/quark-config": "workspace:*",
|
|
28
|
+
"bcryptjs": "^3.0.3",
|
|
29
|
+
"prisma": "^7.10.0"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@prisma/adapter-pg": "^7.10.0",
|
|
33
|
+
"@prisma/client": "^7.10.0",
|
|
34
|
+
"@techstream/quark-core": "^2.0.0",
|
|
35
|
+
"pg": "^8.23.0",
|
|
36
|
+
"zod": "^4.6.5"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
-- CreateSchema
|
|
2
|
+
CREATE SCHEMA IF NOT EXISTS "public";
|
|
3
|
+
|
|
4
|
+
-- CreateEnum
|
|
5
|
+
CREATE TYPE "UserRole" AS ENUM ('admin', 'lead_dev', 'client_admin', 'editor', 'viewer');
|
|
6
|
+
|
|
7
|
+
-- CreateEnum
|
|
8
|
+
CREATE TYPE "JobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED', 'CANCELLED');
|
|
9
|
+
|
|
10
|
+
-- CreateTable
|
|
11
|
+
CREATE TABLE "User" (
|
|
12
|
+
"id" TEXT NOT NULL,
|
|
13
|
+
"email" TEXT NOT NULL,
|
|
14
|
+
"emailVerified" TIMESTAMP(3),
|
|
15
|
+
"name" TEXT,
|
|
16
|
+
"password" TEXT,
|
|
17
|
+
"image" TEXT,
|
|
18
|
+
"role" "UserRole" NOT NULL DEFAULT 'viewer',
|
|
19
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
20
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
21
|
+
|
|
22
|
+
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
-- CreateTable
|
|
26
|
+
CREATE TABLE "Account" (
|
|
27
|
+
"id" TEXT NOT NULL,
|
|
28
|
+
"userId" TEXT NOT NULL,
|
|
29
|
+
"type" TEXT NOT NULL,
|
|
30
|
+
"provider" TEXT NOT NULL,
|
|
31
|
+
"providerAccountId" TEXT NOT NULL,
|
|
32
|
+
"refresh_token" TEXT,
|
|
33
|
+
"access_token" TEXT,
|
|
34
|
+
"expires_at" INTEGER,
|
|
35
|
+
"token_type" TEXT,
|
|
36
|
+
"scope" TEXT,
|
|
37
|
+
"id_token" TEXT,
|
|
38
|
+
"session_state" TEXT,
|
|
39
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
40
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
41
|
+
|
|
42
|
+
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- CreateTable
|
|
46
|
+
CREATE TABLE "Session" (
|
|
47
|
+
"id" TEXT NOT NULL,
|
|
48
|
+
"sessionToken" TEXT NOT NULL,
|
|
49
|
+
"userId" TEXT NOT NULL,
|
|
50
|
+
"expires" TIMESTAMP(3) NOT NULL,
|
|
51
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
52
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
53
|
+
|
|
54
|
+
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
-- CreateTable
|
|
58
|
+
CREATE TABLE "VerificationToken" (
|
|
59
|
+
"identifier" TEXT NOT NULL,
|
|
60
|
+
"token" TEXT NOT NULL,
|
|
61
|
+
"expires" TIMESTAMP(3) NOT NULL,
|
|
62
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
63
|
+
"updatedAt" TIMESTAMP(3) NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- CreateTable
|
|
67
|
+
CREATE TABLE "Job" (
|
|
68
|
+
"id" TEXT NOT NULL,
|
|
69
|
+
"queue" TEXT NOT NULL,
|
|
70
|
+
"name" TEXT NOT NULL,
|
|
71
|
+
"data" JSONB,
|
|
72
|
+
"status" "JobStatus" NOT NULL DEFAULT 'PENDING',
|
|
73
|
+
"error" TEXT,
|
|
74
|
+
"attempts" INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
"maxRetries" INTEGER NOT NULL DEFAULT 3,
|
|
76
|
+
"runAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
77
|
+
"startedAt" TIMESTAMP(3),
|
|
78
|
+
"completedAt" TIMESTAMP(3),
|
|
79
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
80
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
81
|
+
|
|
82
|
+
CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- CreateTable
|
|
86
|
+
CREATE TABLE "File" (
|
|
87
|
+
"id" TEXT NOT NULL,
|
|
88
|
+
"filename" TEXT NOT NULL,
|
|
89
|
+
"originalName" TEXT NOT NULL,
|
|
90
|
+
"mimeType" TEXT NOT NULL,
|
|
91
|
+
"size" INTEGER NOT NULL,
|
|
92
|
+
"storageKey" TEXT NOT NULL,
|
|
93
|
+
"storageProvider" TEXT NOT NULL DEFAULT 'local',
|
|
94
|
+
"uploadedById" TEXT,
|
|
95
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
96
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
97
|
+
|
|
98
|
+
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
-- CreateTable
|
|
102
|
+
CREATE TABLE "AuditLog" (
|
|
103
|
+
"id" TEXT NOT NULL,
|
|
104
|
+
"userId" TEXT NOT NULL,
|
|
105
|
+
"action" TEXT NOT NULL,
|
|
106
|
+
"entity" TEXT NOT NULL,
|
|
107
|
+
"entityId" TEXT NOT NULL,
|
|
108
|
+
"changes" JSONB,
|
|
109
|
+
"metadata" JSONB,
|
|
110
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
112
|
+
|
|
113
|
+
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- CreateIndex
|
|
117
|
+
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
|
118
|
+
|
|
119
|
+
-- CreateIndex
|
|
120
|
+
CREATE INDEX "User_email_idx" ON "User"("email");
|
|
121
|
+
|
|
122
|
+
-- CreateIndex
|
|
123
|
+
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
|
|
124
|
+
|
|
125
|
+
-- CreateIndex
|
|
126
|
+
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
|
|
127
|
+
|
|
128
|
+
-- CreateIndex
|
|
129
|
+
CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
|
|
130
|
+
|
|
131
|
+
-- CreateIndex
|
|
132
|
+
CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
|
133
|
+
|
|
134
|
+
-- CreateIndex
|
|
135
|
+
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
|
136
|
+
|
|
137
|
+
-- CreateIndex
|
|
138
|
+
CREATE INDEX "Session_expires_idx" ON "Session"("expires");
|
|
139
|
+
|
|
140
|
+
-- CreateIndex
|
|
141
|
+
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
|
142
|
+
|
|
143
|
+
-- CreateIndex
|
|
144
|
+
CREATE INDEX "VerificationToken_token_idx" ON "VerificationToken"("token");
|
|
145
|
+
|
|
146
|
+
-- CreateIndex
|
|
147
|
+
CREATE INDEX "VerificationToken_expires_idx" ON "VerificationToken"("expires");
|
|
148
|
+
|
|
149
|
+
-- CreateIndex
|
|
150
|
+
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
|
151
|
+
|
|
152
|
+
-- CreateIndex
|
|
153
|
+
CREATE INDEX "Job_queue_idx" ON "Job"("queue");
|
|
154
|
+
|
|
155
|
+
-- CreateIndex
|
|
156
|
+
CREATE INDEX "Job_status_idx" ON "Job"("status");
|
|
157
|
+
|
|
158
|
+
-- CreateIndex
|
|
159
|
+
CREATE INDEX "Job_runAt_idx" ON "Job"("runAt");
|
|
160
|
+
|
|
161
|
+
-- CreateIndex
|
|
162
|
+
CREATE INDEX "Job_status_runAt_idx" ON "Job"("status", "runAt");
|
|
163
|
+
|
|
164
|
+
-- CreateIndex
|
|
165
|
+
CREATE INDEX "Job_createdAt_idx" ON "Job"("createdAt");
|
|
166
|
+
|
|
167
|
+
-- CreateIndex
|
|
168
|
+
CREATE UNIQUE INDEX "File_storageKey_key" ON "File"("storageKey");
|
|
169
|
+
|
|
170
|
+
-- CreateIndex
|
|
171
|
+
CREATE INDEX "File_uploadedById_idx" ON "File"("uploadedById");
|
|
172
|
+
|
|
173
|
+
-- CreateIndex
|
|
174
|
+
CREATE INDEX "File_mimeType_idx" ON "File"("mimeType");
|
|
175
|
+
|
|
176
|
+
-- CreateIndex
|
|
177
|
+
CREATE INDEX "File_createdAt_idx" ON "File"("createdAt");
|
|
178
|
+
|
|
179
|
+
-- CreateIndex
|
|
180
|
+
CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId");
|
|
181
|
+
|
|
182
|
+
-- CreateIndex
|
|
183
|
+
CREATE INDEX "AuditLog_action_idx" ON "AuditLog"("action");
|
|
184
|
+
|
|
185
|
+
-- CreateIndex
|
|
186
|
+
CREATE INDEX "AuditLog_entity_idx" ON "AuditLog"("entity");
|
|
187
|
+
|
|
188
|
+
-- CreateIndex
|
|
189
|
+
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
|
|
190
|
+
|
|
191
|
+
-- AddForeignKey
|
|
192
|
+
ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
193
|
+
|
|
194
|
+
-- AddForeignKey
|
|
195
|
+
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
196
|
+
|
|
197
|
+
-- AddForeignKey
|
|
198
|
+
ALTER TABLE "File" ADD CONSTRAINT "File_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
199
|
+
|
|
200
|
+
-- AddForeignKey
|
|
201
|
+
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
202
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// This is your Prisma schema file,
|
|
2
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
5
|
+
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
6
|
+
|
|
7
|
+
generator client {
|
|
8
|
+
provider = "prisma-client"
|
|
9
|
+
output = "../src/generated/prisma"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
datasource db {
|
|
13
|
+
provider = "postgresql"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum UserRole {
|
|
17
|
+
admin
|
|
18
|
+
lead_dev
|
|
19
|
+
client_admin
|
|
20
|
+
editor
|
|
21
|
+
viewer
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
model User {
|
|
25
|
+
id String @id @default(cuid())
|
|
26
|
+
email String @unique
|
|
27
|
+
emailVerified DateTime?
|
|
28
|
+
name String?
|
|
29
|
+
password String?
|
|
30
|
+
image String?
|
|
31
|
+
role UserRole @default(viewer)
|
|
32
|
+
createdAt DateTime @default(now())
|
|
33
|
+
updatedAt DateTime @updatedAt
|
|
34
|
+
|
|
35
|
+
accounts Account[]
|
|
36
|
+
sessions Session[]
|
|
37
|
+
auditLogs AuditLog[]
|
|
38
|
+
files File[]
|
|
39
|
+
|
|
40
|
+
@@index([email])
|
|
41
|
+
@@index([createdAt])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// NextAuth Models
|
|
45
|
+
model Account {
|
|
46
|
+
id String @id @default(cuid())
|
|
47
|
+
userId String
|
|
48
|
+
type String
|
|
49
|
+
provider String
|
|
50
|
+
providerAccountId String
|
|
51
|
+
refresh_token String? @db.Text
|
|
52
|
+
access_token String? @db.Text
|
|
53
|
+
expires_at Int?
|
|
54
|
+
token_type String?
|
|
55
|
+
scope String?
|
|
56
|
+
id_token String? @db.Text
|
|
57
|
+
session_state String?
|
|
58
|
+
createdAt DateTime @default(now())
|
|
59
|
+
updatedAt DateTime @updatedAt
|
|
60
|
+
|
|
61
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
62
|
+
|
|
63
|
+
@@unique([provider, providerAccountId])
|
|
64
|
+
@@index([userId])
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
model Session {
|
|
68
|
+
id String @id @default(cuid())
|
|
69
|
+
sessionToken String @unique
|
|
70
|
+
userId String
|
|
71
|
+
expires DateTime
|
|
72
|
+
createdAt DateTime @default(now())
|
|
73
|
+
updatedAt DateTime @updatedAt
|
|
74
|
+
|
|
75
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
76
|
+
|
|
77
|
+
@@index([userId])
|
|
78
|
+
@@index([expires])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
model VerificationToken {
|
|
82
|
+
identifier String
|
|
83
|
+
token String @unique
|
|
84
|
+
expires DateTime
|
|
85
|
+
createdAt DateTime @default(now())
|
|
86
|
+
updatedAt DateTime @updatedAt
|
|
87
|
+
|
|
88
|
+
@@unique([identifier, token])
|
|
89
|
+
@@index([token])
|
|
90
|
+
@@index([expires])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Job Queue Model
|
|
94
|
+
model Job {
|
|
95
|
+
id String @id @default(cuid())
|
|
96
|
+
queue String
|
|
97
|
+
name String
|
|
98
|
+
data Json?
|
|
99
|
+
status JobStatus @default(PENDING)
|
|
100
|
+
error String?
|
|
101
|
+
attempts Int @default(0)
|
|
102
|
+
maxRetries Int @default(3)
|
|
103
|
+
runAt DateTime @default(now())
|
|
104
|
+
startedAt DateTime?
|
|
105
|
+
completedAt DateTime?
|
|
106
|
+
createdAt DateTime @default(now())
|
|
107
|
+
updatedAt DateTime @updatedAt
|
|
108
|
+
|
|
109
|
+
@@index([queue])
|
|
110
|
+
@@index([status])
|
|
111
|
+
@@index([runAt])
|
|
112
|
+
@@index([status, runAt])
|
|
113
|
+
@@index([createdAt])
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
enum JobStatus {
|
|
117
|
+
PENDING
|
|
118
|
+
IN_PROGRESS
|
|
119
|
+
COMPLETED
|
|
120
|
+
FAILED
|
|
121
|
+
CANCELLED
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// File Upload Model
|
|
125
|
+
model File {
|
|
126
|
+
id String @id @default(cuid())
|
|
127
|
+
filename String
|
|
128
|
+
originalName String
|
|
129
|
+
mimeType String
|
|
130
|
+
size Int
|
|
131
|
+
storageKey String @unique
|
|
132
|
+
storageProvider String @default("local")
|
|
133
|
+
uploadedById String?
|
|
134
|
+
uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: SetNull)
|
|
135
|
+
createdAt DateTime @default(now())
|
|
136
|
+
updatedAt DateTime @updatedAt
|
|
137
|
+
|
|
138
|
+
@@index([uploadedById])
|
|
139
|
+
@@index([mimeType])
|
|
140
|
+
@@index([createdAt])
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Audit Log Model
|
|
144
|
+
model AuditLog {
|
|
145
|
+
id String @id @default(cuid())
|
|
146
|
+
userId String
|
|
147
|
+
action String
|
|
148
|
+
entity String
|
|
149
|
+
entityId String
|
|
150
|
+
changes Json?
|
|
151
|
+
metadata Json?
|
|
152
|
+
createdAt DateTime @default(now())
|
|
153
|
+
updatedAt DateTime @updatedAt
|
|
154
|
+
|
|
155
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
156
|
+
|
|
157
|
+
@@index([userId])
|
|
158
|
+
@@index([action])
|
|
159
|
+
@@index([entity])
|
|
160
|
+
@@index([createdAt])
|
|
161
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import bcrypt from "bcryptjs";
|
|
3
|
+
import { prisma } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal seed: creates a single admin user.
|
|
7
|
+
* Used standalone in production initial setup (SEED_PROFILE=minimal).
|
|
8
|
+
* Also called by seedDev to avoid duplication.
|
|
9
|
+
*
|
|
10
|
+
* Required env vars:
|
|
11
|
+
* ADMIN_PASSWORD - min 12 characters, auto-generates if missing (generate: openssl rand -base64 24)
|
|
12
|
+
* Optional env vars:
|
|
13
|
+
* ADMIN_EMAIL - defaults to admin@example.com
|
|
14
|
+
* ADMIN_NAME - defaults to Admin
|
|
15
|
+
*/
|
|
16
|
+
async function seedMinimal(prisma) {
|
|
17
|
+
const seedProfile = process.env.SEED_PROFILE || "dev";
|
|
18
|
+
const adminEmail = process.env.ADMIN_EMAIL;
|
|
19
|
+
let adminPassword = process.env.ADMIN_PASSWORD;
|
|
20
|
+
const adminName = process.env.ADMIN_NAME || "Admin";
|
|
21
|
+
|
|
22
|
+
if (!adminPassword) {
|
|
23
|
+
const generated = randomBytes(24).toString("base64url");
|
|
24
|
+
console.log(
|
|
25
|
+
"⚠️ ADMIN_PASSWORD not set — auto-generated a random password.\n" +
|
|
26
|
+
" Save it if you need to log in as admin.",
|
|
27
|
+
);
|
|
28
|
+
adminPassword = generated;
|
|
29
|
+
}
|
|
30
|
+
if (adminPassword.length < 12) {
|
|
31
|
+
throw new Error("ADMIN_PASSWORD must be at least 12 characters.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!adminEmail && seedProfile === "minimal") {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"ADMIN_EMAIL is required when SEED_PROFILE=minimal.\n" +
|
|
37
|
+
" Set it in your environment to the real admin email address.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedEmail = adminEmail || "admin@example.com";
|
|
42
|
+
|
|
43
|
+
const existing = await prisma.user.findUnique({
|
|
44
|
+
where: { email: resolvedEmail },
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (existing) {
|
|
48
|
+
console.log("✓ Admin user already exists");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const admin = await prisma.user.create({
|
|
53
|
+
data: {
|
|
54
|
+
email: resolvedEmail,
|
|
55
|
+
name: adminName,
|
|
56
|
+
role: "admin",
|
|
57
|
+
password: await bcrypt.hash(adminPassword, 12),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
console.log(`✓ Created admin user: ${admin.email}`);
|
|
62
|
+
return admin;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Development seed: admin user + representative sample data for local testing.
|
|
67
|
+
* Runs by default (SEED_PROFILE=dev).
|
|
68
|
+
*/
|
|
69
|
+
async function seedDev(prisma) {
|
|
70
|
+
const admin = await seedMinimal(prisma);
|
|
71
|
+
|
|
72
|
+
if (admin) {
|
|
73
|
+
const sampleUser = await prisma.user.create({
|
|
74
|
+
data: {
|
|
75
|
+
email: "user@example.com",
|
|
76
|
+
name: "Sample User",
|
|
77
|
+
role: "viewer",
|
|
78
|
+
image:
|
|
79
|
+
"https://api.dicebear.com/7.x/avataaars/svg?seed=user@example.com",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(`✓ Created sample user: ${sampleUser.email}`);
|
|
84
|
+
|
|
85
|
+
await prisma.auditLog.create({
|
|
86
|
+
data: {
|
|
87
|
+
userId: admin.id,
|
|
88
|
+
action: "CREATE",
|
|
89
|
+
entity: "User",
|
|
90
|
+
entityId: sampleUser.id,
|
|
91
|
+
metadata: { email: sampleUser.email, role: sampleUser.role },
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
console.log("✓ Created sample audit log");
|
|
96
|
+
|
|
97
|
+
await prisma.job.create({
|
|
98
|
+
data: {
|
|
99
|
+
queue: "default",
|
|
100
|
+
name: "example-job",
|
|
101
|
+
status: "COMPLETED",
|
|
102
|
+
data: { message: "This is a sample job" },
|
|
103
|
+
completedAt: new Date(),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
console.log("✓ Created sample job");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
// Guard: require an explicit SEED_PROFILE when seeding a remote database.
|
|
113
|
+
// Without this, a missing SEED_PROFILE silently defaults to "dev" and seeds
|
|
114
|
+
// sample data onto production or staging databases.
|
|
115
|
+
if (!process.env.SEED_PROFILE) {
|
|
116
|
+
const host = process.env.POSTGRES_HOST || "";
|
|
117
|
+
const url = process.env.DATABASE_URL || "";
|
|
118
|
+
const isRemote =
|
|
119
|
+
(host && host !== "localhost" && host !== "127.0.0.1") ||
|
|
120
|
+
(url && !url.includes("localhost") && !url.includes("127.0.0.1"));
|
|
121
|
+
if (isRemote) {
|
|
122
|
+
console.error(
|
|
123
|
+
"❌ Set SEED_PROFILE explicitly when seeding a remote database.\n" +
|
|
124
|
+
" Production: SEED_PROFILE=minimal pnpm db:seed\n" +
|
|
125
|
+
" Staging: SEED_PROFILE=dev pnpm db:seed",
|
|
126
|
+
);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// SEED_PROFILE is intentionally separate from NODE_ENV.
|
|
132
|
+
// Railway sets NODE_ENV=production on ALL deployed services (including staging)
|
|
133
|
+
// for build/performance reasons, so NODE_ENV cannot reliably distinguish staging
|
|
134
|
+
// from production at seed time. Set SEED_PROFILE explicitly in your deploy command:
|
|
135
|
+
// production: SEED_PROFILE=minimal pnpm db:seed
|
|
136
|
+
// staging: pnpm db:seed (defaults to "dev")
|
|
137
|
+
const seedProfile = process.env.SEED_PROFILE || "dev";
|
|
138
|
+
console.log(`🌱 Seeding database with profile: "${seedProfile}"`);
|
|
139
|
+
|
|
140
|
+
if (seedProfile === "minimal") {
|
|
141
|
+
await seedMinimal(prisma);
|
|
142
|
+
} else {
|
|
143
|
+
await seedDev(prisma);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log("✅ Database seeding completed");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main().catch((error) => {
|
|
150
|
+
console.error("❌ Seed failed:", error);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { defineConfig } from "prisma/config";
|
|
4
|
+
import { getConnectionString } from "./src/connection.js";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Load .env from monorepo root (needed for standalone commands like db:push, db:seed)
|
|
9
|
+
try {
|
|
10
|
+
process.loadEnvFile(resolve(__dirname, "../../.env"));
|
|
11
|
+
} catch {}
|
|
12
|
+
|
|
13
|
+
// Use shared connection builder - throwOnMissing=false so `prisma generate` works
|
|
14
|
+
// even without database credentials (e.g. in CI). Commands that need a real
|
|
15
|
+
// connection (migrate, push, studio) will fail at connect time.
|
|
16
|
+
const databaseUrl = getConnectionString({ throwOnMissing: false });
|
|
17
|
+
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
schema: "prisma/schema.prisma",
|
|
20
|
+
migrations: {
|
|
21
|
+
path: "prisma/migrations",
|
|
22
|
+
seed: "tsx prisma/seed.js",
|
|
23
|
+
},
|
|
24
|
+
datasource: {
|
|
25
|
+
url: databaseUrl,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { PrismaPg } from "@prisma/adapter-pg";
|
|
2
|
+
import { getConnectionString } from "./connection.js";
|
|
3
|
+
import { PrismaClient } from "./generated/prisma/client.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns the connection pool configuration for the `pg` driver.
|
|
7
|
+
*
|
|
8
|
+
* Reads from environment variables with sensible defaults:
|
|
9
|
+
* - `DB_POOL_MAX` - maximum number of connections (default: 10 in production, 5 in development)
|
|
10
|
+
* - `DB_POOL_IDLE_TIMEOUT` - milliseconds before an idle connection is closed (default: 30 000)
|
|
11
|
+
* - `DB_POOL_CONNECTION_TIMEOUT` - milliseconds to wait for a new connection (default: 5 000)
|
|
12
|
+
*
|
|
13
|
+
* @returns {{ max: number, idleTimeoutMillis: number, connectionTimeoutMillis: number }}
|
|
14
|
+
*/
|
|
15
|
+
export function getPoolConfig() {
|
|
16
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
17
|
+
return {
|
|
18
|
+
max: Number(process.env.DB_POOL_MAX) || (isProduction ? 10 : 5),
|
|
19
|
+
idleTimeoutMillis: Number(process.env.DB_POOL_IDLE_TIMEOUT) || 30_000,
|
|
20
|
+
connectionTimeoutMillis:
|
|
21
|
+
Number(process.env.DB_POOL_CONNECTION_TIMEOUT) || 5_000,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Lazy singleton - the client is created on first property access, not at import time.
|
|
26
|
+
// This allows Next.js to import the module at build time without requiring DB env vars.
|
|
27
|
+
const globalForPrisma = globalThis;
|
|
28
|
+
|
|
29
|
+
function getPrismaClient() {
|
|
30
|
+
if (!globalForPrisma.__prisma) {
|
|
31
|
+
const baseClient = new PrismaClient({
|
|
32
|
+
adapter: new PrismaPg({
|
|
33
|
+
connectionString: getConnectionString(),
|
|
34
|
+
...getPoolConfig(),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Assign base client immediately so the proxy never blocks.
|
|
39
|
+
globalForPrisma.__prisma = baseClient;
|
|
40
|
+
|
|
41
|
+
// Apply optional query instrumentation extension from @techstream/quark-core.
|
|
42
|
+
// Loaded asynchronously — the base client is used in the meantime, then
|
|
43
|
+
// upgraded in-place once the extension module arrives.
|
|
44
|
+
// The extension handles slow-query logging, argument masking, and Prometheus
|
|
45
|
+
// metrics. It is schema-agnostic and respects the DB_INSTRUMENTATION env var.
|
|
46
|
+
import("@techstream/quark-core")
|
|
47
|
+
.then(({ createDbInstrumentation }) => {
|
|
48
|
+
const ext = createDbInstrumentation();
|
|
49
|
+
if (ext) {
|
|
50
|
+
globalForPrisma.__prisma = baseClient.$extends(ext);
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
.catch(() => {
|
|
54
|
+
// @techstream/quark-core not available or old version — base client remains.
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return globalForPrisma.__prisma;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Prisma client singleton. Lazily initialized on first use so the module
|
|
62
|
+
* can be imported safely at build time (no DB env vars needed).
|
|
63
|
+
*
|
|
64
|
+
* Methods are bound to the real client so `this` inside Prisma internals is
|
|
65
|
+
* always the PrismaClient instance, never the Proxy wrapper.
|
|
66
|
+
*/
|
|
67
|
+
export const prisma = new Proxy(/** @type {PrismaClient} */ ({}), {
|
|
68
|
+
get(_target, prop) {
|
|
69
|
+
const client = getPrismaClient();
|
|
70
|
+
const value = client[prop];
|
|
71
|
+
return typeof value === "function" ? value.bind(client) : value;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export * from "./generated/prisma/client.ts";
|