@techstream/quark-create-app 1.5.2 → 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/README.md +38 -0
- package/package.json +5 -2
- package/src/index.js +142 -12
- package/templates/base-project/.github/copilot-instructions.md +7 -0
- package/templates/base-project/.github/dependabot.yml +12 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +106 -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 +10 -0
- 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 +9 -9
- package/templates/base-project/apps/web/src/app/api/files/route.js +3 -2
- 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 +40 -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/migrations/20260202061128_initial/migration.sql +42 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +20 -16
- 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 +58 -68
- package/templates/base-project/packages/db/src/queries.test.js +0 -29
- package/templates/base-project/packages/db/src/schemas.js +4 -10
- 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 -42
- package/templates/ui/src/card.js +0 -14
- package/templates/ui/src/input.js +0 -11
|
@@ -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
|
}
|
package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql
CHANGED
|
@@ -43,6 +43,8 @@ CREATE TABLE "Account" (
|
|
|
43
43
|
"scope" TEXT,
|
|
44
44
|
"id_token" TEXT,
|
|
45
45
|
"session_state" TEXT,
|
|
46
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
47
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
46
48
|
|
|
47
49
|
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
|
48
50
|
);
|
|
@@ -86,6 +88,22 @@ CREATE TABLE "Job" (
|
|
|
86
88
|
CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
|
|
87
89
|
);
|
|
88
90
|
|
|
91
|
+
-- CreateTable
|
|
92
|
+
CREATE TABLE "File" (
|
|
93
|
+
"id" TEXT NOT NULL,
|
|
94
|
+
"filename" TEXT NOT NULL,
|
|
95
|
+
"originalName" TEXT NOT NULL,
|
|
96
|
+
"mimeType" TEXT NOT NULL,
|
|
97
|
+
"size" INTEGER NOT NULL,
|
|
98
|
+
"storageKey" TEXT NOT NULL,
|
|
99
|
+
"storageProvider" TEXT NOT NULL DEFAULT 'local',
|
|
100
|
+
"uploadedById" TEXT,
|
|
101
|
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
102
|
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
103
|
+
|
|
104
|
+
CONSTRAINT "File_pkey" PRIMARY KEY ("id")
|
|
105
|
+
);
|
|
106
|
+
|
|
89
107
|
-- CreateTable
|
|
90
108
|
CREATE TABLE "AuditLog" (
|
|
91
109
|
"id" TEXT NOT NULL,
|
|
@@ -130,6 +148,9 @@ CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken");
|
|
|
130
148
|
-- CreateIndex
|
|
131
149
|
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
|
132
150
|
|
|
151
|
+
-- CreateIndex
|
|
152
|
+
CREATE INDEX "Session_expires_idx" ON "Session"("expires");
|
|
153
|
+
|
|
133
154
|
-- CreateIndex
|
|
134
155
|
CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token");
|
|
135
156
|
|
|
@@ -139,6 +160,9 @@ CREATE INDEX "VerificationToken_token_idx" ON "VerificationToken"("token");
|
|
|
139
160
|
-- CreateIndex
|
|
140
161
|
CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token");
|
|
141
162
|
|
|
163
|
+
-- CreateIndex
|
|
164
|
+
CREATE INDEX "VerificationToken_expires_idx" ON "VerificationToken"("expires");
|
|
165
|
+
|
|
142
166
|
-- CreateIndex
|
|
143
167
|
CREATE INDEX "Job_queue_idx" ON "Job"("queue");
|
|
144
168
|
|
|
@@ -148,9 +172,24 @@ CREATE INDEX "Job_status_idx" ON "Job"("status");
|
|
|
148
172
|
-- CreateIndex
|
|
149
173
|
CREATE INDEX "Job_runAt_idx" ON "Job"("runAt");
|
|
150
174
|
|
|
175
|
+
-- CreateIndex
|
|
176
|
+
CREATE INDEX "Job_status_runAt_idx" ON "Job"("status", "runAt");
|
|
177
|
+
|
|
151
178
|
-- CreateIndex
|
|
152
179
|
CREATE INDEX "Job_createdAt_idx" ON "Job"("createdAt");
|
|
153
180
|
|
|
181
|
+
-- CreateIndex
|
|
182
|
+
CREATE UNIQUE INDEX "File_storageKey_key" ON "File"("storageKey");
|
|
183
|
+
|
|
184
|
+
-- CreateIndex
|
|
185
|
+
CREATE INDEX "File_uploadedById_idx" ON "File"("uploadedById");
|
|
186
|
+
|
|
187
|
+
-- CreateIndex
|
|
188
|
+
CREATE INDEX "File_mimeType_idx" ON "File"("mimeType");
|
|
189
|
+
|
|
190
|
+
-- CreateIndex
|
|
191
|
+
CREATE INDEX "File_createdAt_idx" ON "File"("createdAt");
|
|
192
|
+
|
|
154
193
|
-- CreateIndex
|
|
155
194
|
CREATE INDEX "AuditLog_userId_idx" ON "AuditLog"("userId");
|
|
156
195
|
|
|
@@ -172,5 +211,8 @@ ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId"
|
|
|
172
211
|
-- AddForeignKey
|
|
173
212
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
174
213
|
|
|
214
|
+
-- AddForeignKey
|
|
215
|
+
ALTER TABLE "File" ADD CONSTRAINT "File_uploadedById_fkey" FOREIGN KEY ("uploadedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
216
|
+
|
|
175
217
|
-- AddForeignKey
|
|
176
218
|
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@@ -24,30 +24,15 @@ 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[]
|
|
30
|
+
files File[]
|
|
31
31
|
|
|
32
32
|
@@index([email])
|
|
33
33
|
@@index([createdAt])
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
model Post {
|
|
37
|
-
id String @id @default(cuid())
|
|
38
|
-
title String
|
|
39
|
-
content String? @db.Text
|
|
40
|
-
published Boolean @default(false)
|
|
41
|
-
authorId String
|
|
42
|
-
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
|
43
|
-
createdAt DateTime @default(now())
|
|
44
|
-
updatedAt DateTime @updatedAt
|
|
45
|
-
|
|
46
|
-
@@index([authorId])
|
|
47
|
-
@@index([published])
|
|
48
|
-
@@index([createdAt])
|
|
49
|
-
}
|
|
50
|
-
|
|
51
36
|
// NextAuth Models
|
|
52
37
|
model Account {
|
|
53
38
|
id String @id @default(cuid())
|
|
@@ -127,6 +112,25 @@ enum JobStatus {
|
|
|
127
112
|
CANCELLED
|
|
128
113
|
}
|
|
129
114
|
|
|
115
|
+
// File Upload Model
|
|
116
|
+
model File {
|
|
117
|
+
id String @id @default(cuid())
|
|
118
|
+
filename String
|
|
119
|
+
originalName String
|
|
120
|
+
mimeType String
|
|
121
|
+
size Int
|
|
122
|
+
storageKey String @unique
|
|
123
|
+
storageProvider String @default("local")
|
|
124
|
+
uploadedById String?
|
|
125
|
+
uploadedBy User? @relation(fields: [uploadedById], references: [id], onDelete: SetNull)
|
|
126
|
+
createdAt DateTime @default(now())
|
|
127
|
+
updatedAt DateTime @updatedAt
|
|
128
|
+
|
|
129
|
+
@@index([uploadedById])
|
|
130
|
+
@@index([mimeType])
|
|
131
|
+
@@index([createdAt])
|
|
132
|
+
}
|
|
133
|
+
|
|
130
134
|
// Audit Log Model
|
|
131
135
|
model AuditLog {
|
|
132
136
|
id String @id @default(cuid())
|
|
@@ -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";
|