@techstream/quark-create-app 1.3.0 → 1.5.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 (33) hide show
  1. package/package.json +34 -34
  2. package/src/index.js +44 -16
  3. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +17 -1
  4. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  5. package/templates/base-project/apps/web/src/app/api/files/route.js +132 -0
  6. package/templates/base-project/apps/web/src/app/api/health/route.js +4 -1
  7. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +5 -1
  8. package/templates/base-project/apps/worker/src/handlers/email.js +100 -0
  9. package/templates/base-project/apps/worker/src/handlers/files.js +59 -0
  10. package/templates/base-project/apps/worker/src/handlers/index.js +18 -0
  11. package/templates/base-project/apps/worker/src/index.js +60 -90
  12. package/templates/base-project/package.json +7 -1
  13. package/templates/base-project/packages/db/scripts/seed.js +1 -1
  14. package/templates/base-project/packages/db/src/client.js +40 -24
  15. package/templates/base-project/turbo.json +26 -1
  16. package/templates/jobs/src/definitions.js +2 -1
  17. package/templates/jobs/src/index.js +0 -1
  18. package/templates/base-project/packages/db/src/generated/prisma/browser.ts +0 -53
  19. package/templates/base-project/packages/db/src/generated/prisma/client.ts +0 -82
  20. package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +0 -649
  21. package/templates/base-project/packages/db/src/generated/prisma/enums.ts +0 -19
  22. package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +0 -305
  23. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +0 -1428
  24. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +0 -217
  25. package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +0 -2098
  26. package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +0 -1805
  27. package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +0 -1737
  28. package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +0 -1762
  29. package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +0 -1738
  30. package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +0 -2298
  31. package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +0 -1450
  32. package/templates/base-project/packages/db/src/generated/prisma/models.ts +0 -18
  33. package/templates/jobs/src/handlers.js +0 -20
@@ -5,129 +5,93 @@
5
5
  */
6
6
 
7
7
  import {
8
- createEmailService,
9
8
  createLogger,
9
+ createQueue,
10
10
  createWorker,
11
11
  } from "@techstream/quark-core";
12
- import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
13
12
  import { prisma } from "@techstream/quark-db";
13
+ import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
14
+ import { jobHandlers } from "./handlers/index.js";
14
15
 
15
16
  const logger = createLogger("worker");
16
17
 
17
18
  // Store workers for graceful shutdown
18
19
  const workers = [];
19
20
 
20
- // Initialize email service
21
- const emailService = createEmailService();
22
-
23
21
  /**
24
- * Escape HTML entities to prevent XSS in email body
22
+ * Generic queue processor dispatches jobs to registered handlers
23
+ * @param {string} queueName
25
24
  */
26
- function escapeHtml(str) {
27
- return str
28
- .replace(/&/g, "&")
29
- .replace(/</g, "&lt;")
30
- .replace(/>/g, "&gt;")
31
- .replace(/"/g, "&quot;")
32
- .replace(/'/g, "&#039;");
33
- }
34
-
35
- /**
36
- * Job handler for SEND_WELCOME_EMAIL
37
- * @param {Job} bullJob - BullMQ job object
38
- */
39
- async function handleSendWelcomeEmail(bullJob) {
40
- const { userId } = bullJob.data;
25
+ function createQueueWorker(queueName) {
26
+ const queueWorker = createWorker(
27
+ queueName,
28
+ async (bullJob) => {
29
+ const handler = jobHandlers[bullJob.name];
30
+
31
+ if (!handler) {
32
+ throw new Error(`No handler registered for job: ${bullJob.name}`);
33
+ }
34
+
35
+ return handler(bullJob, logger);
36
+ },
37
+ {
38
+ concurrency: parseInt(process.env.WORKER_CONCURRENCY || "5", 10),
39
+ },
40
+ );
41
41
 
42
- if (!userId) {
43
- throw new Error("userId is required for SEND_WELCOME_EMAIL job");
44
- }
42
+ workers.push(queueWorker);
45
43
 
46
- logger.info(`Sending welcome email for user ${userId}`, {
47
- job: JOB_NAMES.SEND_WELCOME_EMAIL,
48
- userId,
44
+ queueWorker.on("completed", (job, result) => {
45
+ logger.info(`Job ${job.id} (${job.name}) completed`, { result });
49
46
  });
50
47
 
51
- // Look up the user's email
52
- const userRecord = await prisma.user.findUnique({
53
- where: { id: userId },
54
- select: { email: true, name: true },
48
+ queueWorker.on("failed", (job, error) => {
49
+ logger.error(
50
+ `Job ${job.id} (${job.name}) failed after ${job.attemptsMade} attempts`,
51
+ {
52
+ error: error.message,
53
+ jobName: job.name,
54
+ attemptsMade: job.attemptsMade,
55
+ },
56
+ );
55
57
  });
56
58
 
57
- if (!userRecord?.email) {
58
- throw new Error(`User ${userId} not found or has no email`);
59
- }
60
-
61
- const displayName = userRecord.name || "there";
62
- const safeDisplayName = escapeHtml(displayName);
63
-
64
- await emailService.sendEmail(
65
- userRecord.email,
66
- "Welcome to Quark!",
67
- `<h1>Welcome, ${safeDisplayName}!</h1>
68
- <p>Your account has been created successfully.</p>
69
- <p>You can now sign in and start using the application.</p>`,
70
- `Welcome, ${displayName}!\n\nYour account has been created successfully.\nYou can now sign in and start using the application.`,
59
+ logger.info(
60
+ `Queue "${queueName}" worker started (concurrency: ${queueWorker.opts.concurrency})`,
71
61
  );
72
62
 
73
- return { success: true, userId, email: userRecord.email };
63
+ return queueWorker;
74
64
  }
75
65
 
76
- /**
77
- * Initialize job handlers
78
- * Maps job names to their handler functions
79
- */
80
- const jobHandlers = {
81
- [JOB_NAMES.SEND_WELCOME_EMAIL]: handleSendWelcomeEmail,
82
- };
83
-
84
66
  /**
85
67
  * Start the worker service
86
- * Creates workers for all queues and registers handlers
87
68
  */
88
69
  async function startWorker() {
89
70
  logger.info("Starting Quark Worker Service");
90
71
 
91
72
  try {
92
- // Create worker for email queue
93
- const emailQueueWorker = createWorker(
94
- JOB_QUEUES.EMAIL,
95
- async (bullJob) => {
96
- const handler = jobHandlers[bullJob.name];
97
-
98
- if (!handler) {
99
- throw new Error(`No handler registered for job: ${bullJob.name}`);
100
- }
73
+ // Register a worker for each queue
74
+ for (const queueName of Object.values(JOB_QUEUES)) {
75
+ createQueueWorker(queueName);
76
+ }
101
77
 
102
- return handler(bullJob);
103
- },
78
+ // Schedule repeating cleanup job (runs every 24 hours)
79
+ const filesQueue = createQueue(JOB_QUEUES.FILES);
80
+ await filesQueue.add(
81
+ JOB_NAMES.CLEANUP_ORPHANED_FILES,
82
+ { retentionHours: 24 },
104
83
  {
105
- concurrency: parseInt(process.env.WORKER_CONCURRENCY || "5", 10),
84
+ repeat: { every: 24 * 60 * 60 * 1000 }, // 24h
85
+ jobId: "cleanup-orphaned-files-repeat",
106
86
  },
107
87
  );
108
88
 
109
- workers.push(emailQueueWorker);
110
-
111
- emailQueueWorker.on("completed", (job, result) => {
112
- logger.info(`Job ${job.id} (${job.name}) completed`, { result });
113
- });
114
-
115
- emailQueueWorker.on("failed", (job, error) => {
116
- logger.error(`Job ${job.id} (${job.name}) failed after ${job.attemptsMade} attempts`, {
117
- error: error.message,
118
- jobName: job.name,
119
- attemptsMade: job.attemptsMade,
120
- });
121
- });
122
-
123
- logger.info(
124
- `Email queue worker started (concurrency: ${emailQueueWorker.opts.concurrency})`,
125
- );
126
-
127
- // Ready to process jobs
128
89
  logger.info("Worker service ready");
129
90
  } catch (error) {
130
- logger.error("Failed to start worker service", { error: error.message, stack: error.stack });
91
+ logger.error("Failed to start worker service", {
92
+ error: error.message,
93
+ stack: error.stack,
94
+ });
131
95
  process.exit(1);
132
96
  }
133
97
  }
@@ -139,22 +103,28 @@ async function shutdown() {
139
103
  logger.info("Shutting down worker service");
140
104
 
141
105
  try {
142
- // Close all workers
143
106
  for (const worker of workers) {
144
107
  await worker.close();
145
108
  }
146
109
 
147
- // Disconnect Prisma client
148
110
  await prisma.$disconnect();
149
111
 
150
112
  logger.info("All workers closed");
151
113
  process.exit(0);
152
114
  } catch (error) {
153
- logger.error("Error during shutdown", { error: error.message, stack: error.stack });
115
+ logger.error("Error during shutdown", {
116
+ error: error.message,
117
+ stack: error.stack,
118
+ });
154
119
  process.exit(1);
155
120
  }
156
121
  }
157
122
 
123
+ process.on("SIGTERM", shutdown);
124
+ process.on("SIGINT", shutdown);
125
+
126
+ startWorker();
127
+
158
128
  // Register shutdown handlers
159
129
  process.on("SIGTERM", shutdown);
160
130
  process.on("SIGINT", shutdown);
@@ -18,7 +18,13 @@
18
18
  "license": "ISC",
19
19
  "packageManager": "pnpm@10.12.1",
20
20
  "pnpm": {
21
- "onlyBuiltDependencies": ["@prisma/engines", "esbuild", "msgpackr-extract", "prisma", "sharp"],
21
+ "onlyBuiltDependencies": [
22
+ "@prisma/engines",
23
+ "esbuild",
24
+ "msgpackr-extract",
25
+ "prisma",
26
+ "sharp"
27
+ ],
22
28
  "peerDependencyRules": {
23
29
  "allowedVersions": {
24
30
  "nodemailer": "*"
@@ -1,5 +1,5 @@
1
- import { PrismaClient } from "../src/generated/prisma/client.js";
2
1
  import bcrypt from "bcryptjs";
2
+ import { PrismaClient } from "../src/generated/prisma/client.js";
3
3
 
4
4
  const prisma = new PrismaClient();
5
5
 
@@ -1,19 +1,23 @@
1
1
  import { PrismaPg } from "@prisma/adapter-pg";
2
2
  import { PrismaClient } from "./generated/prisma/client.ts";
3
3
 
4
- // Construct DATABASE_URL from individual env vars (mirrors prisma.config.ts)
5
- // POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB are required no silent fallbacks.
6
- const user = process.env.POSTGRES_USER;
7
- if (!user) throw new Error("POSTGRES_USER environment variable is required");
8
- const password = process.env.POSTGRES_PASSWORD;
9
- if (!password) throw new Error("POSTGRES_PASSWORD environment variable is required");
10
- const host = process.env.POSTGRES_HOST || "localhost";
11
- const port = process.env.POSTGRES_PORT || "5432";
12
- const db = process.env.POSTGRES_DB;
13
- if (!db) throw new Error("POSTGRES_DB environment variable is required");
14
- const connectionString = `postgresql://${user}:${password}@${host}:${port}/${db}?schema=public`;
15
-
16
- const isProduction = process.env.NODE_ENV === "production";
4
+ /**
5
+ * Builds a Postgres connection string from individual env vars (mirrors prisma.config.ts).
6
+ * Throws if any required variable is missing — but only when actually called,
7
+ * so the module can be safely imported at build time (e.g. during `next build`).
8
+ */
9
+ function getConnectionString() {
10
+ const user = process.env.POSTGRES_USER;
11
+ if (!user) throw new Error("POSTGRES_USER environment variable is required");
12
+ const password = process.env.POSTGRES_PASSWORD;
13
+ if (!password)
14
+ throw new Error("POSTGRES_PASSWORD environment variable is required");
15
+ const host = process.env.POSTGRES_HOST || "localhost";
16
+ const port = process.env.POSTGRES_PORT || "5432";
17
+ const db = process.env.POSTGRES_DB;
18
+ if (!db) throw new Error("POSTGRES_DB environment variable is required");
19
+ return `postgresql://${user}:${password}@${host}:${port}/${db}?schema=public`;
20
+ }
17
21
 
18
22
  /**
19
23
  * Returns the connection pool configuration for the `pg` driver.
@@ -26,6 +30,7 @@ const isProduction = process.env.NODE_ENV === "production";
26
30
  * @returns {{ max: number, idleTimeoutMillis: number, connectionTimeoutMillis: number }}
27
31
  */
28
32
  export function getPoolConfig() {
33
+ const isProduction = process.env.NODE_ENV === "production";
29
34
  return {
30
35
  max: Number(process.env.DB_POOL_MAX) || (isProduction ? 10 : 5),
31
36
  idleTimeoutMillis: Number(process.env.DB_POOL_IDLE_TIMEOUT) || 30_000,
@@ -34,19 +39,30 @@ export function getPoolConfig() {
34
39
  };
35
40
  }
36
41
 
37
- // Create a singleton Prisma client with the PostgreSQL driver adapter and pool settings
42
+ // Lazy singleton the client is created on first property access, not at import time.
43
+ // This allows Next.js to import the module at build time without requiring DB env vars.
38
44
  const globalForPrisma = globalThis;
39
- export const prisma =
40
- globalForPrisma.prisma ||
41
- new PrismaClient({
42
- adapter: new PrismaPg({
43
- connectionString,
44
- ...getPoolConfig(),
45
- }),
46
- });
47
45
 
48
- if (!isProduction) {
49
- globalForPrisma.prisma = prisma;
46
+ function getPrismaClient() {
47
+ if (!globalForPrisma.__prisma) {
48
+ globalForPrisma.__prisma = new PrismaClient({
49
+ adapter: new PrismaPg({
50
+ connectionString: getConnectionString(),
51
+ ...getPoolConfig(),
52
+ }),
53
+ });
54
+ }
55
+ return globalForPrisma.__prisma;
50
56
  }
51
57
 
58
+ /**
59
+ * Prisma client singleton. Lazily initialized on first use so the module
60
+ * can be imported safely at build time (no DB env vars needed).
61
+ */
62
+ export const prisma = new Proxy(/** @type {PrismaClient} */ ({}), {
63
+ get(_target, prop) {
64
+ return getPrismaClient()[prop];
65
+ },
66
+ });
67
+
52
68
  export * from "./generated/prisma/client.ts";
@@ -20,7 +20,32 @@
20
20
  "dev": {
21
21
  "cache": false,
22
22
  "persistent": true,
23
- "passThroughEnv": ["PORT", "POSTGRES_HOST", "POSTGRES_PORT", "POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_DB", "REDIS_HOST", "REDIS_PORT", "MAILHOG_HOST", "MAILHOG_SMTP_PORT", "MAILHOG_UI_PORT", "NEXTAUTH_SECRET", "APP_URL", "WORKER_CONCURRENCY"]
23
+ "passThroughEnv": [
24
+ "PORT",
25
+ "POSTGRES_HOST",
26
+ "POSTGRES_PORT",
27
+ "POSTGRES_USER",
28
+ "POSTGRES_PASSWORD",
29
+ "POSTGRES_DB",
30
+ "REDIS_HOST",
31
+ "REDIS_PORT",
32
+ "MAILHOG_HOST",
33
+ "MAILHOG_SMTP_PORT",
34
+ "MAILHOG_UI_PORT",
35
+ "NEXTAUTH_SECRET",
36
+ "APP_URL",
37
+ "WORKER_CONCURRENCY",
38
+ "STORAGE_PROVIDER",
39
+ "STORAGE_LOCAL_DIR",
40
+ "S3_BUCKET",
41
+ "S3_REGION",
42
+ "S3_ENDPOINT",
43
+ "S3_ACCESS_KEY_ID",
44
+ "S3_SECRET_ACCESS_KEY",
45
+ "S3_PUBLIC_URL",
46
+ "UPLOAD_MAX_SIZE",
47
+ "UPLOAD_ALLOWED_TYPES"
48
+ ]
24
49
  }
25
50
  }
26
51
  }
@@ -1,9 +1,10 @@
1
1
  export const JOB_QUEUES = {
2
2
  EMAIL: "email-queue",
3
- NOTIFICATIONS: "notifications-queue",
3
+ FILES: "files-queue",
4
4
  };
5
5
 
6
6
  export const JOB_NAMES = {
7
7
  SEND_WELCOME_EMAIL: "send-welcome-email",
8
8
  SEND_RESET_PASSWORD_EMAIL: "send-reset-password-email",
9
+ CLEANUP_ORPHANED_FILES: "cleanup-orphaned-files",
9
10
  };
@@ -1,2 +1 @@
1
1
  export { JOB_NAMES, JOB_QUEUES } from "./definitions.js";
2
- export { jobHandlers } from "./handlers.js";
@@ -1,53 +0,0 @@
1
- /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
- /* eslint-disable */
3
- // biome-ignore-all lint: generated file
4
- // @ts-nocheck
5
- /*
6
- * This file should be your main import to use Prisma-related types and utilities in a browser.
7
- * Use it to get access to models, enums, and input types.
8
- *
9
- * This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
10
- * See `client.ts` for the standard, server-side entry point.
11
- *
12
- * 🟢 You can import this file directly.
13
- */
14
-
15
- import * as Prisma from "./internal/prismaNamespaceBrowser.ts";
16
- export { Prisma };
17
- export * as $Enums from "./enums.ts";
18
- export * from "./enums.ts";
19
- /**
20
- * Model User
21
- *
22
- */
23
- export type User = Prisma.UserModel;
24
- /**
25
- * Model Post
26
- *
27
- */
28
- export type Post = Prisma.PostModel;
29
- /**
30
- * Model Account
31
- *
32
- */
33
- export type Account = Prisma.AccountModel;
34
- /**
35
- * Model Session
36
- *
37
- */
38
- export type Session = Prisma.SessionModel;
39
- /**
40
- * Model VerificationToken
41
- *
42
- */
43
- export type VerificationToken = Prisma.VerificationTokenModel;
44
- /**
45
- * Model Job
46
- *
47
- */
48
- export type Job = Prisma.JobModel;
49
- /**
50
- * Model AuditLog
51
- *
52
- */
53
- export type AuditLog = Prisma.AuditLogModel;
@@ -1,82 +0,0 @@
1
- /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
- /* eslint-disable */
3
- // biome-ignore-all lint: generated file
4
- // @ts-nocheck
5
- /*
6
- * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
7
- * If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
8
- *
9
- * 🟢 You can import this file directly.
10
- */
11
-
12
- import * as path from "node:path";
13
- import * as process from "node:process";
14
- import { fileURLToPath } from "node:url";
15
-
16
- globalThis["__dirname"] = path.dirname(fileURLToPath(import.meta.url));
17
-
18
- import * as runtime from "@prisma/client/runtime/client";
19
- import * as $Enums from "./enums.ts";
20
- import * as $Class from "./internal/class.ts";
21
- import * as Prisma from "./internal/prismaNamespace.ts";
22
-
23
- export * as $Enums from "./enums.ts";
24
- export * from "./enums.ts";
25
- /**
26
- * ## Prisma Client
27
- *
28
- * Type-safe database client for TypeScript
29
- * @example
30
- * ```
31
- * const prisma = new PrismaClient()
32
- * // Fetch zero or more Users
33
- * const users = await prisma.user.findMany()
34
- * ```
35
- *
36
- * Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
37
- */
38
- export const PrismaClient = $Class.getPrismaClientClass();
39
- export type PrismaClient<
40
- LogOpts extends Prisma.LogLevel = never,
41
- OmitOpts extends
42
- Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"],
43
- ExtArgs extends
44
- runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
45
- > = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>;
46
- export { Prisma };
47
-
48
- /**
49
- * Model User
50
- *
51
- */
52
- export type User = Prisma.UserModel;
53
- /**
54
- * Model Post
55
- *
56
- */
57
- export type Post = Prisma.PostModel;
58
- /**
59
- * Model Account
60
- *
61
- */
62
- export type Account = Prisma.AccountModel;
63
- /**
64
- * Model Session
65
- *
66
- */
67
- export type Session = Prisma.SessionModel;
68
- /**
69
- * Model VerificationToken
70
- *
71
- */
72
- export type VerificationToken = Prisma.VerificationTokenModel;
73
- /**
74
- * Model Job
75
- *
76
- */
77
- export type Job = Prisma.JobModel;
78
- /**
79
- * Model AuditLog
80
- *
81
- */
82
- export type AuditLog = Prisma.AuditLogModel;