@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
|
@@ -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";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-config - Configuration Loader
|
|
3
|
+
* Centralised configuration management that combines environment validation,
|
|
4
|
+
* environment-specific defaults, and user overrides into a single config object.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { loadConfig } from "@techstream/quark-config";
|
|
8
|
+
* const config = loadConfig(); // validates env + returns typed config
|
|
9
|
+
* const config = loadConfig({ cache: { defaultTtl: 120 } }); // with overrides
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getAllowedOrigins, getAppUrl } from "./app-url.js";
|
|
13
|
+
import { getEnvironmentConfig, mergeConfig } from "./environment.js";
|
|
14
|
+
import { validateEnv } from "./validate-env.js";
|
|
15
|
+
|
|
16
|
+
/** @type {import("./environment.js").EnvironmentConfig | null} */
|
|
17
|
+
let cachedConfig = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Loads the full application configuration.
|
|
21
|
+
*
|
|
22
|
+
* 1. Validates all required environment variables (throws on failure).
|
|
23
|
+
* 2. Resolves the current environment (dev/test/staging/production).
|
|
24
|
+
* 3. Merges environment-specific defaults with any user overrides.
|
|
25
|
+
* 4. Enriches with computed values (APP_URL, allowed origins, ports).
|
|
26
|
+
* 5. Caches the result — subsequent calls return the same object.
|
|
27
|
+
*
|
|
28
|
+
* @param {Record<string, unknown>} [overrides] - Optional partial overrides
|
|
29
|
+
* @param {Object} [options]
|
|
30
|
+
* @param {boolean} [options.fresh=false] - Force re-computation (bypass cache)
|
|
31
|
+
* @returns {import("./environment.js").EnvironmentConfig & { appUrl: string, allowedOrigins: string[], validated: Record<string, string> }}
|
|
32
|
+
*/
|
|
33
|
+
export function loadConfig(overrides = {}, options = {}) {
|
|
34
|
+
if (cachedConfig && !options.fresh) {
|
|
35
|
+
return cachedConfig;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Step 1: Validate environment variables
|
|
39
|
+
const validated = validateEnv();
|
|
40
|
+
|
|
41
|
+
// Step 2: Resolve environment and get defaults
|
|
42
|
+
const envConfig = getEnvironmentConfig();
|
|
43
|
+
|
|
44
|
+
// Step 3: Apply env-var driven overrides
|
|
45
|
+
const envOverrides = buildEnvOverrides(envConfig);
|
|
46
|
+
|
|
47
|
+
// Step 4: Merge: env defaults → env-var overrides → user overrides
|
|
48
|
+
let config = mergeConfig(envConfig, envOverrides);
|
|
49
|
+
config = mergeConfig(config, overrides);
|
|
50
|
+
|
|
51
|
+
// Step 5: Attach computed values
|
|
52
|
+
config.appUrl = getAppUrl();
|
|
53
|
+
config.allowedOrigins = getAllowedOrigins();
|
|
54
|
+
config.validated = validated;
|
|
55
|
+
|
|
56
|
+
cachedConfig = config;
|
|
57
|
+
return config;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clears the cached configuration. Useful in tests.
|
|
62
|
+
*/
|
|
63
|
+
export function resetConfig() {
|
|
64
|
+
cachedConfig = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the cached configuration if already loaded, otherwise null.
|
|
69
|
+
* Does NOT trigger validation — use `loadConfig()` for that.
|
|
70
|
+
* @returns {ReturnType<typeof loadConfig> | null}
|
|
71
|
+
*/
|
|
72
|
+
export function getConfig() {
|
|
73
|
+
return cachedConfig;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reads specific environment variables and converts them to config overrides.
|
|
78
|
+
* This allows env vars to take precedence over environment defaults.
|
|
79
|
+
*
|
|
80
|
+
* @param {import("./environment.js").EnvironmentConfig} envConfig
|
|
81
|
+
* @returns {Record<string, unknown>}
|
|
82
|
+
*/
|
|
83
|
+
function buildEnvOverrides(_envConfig) {
|
|
84
|
+
const overrides = {};
|
|
85
|
+
|
|
86
|
+
// Server
|
|
87
|
+
if (process.env.PORT) {
|
|
88
|
+
const port = Number.parseInt(process.env.PORT, 10);
|
|
89
|
+
if (!Number.isNaN(port)) overrides.server = { port };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Rate limiting
|
|
93
|
+
if (process.env.RATE_LIMIT_MAX) {
|
|
94
|
+
const maxRequests = Number.parseInt(process.env.RATE_LIMIT_MAX, 10);
|
|
95
|
+
if (!Number.isNaN(maxRequests)) {
|
|
96
|
+
overrides.rateLimit = { ...overrides.rateLimit, maxRequests };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (process.env.RATE_LIMIT_WINDOW_MS) {
|
|
100
|
+
const windowMs = Number.parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10);
|
|
101
|
+
if (!Number.isNaN(windowMs)) {
|
|
102
|
+
overrides.rateLimit = { ...overrides.rateLimit, windowMs };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Cache
|
|
107
|
+
if (process.env.CACHE_TTL) {
|
|
108
|
+
const defaultTtl = Number.parseInt(process.env.CACHE_TTL, 10);
|
|
109
|
+
if (!Number.isNaN(defaultTtl)) overrides.cache = { defaultTtl };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Logging
|
|
113
|
+
if (process.env.LOG_LEVEL) {
|
|
114
|
+
overrides.logging = { ...overrides.logging, level: process.env.LOG_LEVEL };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Database pool
|
|
118
|
+
if (process.env.DB_POOL_MAX) {
|
|
119
|
+
const poolMax = Number.parseInt(process.env.DB_POOL_MAX, 10);
|
|
120
|
+
if (!Number.isNaN(poolMax)) {
|
|
121
|
+
overrides.db = { ...overrides.db, poolMax };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (process.env.DB_POOL_IDLE_TIMEOUT) {
|
|
125
|
+
const poolIdleTimeout = Number.parseInt(
|
|
126
|
+
process.env.DB_POOL_IDLE_TIMEOUT,
|
|
127
|
+
10,
|
|
128
|
+
);
|
|
129
|
+
if (!Number.isNaN(poolIdleTimeout)) {
|
|
130
|
+
overrides.db = { ...overrides.db, poolIdleTimeout };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return overrides;
|
|
135
|
+
}
|
|
@@ -22,16 +22,32 @@ const envSchema = {
|
|
|
22
22
|
REDIS_HOST: { required: false, description: "Redis host" },
|
|
23
23
|
REDIS_PORT: { required: false, description: "Redis port" },
|
|
24
24
|
|
|
25
|
-
// Mail (local SMTP
|
|
25
|
+
// Mail (local SMTP — Mailpit in dev)
|
|
26
26
|
MAIL_SMTP_URL: { required: false, description: "Mail SMTP URL" },
|
|
27
27
|
MAIL_HOST: { required: false, description: "Mail host" },
|
|
28
28
|
MAIL_SMTP_PORT: { required: false, description: "Mail SMTP port" },
|
|
29
29
|
MAIL_UI_PORT: { required: false, description: "Mail UI port" },
|
|
30
30
|
|
|
31
|
+
// Production SMTP (used when SMTP_HOST is set)
|
|
32
|
+
SMTP_HOST: { required: false, description: "Production SMTP host" },
|
|
33
|
+
SMTP_PORT: { required: false, description: "Production SMTP port" },
|
|
34
|
+
SMTP_SECURE: { required: false, description: "Use TLS for SMTP" },
|
|
35
|
+
SMTP_USER: { required: false, description: "SMTP username" },
|
|
36
|
+
SMTP_PASSWORD: { required: false, description: "SMTP password" },
|
|
37
|
+
|
|
38
|
+
// Email provider
|
|
39
|
+
EMAIL_PROVIDER: {
|
|
40
|
+
required: false,
|
|
41
|
+
description: 'Email provider — "smtp" (default) or "resend"',
|
|
42
|
+
},
|
|
43
|
+
EMAIL_FROM: { required: false, description: "Sender email address" },
|
|
44
|
+
RESEND_API_KEY: { required: false, description: "Resend API key" },
|
|
45
|
+
|
|
31
46
|
// NextAuth
|
|
32
47
|
NEXTAUTH_SECRET: {
|
|
33
48
|
required: true,
|
|
34
49
|
description: "NextAuth secret for JWT signing",
|
|
50
|
+
minLength: 32,
|
|
35
51
|
},
|
|
36
52
|
NEXTAUTH_URL: {
|
|
37
53
|
required: false,
|
|
@@ -46,9 +62,25 @@ const envSchema = {
|
|
|
46
62
|
},
|
|
47
63
|
NODE_ENV: {
|
|
48
64
|
required: false,
|
|
49
|
-
description: "Environment (development, test, production)",
|
|
65
|
+
description: "Environment (development, test, staging, production)",
|
|
50
66
|
},
|
|
51
67
|
PORT: { required: false, description: "Web server port" },
|
|
68
|
+
|
|
69
|
+
// Storage
|
|
70
|
+
STORAGE_PROVIDER: {
|
|
71
|
+
required: false,
|
|
72
|
+
description: 'Storage provider — "local" (default) or "s3"',
|
|
73
|
+
},
|
|
74
|
+
STORAGE_LOCAL_DIR: {
|
|
75
|
+
required: false,
|
|
76
|
+
description: "Local storage directory",
|
|
77
|
+
},
|
|
78
|
+
S3_BUCKET: { required: false, description: "S3 bucket name" },
|
|
79
|
+
S3_REGION: { required: false, description: "S3 region" },
|
|
80
|
+
S3_ENDPOINT: { required: false, description: "S3-compatible endpoint URL" },
|
|
81
|
+
S3_ACCESS_KEY_ID: { required: false, description: "S3 access key" },
|
|
82
|
+
S3_SECRET_ACCESS_KEY: { required: false, description: "S3 secret key" },
|
|
83
|
+
S3_PUBLIC_URL: { required: false, description: "S3 public URL prefix" },
|
|
52
84
|
};
|
|
53
85
|
|
|
54
86
|
/**
|
|
@@ -69,11 +101,37 @@ export function validateEnv() {
|
|
|
69
101
|
);
|
|
70
102
|
}
|
|
71
103
|
|
|
104
|
+
if (value && config.minLength && value.length < config.minLength) {
|
|
105
|
+
errors.push(
|
|
106
|
+
`${key} must be at least ${config.minLength} characters (${config.description})`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
if (value) {
|
|
73
111
|
validated[key] = value;
|
|
74
112
|
}
|
|
75
113
|
}
|
|
76
114
|
|
|
115
|
+
// Conditional: S3 storage requires bucket + credentials
|
|
116
|
+
if (process.env.STORAGE_PROVIDER === "s3") {
|
|
117
|
+
for (const key of [
|
|
118
|
+
"S3_BUCKET",
|
|
119
|
+
"S3_ACCESS_KEY_ID",
|
|
120
|
+
"S3_SECRET_ACCESS_KEY",
|
|
121
|
+
]) {
|
|
122
|
+
if (!process.env[key]) {
|
|
123
|
+
errors.push(
|
|
124
|
+
`Missing ${key} — required when STORAGE_PROVIDER=s3 (${envSchema[key].description})`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Conditional: Resend provider requires API key
|
|
131
|
+
if (process.env.EMAIL_PROVIDER === "resend" && !process.env.RESEND_API_KEY) {
|
|
132
|
+
errors.push("Missing RESEND_API_KEY — required when EMAIL_PROVIDER=resend");
|
|
133
|
+
}
|
|
134
|
+
|
|
77
135
|
if (errors.length > 0) {
|
|
78
136
|
const errorMessage = `Environment Validation Failed:\n${errors.join("\n")}`;
|
|
79
137
|
throw new Error(errorMessage);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
|
|
4
|
+
|
|
5
|
+
test("Job Definitions - defines email queue", () => {
|
|
6
|
+
assert.strictEqual(JOB_QUEUES.EMAIL, "email-queue");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Job Definitions - defines files queue", () => {
|
|
10
|
+
assert.strictEqual(JOB_QUEUES.FILES, "files-queue");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("Job Definitions - defines welcome email job name", () => {
|
|
14
|
+
assert.strictEqual(JOB_NAMES.SEND_WELCOME_EMAIL, "send-welcome-email");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("Job Definitions - defines reset password email job name", () => {
|
|
18
|
+
assert.strictEqual(
|
|
19
|
+
JOB_NAMES.SEND_RESET_PASSWORD_EMAIL,
|
|
20
|
+
"send-reset-password-email",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("Job Definitions - defines cleanup orphaned files job name", () => {
|
|
25
|
+
assert.strictEqual(
|
|
26
|
+
JOB_NAMES.CLEANUP_ORPHANED_FILES,
|
|
27
|
+
"cleanup-orphaned-files",
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("Job Definitions - queues are readonly", () => {
|
|
32
|
+
assert.strictEqual(typeof JOB_QUEUES.EMAIL, "string");
|
|
33
|
+
assert.strictEqual(typeof JOB_QUEUES.FILES, "string");
|
|
34
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from "./definitions.js";
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "@myquark/ui",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"exports": {
|
|
6
|
-
".": "./src/index.js"
|
|
7
|
-
},
|
|
8
5
|
"devDependencies": {
|
|
9
|
-
"react": "^
|
|
6
|
+
"@types/react": "^19.2.13",
|
|
7
|
+
"@types/react-dom": "^19.2.3",
|
|
8
|
+
"react": "19.2.0",
|
|
9
|
+
"react-dom": "19.2.0"
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
|
|
5
|
+
test("Button - component exports correctly", () => {
|
|
6
|
+
assert(typeof Button === "function", "Button should be a function");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("Button - component accepts props", () => {
|
|
10
|
+
// Test that component can be called with props
|
|
11
|
+
const result = Button({ variant: "primary", className: "custom" });
|
|
12
|
+
assert.ok(result, "Component should return an element");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("Button - supports primary variant", () => {
|
|
16
|
+
const result = Button({ variant: "primary" });
|
|
17
|
+
assert.ok(result, "Primary variant should be supported");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Button - supports secondary variant", () => {
|
|
21
|
+
const result = Button({ variant: "secondary" });
|
|
22
|
+
assert.ok(result, "Secondary variant should be supported");
|
|
23
|
+
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
UnauthorizedError,
|
|
3
|
-
validateBody,
|
|
4
|
-
withCsrfProtection,
|
|
5
|
-
} from "@techstream/quark-core";
|
|
6
|
-
import { post, postUpdateSchema } from "@techstream/quark-db";
|
|
7
|
-
import { NextResponse } from "next/server";
|
|
8
|
-
import { requireAuth } from "@/lib/auth-middleware";
|
|
9
|
-
import { handleError } from "../../error-handler";
|
|
10
|
-
|
|
11
|
-
export async function GET(_request, { params }) {
|
|
12
|
-
try {
|
|
13
|
-
const { id } = await params;
|
|
14
|
-
const foundPost = await post.findById(id);
|
|
15
|
-
if (!foundPost) {
|
|
16
|
-
return NextResponse.json({ message: "Post not found" }, { status: 404 });
|
|
17
|
-
}
|
|
18
|
-
return NextResponse.json(foundPost);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
return handleError(error);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const PATCH = withCsrfProtection(async (request, { params }) => {
|
|
25
|
-
try {
|
|
26
|
-
const session = await requireAuth();
|
|
27
|
-
const { id } = await params;
|
|
28
|
-
|
|
29
|
-
const foundPost = await post.findById(id);
|
|
30
|
-
if (!foundPost) {
|
|
31
|
-
return NextResponse.json({ message: "Post not found" }, { status: 404 });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (foundPost.authorId !== session.user.id) {
|
|
35
|
-
throw new UnauthorizedError("You can only edit your own posts");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const data = await validateBody(request, postUpdateSchema);
|
|
39
|
-
const updatedPost = await post.update(id, data);
|
|
40
|
-
return NextResponse.json(updatedPost);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
return handleError(error);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export const DELETE = withCsrfProtection(async (_request, { params }) => {
|
|
47
|
-
try {
|
|
48
|
-
const session = await requireAuth();
|
|
49
|
-
const { id } = await params;
|
|
50
|
-
|
|
51
|
-
const foundPost = await post.findById(id);
|
|
52
|
-
if (!foundPost) {
|
|
53
|
-
return NextResponse.json({ message: "Post not found" }, { status: 404 });
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (foundPost.authorId !== session.user.id) {
|
|
57
|
-
throw new UnauthorizedError("You can only delete your own posts");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
await post.delete(id);
|
|
61
|
-
return NextResponse.json({ success: true });
|
|
62
|
-
} catch (error) {
|
|
63
|
-
return handleError(error);
|
|
64
|
-
}
|
|
65
|
-
});
|