@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.
Files changed (53) hide show
  1. package/README.md +38 -0
  2. package/package.json +5 -2
  3. package/src/index.js +142 -12
  4. package/templates/base-project/.github/copilot-instructions.md +7 -0
  5. package/templates/base-project/.github/dependabot.yml +12 -0
  6. package/templates/base-project/.github/skills/project-context/SKILL.md +106 -0
  7. package/templates/base-project/.github/workflows/ci.yml +97 -0
  8. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  9. package/templates/base-project/.github/workflows/release.yml +38 -0
  10. package/templates/base-project/apps/web/biome.json +7 -0
  11. package/templates/base-project/apps/web/jsconfig.json +10 -0
  12. package/templates/base-project/apps/web/next.config.js +86 -1
  13. package/templates/base-project/apps/web/package.json +4 -4
  14. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +9 -9
  15. package/templates/base-project/apps/web/src/app/api/files/route.js +3 -2
  16. package/templates/base-project/apps/web/src/app/layout.js +3 -4
  17. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  18. package/templates/base-project/apps/web/src/app/robots.js +21 -0
  19. package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
  20. package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
  21. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
  22. package/templates/base-project/apps/web/src/proxy.js +1 -2
  23. package/templates/base-project/apps/worker/package.json +4 -4
  24. package/templates/base-project/apps/worker/src/index.js +40 -12
  25. package/templates/base-project/apps/worker/src/index.test.js +296 -15
  26. package/templates/base-project/biome.json +44 -0
  27. package/templates/base-project/docker-compose.yml +7 -4
  28. package/templates/base-project/package.json +1 -1
  29. package/templates/base-project/packages/db/package.json +1 -1
  30. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +42 -0
  31. package/templates/base-project/packages/db/prisma/schema.prisma +20 -16
  32. package/templates/base-project/packages/db/prisma.config.ts +3 -2
  33. package/templates/base-project/packages/db/scripts/seed.js +117 -30
  34. package/templates/base-project/packages/db/src/queries.js +58 -68
  35. package/templates/base-project/packages/db/src/queries.test.js +0 -29
  36. package/templates/base-project/packages/db/src/schemas.js +4 -10
  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 +10 -18
  42. package/templates/config/src/load-config.js +135 -0
  43. package/templates/config/src/validate-env.js +60 -2
  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 -42
  52. package/templates/ui/src/card.js +0 -14
  53. 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 { PrismaClient } from "../src/generated/prisma/client.js";
3
+ import { prisma } from "../src/index.js";
3
4
 
4
- const prisma = new PrismaClient();
5
+ // Deterministic output change this integer to get a different but consistent dataset
6
+ faker.seed(42);
5
7
 
6
- async function main() {
7
- console.log("Seeding database...");
8
-
9
- const email = "test@example.com";
10
- const password = await bcrypt.hash("Password1", 12);
11
-
12
- const user = await prisma.user.upsert({
13
- where: { email },
14
- update: {},
15
- create: {
16
- email,
17
- name: "Test User",
18
- password,
19
- image: "https://api.dicebear.com/7.x/avataaars/svg?seed=test",
20
- posts: {
21
- create: [
22
- {
23
- title: "Hello World",
24
- content: "This is a seeded post. Welcome to Quark!",
25
- published: true,
26
- },
27
- {
28
- title: "Draft Post",
29
- content: "This is a draft post. It is not published yet.",
30
- published: false,
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("Seeded user:", user.email);
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
- findByIdWithPosts: (id) => {
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 postCreateSchema = z.object({
27
- title: z.string().min(1, "Title is required"),
28
- content: z.string().optional(),
29
- published: z.boolean().optional(),
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
  });
@@ -5,3 +5,7 @@ packages:
5
5
  onlyBuiltDependencies:
6
6
  - '@prisma/engines'
7
7
  - prisma
8
+ - esbuild
9
+ - msgpackr-extract
10
+ - sharp
11
+ - simple-git-hooks
@@ -26,9 +26,11 @@
26
26
  "db:seed": {
27
27
  "cache": false
28
28
  },
29
- "db:studio": {
30
- "cache": false,
31
- "persistent": true
29
+ "sync-templates": {
30
+ "cache": false
31
+ },
32
+ "sync-templates:check": {
33
+ "cache": false
32
34
  },
33
35
  "dev": {
34
36
  "cache": false,
@@ -5,6 +5,8 @@
5
5
  "exports": {
6
6
  ".": "./src/index.js",
7
7
  "./app-url": "./src/app-url.js",
8
+ "./environment": "./src/environment.js",
9
+ "./load-config": "./src/load-config.js",
8
10
  "./validate-env": "./src/validate-env.js"
9
11
  }
10
12
  }
@@ -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: "My Quark App",
3
- environment: process.env.NODE_ENV || "development",
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 default config;
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";