@techstream/quark-create-app 1.5.3 → 1.6.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.
- package/package.json +4 -2
- package/src/index.js +52 -9
- package/templates/base-project/.github/dependabot.yml +12 -0
- package/templates/base-project/.github/workflows/ci.yml +97 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/release.yml +38 -0
- package/templates/base-project/apps/web/biome.json +7 -0
- package/templates/base-project/apps/web/jsconfig.json +5 -5
- package/templates/base-project/apps/web/next.config.js +86 -1
- package/templates/base-project/apps/web/package.json +4 -4
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
- package/templates/base-project/apps/web/src/app/layout.js +3 -4
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/robots.js +21 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
- package/templates/base-project/apps/web/src/proxy.js +1 -2
- package/templates/base-project/apps/worker/package.json +4 -4
- package/templates/base-project/apps/worker/src/index.js +26 -12
- package/templates/base-project/apps/worker/src/index.test.js +296 -15
- package/templates/base-project/biome.json +44 -0
- package/templates/base-project/docker-compose.yml +7 -4
- package/templates/base-project/package.json +1 -1
- package/templates/base-project/packages/db/package.json +1 -1
- package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
- package/templates/base-project/packages/db/prisma.config.ts +3 -2
- package/templates/base-project/packages/db/scripts/seed.js +117 -30
- package/templates/base-project/packages/db/src/queries.js +52 -118
- package/templates/base-project/packages/db/src/queries.test.js +0 -29
- package/templates/base-project/packages/db/src/schemas.js +0 -12
- package/templates/base-project/pnpm-workspace.yaml +4 -0
- package/templates/base-project/turbo.json +5 -3
- package/templates/config/package.json +2 -0
- package/templates/config/src/environment.js +270 -0
- package/templates/config/src/index.js +10 -18
- package/templates/config/src/load-config.js +135 -0
- package/templates/config/src/validate-env.js +60 -2
- package/templates/jobs/package.json +2 -2
- package/templates/jobs/src/definitions.test.js +34 -0
- package/templates/jobs/src/index.js +1 -1
- package/templates/ui/package.json +4 -4
- package/templates/ui/src/button.test.js +23 -0
- package/templates/ui/src/index.js +1 -3
- package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
- package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
- package/templates/ui/src/card.js +0 -14
- package/templates/ui/src/input.js +0 -11
|
@@ -17,6 +17,7 @@ const logger = createLogger("worker");
|
|
|
17
17
|
|
|
18
18
|
// Store workers for graceful shutdown
|
|
19
19
|
const workers = [];
|
|
20
|
+
let isShuttingDown = false;
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Generic queue processor — dispatches jobs to registered handlers
|
|
@@ -113,15 +114,31 @@ async function startWorker() {
|
|
|
113
114
|
/**
|
|
114
115
|
* Graceful shutdown handler
|
|
115
116
|
*/
|
|
116
|
-
async function shutdown() {
|
|
117
|
-
|
|
117
|
+
async function shutdown(signal = "unknown") {
|
|
118
|
+
if (isShuttingDown) {
|
|
119
|
+
logger.warn("Shutdown already in progress", { signal });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isShuttingDown = true;
|
|
124
|
+
logger.info("Shutting down worker service", { signal });
|
|
118
125
|
|
|
119
126
|
try {
|
|
120
127
|
for (const worker of workers) {
|
|
121
128
|
await worker.close();
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
131
|
+
try {
|
|
132
|
+
await prisma.$disconnect();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (error.message?.includes("environment variable is required")) {
|
|
135
|
+
logger.warn("Skipping Prisma disconnect due missing database env", {
|
|
136
|
+
error: error.message,
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
125
142
|
|
|
126
143
|
logger.info("All workers closed");
|
|
127
144
|
process.exit(0);
|
|
@@ -134,14 +151,11 @@ async function shutdown() {
|
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
process.on("SIGTERM",
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
process.on("SIGTERM", shutdown);
|
|
144
|
-
process.on("SIGINT", shutdown);
|
|
154
|
+
process.on("SIGTERM", () => {
|
|
155
|
+
void shutdown("SIGTERM");
|
|
156
|
+
});
|
|
157
|
+
process.on("SIGINT", () => {
|
|
158
|
+
void shutdown("SIGINT");
|
|
159
|
+
});
|
|
145
160
|
|
|
146
|
-
// Start the worker service
|
|
147
161
|
startWorker();
|
|
@@ -1,19 +1,300 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"license": "ISC",
|
|
22
22
|
"packageManager": "pnpm@10.12.1",
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"@faker-js/faker": "^9.0.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,9 +1,10 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
-
import { config } from "dotenv";
|
|
3
2
|
import { defineConfig } from "prisma/config";
|
|
4
3
|
|
|
5
4
|
// Load .env from monorepo root (needed for standalone commands like db:push, db:seed)
|
|
6
|
-
|
|
5
|
+
try {
|
|
6
|
+
process.loadEnvFile(resolve(__dirname, "../../.env"));
|
|
7
|
+
} catch {}
|
|
7
8
|
|
|
8
9
|
// Construct DATABASE_URL from individual env vars - single source of truth
|
|
9
10
|
const user = process.env.POSTGRES_USER || "quark_user";
|