@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,40 +1,127 @@
|
|
|
1
|
+
import { faker } from "@faker-js/faker";
|
|
1
2
|
import bcrypt from "bcryptjs";
|
|
2
|
-
import {
|
|
3
|
+
import { prisma } from "../src/index.js";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
// Deterministic output — change this integer to get a different but consistent dataset
|
|
6
|
+
faker.seed(42);
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
const PROFILE = process.env.SEED_PROFILE ?? "dev";
|
|
9
|
+
const VALID_PROFILES = ["minimal", "dev"];
|
|
10
|
+
|
|
11
|
+
if (!VALID_PROFILES.includes(PROFILE)) {
|
|
12
|
+
console.error(
|
|
13
|
+
`Unknown SEED_PROFILE "${PROFILE}". Valid options: ${VALID_PROFILES.join(", ")}`,
|
|
14
|
+
);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Seeders
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
async function seedUsers() {
|
|
23
|
+
const users = [
|
|
24
|
+
{
|
|
25
|
+
email: "admin@example.com",
|
|
26
|
+
name: "Admin User",
|
|
27
|
+
role: "admin",
|
|
28
|
+
password: "Password1",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
email: "viewer@example.com",
|
|
32
|
+
name: "Viewer User",
|
|
33
|
+
role: "viewer",
|
|
34
|
+
password: "Password1",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const seeded = [];
|
|
39
|
+
for (const userData of users) {
|
|
40
|
+
// Skip hashing if the user already exists — upsert update:{} won't use it anyway.
|
|
41
|
+
const existing = await prisma.user.findUnique({
|
|
42
|
+
where: { email: userData.email },
|
|
43
|
+
});
|
|
44
|
+
const hashed =
|
|
45
|
+
existing?.password ?? (await bcrypt.hash(userData.password, 12));
|
|
46
|
+
const user = await prisma.user.upsert({
|
|
47
|
+
where: { email: userData.email },
|
|
48
|
+
// update:{} intentionally leaves existing users unchanged on re-seed.
|
|
49
|
+
// To reset a user's password or role, delete the row first or update the create block.
|
|
50
|
+
update: {},
|
|
51
|
+
create: {
|
|
52
|
+
email: userData.email,
|
|
53
|
+
name: userData.name,
|
|
54
|
+
role: userData.role,
|
|
55
|
+
password: hashed,
|
|
56
|
+
image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${userData.email}`,
|
|
33
57
|
},
|
|
58
|
+
});
|
|
59
|
+
seeded.push(user);
|
|
60
|
+
console.log(` ✓ User: ${user.email} (${user.role})`);
|
|
61
|
+
}
|
|
62
|
+
return seeded;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function seedDevData(users) {
|
|
66
|
+
// Audit logs — representative actions per user
|
|
67
|
+
const actions = ["user.login", "user.update", "file.upload"];
|
|
68
|
+
const userIds = users.map((u) => u.id);
|
|
69
|
+
// Scoped to seeded user IDs so real audit entries in a shared dev DB are not wiped.
|
|
70
|
+
await prisma.auditLog.deleteMany({
|
|
71
|
+
where: { userId: { in: userIds }, action: { in: actions } },
|
|
72
|
+
});
|
|
73
|
+
for (const user of users) {
|
|
74
|
+
for (const action of actions) {
|
|
75
|
+
await prisma.auditLog.create({
|
|
76
|
+
data: {
|
|
77
|
+
userId: user.id,
|
|
78
|
+
action,
|
|
79
|
+
entity: "User",
|
|
80
|
+
entityId: user.id,
|
|
81
|
+
metadata: {
|
|
82
|
+
ip: faker.internet.ip(),
|
|
83
|
+
userAgent: faker.internet.userAgent(),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
console.log(` ✓ AuditLogs: ${users.length * actions.length} rows`);
|
|
90
|
+
|
|
91
|
+
// A sample pending job so the worker dashboard has something to show
|
|
92
|
+
await prisma.job.deleteMany({ where: { name: "seed-example-job" } });
|
|
93
|
+
await prisma.job.create({
|
|
94
|
+
data: {
|
|
95
|
+
queue: "default",
|
|
96
|
+
name: "seed-example-job",
|
|
97
|
+
data: { message: "Hello from seed" },
|
|
98
|
+
status: "PENDING",
|
|
34
99
|
},
|
|
35
100
|
});
|
|
101
|
+
console.log(" ✓ Job: seed-example-job (PENDING)");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Add your own model seeders below and call them from main()
|
|
106
|
+
// Example:
|
|
107
|
+
// async function seedTeams(users) {
|
|
108
|
+
// await prisma.team.upsert({ where: { slug: "acme" }, update: {}, create: { ... } });
|
|
109
|
+
// }
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
async function main() {
|
|
113
|
+
console.log(`\nSeeding database [profile: ${PROFILE}]...\n`);
|
|
114
|
+
|
|
115
|
+
const users = await seedUsers();
|
|
116
|
+
|
|
117
|
+
if (PROFILE === "dev") {
|
|
118
|
+
await seedDevData(users);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add your seeders here:
|
|
122
|
+
// await seedTeams(users);
|
|
36
123
|
|
|
37
|
-
console.log("
|
|
124
|
+
console.log("\nDone.\n");
|
|
38
125
|
}
|
|
39
126
|
|
|
40
127
|
main()
|
|
@@ -15,6 +15,8 @@ const USER_SAFE_SELECT = {
|
|
|
15
15
|
updatedAt: true,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
export { USER_SAFE_SELECT };
|
|
19
|
+
|
|
18
20
|
// User queries
|
|
19
21
|
export const user = {
|
|
20
22
|
findById: (id) => {
|
|
@@ -23,12 +25,7 @@ export const user = {
|
|
|
23
25
|
select: USER_SAFE_SELECT,
|
|
24
26
|
});
|
|
25
27
|
},
|
|
26
|
-
|
|
27
|
-
return prisma.user.findUnique({
|
|
28
|
-
where: { id },
|
|
29
|
-
select: { ...USER_SAFE_SELECT, posts: true },
|
|
30
|
-
});
|
|
31
|
-
},
|
|
28
|
+
|
|
32
29
|
/**
|
|
33
30
|
* findByEmail returns ALL fields including password.
|
|
34
31
|
* Only use for internal auth — never expose the result directly to clients.
|
|
@@ -67,68 +64,6 @@ export const user = {
|
|
|
67
64
|
},
|
|
68
65
|
};
|
|
69
66
|
|
|
70
|
-
/**
|
|
71
|
-
* Safe author include — returns author without sensitive fields.
|
|
72
|
-
*/
|
|
73
|
-
const AUTHOR_SAFE_INCLUDE = { author: { select: USER_SAFE_SELECT } };
|
|
74
|
-
|
|
75
|
-
// Post queries
|
|
76
|
-
export const post = {
|
|
77
|
-
findById: (id) => {
|
|
78
|
-
return prisma.post.findUnique({
|
|
79
|
-
where: { id },
|
|
80
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
81
|
-
});
|
|
82
|
-
},
|
|
83
|
-
findAll: (options = {}) => {
|
|
84
|
-
const { skip = 0, take = 10 } = options;
|
|
85
|
-
return prisma.post.findMany({
|
|
86
|
-
skip,
|
|
87
|
-
take,
|
|
88
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
89
|
-
orderBy: { createdAt: "desc" },
|
|
90
|
-
});
|
|
91
|
-
},
|
|
92
|
-
findPublished: (options = {}) => {
|
|
93
|
-
const { skip = 0, take = 10 } = options;
|
|
94
|
-
return prisma.post.findMany({
|
|
95
|
-
where: { published: true },
|
|
96
|
-
skip,
|
|
97
|
-
take,
|
|
98
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
99
|
-
orderBy: { createdAt: "desc" },
|
|
100
|
-
});
|
|
101
|
-
},
|
|
102
|
-
findByAuthor: (authorId, options = {}) => {
|
|
103
|
-
const { skip = 0, take = 10 } = options;
|
|
104
|
-
return prisma.post.findMany({
|
|
105
|
-
where: { authorId },
|
|
106
|
-
skip,
|
|
107
|
-
take,
|
|
108
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
109
|
-
orderBy: { createdAt: "desc" },
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
create: (data) => {
|
|
113
|
-
return prisma.post.create({
|
|
114
|
-
data,
|
|
115
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
116
|
-
});
|
|
117
|
-
},
|
|
118
|
-
update: (id, data) => {
|
|
119
|
-
return prisma.post.update({
|
|
120
|
-
where: { id },
|
|
121
|
-
data,
|
|
122
|
-
include: AUTHOR_SAFE_INCLUDE,
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
delete: (id) => {
|
|
126
|
-
return prisma.post.delete({
|
|
127
|
-
where: { id },
|
|
128
|
-
});
|
|
129
|
-
},
|
|
130
|
-
};
|
|
131
|
-
|
|
132
67
|
// Note: Job tracking is handled by BullMQ's built-in Redis persistence.
|
|
133
68
|
// The Prisma Job model is retained in the schema for optional audit/reporting
|
|
134
69
|
// but these query helpers have been removed to avoid confusion with BullMQ.
|
|
@@ -278,3 +213,58 @@ export const auditLog = {
|
|
|
278
213
|
});
|
|
279
214
|
},
|
|
280
215
|
};
|
|
216
|
+
|
|
217
|
+
// File queries
|
|
218
|
+
export const file = {
|
|
219
|
+
create: (data) => {
|
|
220
|
+
return prisma.file.create({ data });
|
|
221
|
+
},
|
|
222
|
+
findById: (id) => {
|
|
223
|
+
return prisma.file.findUnique({
|
|
224
|
+
where: { id },
|
|
225
|
+
include: {
|
|
226
|
+
uploadedBy: { select: { id: true, email: true, name: true } },
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
},
|
|
230
|
+
findByStorageKey: (storageKey) => {
|
|
231
|
+
return prisma.file.findUnique({ where: { storageKey } });
|
|
232
|
+
},
|
|
233
|
+
findByUploader: (uploadedById, options = {}) => {
|
|
234
|
+
const { skip = 0, take = 50 } = options;
|
|
235
|
+
return prisma.file.findMany({
|
|
236
|
+
where: { uploadedById },
|
|
237
|
+
skip,
|
|
238
|
+
take,
|
|
239
|
+
orderBy: { createdAt: "desc" },
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
findOrphaned: (options = {}) => {
|
|
243
|
+
const { take = 100 } = options;
|
|
244
|
+
return prisma.file.findMany({
|
|
245
|
+
where: { uploadedById: null },
|
|
246
|
+
take,
|
|
247
|
+
orderBy: { createdAt: "asc" },
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
findOlderThan: (date, options = {}) => {
|
|
251
|
+
const { take = 100 } = options;
|
|
252
|
+
return prisma.file.findMany({
|
|
253
|
+
where: {
|
|
254
|
+
uploadedById: null,
|
|
255
|
+
createdAt: { lt: date },
|
|
256
|
+
},
|
|
257
|
+
take,
|
|
258
|
+
orderBy: { createdAt: "asc" },
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
delete: (id) => {
|
|
262
|
+
return prisma.file.delete({ where: { id } });
|
|
263
|
+
},
|
|
264
|
+
deleteMany: (ids) => {
|
|
265
|
+
return prisma.file.deleteMany({ where: { id: { in: ids } } });
|
|
266
|
+
},
|
|
267
|
+
count: (where = {}) => {
|
|
268
|
+
return prisma.file.count({ where });
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -7,10 +7,6 @@ const _mockPrisma = {
|
|
|
7
7
|
findUnique: async (params) => params,
|
|
8
8
|
create: async (params) => params,
|
|
9
9
|
},
|
|
10
|
-
post: {
|
|
11
|
-
create: async (params) => params,
|
|
12
|
-
findMany: async (params) => params,
|
|
13
|
-
},
|
|
14
10
|
};
|
|
15
11
|
|
|
16
12
|
// Mock module for queries
|
|
@@ -31,15 +27,6 @@ const mockQueries = {
|
|
|
31
27
|
...data,
|
|
32
28
|
}),
|
|
33
29
|
},
|
|
34
|
-
post: {
|
|
35
|
-
create: async (data) => ({
|
|
36
|
-
id: "1",
|
|
37
|
-
...data,
|
|
38
|
-
}),
|
|
39
|
-
findPublished: async () => [
|
|
40
|
-
{ id: "1", title: "Published", published: true },
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
30
|
};
|
|
44
31
|
|
|
45
32
|
test("User Queries - findById calls prisma with correct params", async () => {
|
|
@@ -61,19 +48,3 @@ test("User Queries - create calls prisma with correct params", async () => {
|
|
|
61
48
|
assert.strictEqual(result.email, "new@example.com");
|
|
62
49
|
assert.strictEqual(result.name, "New User");
|
|
63
50
|
});
|
|
64
|
-
|
|
65
|
-
test("Post Queries - create calls prisma with correct params", async () => {
|
|
66
|
-
const result = await mockQueries.post.create({
|
|
67
|
-
title: "Test Post",
|
|
68
|
-
authorId: "1",
|
|
69
|
-
});
|
|
70
|
-
assert.strictEqual(result.title, "Test Post");
|
|
71
|
-
assert.strictEqual(result.authorId, "1");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("Post Queries - findPublished returns only published posts", async () => {
|
|
75
|
-
const result = await mockQueries.post.findPublished();
|
|
76
|
-
assert.ok(Array.isArray(result));
|
|
77
|
-
assert.strictEqual(result.length, 1);
|
|
78
|
-
assert.strictEqual(result[0].published, true);
|
|
79
|
-
});
|
|
@@ -23,14 +23,8 @@ export const userUpdateSchema = z.object({
|
|
|
23
23
|
image: z.string().url().optional(),
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
export const postUpdateSchema = z.object({
|
|
33
|
-
title: z.string().min(1, "Title is required").optional(),
|
|
34
|
-
content: z.string().optional(),
|
|
35
|
-
published: z.boolean().optional(),
|
|
26
|
+
export const fileUploadSchema = z.object({
|
|
27
|
+
filename: z.string().min(1, "Filename is required"),
|
|
28
|
+
mimeType: z.string().min(1, "MIME type is required"),
|
|
29
|
+
size: z.number().int().positive("File size must be positive"),
|
|
36
30
|
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-config - Environment Configuration
|
|
3
|
+
* Provides environment-specific defaults for dev, test, staging, and production.
|
|
4
|
+
* Each environment defines sensible defaults that can be overridden via env vars.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {"development" | "test" | "staging" | "production"} Environment
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} EnvironmentConfig
|
|
11
|
+
* @property {Environment} environment - Resolved environment name
|
|
12
|
+
* @property {boolean} isProduction - True in production and staging
|
|
13
|
+
* @property {boolean} isDevelopment - True in development
|
|
14
|
+
* @property {boolean} isTest - True in test
|
|
15
|
+
* @property {Object} server - Server configuration
|
|
16
|
+
* @property {number} server.port - HTTP port
|
|
17
|
+
* @property {Object} rateLimit - Rate limiting defaults
|
|
18
|
+
* @property {number} rateLimit.windowMs - Rate limit window in ms
|
|
19
|
+
* @property {number} rateLimit.maxRequests - Max requests per window
|
|
20
|
+
* @property {number} rateLimit.authMaxRequests - Max auth requests per window
|
|
21
|
+
* @property {Object} cache - Cache configuration
|
|
22
|
+
* @property {number} cache.defaultTtl - Default cache TTL in seconds
|
|
23
|
+
* @property {Object} logging - Logging configuration
|
|
24
|
+
* @property {string} logging.level - Minimum log level
|
|
25
|
+
* @property {boolean} logging.json - Use JSON format
|
|
26
|
+
* @property {Object} db - Database configuration
|
|
27
|
+
* @property {number} db.poolMax - Max DB pool connections
|
|
28
|
+
* @property {number} db.poolIdleTimeout - Idle timeout in seconds
|
|
29
|
+
* @property {number} db.connectionTimeout - Connection timeout in seconds
|
|
30
|
+
* @property {Object} email - Email configuration
|
|
31
|
+
* @property {number} email.timeout - SMTP/API timeout in ms
|
|
32
|
+
* @property {Object} security - Security configuration
|
|
33
|
+
* @property {boolean} security.enforceHttps - Require HTTPS
|
|
34
|
+
* @property {boolean} security.trustProxy - Trust proxy headers
|
|
35
|
+
* @property {Object} features - Feature flags
|
|
36
|
+
* @property {boolean} features.debugRoutes - Enable debug endpoints
|
|
37
|
+
* @property {boolean} features.seedOnStart - Auto-seed database on startup
|
|
38
|
+
* @property {boolean} features.detailedErrors - Include stack traces in error responses
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** @type {Record<Environment, EnvironmentConfig>} */
|
|
42
|
+
const ENVIRONMENT_CONFIGS = {
|
|
43
|
+
development: {
|
|
44
|
+
environment: "development",
|
|
45
|
+
isProduction: false,
|
|
46
|
+
isDevelopment: true,
|
|
47
|
+
isTest: false,
|
|
48
|
+
server: {
|
|
49
|
+
port: 3000,
|
|
50
|
+
},
|
|
51
|
+
rateLimit: {
|
|
52
|
+
windowMs: 15 * 60 * 1000,
|
|
53
|
+
maxRequests: 1000,
|
|
54
|
+
authMaxRequests: 50,
|
|
55
|
+
},
|
|
56
|
+
cache: {
|
|
57
|
+
defaultTtl: 60,
|
|
58
|
+
},
|
|
59
|
+
logging: {
|
|
60
|
+
level: "debug",
|
|
61
|
+
json: false,
|
|
62
|
+
},
|
|
63
|
+
db: {
|
|
64
|
+
poolMax: 5,
|
|
65
|
+
poolIdleTimeout: 30,
|
|
66
|
+
connectionTimeout: 5,
|
|
67
|
+
},
|
|
68
|
+
email: {
|
|
69
|
+
timeout: 10_000,
|
|
70
|
+
},
|
|
71
|
+
security: {
|
|
72
|
+
enforceHttps: false,
|
|
73
|
+
trustProxy: false,
|
|
74
|
+
},
|
|
75
|
+
features: {
|
|
76
|
+
debugRoutes: true,
|
|
77
|
+
seedOnStart: false,
|
|
78
|
+
detailedErrors: true,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
test: {
|
|
83
|
+
environment: "test",
|
|
84
|
+
isProduction: false,
|
|
85
|
+
isDevelopment: false,
|
|
86
|
+
isTest: true,
|
|
87
|
+
server: {
|
|
88
|
+
port: 3001,
|
|
89
|
+
},
|
|
90
|
+
rateLimit: {
|
|
91
|
+
windowMs: 15 * 60 * 1000,
|
|
92
|
+
maxRequests: 10_000,
|
|
93
|
+
authMaxRequests: 10_000,
|
|
94
|
+
},
|
|
95
|
+
cache: {
|
|
96
|
+
defaultTtl: 0,
|
|
97
|
+
},
|
|
98
|
+
logging: {
|
|
99
|
+
level: "warn",
|
|
100
|
+
json: false,
|
|
101
|
+
},
|
|
102
|
+
db: {
|
|
103
|
+
poolMax: 3,
|
|
104
|
+
poolIdleTimeout: 10,
|
|
105
|
+
connectionTimeout: 5,
|
|
106
|
+
},
|
|
107
|
+
email: {
|
|
108
|
+
timeout: 5_000,
|
|
109
|
+
},
|
|
110
|
+
security: {
|
|
111
|
+
enforceHttps: false,
|
|
112
|
+
trustProxy: false,
|
|
113
|
+
},
|
|
114
|
+
features: {
|
|
115
|
+
debugRoutes: true,
|
|
116
|
+
seedOnStart: false,
|
|
117
|
+
detailedErrors: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
staging: {
|
|
122
|
+
environment: "staging",
|
|
123
|
+
isProduction: true,
|
|
124
|
+
isDevelopment: false,
|
|
125
|
+
isTest: false,
|
|
126
|
+
server: {
|
|
127
|
+
port: 3000,
|
|
128
|
+
},
|
|
129
|
+
rateLimit: {
|
|
130
|
+
windowMs: 15 * 60 * 1000,
|
|
131
|
+
maxRequests: 100,
|
|
132
|
+
authMaxRequests: 5,
|
|
133
|
+
},
|
|
134
|
+
cache: {
|
|
135
|
+
defaultTtl: 300,
|
|
136
|
+
},
|
|
137
|
+
logging: {
|
|
138
|
+
level: "info",
|
|
139
|
+
json: true,
|
|
140
|
+
},
|
|
141
|
+
db: {
|
|
142
|
+
poolMax: 10,
|
|
143
|
+
poolIdleTimeout: 30,
|
|
144
|
+
connectionTimeout: 5,
|
|
145
|
+
},
|
|
146
|
+
email: {
|
|
147
|
+
timeout: 10_000,
|
|
148
|
+
},
|
|
149
|
+
security: {
|
|
150
|
+
enforceHttps: true,
|
|
151
|
+
trustProxy: true,
|
|
152
|
+
},
|
|
153
|
+
features: {
|
|
154
|
+
debugRoutes: false,
|
|
155
|
+
seedOnStart: false,
|
|
156
|
+
detailedErrors: false,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
production: {
|
|
161
|
+
environment: "production",
|
|
162
|
+
isProduction: true,
|
|
163
|
+
isDevelopment: false,
|
|
164
|
+
isTest: false,
|
|
165
|
+
server: {
|
|
166
|
+
port: 3000,
|
|
167
|
+
},
|
|
168
|
+
rateLimit: {
|
|
169
|
+
windowMs: 15 * 60 * 1000,
|
|
170
|
+
maxRequests: 100,
|
|
171
|
+
authMaxRequests: 5,
|
|
172
|
+
},
|
|
173
|
+
cache: {
|
|
174
|
+
defaultTtl: 600,
|
|
175
|
+
},
|
|
176
|
+
logging: {
|
|
177
|
+
level: "info",
|
|
178
|
+
json: true,
|
|
179
|
+
},
|
|
180
|
+
db: {
|
|
181
|
+
poolMax: 10,
|
|
182
|
+
poolIdleTimeout: 30,
|
|
183
|
+
connectionTimeout: 5,
|
|
184
|
+
},
|
|
185
|
+
email: {
|
|
186
|
+
timeout: 10_000,
|
|
187
|
+
},
|
|
188
|
+
security: {
|
|
189
|
+
enforceHttps: true,
|
|
190
|
+
trustProxy: true,
|
|
191
|
+
},
|
|
192
|
+
features: {
|
|
193
|
+
debugRoutes: false,
|
|
194
|
+
seedOnStart: false,
|
|
195
|
+
detailedErrors: false,
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/** Valid environment names */
|
|
201
|
+
export const ENVIRONMENTS = /** @type {const} */ ([
|
|
202
|
+
"development",
|
|
203
|
+
"test",
|
|
204
|
+
"staging",
|
|
205
|
+
"production",
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolves the current environment from NODE_ENV.
|
|
210
|
+
* Maps common aliases (e.g. "dev" → "development", "prod" → "production").
|
|
211
|
+
* Defaults to "development" if unset or unrecognized.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} [nodeEnv] - Override for NODE_ENV (defaults to process.env.NODE_ENV)
|
|
214
|
+
* @returns {Environment}
|
|
215
|
+
*/
|
|
216
|
+
export function resolveEnvironment(nodeEnv) {
|
|
217
|
+
const raw = (nodeEnv ?? process.env.NODE_ENV ?? "").toLowerCase().trim();
|
|
218
|
+
|
|
219
|
+
const aliases = {
|
|
220
|
+
dev: "development",
|
|
221
|
+
development: "development",
|
|
222
|
+
test: "test",
|
|
223
|
+
testing: "test",
|
|
224
|
+
staging: "staging",
|
|
225
|
+
stage: "staging",
|
|
226
|
+
prod: "production",
|
|
227
|
+
production: "production",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return aliases[raw] || "development";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Returns the full environment configuration for a given environment.
|
|
235
|
+
* Unknown environments fall back to development.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} [nodeEnv] - Override for NODE_ENV
|
|
238
|
+
* @returns {EnvironmentConfig}
|
|
239
|
+
*/
|
|
240
|
+
export function getEnvironmentConfig(nodeEnv) {
|
|
241
|
+
const env = resolveEnvironment(nodeEnv);
|
|
242
|
+
return { ...ENVIRONMENT_CONFIGS[env] };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Shallow-merges environment config with user-provided overrides (one level deep).
|
|
247
|
+
* Top-level scalars are replaced; top-level objects are spread-merged.
|
|
248
|
+
* Useful when downstream apps need to adjust defaults per-environment.
|
|
249
|
+
*
|
|
250
|
+
* @param {EnvironmentConfig} base - Base environment config
|
|
251
|
+
* @param {Record<string, unknown>} overrides - Partial overrides to merge
|
|
252
|
+
* @returns {EnvironmentConfig}
|
|
253
|
+
*/
|
|
254
|
+
export function mergeConfig(base, overrides) {
|
|
255
|
+
const result = { ...base };
|
|
256
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
257
|
+
if (
|
|
258
|
+
value != null &&
|
|
259
|
+
typeof value === "object" &&
|
|
260
|
+
!Array.isArray(value) &&
|
|
261
|
+
typeof result[key] === "object" &&
|
|
262
|
+
result[key] != null
|
|
263
|
+
) {
|
|
264
|
+
result[key] = { ...result[key], ...value };
|
|
265
|
+
} else {
|
|
266
|
+
result[key] = value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
export const config = {
|
|
2
|
-
appName: "
|
|
3
|
-
|
|
4
|
-
api: {
|
|
5
|
-
baseUrl: process.env.API_BASE_URL || "http://localhost:3000",
|
|
6
|
-
},
|
|
7
|
-
database: {
|
|
8
|
-
url:
|
|
9
|
-
process.env.DATABASE_URL ||
|
|
10
|
-
"postgresql://user:password@localhost:5432/myapp",
|
|
11
|
-
},
|
|
12
|
-
redis: {
|
|
13
|
-
url: process.env.REDIS_URL || "redis://localhost:6379",
|
|
14
|
-
},
|
|
15
|
-
email: {
|
|
16
|
-
from: process.env.EMAIL_FROM || "noreply@myquarkapp.com",
|
|
17
|
-
provider: process.env.EMAIL_PROVIDER || "smtp",
|
|
18
|
-
},
|
|
2
|
+
appName: "Quark",
|
|
3
|
+
appDescription: "A modern monorepo with Next.js, React, and Prisma",
|
|
19
4
|
};
|
|
20
5
|
|
|
21
|
-
export
|
|
6
|
+
export { getAllowedOrigins, getAppUrl, syncNextAuthUrl } from "./app-url.js";
|
|
7
|
+
export {
|
|
8
|
+
ENVIRONMENTS,
|
|
9
|
+
getEnvironmentConfig,
|
|
10
|
+
mergeConfig,
|
|
11
|
+
resolveEnvironment,
|
|
12
|
+
} from "./environment.js";
|
|
13
|
+
export { getConfig, loadConfig, resetConfig } from "./load-config.js";
|