@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.
Files changed (48) hide show
  1. package/package.json +4 -2
  2. package/src/index.js +52 -9
  3. package/templates/base-project/.github/dependabot.yml +12 -0
  4. package/templates/base-project/.github/workflows/ci.yml +97 -0
  5. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  6. package/templates/base-project/.github/workflows/release.yml +38 -0
  7. package/templates/base-project/apps/web/biome.json +7 -0
  8. package/templates/base-project/apps/web/jsconfig.json +5 -5
  9. package/templates/base-project/apps/web/next.config.js +86 -1
  10. package/templates/base-project/apps/web/package.json +4 -4
  11. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +6 -7
  12. package/templates/base-project/apps/web/src/app/layout.js +3 -4
  13. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  14. package/templates/base-project/apps/web/src/app/robots.js +21 -0
  15. package/templates/base-project/apps/web/src/app/sitemap.js +20 -0
  16. package/templates/base-project/apps/web/src/lib/seo/indexing.js +23 -0
  17. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +33 -0
  18. package/templates/base-project/apps/web/src/proxy.js +1 -2
  19. package/templates/base-project/apps/worker/package.json +4 -4
  20. package/templates/base-project/apps/worker/src/index.js +26 -12
  21. package/templates/base-project/apps/worker/src/index.test.js +296 -15
  22. package/templates/base-project/biome.json +44 -0
  23. package/templates/base-project/docker-compose.yml +7 -4
  24. package/templates/base-project/package.json +1 -1
  25. package/templates/base-project/packages/db/package.json +1 -1
  26. package/templates/base-project/packages/db/prisma/schema.prisma +1 -17
  27. package/templates/base-project/packages/db/prisma.config.ts +3 -2
  28. package/templates/base-project/packages/db/scripts/seed.js +117 -30
  29. package/templates/base-project/packages/db/src/queries.js +52 -118
  30. package/templates/base-project/packages/db/src/queries.test.js +0 -29
  31. package/templates/base-project/packages/db/src/schemas.js +0 -12
  32. package/templates/base-project/pnpm-workspace.yaml +4 -0
  33. package/templates/base-project/turbo.json +5 -3
  34. package/templates/config/package.json +2 -0
  35. package/templates/config/src/environment.js +270 -0
  36. package/templates/config/src/index.js +10 -18
  37. package/templates/config/src/load-config.js +135 -0
  38. package/templates/config/src/validate-env.js +60 -2
  39. package/templates/jobs/package.json +2 -2
  40. package/templates/jobs/src/definitions.test.js +34 -0
  41. package/templates/jobs/src/index.js +1 -1
  42. package/templates/ui/package.json +4 -4
  43. package/templates/ui/src/button.test.js +23 -0
  44. package/templates/ui/src/index.js +1 -3
  45. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +0 -65
  46. package/templates/base-project/apps/web/src/app/api/posts/route.js +0 -95
  47. package/templates/ui/src/card.js +0 -14
  48. 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: "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";
@@ -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 server)
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);
@@ -2,7 +2,7 @@
2
2
  "name": "@myquark/jobs",
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
- "exports": {
6
- ".": "./src/index.js"
5
+ "dependencies": {
6
+ "bullmq": "^5.67.3"
7
7
  }
8
8
  }
@@ -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 { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
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": "^18.0.0"
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,3 +1 @@
1
- export { Button } from "./button.js";
2
- export { Card } from "./card.js";
3
- export { Input } from "./input.js";
1
+ export * from "./button.js";
@@ -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
- });