@usequark/quark-create-app 1.23.11 → 1.23.13
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/LICENSE +21 -0
- package/README.md +26 -26
- package/package.json +67 -55
- package/src/index.js +17 -17
- package/src/utils.js +1 -1
- package/templates/base-project/.cursor/rules/quark.mdc +17 -17
- package/templates/base-project/.github/copilot-instructions.md +5 -5
- package/templates/base-project/.github/skills/project-context/SKILL.md +6 -6
- package/templates/base-project/CLAUDE.md +12 -12
- package/templates/base-project/README.md +3 -3
- package/templates/base-project/apps/web/next.config.js +5 -5
- package/templates/base-project/apps/web/package.json +5 -5
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +3 -3
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +1 -1
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/error-handler.js +1 -1
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/files/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/health/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +2 -2
- package/templates/base-project/apps/web/src/app/api/users/route.js +2 -2
- package/templates/base-project/apps/web/src/app/auth/error/page.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +1 -1
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +1 -1
- package/templates/base-project/apps/web/src/app/layout.js +1 -1
- package/templates/base-project/apps/web/src/app/manifest.js +1 -1
- package/templates/base-project/apps/web/src/app/page.js +19 -19
- package/templates/base-project/apps/web/src/app/robots.js +1 -1
- package/templates/base-project/apps/web/src/app/sitemap.js +1 -1
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth-signup.js +1 -1
- package/templates/base-project/apps/web/src/lib/auth.js +2 -2
- package/templates/base-project/apps/web/src/lib/jwt.js +1 -1
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +1 -1
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +1 -1
- package/templates/base-project/apps/web/src/preflight.js +3 -3
- package/templates/base-project/apps/web/src/proxy.js +1 -1
- package/templates/base-project/docs/START_HERE.md +2 -2
- package/templates/base-project/packages/db/package.json +2 -2
- package/templates/base-project/packages/db/src/client.js +3 -3
- package/templates/base-project/packages/db/src/umami.js +1 -1
- package/templates/base-project/scripts/check-loading.mjs +1 -1
- package/templates/base-project/scripts/doctor.js +1 -1
- package/templates/base-project/skills/add-endpoint/SKILL.md +1 -1
- package/templates/base-project/skills/ecommerce/SKILL.md +2 -2
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +3 -3
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +3 -3
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +2 -2
- package/templates/base-project/skills/i18n/SKILL.md +6 -6
- package/templates/base-project/skills/payment/SKILL.md +8 -8
- package/templates/config/package.json +1 -1
- package/templates/config/src/app-url.js +1 -1
- package/templates/config/src/environment.js +1 -1
- package/templates/config/src/load-config.js +2 -2
- package/templates/config/src/redis.js +1 -1
- package/templates/config/src/validate-env.js +2 -2
- package/templates/jobs/README.md +2 -2
- package/templates/mobile/lib/errors.ts +1 -1
- package/templates/ui/src/theme-constants.js +1 -1
- package/templates/worker/README.md +2 -2
- package/templates/worker/package.json +4 -4
- package/templates/worker/src/handlers/email-job-validation.js +1 -1
- package/templates/worker/src/handlers/email.js +5 -5
- package/templates/worker/src/handlers/files.js +4 -4
- package/templates/worker/src/handlers/index.js +1 -1
- package/templates/worker/src/handlers/push-notification.js +3 -3
- package/templates/worker/src/index.js +6 -6
- package/templates/worker/src/lib/railway.js +2 -2
- package/templates/worker/src/lib/tokens.js +1 -1
- package/src/dependabot-config.test.js +0 -128
- package/src/deploy/adapters/railway.test.js +0 -318
- package/src/deploy/deploy.integration.test.js +0 -183
- package/src/deploy/deploy.test.js +0 -105
- package/src/deploy/discovery.test.js +0 -141
- package/src/mobile-template.test.js +0 -232
- package/src/prepare-hooks.test.js +0 -535
- package/src/scaffold-guards.test.js +0 -140
- package/src/scaffold-output.test.js +0 -228
- package/src/template-scripts.test.js +0 -160
- package/src/utils.test.js +0 -95
- package/src/worker-dockerfile.test.js +0 -85
|
@@ -17,7 +17,7 @@ let failed = false;
|
|
|
17
17
|
function isDbBacked(pagePath) {
|
|
18
18
|
const content = readFileSync(pagePath, "utf-8");
|
|
19
19
|
// Matches scoped db packages in scaffolds (@scope/db) and the monorepo
|
|
20
|
-
// package name (@
|
|
20
|
+
// package name (@usequark/quark-db).
|
|
21
21
|
return /@[^/"'\s]+\/(?:quark-)?db["']/.test(content);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -186,7 +186,7 @@ if (forgotPwContent && forgotPwContent.includes("quark-auth-layout")) {
|
|
|
186
186
|
"imports",
|
|
187
187
|
`Source files import "${importPattern}" but "${pkg}" is not installed`,
|
|
188
188
|
badFiles.map((f) => ` ${path.relative(ROOT, f)}`).join("\n"),
|
|
189
|
-
`Remove these imports or run: npx @
|
|
189
|
+
`Remove these imports or run: npx @usequark/quark-create-app add ${pkg}`,
|
|
190
190
|
);
|
|
191
191
|
}
|
|
192
192
|
}
|
|
@@ -11,7 +11,7 @@ Add a REST CRUD endpoint for an existing model: list/create on the collection ro
|
|
|
11
11
|
|
|
12
12
|
- **API routes** live in `apps/web/src/app/api/<resource>/`.
|
|
13
13
|
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly.
|
|
14
|
-
- **Auth** via `requireRole` from `@/lib/auth-middleware`; **validation** via Zod `validateBody`; **CSRF** via `withCsrfProtection` from `@
|
|
14
|
+
- **Auth** via `requireRole` from `@/lib/auth-middleware`; **validation** via Zod `validateBody`; **CSRF** via `withCsrfProtection` from `@usequark/quark-core`.
|
|
15
15
|
|
|
16
16
|
## Workflow
|
|
17
17
|
|
|
@@ -22,9 +22,9 @@ An ecommerce system manages a product catalog, shopping cart, checkout flow, ord
|
|
|
22
22
|
|
|
23
23
|
- **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
|
|
24
24
|
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
|
|
25
|
-
- **Server Actions** (preferred for mutations) use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@
|
|
25
|
+
- **Server Actions** (preferred for mutations) use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@usequark/quark-core/errors`.
|
|
26
26
|
- **Admin views** live under `apps/web/src/app/admin/` (see the admin-dashboard skill for product/order management).
|
|
27
|
-
- **Auth** via `getCurrentSession` from `@
|
|
27
|
+
- **Auth** via `getCurrentSession` from `@usequark/quark-core`.
|
|
28
28
|
- **Payment** integration follows the payment skill for Stripe Checkout or similar processors.
|
|
29
29
|
|
|
30
30
|
## Workflow
|
|
@@ -19,8 +19,8 @@ The cart is a temporary collection of items a user intends to purchase. Key conc
|
|
|
19
19
|
|
|
20
20
|
- **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
|
|
21
21
|
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
|
|
22
|
-
- **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@
|
|
23
|
-
- **Auth** via `getCurrentSession` from `@
|
|
22
|
+
- **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@usequark/quark-core/errors`.
|
|
23
|
+
- **Auth** via `getCurrentSession` from `@usequark/quark-core`.
|
|
24
24
|
|
|
25
25
|
## Models
|
|
26
26
|
|
|
@@ -88,7 +88,7 @@ Use a Prisma transaction to prevent race conditions. Check stock availability be
|
|
|
88
88
|
|
|
89
89
|
```js
|
|
90
90
|
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
91
|
-
import { AppError } from "@
|
|
91
|
+
import { AppError } from "@usequark/quark-core/errors";
|
|
92
92
|
|
|
93
93
|
export async function addToCart(cartId, variantId, quantity = 1) {
|
|
94
94
|
return prisma.$transaction(async (tx) => {
|
|
@@ -20,7 +20,7 @@ The catalog is the foundation of an ecommerce system. It defines what you sell,
|
|
|
20
20
|
- **Models** live in `packages/db/prisma/schema.prisma`. Every model needs `id`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`. After changes: `pnpm db:generate && pnpm db:migrate`.
|
|
21
21
|
- **Data access** goes through query helpers in `packages/db/src/queries.js` — never call `prisma.*` directly in pages/actions.
|
|
22
22
|
- **API routes** live in `apps/web/src/app/api/<resource>/`. Guard with `requireRole`, validate with Zod via `validateBody`, wrap mutations in `withCsrfProtection`. Follow `apps/web/src/app/api/users/route.js` as the reference shape.
|
|
23
|
-
- **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@
|
|
23
|
+
- **Server Actions** use `"use server"`, Zod validation, and `AppError`/`ValidationError` from `@usequark/quark-core/errors`.
|
|
24
24
|
- **Admin views** live under `apps/web/src/app/admin/products/` and `apps/web/src/app/admin/categories/`.
|
|
25
25
|
|
|
26
26
|
## Models
|
|
@@ -127,8 +127,8 @@ export async function getCategoryTree() {
|
|
|
127
127
|
"use server";
|
|
128
128
|
import { z } from "zod";
|
|
129
129
|
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
130
|
-
import { ValidationError, AppError } from "@
|
|
131
|
-
import { createLogger } from "@
|
|
130
|
+
import { ValidationError, AppError } from "@usequark/quark-core/errors";
|
|
131
|
+
import { createLogger } from "@usequark/quark-core";
|
|
132
132
|
|
|
133
133
|
const log = createLogger("action:product");
|
|
134
134
|
|
|
@@ -68,8 +68,8 @@ model OrderItem {
|
|
|
68
68
|
"use server";
|
|
69
69
|
import { z } from "zod";
|
|
70
70
|
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
71
|
-
import { AppError, ValidationError } from "@
|
|
72
|
-
import { createLogger } from "@
|
|
71
|
+
import { AppError, ValidationError } from "@usequark/quark-core/errors";
|
|
72
|
+
import { createLogger } from "@usequark/quark-core";
|
|
73
73
|
|
|
74
74
|
const log = createLogger("action:checkout");
|
|
75
75
|
|
|
@@ -34,8 +34,8 @@ A localized app must handle locale detection, routing, content translation, and
|
|
|
34
34
|
- **Request config:** `getRequestConfig` from `next-intl/server` resolves the locale per request and loads message files.
|
|
35
35
|
- **Models:** CMS models get a `locale String` field. Queries filter by locale with fallback chain.
|
|
36
36
|
- **Validation** — Zod for all Server Actions and API routes. Use a locale enum for localized content submissions.
|
|
37
|
-
- **Auth** via `getCurrentSession` from `@
|
|
38
|
-
- **Locale utilities** from `@
|
|
37
|
+
- **Auth** via `getCurrentSession` from `@usequark/quark-core`.
|
|
38
|
+
- **Locale utilities** from `@usequark/quark-core/locale`: `getDefaultLocale()`, `getSupportedLocales()`, `isLocaleSupported()`.
|
|
39
39
|
|
|
40
40
|
## Workflow
|
|
41
41
|
|
|
@@ -52,7 +52,7 @@ A localized app must handle locale detection, routing, content translation, and
|
|
|
52
52
|
```js
|
|
53
53
|
// src/i18n/routing.js
|
|
54
54
|
import { defineRouting } from "next-intl/routing";
|
|
55
|
-
import { getDefaultLocale, getSupportedLocales } from "@
|
|
55
|
+
import { getDefaultLocale, getSupportedLocales } from "@usequark/quark-core/locale";
|
|
56
56
|
|
|
57
57
|
const supportedLocales = getSupportedLocales();
|
|
58
58
|
|
|
@@ -104,7 +104,7 @@ export const config = {
|
|
|
104
104
|
"use client";
|
|
105
105
|
import { useLocale, useTranslations } from "next-intl";
|
|
106
106
|
import { useRouter, usePathname } from "next/navigation";
|
|
107
|
-
import { getSupportedLocales } from "@
|
|
107
|
+
import { getSupportedLocales } from "@usequark/quark-core/locale";
|
|
108
108
|
|
|
109
109
|
export function LocaleSwitcher() {
|
|
110
110
|
const locale = useLocale();
|
|
@@ -152,7 +152,7 @@ model Page {
|
|
|
152
152
|
## Example: locale-aware query
|
|
153
153
|
|
|
154
154
|
```js
|
|
155
|
-
import { getDefaultLocale, isLocaleSupported } from "@
|
|
155
|
+
import { getDefaultLocale, isLocaleSupported } from "@usequark/quark-core/locale";
|
|
156
156
|
|
|
157
157
|
export async function getPageBySlug(slug, locale) {
|
|
158
158
|
const validLocale = isLocaleSupported(locale) ? locale : getDefaultLocale();
|
|
@@ -185,7 +185,7 @@ export async function generateMetadata({ params }) {
|
|
|
185
185
|
|
|
186
186
|
```js
|
|
187
187
|
import { z } from "zod";
|
|
188
|
-
import { isLocaleSupported } from "@
|
|
188
|
+
import { isLocaleSupported } from "@usequark/quark-core/locale";
|
|
189
189
|
|
|
190
190
|
export const localizedPageSchema = z.object({
|
|
191
191
|
title: z.string().min(1).max(255),
|
|
@@ -24,11 +24,11 @@ Key concepts:
|
|
|
24
24
|
|
|
25
25
|
## Framework context
|
|
26
26
|
|
|
27
|
-
- **Stripe client** — `createStripeClient()` from `@
|
|
28
|
-
- **Webhook route** — `apps/web/src/app/api/stripe/webhook/route.js`. Use `getStripeWebhookEvent()` from `@
|
|
27
|
+
- **Stripe client** — `createStripeClient()` from `@usequark/quark-core/stripe`. Reads `STRIPE_SECRET_KEY` from env.
|
|
28
|
+
- **Webhook route** — `apps/web/src/app/api/stripe/webhook/route.js`. Use `getStripeWebhookEvent()` from `@usequark/quark-core/stripe` to verify signatures.
|
|
29
29
|
- **Validation** — Zod for all incoming data. Webhook event data is already validated by Stripe's SDK.
|
|
30
|
-
- **Errors** — `AppError` / `ValidationError` from `@
|
|
31
|
-
- **Logging** — `createLogger("stripe:webhook")` from `@
|
|
30
|
+
- **Errors** — `AppError` / `ValidationError` from `@usequark/quark-core/errors` for app errors. Never throw on webhook failures — return 200 to prevent Stripe retries for unfixable errors.
|
|
31
|
+
- **Logging** — `createLogger("stripe:webhook")` from `@usequark/quark-core`.
|
|
32
32
|
- **Models** — add `stripeCustomerId String?` to User, `stripeSessionId String?` / `stripeSubscriptionId String?` to relevant models. Every model needs `createdAt` / `updatedAt`.
|
|
33
33
|
|
|
34
34
|
## Checkout sessions
|
|
@@ -49,7 +49,7 @@ export const checkoutSchema = z.object({
|
|
|
49
49
|
### One-time payment
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
|
-
import { createStripeClient } from "@
|
|
52
|
+
import { createStripeClient } from "@usequark/quark-core/stripe";
|
|
53
53
|
|
|
54
54
|
const stripe = await createStripeClient();
|
|
55
55
|
const session = await stripe.checkout.sessions.create({
|
|
@@ -82,8 +82,8 @@ const session = await stripe.checkout.sessions.create({
|
|
|
82
82
|
```js
|
|
83
83
|
// apps/web/src/app/api/stripe/webhook/route.js
|
|
84
84
|
import { NextResponse } from "next/server";
|
|
85
|
-
import { getStripeWebhookEvent } from "@
|
|
86
|
-
import { createLogger } from "@
|
|
85
|
+
import { getStripeWebhookEvent } from "@usequark/quark-core/stripe";
|
|
86
|
+
import { createLogger } from "@usequark/quark-core";
|
|
87
87
|
import { prisma } from "@__QUARK_SCOPE__/db";
|
|
88
88
|
|
|
89
89
|
const log = createLogger("stripe:webhook");
|
|
@@ -180,7 +180,7 @@ model WebhookEvent {
|
|
|
180
180
|
### Create or retrieve Stripe customer
|
|
181
181
|
|
|
182
182
|
```js
|
|
183
|
-
import { createStripeClient } from "@
|
|
183
|
+
import { createStripeClient } from "@usequark/quark-core/stripe";
|
|
184
184
|
|
|
185
185
|
export async function getOrCreateStripeCustomer(user) {
|
|
186
186
|
if (user.stripeCustomerId) return user.stripeCustomerId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @usequark/quark-config - Environment Configuration
|
|
3
3
|
* Provides environment-specific defaults for dev, test, staging, and production.
|
|
4
4
|
* Each environment defines sensible defaults that can be overridden via env vars.
|
|
5
5
|
*/
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @usequark/quark-config - Configuration Loader
|
|
3
3
|
* Centralised configuration management that combines environment validation,
|
|
4
4
|
* environment-specific defaults, and user overrides into a single config object.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* import { loadConfig } from "@
|
|
7
|
+
* import { loadConfig } from "@usequark/quark-config";
|
|
8
8
|
* const config = loadConfig(); // validates env + returns typed config
|
|
9
9
|
* const config = loadConfig({ cache: { defaultTtl: 120 } }); // with overrides
|
|
10
10
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createLogger } from "@
|
|
2
|
-
import { ValidationError } from "@
|
|
1
|
+
import { createLogger } from "@usequark/quark-core";
|
|
2
|
+
import { ValidationError } from "@usequark/quark-core/errors";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { syncNextAuthUrl } from "./app-url.js";
|
|
5
5
|
|
package/templates/jobs/README.md
CHANGED
|
@@ -41,11 +41,11 @@ Treat them as examples, not a required domain model.
|
|
|
41
41
|
1. Add a queue or job name in `src/definitions.js`
|
|
42
42
|
2. Create a handler in `apps/worker/src/handlers/`
|
|
43
43
|
3. Register that handler in `apps/worker/src/handlers/index.js`
|
|
44
|
-
4. Dispatch the job from the web app using `createQueue()` and `addJob()` from `@
|
|
44
|
+
4. Dispatch the job from the web app using `createQueue()` and `addJob()` from `@usequark/quark-core`
|
|
45
45
|
|
|
46
46
|
## Relationship to Quark Core
|
|
47
47
|
|
|
48
|
-
`@
|
|
48
|
+
`@usequark/quark-core` provides the queue infrastructure.
|
|
49
49
|
|
|
50
50
|
`@yourscope/jobs` defines **your** queue names and job names.
|
|
51
51
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Mobile equivalent of @
|
|
2
|
+
* Mobile equivalent of @usequark/quark-core's AppError.
|
|
3
3
|
*
|
|
4
4
|
* React Native cannot import Node.js packages, so this module defines a
|
|
5
5
|
* client-side error class whose shape mirrors AppError.toJSON() — same
|
|
@@ -16,7 +16,7 @@ pnpm dev # Starts worker with file watching
|
|
|
16
16
|
2. Create a handler in `apps/worker/src/handlers/<job-name>.js`:
|
|
17
17
|
|
|
18
18
|
```javascript
|
|
19
|
-
import { createLogger } from "@
|
|
19
|
+
import { createLogger } from "@usequark/quark-core";
|
|
20
20
|
|
|
21
21
|
const log = createLogger("job:my-task");
|
|
22
22
|
|
|
@@ -39,7 +39,7 @@ const jobHandlers = {
|
|
|
39
39
|
## Dispatching Jobs
|
|
40
40
|
|
|
41
41
|
```javascript
|
|
42
|
-
import { createQueue, addJob } from "@
|
|
42
|
+
import { createQueue, addJob } from "@usequark/quark-core";
|
|
43
43
|
import { JOB_NAMES } from "@yourscope/jobs";
|
|
44
44
|
|
|
45
45
|
const queue = createQueue("default");
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"license": "MIT",
|
|
23
23
|
"packageManager": "pnpm@10.12.1",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@
|
|
26
|
-
"@
|
|
27
|
-
"@
|
|
28
|
-
"@
|
|
25
|
+
"@usequark/quark-config": "workspace:*",
|
|
26
|
+
"@usequark/quark-core": "^2.0.0",
|
|
27
|
+
"@usequark/quark-db": "workspace:*",
|
|
28
|
+
"@usequark/quark-jobs": "workspace:*",
|
|
29
29
|
"bullmq": "^6.3.8",
|
|
30
30
|
"jose": "^6.2.12"
|
|
31
31
|
},
|
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
createEmailService,
|
|
8
8
|
passwordResetEmail,
|
|
9
9
|
welcomeEmail,
|
|
10
|
-
} from "@
|
|
11
|
-
import { prisma } from "@
|
|
12
|
-
import { JOB_NAMES } from "@
|
|
10
|
+
} from "@usequark/quark-core";
|
|
11
|
+
import { prisma } from "@usequark/quark-db";
|
|
12
|
+
import { JOB_NAMES } from "@usequark/quark-jobs";
|
|
13
13
|
import {
|
|
14
14
|
requireResetPasswordEmailData,
|
|
15
15
|
requireUserEmailRecord,
|
|
@@ -21,7 +21,7 @@ const emailService = createEmailService();
|
|
|
21
21
|
/**
|
|
22
22
|
* Job handler for SEND_WELCOME_EMAIL
|
|
23
23
|
* @param {import("bullmq").Job} bullJob
|
|
24
|
-
* @param {import("@
|
|
24
|
+
* @param {import("@usequark/quark-core").Logger} logger
|
|
25
25
|
*/
|
|
26
26
|
export async function handleSendWelcomeEmail(bullJob, logger) {
|
|
27
27
|
const userId = requireWelcomeEmailUserId(bullJob.data);
|
|
@@ -60,7 +60,7 @@ export async function handleSendWelcomeEmail(bullJob, logger) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Job handler for SEND_RESET_PASSWORD_EMAIL
|
|
62
62
|
* @param {import("bullmq").Job} bullJob
|
|
63
|
-
* @param {import("@
|
|
63
|
+
* @param {import("@usequark/quark-core").Logger} logger
|
|
64
64
|
*/
|
|
65
65
|
export async function handleSendResetPasswordEmail(bullJob, logger) {
|
|
66
66
|
const { userId, resetUrl } = requireResetPasswordEmailData(bullJob.data);
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
* Processes file-related background jobs (cleanup, etc.)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createStorage } from "@
|
|
7
|
-
import { file } from "@
|
|
8
|
-
import { JOB_NAMES } from "@
|
|
6
|
+
import { createStorage } from "@usequark/quark-core";
|
|
7
|
+
import { file } from "@usequark/quark-db";
|
|
8
|
+
import { JOB_NAMES } from "@usequark/quark-jobs";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Job handler for CLEANUP_ORPHANED_FILES
|
|
12
12
|
* Deletes files with no owner that are older than a retention period.
|
|
13
13
|
*
|
|
14
14
|
* @param {import("bullmq").Job} bullJob
|
|
15
|
-
* @param {import("@
|
|
15
|
+
* @param {import("@usequark/quark-core").Logger} logger
|
|
16
16
|
*/
|
|
17
17
|
export async function handleCleanupOrphanedFiles(bullJob, logger) {
|
|
18
18
|
const retentionHours = bullJob.data?.retentionHours || 24;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* No Firebase SDK — uses HTTP APIs with service account credentials from env.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { AppError, ValidationError } from "@
|
|
8
|
-
import { prisma } from "@
|
|
7
|
+
import { AppError, ValidationError } from "@usequark/quark-core/errors";
|
|
8
|
+
import { prisma } from "@usequark/quark-db";
|
|
9
9
|
import { importPKCS8, SignJWT } from "jose";
|
|
10
10
|
|
|
11
11
|
const APNS_KEY = process.env.APNS_KEY;
|
|
@@ -19,7 +19,7 @@ const GCP_SERVICE_ACCOUNT = process.env.GCP_SERVICE_ACCOUNT;
|
|
|
19
19
|
/**
|
|
20
20
|
* Job handler for SEND_PUSH_NOTIFICATION
|
|
21
21
|
* @param {import("bullmq").Job} bullJob
|
|
22
|
-
* @param {import("@
|
|
22
|
+
* @param {import("@usequark/quark-core").Logger} logger
|
|
23
23
|
*/
|
|
24
24
|
export async function handleSendPushNotification(bullJob, logger) {
|
|
25
25
|
const { userId, title, body: notificationBody, data } = bullJob.data;
|
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
createWorker,
|
|
12
12
|
getRedisUrl,
|
|
13
13
|
updateQueueDepths,
|
|
14
|
-
} from "@
|
|
15
|
-
import { AppError } from "@
|
|
16
|
-
import { job, prisma } from "@
|
|
17
|
-
import { JOB_NAMES, JOB_QUEUES } from "@
|
|
14
|
+
} from "@usequark/quark-core";
|
|
15
|
+
import { AppError } from "@usequark/quark-core/errors";
|
|
16
|
+
import { job, prisma } from "@usequark/quark-db";
|
|
17
|
+
import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
|
|
18
18
|
import { jobHandlers } from "./handlers/index.js";
|
|
19
19
|
|
|
20
20
|
const logger = createLogger("worker");
|
|
@@ -242,7 +242,7 @@ export async function waitForDatabase(
|
|
|
242
242
|
*/
|
|
243
243
|
async function preflight() {
|
|
244
244
|
// Load and validate environment variables
|
|
245
|
-
const { loadEnv } = await import("@
|
|
245
|
+
const { loadEnv } = await import("@usequark/quark-config");
|
|
246
246
|
loadEnv("worker");
|
|
247
247
|
|
|
248
248
|
logger.info("Running preflight health checks");
|
|
@@ -362,7 +362,7 @@ async function createQueueWorker(queueName) {
|
|
|
362
362
|
*/
|
|
363
363
|
async function startWorker() {
|
|
364
364
|
// Load and validate environment variables
|
|
365
|
-
const { loadEnv } = await import("@
|
|
365
|
+
const { loadEnv } = await import("@usequark/quark-config");
|
|
366
366
|
loadEnv("worker");
|
|
367
367
|
|
|
368
368
|
logger.info("Starting Quark Worker Service");
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Fetches estimated usage and deployment data from Railway's GraphQL API.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createLogger } from "@
|
|
7
|
-
import { AppError } from "@
|
|
6
|
+
import { createLogger } from "@usequark/quark-core";
|
|
7
|
+
import { AppError } from "@usequark/quark-core/errors";
|
|
8
8
|
|
|
9
9
|
const log = createLogger("lib:railway");
|
|
10
10
|
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Guard the scaffold template's dependabot config.
|
|
5
|
-
*
|
|
6
|
-
* The template used to group by `dependency-type` alone, which produced one
|
|
7
|
-
* PR bundling patch, minor and major bumps together. That is how several
|
|
8
|
-
* breaking changes reached this repo unnoticed - bullmq 6 removed
|
|
9
|
-
* `queue.client` and shipped two silent production regressions inside a
|
|
10
|
-
* 35-package diff.
|
|
11
|
-
*
|
|
12
|
-
* This checks the shipped template still separates majors, so a scaffolded
|
|
13
|
-
* project does not inherit the problem.
|
|
14
|
-
*
|
|
15
|
-
* Run with: node --test packages/cli/src/dependabot-config.test.js
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import assert from "node:assert/strict";
|
|
19
|
-
import fs from "node:fs";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { describe, it } from "node:test";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
-
|
|
24
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
-
const REPO_ROOT = path.resolve(__dirname, "../../..");
|
|
26
|
-
|
|
27
|
-
const CONFIGS = [
|
|
28
|
-
["monorepo", ".github/dependabot.yml"],
|
|
29
|
-
[
|
|
30
|
-
"scaffold template",
|
|
31
|
-
"packages/cli/templates/base-project/.github/dependabot.yml",
|
|
32
|
-
],
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
function readConfig(relativePath) {
|
|
36
|
-
return fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("dependabot config", () => {
|
|
40
|
-
for (const [label, relativePath] of CONFIGS) {
|
|
41
|
-
it(`${label} isolates major bumps into their own group`, () => {
|
|
42
|
-
const text = readConfig(relativePath);
|
|
43
|
-
|
|
44
|
-
assert.match(
|
|
45
|
-
text,
|
|
46
|
-
/production-majors:[\s\S]*?dependency-type:\s*production[\s\S]*?update-types:\s*\["major"\]/,
|
|
47
|
-
"production majors must be grouped separately so a breaking change is reviewed alone",
|
|
48
|
-
);
|
|
49
|
-
assert.match(
|
|
50
|
-
text,
|
|
51
|
-
/development-majors:[\s\S]*?dependency-type:\s*development[\s\S]*?update-types:\s*\["major"\]/,
|
|
52
|
-
"development majors must be grouped separately too",
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it(`${label} does not lump every production bump into one group`, () => {
|
|
57
|
-
const text = readConfig(relativePath);
|
|
58
|
-
|
|
59
|
-
// A bare `production:` group with no update-types collects majors,
|
|
60
|
-
// minors and patches into a single reviewable-looking PR.
|
|
61
|
-
assert.doesNotMatch(
|
|
62
|
-
text,
|
|
63
|
-
/\n {6}production:\n {8}dependency-type:\s*production\n/,
|
|
64
|
-
"a catch-all production group re-creates the 35-package PR problem",
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it(`${label} keeps a PR limit that can hold the extra groups`, () => {
|
|
69
|
-
const text = readConfig(relativePath);
|
|
70
|
-
const limit = Number(
|
|
71
|
-
(text.match(/open-pull-requests-limit:\s*(\d+)/) || [])[1],
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
assert.ok(
|
|
75
|
-
Number.isInteger(limit) && limit >= 6,
|
|
76
|
-
`open-pull-requests-limit should be at least 6 for the split groups (got ${limit})`,
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* The auto-merge workflow is the other half of the split, and the only part
|
|
84
|
-
* that actually merges anything.
|
|
85
|
-
*
|
|
86
|
-
* Splitting dependabot into per-severity groups changes what a human sees. It
|
|
87
|
-
* does not change what merges unattended - that is gated solely by
|
|
88
|
-
* `update-type == 'version-update:semver-patch'`. A dependency that ships a
|
|
89
|
-
* breaking change in a *patch* passes straight through, and that is exactly how
|
|
90
|
-
* bullmq 6's `queue.client` removal reached this repo. Majors already need a
|
|
91
|
-
* human by virtue of not matching; this test keeps it that way.
|
|
92
|
-
*
|
|
93
|
-
* The monorepo runs this workflow; the template ships an equivalent one, so a
|
|
94
|
-
* scaffolded project inherits the same gate.
|
|
95
|
-
*/
|
|
96
|
-
const AUTO_MERGE_WORKFLOWS = [
|
|
97
|
-
["monorepo", ".github/workflows/dependabot-auto-merge.yml"],
|
|
98
|
-
[
|
|
99
|
-
"scaffold template",
|
|
100
|
-
"packages/cli/templates/base-project/.github/workflows/dependabot-auto-merge.yml",
|
|
101
|
-
],
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
describe("dependabot auto-merge gate", () => {
|
|
105
|
-
for (const [label, relativePath] of AUTO_MERGE_WORKFLOWS) {
|
|
106
|
-
it(`${label} gates auto-merge on the semver update type`, () => {
|
|
107
|
-
const text = readConfig(relativePath);
|
|
108
|
-
|
|
109
|
-
assert.match(
|
|
110
|
-
text,
|
|
111
|
-
/version-update:semver-patch/,
|
|
112
|
-
"the auto-merge gate must key on the semver patch update type, so minors and majors still need a human",
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it(`${label} does not merge every dependabot update`, () => {
|
|
117
|
-
const text = readConfig(relativePath);
|
|
118
|
-
|
|
119
|
-
// An unconditional `gh pr merge` would auto-merge majors unattended,
|
|
120
|
-
// which is precisely how the bullmq break nearly landed.
|
|
121
|
-
assert.doesNotMatch(
|
|
122
|
-
text,
|
|
123
|
-
/if:\s*github\.actor\s*==\s*'dependabot\[bot\]'\n\s*steps:/,
|
|
124
|
-
"auto-merge must be gated on the update type, not just the actor",
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
});
|