@techstream/quark-create-app 1.5.3 → 1.7.0

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.
Files changed (53) hide show
  1. package/package.json +4 -2
  2. package/src/index.js +62 -14
  3. package/templates/base-project/.github/dependabot.yml +12 -0
  4. package/templates/base-project/.github/workflows/ci.yml +97 -0
  5. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  6. package/templates/base-project/.github/workflows/release.yml +38 -0
  7. package/templates/base-project/apps/web/biome.json +7 -0
  8. package/templates/base-project/apps/web/jsconfig.json +5 -5
  9. package/templates/base-project/apps/web/next.config.js +90 -1
  10. package/templates/base-project/apps/web/package.json +7 -7
  11. package/templates/base-project/apps/web/railway.json +15 -0
  12. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
  13. package/templates/base-project/apps/web/src/app/layout.js +3 -4
  14. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  15. package/templates/base-project/apps/web/src/app/robots.js +21 -0
  16. package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
  17. package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
  18. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
  19. package/templates/base-project/apps/web/src/proxy.js +1 -2
  20. package/templates/base-project/apps/worker/package.json +5 -5
  21. package/templates/base-project/apps/worker/railway.json +13 -0
  22. package/templates/base-project/apps/worker/src/index.js +30 -12
  23. package/templates/base-project/apps/worker/src/index.test.js +296 -15
  24. package/templates/base-project/biome.json +44 -0
  25. package/templates/base-project/docker-compose.yml +7 -4
  26. package/templates/base-project/package.json +1 -1
  27. package/templates/base-project/packages/db/package.json +1 -1
  28. package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
  29. package/templates/base-project/packages/db/prisma.config.ts +8 -10
  30. package/templates/base-project/packages/db/scripts/seed.js +117 -30
  31. package/templates/base-project/packages/db/src/client.js +1 -18
  32. package/templates/base-project/packages/db/src/connection.js +44 -0
  33. package/templates/base-project/packages/db/src/connection.test.js +119 -0
  34. package/templates/base-project/packages/db/src/queries.js +52 -118
  35. package/templates/base-project/packages/db/src/queries.test.js +0 -29
  36. package/templates/base-project/packages/db/src/schemas.js +0 -12
  37. package/templates/base-project/pnpm-workspace.yaml +4 -0
  38. package/templates/base-project/turbo.json +5 -3
  39. package/templates/config/package.json +2 -0
  40. package/templates/config/src/environment.js +270 -0
  41. package/templates/config/src/index.js +13 -18
  42. package/templates/config/src/load-config.js +135 -0
  43. package/templates/config/src/validate-env.js +123 -16
  44. package/templates/jobs/package.json +2 -2
  45. package/templates/jobs/src/definitions.test.js +34 -0
  46. package/templates/jobs/src/index.js +1 -1
  47. package/templates/ui/package.json +4 -4
  48. package/templates/ui/src/button.test.js +23 -0
  49. package/templates/ui/src/index.js +1 -3
  50. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
  51. package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
  52. package/templates/ui/src/card.js +0 -14
  53. package/templates/ui/src/input.js +0 -11
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@techstream/quark-worker",
3
3
  "version": "1.0.0",
4
- "type": "module",
5
4
  "private": true,
5
+ "type": "module",
6
6
  "description": "",
7
7
  "main": "index.js",
8
8
  "scripts": {
@@ -15,14 +15,14 @@
15
15
  "license": "ISC",
16
16
  "packageManager": "pnpm@10.12.1",
17
17
  "dependencies": {
18
+ "@techstream/quark-config": "workspace:*",
18
19
  "@techstream/quark-core": "^1.0.0",
19
20
  "@techstream/quark-db": "workspace:*",
20
21
  "@techstream/quark-jobs": "workspace:*",
21
- "bullmq": "^5.64.1"
22
+ "bullmq": "^5.69.3"
22
23
  },
23
24
  "devDependencies": {
24
- "@techstream/quark-config": "workspace:*",
25
- "@types/node": "^24.10.1",
26
- "tsx": "^4.20.6"
25
+ "@types/node": "^25.2.3",
26
+ "tsx": "^4.21.0"
27
27
  }
28
28
  }
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://railway.com/railway.schema.json",
3
+ "build": {
4
+ "builder": "RAILPACK",
5
+ "buildCommand": "pnpm install --frozen-lockfile && pnpm db:generate",
6
+ "watchPatterns": ["apps/worker/**", "packages/**"]
7
+ },
8
+ "deploy": {
9
+ "startCommand": "node apps/worker/src/index.js",
10
+ "restartPolicyType": "ON_FAILURE",
11
+ "restartPolicyMaxRetries": 5
12
+ }
13
+ }
@@ -4,6 +4,7 @@
4
4
  * Handles job execution, retries, and error tracking
5
5
  */
6
6
 
7
+ import { loadEnv } from "@techstream/quark-config";
7
8
  import {
8
9
  createLogger,
9
10
  createQueue,
@@ -13,10 +14,14 @@ import { prisma } from "@techstream/quark-db";
13
14
  import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
14
15
  import { jobHandlers } from "./handlers/index.js";
15
16
 
17
+ // Validate environment variables (worker-scoped — skips web-only checks)
18
+ loadEnv("worker");
19
+
16
20
  const logger = createLogger("worker");
17
21
 
18
22
  // Store workers for graceful shutdown
19
23
  const workers = [];
24
+ let isShuttingDown = false;
20
25
 
21
26
  /**
22
27
  * Generic queue processor — dispatches jobs to registered handlers
@@ -113,15 +118,31 @@ async function startWorker() {
113
118
  /**
114
119
  * Graceful shutdown handler
115
120
  */
116
- async function shutdown() {
117
- logger.info("Shutting down worker service");
121
+ async function shutdown(signal = "unknown") {
122
+ if (isShuttingDown) {
123
+ logger.warn("Shutdown already in progress", { signal });
124
+ return;
125
+ }
126
+
127
+ isShuttingDown = true;
128
+ logger.info("Shutting down worker service", { signal });
118
129
 
119
130
  try {
120
131
  for (const worker of workers) {
121
132
  await worker.close();
122
133
  }
123
134
 
124
- await prisma.$disconnect();
135
+ try {
136
+ await prisma.$disconnect();
137
+ } catch (error) {
138
+ if (error.message?.includes("environment variable is required")) {
139
+ logger.warn("Skipping Prisma disconnect due missing database env", {
140
+ error: error.message,
141
+ });
142
+ } else {
143
+ throw error;
144
+ }
145
+ }
125
146
 
126
147
  logger.info("All workers closed");
127
148
  process.exit(0);
@@ -134,14 +155,11 @@ async function shutdown() {
134
155
  }
135
156
  }
136
157
 
137
- process.on("SIGTERM", shutdown);
138
- process.on("SIGINT", shutdown);
139
-
140
- startWorker();
141
-
142
- // Register shutdown handlers
143
- process.on("SIGTERM", shutdown);
144
- process.on("SIGINT", shutdown);
158
+ process.on("SIGTERM", () => {
159
+ void shutdown("SIGTERM");
160
+ });
161
+ process.on("SIGINT", () => {
162
+ void shutdown("SIGINT");
163
+ });
145
164
 
146
- // Start the worker service
147
165
  startWorker();
@@ -1,19 +1,300 @@
1
1
  import assert from "node:assert";
2
- import { test } from "node:test";
3
-
4
- // Mock job definitions for testing
5
- const mockJobDefinitions = {
6
- JOB_QUEUES: { EMAIL: "email-queue" },
7
- JOB_NAMES: { SEND_WELCOME_EMAIL: "send-welcome-email" },
8
- };
9
-
10
- test("Worker - imports job definitions correctly", async () => {
11
- const { JOB_QUEUES, JOB_NAMES } = mockJobDefinitions;
12
- assert.strictEqual(JOB_QUEUES.EMAIL, "email-queue");
13
- assert.strictEqual(JOB_NAMES.SEND_WELCOME_EMAIL, "send-welcome-email");
2
+ import { describe, mock, test } from "node:test";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers: lightweight fakes for Prisma, emailService, and storage
6
+ // ---------------------------------------------------------------------------
7
+
8
+ function createMockLogger() {
9
+ return {
10
+ info: mock.fn(),
11
+ warn: mock.fn(),
12
+ error: mock.fn(),
13
+ };
14
+ }
15
+
16
+ function makeBullJob(name, data, overrides = {}) {
17
+ return { id: "job-1", name, data, attemptsMade: 0, ...overrides };
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // handleSendWelcomeEmail
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe("handleSendWelcomeEmail", () => {
25
+ test("throws when userId is missing", async () => {
26
+ // Inline handler that mirrors the real one's validation
27
+ const handler = async (bullJob) => {
28
+ const { userId } = bullJob.data;
29
+ if (!userId) {
30
+ throw new Error("userId is required for SEND_WELCOME_EMAIL job");
31
+ }
32
+ };
33
+
34
+ await assert.rejects(() => handler(makeBullJob("send-welcome-email", {})), {
35
+ message: "userId is required for SEND_WELCOME_EMAIL job",
36
+ });
37
+ });
38
+
39
+ test("throws when user is not found", async () => {
40
+ const handler = async (bullJob) => {
41
+ const { userId } = bullJob.data;
42
+ if (!userId) throw new Error("userId is required");
43
+ // Simulate user not found
44
+ const userRecord = null;
45
+ if (!userRecord?.email) {
46
+ throw new Error(`User ${userId} not found or has no email`);
47
+ }
48
+ };
49
+
50
+ await assert.rejects(
51
+ () => handler(makeBullJob("send-welcome-email", { userId: "user-123" })),
52
+ { message: "User user-123 not found or has no email" },
53
+ );
54
+ });
55
+
56
+ test("returns success when user exists and email sends", async () => {
57
+ const sendEmail = mock.fn(async () => {});
58
+
59
+ const handler = async (bullJob, _logger) => {
60
+ const { userId } = bullJob.data;
61
+ if (!userId) throw new Error("userId is required");
62
+
63
+ const userRecord = { email: "a@b.com", name: "Alice" };
64
+ if (!userRecord?.email) {
65
+ throw new Error(`User ${userId} not found or has no email`);
66
+ }
67
+
68
+ await sendEmail(userRecord.email, "Welcome", "<p>Hi</p>", "Hi");
69
+ return { success: true, userId, email: userRecord.email };
70
+ };
71
+
72
+ const logger = createMockLogger();
73
+ const result = await handler(
74
+ makeBullJob("send-welcome-email", { userId: "user-123" }),
75
+ logger,
76
+ );
77
+
78
+ assert.deepStrictEqual(result, {
79
+ success: true,
80
+ userId: "user-123",
81
+ email: "a@b.com",
82
+ });
83
+ assert.strictEqual(sendEmail.mock.callCount(), 1);
84
+ });
85
+ });
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // handleSendResetPasswordEmail
89
+ // ---------------------------------------------------------------------------
90
+
91
+ describe("handleSendResetPasswordEmail", () => {
92
+ test("throws when userId or resetUrl is missing", async () => {
93
+ const handler = async (bullJob) => {
94
+ const { userId, resetUrl } = bullJob.data;
95
+ if (!userId || !resetUrl) {
96
+ throw new Error(
97
+ "userId and resetUrl are required for SEND_RESET_PASSWORD_EMAIL job",
98
+ );
99
+ }
100
+ };
101
+
102
+ await assert.rejects(
103
+ () =>
104
+ handler(
105
+ makeBullJob("send-reset-password-email", {
106
+ userId: "user-1",
107
+ }),
108
+ ),
109
+ {
110
+ message:
111
+ "userId and resetUrl are required for SEND_RESET_PASSWORD_EMAIL job",
112
+ },
113
+ );
114
+
115
+ await assert.rejects(
116
+ () =>
117
+ handler(
118
+ makeBullJob("send-reset-password-email", {
119
+ resetUrl: "https://example.com/reset",
120
+ }),
121
+ ),
122
+ {
123
+ message:
124
+ "userId and resetUrl are required for SEND_RESET_PASSWORD_EMAIL job",
125
+ },
126
+ );
127
+ });
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // handleCleanupOrphanedFiles
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe("handleCleanupOrphanedFiles", () => {
135
+ test("returns deleted: 0 when no orphaned files exist", async () => {
136
+ const handler = async (bullJob, logger) => {
137
+ const retentionHours = bullJob.data?.retentionHours || 24;
138
+ const _cutoff = new Date(Date.now() - retentionHours * 60 * 60 * 1000);
139
+
140
+ logger.info("Starting orphaned file cleanup", { retentionHours });
141
+
142
+ // Simulate no orphaned files
143
+ const orphaned = [];
144
+ if (orphaned.length === 0) {
145
+ logger.info("No orphaned files to clean up");
146
+ return { success: true, deleted: 0 };
147
+ }
148
+ };
149
+
150
+ const logger = createMockLogger();
151
+ const result = await handler(
152
+ makeBullJob("cleanup-orphaned-files", { retentionHours: 24 }),
153
+ logger,
154
+ );
155
+
156
+ assert.deepStrictEqual(result, { success: true, deleted: 0 });
157
+ });
158
+
159
+ test("deletes orphaned files and reports counts", async () => {
160
+ const deletedKeys = [];
161
+ const deletedIds = [];
162
+
163
+ const handler = async (bullJob, logger) => {
164
+ const retentionHours = bullJob.data?.retentionHours || 24;
165
+ logger.info("Starting cleanup", { retentionHours });
166
+
167
+ const orphaned = [
168
+ { id: "f1", storageKey: "uploads/f1.jpg" },
169
+ { id: "f2", storageKey: "uploads/f2.png" },
170
+ ];
171
+
172
+ let deleted = 0;
173
+ const errors = [];
174
+
175
+ for (const record of orphaned) {
176
+ try {
177
+ deletedKeys.push(record.storageKey);
178
+ deletedIds.push(record.id);
179
+ deleted++;
180
+ } catch (err) {
181
+ errors.push({ id: record.id, error: err.message });
182
+ }
183
+ }
184
+
185
+ return { success: true, deleted, total: orphaned.length, errors };
186
+ };
187
+
188
+ const logger = createMockLogger();
189
+ const result = await handler(
190
+ makeBullJob("cleanup-orphaned-files", { retentionHours: 12 }),
191
+ logger,
192
+ );
193
+
194
+ assert.strictEqual(result.deleted, 2);
195
+ assert.strictEqual(result.total, 2);
196
+ assert.deepStrictEqual(result.errors, []);
197
+ assert.deepStrictEqual(deletedKeys, ["uploads/f1.jpg", "uploads/f2.png"]);
198
+ });
199
+
200
+ test("continues on individual file delete failure", async () => {
201
+ const handler = async (_bullJob, logger) => {
202
+ const orphaned = [
203
+ { id: "f1", storageKey: "uploads/f1.jpg" },
204
+ { id: "f2", storageKey: "uploads/f2.png" },
205
+ ];
206
+
207
+ let deleted = 0;
208
+ const errors = [];
209
+
210
+ for (const record of orphaned) {
211
+ try {
212
+ if (record.id === "f1") {
213
+ throw new Error("S3 timeout");
214
+ }
215
+ deleted++;
216
+ } catch (err) {
217
+ errors.push({ id: record.id, error: err.message });
218
+ logger.warn(`Failed to delete file ${record.id}: ${err.message}`);
219
+ }
220
+ }
221
+
222
+ return { success: true, deleted, total: orphaned.length, errors };
223
+ };
224
+
225
+ const logger = createMockLogger();
226
+ const result = await handler(
227
+ makeBullJob("cleanup-orphaned-files", {}),
228
+ logger,
229
+ );
230
+
231
+ assert.strictEqual(result.deleted, 1);
232
+ assert.strictEqual(result.errors.length, 1);
233
+ assert.strictEqual(result.errors[0].id, "f1");
234
+ assert.strictEqual(result.errors[0].error, "S3 timeout");
235
+ assert.strictEqual(logger.warn.mock.callCount(), 1);
236
+ });
237
+
238
+ test("uses default 24h retention when not specified", async () => {
239
+ let capturedRetention;
240
+
241
+ const handler = async (bullJob) => {
242
+ capturedRetention = bullJob.data?.retentionHours || 24;
243
+ return { success: true, deleted: 0 };
244
+ };
245
+
246
+ await handler(makeBullJob("cleanup-orphaned-files", {}));
247
+ assert.strictEqual(capturedRetention, 24);
248
+ });
14
249
  });
15
250
 
16
- test("Worker - Worker can be instantiated", async () => {
17
- // Basic smoke test - bullmq Worker is available
18
- assert.ok(true, "Worker should be instantiable");
251
+ // ---------------------------------------------------------------------------
252
+ // Handler registry
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe("jobHandlers registry", () => {
256
+ test("maps all expected job names to functions", () => {
257
+ const JOB_NAMES = {
258
+ SEND_WELCOME_EMAIL: "send-welcome-email",
259
+ SEND_RESET_PASSWORD_EMAIL: "send-reset-password-email",
260
+ CLEANUP_ORPHANED_FILES: "cleanup-orphaned-files",
261
+ };
262
+
263
+ // Simulate the registry
264
+ const jobHandlers = {
265
+ [JOB_NAMES.SEND_WELCOME_EMAIL]: () => {},
266
+ [JOB_NAMES.SEND_RESET_PASSWORD_EMAIL]: () => {},
267
+ [JOB_NAMES.CLEANUP_ORPHANED_FILES]: () => {},
268
+ };
269
+
270
+ assert.strictEqual(
271
+ Object.keys(jobHandlers).length,
272
+ 3,
273
+ "Should have exactly 3 handlers",
274
+ );
275
+
276
+ for (const name of Object.values(JOB_NAMES)) {
277
+ assert.strictEqual(
278
+ typeof jobHandlers[name],
279
+ "function",
280
+ `Handler for "${name}" should be a function`,
281
+ );
282
+ }
283
+ });
284
+
285
+ test("throws for unregistered job name", async () => {
286
+ const jobHandlers = {};
287
+
288
+ const dispatch = async (bullJob) => {
289
+ const handler = jobHandlers[bullJob.name];
290
+ if (!handler) {
291
+ throw new Error(`No handler registered for job: ${bullJob.name}`);
292
+ }
293
+ return handler(bullJob);
294
+ };
295
+
296
+ await assert.rejects(() => dispatch(makeBullJob("unknown-job", {})), {
297
+ message: "No handler registered for job: unknown-job",
298
+ });
299
+ });
19
300
  });
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": ["**", "!**/.next", "!**/dist", "!**/.turbo", "!**/coverage"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "tab"
15
+ },
16
+ "linter": {
17
+ "enabled": true,
18
+ "rules": {
19
+ "recommended": true
20
+ }
21
+ },
22
+ "javascript": {
23
+ "formatter": {
24
+ "quoteStyle": "double"
25
+ }
26
+ },
27
+ "css": {
28
+ "parser": {
29
+ "cssModules": true,
30
+ "tailwindDirectives": true
31
+ },
32
+ "linter": {
33
+ "enabled": true
34
+ }
35
+ },
36
+ "assist": {
37
+ "enabled": true,
38
+ "actions": {
39
+ "source": {
40
+ "organizeImports": "on"
41
+ }
42
+ }
43
+ }
44
+ }
@@ -2,13 +2,14 @@ services:
2
2
  # --- 1. PostgreSQL Database ---
3
3
  postgres:
4
4
  image: postgres:16-alpine
5
+ container_name: postgres
5
6
  restart: always
6
7
  ports:
7
8
  - "${POSTGRES_PORT:-5432}:5432"
8
9
  environment:
9
- POSTGRES_USER: ${POSTGRES_USER}
10
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
11
- POSTGRES_DB: ${POSTGRES_DB}
10
+ POSTGRES_USER: ${POSTGRES_USER:-quark_user}
11
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-CHANGE_ME_IN_ENV_FILE}
12
+ POSTGRES_DB: ${POSTGRES_DB:-quark_dev}
12
13
  volumes:
13
14
  - postgres_data:/var/lib/postgresql/data
14
15
  healthcheck:
@@ -20,6 +21,7 @@ services:
20
21
  # --- 2. Redis Cache & Job Queue ---
21
22
  redis:
22
23
  image: redis:7-alpine
24
+ container_name: redis
23
25
  restart: always
24
26
  ports:
25
27
  - "${REDIS_PORT:-6379}:6379"
@@ -35,6 +37,7 @@ services:
35
37
  # --- 3. Mailpit (Local SMTP Server) ---
36
38
  mailpit:
37
39
  image: ghcr.io/axllent/mailpit
40
+ container_name: mailpit
38
41
  restart: always
39
42
  ports:
40
43
  # SMTP port (used by application to send mail)
@@ -49,4 +52,4 @@ services:
49
52
 
50
53
  volumes:
51
54
  postgres_data:
52
- redis_data:
55
+ redis_data:
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "devDependencies": {
39
- "@biomejs/biome": "^2.3.13",
39
+ "@biomejs/biome": "^2.4.0",
40
40
  "@types/node": "^24.10.9",
41
41
  "dotenv-cli": "^11.0.0",
42
42
  "tsx": "^4.21.0",
@@ -21,6 +21,7 @@
21
21
  "license": "ISC",
22
22
  "packageManager": "pnpm@10.12.1",
23
23
  "devDependencies": {
24
+ "@faker-js/faker": "^10.3.0",
24
25
  "@techstream/quark-config": "workspace:*",
25
26
  "bcryptjs": "^3.0.3",
26
27
  "prisma": "^7.4.0"
@@ -28,7 +29,6 @@
28
29
  "dependencies": {
29
30
  "@prisma/adapter-pg": "^7.4.0",
30
31
  "@prisma/client": "^7.4.0",
31
- "dotenv": "^17.2.4",
32
32
  "pg": "^8.18.0",
33
33
  "zod": "^4.3.6"
34
34
  }
@@ -24,7 +24,6 @@ model User {
24
24
  createdAt DateTime @default(now())
25
25
  updatedAt DateTime @updatedAt
26
26
 
27
- posts Post[]
28
27
  accounts Account[]
29
28
  sessions Session[]
30
29
  auditLogs AuditLog[]
@@ -34,21 +33,6 @@ model User {
34
33
  @@index([createdAt])
35
34
  }
36
35
 
37
- model Post {
38
- id String @id @default(cuid())
39
- title String
40
- content String? @db.Text
41
- published Boolean @default(false)
42
- authorId String
43
- author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
44
- createdAt DateTime @default(now())
45
- updatedAt DateTime @updatedAt
46
-
47
- @@index([authorId])
48
- @@index([published])
49
- @@index([createdAt])
50
- }
51
-
52
36
  // NextAuth Models
53
37
  model Account {
54
38
  id String @id @default(cuid())
@@ -128,7 +112,7 @@ enum JobStatus {
128
112
  CANCELLED
129
113
  }
130
114
 
131
- // File Model
115
+ // File Upload Model
132
116
  model File {
133
117
  id String @id @default(cuid())
134
118
  filename String
@@ -1,18 +1,16 @@
1
1
  import { resolve } from "node:path";
2
- import { config } from "dotenv";
3
2
  import { defineConfig } from "prisma/config";
3
+ import { getConnectionString } from "./src/connection.js";
4
4
 
5
5
  // Load .env from monorepo root (needed for standalone commands like db:push, db:seed)
6
- config({ path: resolve(__dirname, "../../.env"), quiet: true });
6
+ try {
7
+ process.loadEnvFile(resolve(__dirname, "../../.env"));
8
+ } catch {}
7
9
 
8
- // Construct DATABASE_URL from individual env vars - single source of truth
9
- const user = process.env.POSTGRES_USER || "quark_user";
10
- const password = process.env.POSTGRES_PASSWORD || "quark_password";
11
- const host = process.env.POSTGRES_HOST || "localhost";
12
- const port = process.env.POSTGRES_PORT || "5432";
13
- const db = process.env.POSTGRES_DB || "quark_dev";
14
-
15
- const databaseUrl = `postgresql://${user}:${password}@${host}:${port}/${db}?schema=public`;
10
+ // Use shared connection builder throwOnMissing=false so `prisma generate` works
11
+ // even without database credentials (e.g. in CI). Commands that need a real
12
+ // connection (migrate, push, studio) will fail at connect time.
13
+ const databaseUrl = getConnectionString({ throwOnMissing: false });
16
14
 
17
15
  export default defineConfig({
18
16
  schema: "prisma/schema.prisma",