create-raredays-app 0.1.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/README.md +89 -0
- package/bin/index.js +1024 -0
- package/package.json +43 -0
- package/scaffold-fragments/billing/files/app/api/billing/webhook/route.ts +5 -0
- package/scaffold-fragments/billing/module.json +31 -0
- package/scaffold-fragments/billing/runtime/config.ts +8 -0
- package/scaffold-fragments/billing/runtime/exports.ts +6 -0
- package/scaffold-fragments/billing/runtime/imports.ts +6 -0
- package/scaffold-fragments/billing/runtime/ports.ts +3 -0
- package/scaffold-fragments/cms/module.json +29 -0
- package/scaffold-fragments/cms/runtime/config.ts +4 -0
- package/scaffold-fragments/cms/runtime/exports.ts +1 -0
- package/scaffold-fragments/cms/runtime/imports.ts +1 -0
- package/scaffold-fragments/email/module.json +30 -0
- package/scaffold-fragments/email/runtime/config.ts +1 -0
- package/scaffold-fragments/email/runtime/imports.ts +1 -0
- package/scaffold-fragments/email/runtime/ports.ts +1 -0
- package/scaffold-fragments/feature-flags/files/app/lib/flags.ts +48 -0
- package/scaffold-fragments/feature-flags/module.json +66 -0
- package/scaffold-fragments/i18n/files/messages/en-US.json +15 -0
- package/scaffold-fragments/i18n/module.json +9 -0
- package/scaffold-fragments/i18n/runtime/config.ts +7 -0
- package/scaffold-fragments/i18n/runtime/exports.ts +1 -0
- package/scaffold-fragments/i18n/runtime/imports.ts +2 -0
- package/scaffold-fragments/i18n/runtime/ports.ts +1 -0
- package/scaffold-fragments/identity/module.json +30 -0
- package/scaffold-fragments/identity/vendors/better-auth/files/app/api/auth/[...all]/route.ts +15 -0
- package/scaffold-fragments/identity/vendors/better-auth/files/app/lib/auth.ts +20 -0
- package/scaffold-fragments/jobs/module.json +12 -0
- package/scaffold-fragments/jobs/runtime/config.ts +4 -0
- package/scaffold-fragments/jobs/runtime/imports.ts +1 -0
- package/scaffold-fragments/jobs/runtime/ports.ts +1 -0
- package/scaffold-fragments/notifications/module.json +12 -0
- package/scaffold-fragments/notifications/runtime/config.ts +8 -0
- package/scaffold-fragments/notifications/runtime/imports.ts +5 -0
- package/scaffold-fragments/notifications/runtime/ports.ts +1 -0
- package/scaffold-fragments/organizations/module.json +12 -0
- package/scaffold-fragments/telemetry/module.json +43 -0
- package/scaffold-fragments/telemetry/runtime/config.ts +7 -0
- package/scaffold-fragments/telemetry/runtime/imports.ts +7 -0
- package/scaffold-fragments/telemetry/runtime/ports.ts +1 -0
- package/skillex/private/contribute.md +193 -0
- package/skillex/public/consumer.md +157 -0
- package/templates/client/.storybook/main.ts +77 -0
- package/templates/client/.storybook/preview.ts +12 -0
- package/templates/client/CLAUDE.md +50 -0
- package/templates/client/_claude/settings.json +107 -0
- package/templates/client/_github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/templates/client/_github/dependabot.yml +48 -0
- package/templates/client/_github/workflows/ci.yml +104 -0
- package/templates/client/_gitignore +15 -0
- package/templates/client/_npmrc +1 -0
- package/templates/client/app/(app)/layout.tsx +49 -0
- package/templates/client/app/(marketing)/layout.tsx +21 -0
- package/templates/client/app/(marketing)/page.tsx +34 -0
- package/templates/client/app/api/backend/[...path]/route.ts +37 -0
- package/templates/client/app/api/dev/login/route.ts +14 -0
- package/templates/client/app/api/dev/logout/route.ts +18 -0
- package/templates/client/app/globals.css +22 -0
- package/templates/client/app/home-auth-controls.tsx +28 -0
- package/templates/client/app/layout.tsx +38 -0
- package/templates/client/app/lib/backend-client.ts +55 -0
- package/templates/client/app/lib/runtime.test.ts +38 -0
- package/templates/client/app/lib/runtime.ts +54 -0
- package/templates/client/app/protected/page.tsx +31 -0
- package/templates/client/app/theme.override.css +9 -0
- package/templates/client/instrumentation.ts +10 -0
- package/templates/client/mocks/browser.tsx +28 -0
- package/templates/client/mocks/handlers.ts +20 -0
- package/templates/client/mocks/node.ts +7 -0
- package/templates/client/next.config.ts +8 -0
- package/templates/client/oxfmt.config.ts +8 -0
- package/templates/client/oxlint.config.ts +8 -0
- package/templates/client/package.json +61 -0
- package/templates/client/postcss.config.mjs +1 -0
- package/templates/client/public/mockServiceWorker.js +349 -0
- package/templates/client/scripts/build-theme.mjs +18 -0
- package/templates/client/site.spec.ts +58 -0
- package/templates/client/skillex.json +12 -0
- package/templates/client/skills/repo.md +56 -0
- package/templates/client/stylelint.config.mjs +4 -0
- package/templates/client/tsconfig.json +11 -0
- package/templates/client/vitest.config.ts +27 -0
- package/templates/client/vitest.setup.ts +18 -0
- package/templates/full-stack/.storybook/main.ts +77 -0
- package/templates/full-stack/.storybook/preview.ts +12 -0
- package/templates/full-stack/CLAUDE.md +50 -0
- package/templates/full-stack/_claude/settings.json +107 -0
- package/templates/full-stack/_github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/templates/full-stack/_github/dependabot.yml +57 -0
- package/templates/full-stack/_github/workflows/ci.yml +109 -0
- package/templates/full-stack/_github/workflows/db-check.yml +36 -0
- package/templates/full-stack/_github/workflows/db-migrate.yml +39 -0
- package/templates/full-stack/_gitignore +15 -0
- package/templates/full-stack/_npmrc +1 -0
- package/templates/full-stack/app/(app)/layout.tsx +53 -0
- package/templates/full-stack/app/(marketing)/layout.tsx +21 -0
- package/templates/full-stack/app/(marketing)/page.tsx +38 -0
- package/templates/full-stack/app/api/dev/login/route.ts +14 -0
- package/templates/full-stack/app/api/dev/logout/route.ts +18 -0
- package/templates/full-stack/app/api/widgets/route.ts +10 -0
- package/templates/full-stack/app/demo/widgets/page.tsx +86 -0
- package/templates/full-stack/app/globals.css +22 -0
- package/templates/full-stack/app/home-auth-controls.tsx +28 -0
- package/templates/full-stack/app/layout.tsx +38 -0
- package/templates/full-stack/app/lib/runtime.test.ts +38 -0
- package/templates/full-stack/app/lib/runtime.ts +54 -0
- package/templates/full-stack/app/protected/page.tsx +31 -0
- package/templates/full-stack/app/theme.override.css +9 -0
- package/templates/full-stack/db/check.ts +46 -0
- package/templates/full-stack/db/index.ts +109 -0
- package/templates/full-stack/db/migrate.ts +66 -0
- package/templates/full-stack/db/migrations/.gitkeep +0 -0
- package/templates/full-stack/db/schema.ts +20 -0
- package/templates/full-stack/drizzle.config.ts +16 -0
- package/templates/full-stack/instrumentation.ts +10 -0
- package/templates/full-stack/mocks/browser.tsx +28 -0
- package/templates/full-stack/mocks/handlers.ts +34 -0
- package/templates/full-stack/mocks/node.ts +7 -0
- package/templates/full-stack/next.config.ts +8 -0
- package/templates/full-stack/oxfmt.config.ts +8 -0
- package/templates/full-stack/oxlint.config.ts +8 -0
- package/templates/full-stack/package.json +72 -0
- package/templates/full-stack/postcss.config.mjs +1 -0
- package/templates/full-stack/public/mockServiceWorker.js +349 -0
- package/templates/full-stack/scripts/build-theme.mjs +18 -0
- package/templates/full-stack/site.spec.ts +58 -0
- package/templates/full-stack/skillex.json +12 -0
- package/templates/full-stack/skills/repo.md +56 -0
- package/templates/full-stack/stylelint.config.mjs +4 -0
- package/templates/full-stack/tsconfig.json +11 -0
- package/templates/full-stack/vitest.config.ts +42 -0
- package/templates/full-stack/vitest.setup.ts +18 -0
- package/templates/static/.storybook/main.ts +77 -0
- package/templates/static/.storybook/preview.ts +12 -0
- package/templates/static/CLAUDE.md +50 -0
- package/templates/static/_claude/settings.json +107 -0
- package/templates/static/_github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/templates/static/_github/dependabot.yml +43 -0
- package/templates/static/_github/workflows/ci.yml +91 -0
- package/templates/static/_gitignore +15 -0
- package/templates/static/_npmrc +1 -0
- package/templates/static/app/(marketing)/layout.tsx +21 -0
- package/templates/static/app/(marketing)/page.tsx +17 -0
- package/templates/static/app/globals.css +22 -0
- package/templates/static/app/layout.tsx +24 -0
- package/templates/static/app/theme.override.css +9 -0
- package/templates/static/next.config.ts +19 -0
- package/templates/static/oxfmt.config.ts +8 -0
- package/templates/static/oxlint.config.ts +8 -0
- package/templates/static/package.json +53 -0
- package/templates/static/postcss.config.mjs +1 -0
- package/templates/static/scripts/build-theme.mjs +18 -0
- package/templates/static/site.spec.ts +58 -0
- package/templates/static/skillex.json +12 -0
- package/templates/static/skills/repo.md +56 -0
- package/templates/static/stylelint.config.mjs +4 -0
- package/templates/static/tsconfig.json +11 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Demo endpoint paired with `/demo/widgets`. Returns an empty list by
|
|
2
|
+
// default so a fresh scaffold renders the Empty slot — flip MOCKS on
|
|
3
|
+
// to see canned data via MSW. Replace this handler with a real query
|
|
4
|
+
// once you have a backing store.
|
|
5
|
+
|
|
6
|
+
import { NextResponse } from "next/server";
|
|
7
|
+
|
|
8
|
+
export function GET(): NextResponse {
|
|
9
|
+
return NextResponse.json({ widgets: [], total: 0 });
|
|
10
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Demo of `defineAsyncLoad` + MSW.
|
|
4
|
+
//
|
|
5
|
+
// Without mocks (`pnpm dev`): /api/widgets returns an empty list, so the
|
|
6
|
+
// Empty slot renders with a nudge toward `pnpm dev:mocks`. With mocks
|
|
7
|
+
// on, MSW intercepts before the route fires and the Success slot
|
|
8
|
+
// renders canned data.
|
|
9
|
+
//
|
|
10
|
+
// Delete this `/demo` subtree once you have your own pages.
|
|
11
|
+
|
|
12
|
+
import { asyncSource, defineAsyncLoad } from "@raredays/core";
|
|
13
|
+
|
|
14
|
+
interface Widget {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
inventory: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface WidgetsData {
|
|
21
|
+
widgets: Widget[];
|
|
22
|
+
total: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const Widgets = defineAsyncLoad<WidgetsData>({
|
|
26
|
+
source: asyncSource(async () => {
|
|
27
|
+
const r = await fetch("/api/widgets");
|
|
28
|
+
if (!r.ok) throw new Error(`/api/widgets responded ${r.status}`);
|
|
29
|
+
return (await r.json()) as WidgetsData;
|
|
30
|
+
}),
|
|
31
|
+
Loading: () => <p data-testid="widgets-state">Loading widgets…</p>,
|
|
32
|
+
Empty: ({ retry }) => (
|
|
33
|
+
<div data-testid="widgets-state">
|
|
34
|
+
<p>No widgets yet.</p>
|
|
35
|
+
<p className="mt-1 text-sm text-neutral-600">
|
|
36
|
+
Run <code>pnpm dev:mocks</code> to see canned data, or add real ones via the API route at{" "}
|
|
37
|
+
<code>app/api/widgets/route.ts</code>.
|
|
38
|
+
</p>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={retry}
|
|
42
|
+
className="mt-3 rounded border border-neutral-300 px-3 py-1 text-sm"
|
|
43
|
+
>
|
|
44
|
+
Reload
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
Success: ({ widgets, total }) => (
|
|
49
|
+
<ul data-testid="widgets-state" className="mt-2 space-y-1">
|
|
50
|
+
<li className="text-sm text-neutral-600" data-testid="widgets-total">
|
|
51
|
+
{total} widget{total === 1 ? "" : "s"}
|
|
52
|
+
</li>
|
|
53
|
+
{widgets.map((w) => (
|
|
54
|
+
<li key={w.id} data-testid="widget-row">
|
|
55
|
+
<span className="font-medium">{w.name}</span>
|
|
56
|
+
<span className="ml-2 text-sm text-neutral-600">({w.inventory} in stock)</span>
|
|
57
|
+
</li>
|
|
58
|
+
))}
|
|
59
|
+
</ul>
|
|
60
|
+
),
|
|
61
|
+
Failure: ({ error, retry }) => (
|
|
62
|
+
<div data-testid="widgets-state">
|
|
63
|
+
<p className="text-red-700">Failed to load widgets: {error.message}</p>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={retry}
|
|
67
|
+
className="mt-3 rounded border border-neutral-300 px-3 py-1 text-sm"
|
|
68
|
+
>
|
|
69
|
+
Retry
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default function WidgetsDemoPage(): React.ReactElement {
|
|
76
|
+
return (
|
|
77
|
+
<main className="mx-auto max-w-2xl p-8 font-sans">
|
|
78
|
+
<h1 className="text-2xl font-bold">Widgets demo</h1>
|
|
79
|
+
<p className="mt-2 text-sm text-neutral-600">
|
|
80
|
+
<code>defineAsyncLoad</code> against <code>/api/widgets</code>. MSW intercepts in dev when{" "}
|
|
81
|
+
<code>NEXT_PUBLIC_MOCKS=on</code>.
|
|
82
|
+
</p>
|
|
83
|
+
<Widgets />
|
|
84
|
+
</main>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* CSS @import lines must precede all other rules. If you add a remote-font
|
|
2
|
+
* import (e.g. @import url("https://rsms.me/inter/inter.css")), it MUST go
|
|
3
|
+
* BEFORE the lines below — @raredays/style inlines ~6800 lines of CSS,
|
|
4
|
+
* after which any further @import is mid-document and rejected by Lightning
|
|
5
|
+
* CSS / PostCSS / browsers per the CSS spec. */
|
|
6
|
+
@import "@raredays/style";
|
|
7
|
+
@import "./theme.override.css";
|
|
8
|
+
|
|
9
|
+
/* Scan compiled output from @raredays/* packages so Tailwind emits
|
|
10
|
+
* the arbitrary utilities used inside their components (e.g. underline
|
|
11
|
+
* positions, data-attribute variants). The relative `@source` paths in
|
|
12
|
+
* @raredays/style/index.css don't resolve under pnpm's strict
|
|
13
|
+
* node_modules layout — each @raredays/* package gets its own
|
|
14
|
+
* .pnpm/<pkg>/node_modules/<pkg>/ directory, so style's
|
|
15
|
+
* `../components/dist` points nowhere. These directives are relative
|
|
16
|
+
* to THIS file (`app/globals.css`), which is the consumer's cwd —
|
|
17
|
+
* resolves correctly.
|
|
18
|
+
*
|
|
19
|
+
* Add `@source "../node_modules/@raredays/<pkg>/dist"` for any
|
|
20
|
+
* additional @raredays/* package whose component utilities you use. */
|
|
21
|
+
@source "../node_modules/@raredays/components/dist";
|
|
22
|
+
@source "../node_modules/@raredays/core/dist";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function HomeAuthControls(): React.ReactElement {
|
|
4
|
+
return (
|
|
5
|
+
<div className="mt-6 flex flex-wrap gap-2">
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
onClick={async () => {
|
|
9
|
+
await fetch("/api/dev/login", { method: "POST" });
|
|
10
|
+
window.location.href = "/protected";
|
|
11
|
+
}}
|
|
12
|
+
className="rounded-md bg-brand-primary px-4 py-2 text-white hover:bg-brand-secondary"
|
|
13
|
+
>
|
|
14
|
+
Sign in as Alice → /protected
|
|
15
|
+
</button>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
onClick={async () => {
|
|
19
|
+
await fetch("/api/dev/logout", { method: "POST" });
|
|
20
|
+
window.location.reload();
|
|
21
|
+
}}
|
|
22
|
+
className="rounded-md border border-neutral-300 px-4 py-2 hover:bg-neutral-50"
|
|
23
|
+
>
|
|
24
|
+
Sign out
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ThemeProvider, Toaster } from "@raredays/components";
|
|
2
|
+
import { RaredaysProvider } from "@raredays/core";
|
|
3
|
+
import type { Metadata, Viewport } from "next";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { StartMocks } from "../mocks/browser";
|
|
6
|
+
import "./globals.css";
|
|
7
|
+
|
|
8
|
+
export const metadata: Metadata = {
|
|
9
|
+
title: "RareDays App",
|
|
10
|
+
description: "A RareDays Next.js application.",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const viewport: Viewport = {
|
|
14
|
+
width: "device-width",
|
|
15
|
+
initialScale: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="en" suppressHydrationWarning>
|
|
21
|
+
<body>
|
|
22
|
+
<StartMocks>
|
|
23
|
+
<RaredaysProvider>
|
|
24
|
+
<ThemeProvider
|
|
25
|
+
attribute="class"
|
|
26
|
+
defaultTheme="system"
|
|
27
|
+
enableSystem
|
|
28
|
+
disableTransitionOnChange
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
<Toaster position="bottom-center" />
|
|
32
|
+
</ThemeProvider>
|
|
33
|
+
</RaredaysProvider>
|
|
34
|
+
</StartMocks>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
// Runs in the Node environment (not happy-dom) because the WHATWG `Request`
|
|
3
|
+
// in happy-dom strips the `cookie` header on construction (browsers forbid
|
|
4
|
+
// setting it from script). Server-side identity tests need the real Node
|
|
5
|
+
// `Request`, which preserves arbitrary headers.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { app, SEEDED } from "./runtime.js";
|
|
9
|
+
|
|
10
|
+
// Sample test demonstrating how to test app code against the framework's
|
|
11
|
+
// port contracts. The composition root provides the wired ports; tests
|
|
12
|
+
// drive them directly. For deeper testing of your own custom adapters,
|
|
13
|
+
// import the framework's conformance harnesses (e.g.
|
|
14
|
+
// `identityProviderConformance` from `@raredays/identity/testing`) and
|
|
15
|
+
// pass your factory.
|
|
16
|
+
|
|
17
|
+
describe("identity flow (in-memory provider)", () => {
|
|
18
|
+
it("resolves the seeded session when the dev cookie is present", async () => {
|
|
19
|
+
const provider = app.require("identityProvider");
|
|
20
|
+
const req = new Request("https://example.com/", {
|
|
21
|
+
headers: { cookie: `rd-dev-session=${SEEDED.sessionId}` },
|
|
22
|
+
});
|
|
23
|
+
const session = await provider.getSession(req);
|
|
24
|
+
expect(session).not.toBeNull();
|
|
25
|
+
expect(session?.user.id).toBe(SEEDED.user.id);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns null when no session cookie is present", async () => {
|
|
29
|
+
const provider = app.require("identityProvider");
|
|
30
|
+
const session = await provider.getSession(new Request("https://example.com/"));
|
|
31
|
+
expect(session).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("seeded user has content.read but not content.manage", () => {
|
|
35
|
+
expect(SEEDED.user.hasPermission("content.read")).toBe(true);
|
|
36
|
+
expect(SEEDED.user.hasPermission("content.manage")).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Composition root. Every port the framework knows wires here; routes and
|
|
2
|
+
// helpers read ports from the resulting `RaredaysApp` instance.
|
|
3
|
+
//
|
|
4
|
+
// Default uses the in-memory IdentityProvider so `pnpm dev` runs out of the
|
|
5
|
+
// box. To go to production:
|
|
6
|
+
// 1. Install `@raredays/identity-better-auth`, configure Better Auth
|
|
7
|
+
// against your Drizzle database.
|
|
8
|
+
// 2. Replace `createInMemoryProvider(...)` below with
|
|
9
|
+
// `createBetterAuthProvider({ auth })`.
|
|
10
|
+
// 3. Mount Better Auth's handler at `app/api/auth/[...all]/route.ts`.
|
|
11
|
+
|
|
12
|
+
import { createRaredaysApp, definePermissions, Session, User } from "@raredays/core";
|
|
13
|
+
import { createInMemoryProvider } from "@raredays/identity/testing";
|
|
14
|
+
// CRDA:imports
|
|
15
|
+
|
|
16
|
+
// — Authorization (what each role can do) ─────────────────────────────────
|
|
17
|
+
definePermissions({
|
|
18
|
+
rolePermissions: {
|
|
19
|
+
member: ["content.read"],
|
|
20
|
+
admin: ["content.read", "content.manage"],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// — Seeded dev user (replace with Better Auth in production) ──────────────
|
|
25
|
+
const ALICE = User.from({
|
|
26
|
+
id: "u_alice",
|
|
27
|
+
email: "alice@example.com",
|
|
28
|
+
organizationId: "default",
|
|
29
|
+
roles: ["member"],
|
|
30
|
+
emailVerifiedAt: new Date("2026-01-01T00:00:00Z"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ALICE_SESSION = Session.from({
|
|
34
|
+
id: "s_alice",
|
|
35
|
+
userId: ALICE.id,
|
|
36
|
+
user: ALICE.toJSON(),
|
|
37
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const identityProvider = createInMemoryProvider({
|
|
41
|
+
users: [ALICE],
|
|
42
|
+
sessions: [ALICE_SESSION],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const SEEDED = { user: ALICE, sessionId: ALICE_SESSION.id };
|
|
46
|
+
|
|
47
|
+
// CRDA:config
|
|
48
|
+
|
|
49
|
+
export const app = createRaredaysApp({
|
|
50
|
+
identityProvider,
|
|
51
|
+
// CRDA:ports
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// CRDA:exports
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { redirect } from "next/navigation";
|
|
2
|
+
import { UnauthenticatedError } from "@raredays/core";
|
|
3
|
+
import { getServerSession } from "@raredays/identity/server";
|
|
4
|
+
import { app } from "@/lib/runtime";
|
|
5
|
+
|
|
6
|
+
export default async function ProtectedPage(): Promise<React.ReactElement> {
|
|
7
|
+
const session = await getServerSession(app);
|
|
8
|
+
if (!session) {
|
|
9
|
+
redirect("/");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Framework-owned authz: gate on a permission, not a role string.
|
|
14
|
+
session.user.requirePermission("content.read");
|
|
15
|
+
} catch (err) {
|
|
16
|
+
if (err instanceof UnauthenticatedError) redirect("/");
|
|
17
|
+
throw err;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<main data-testid="protected" className="p-8 font-sans">
|
|
22
|
+
<h1 className="text-2xl font-bold">Protected</h1>
|
|
23
|
+
<p className="mt-3" data-testid="signed-in-as">
|
|
24
|
+
Signed in as {session.user.email}
|
|
25
|
+
</p>
|
|
26
|
+
<p className="mt-1 text-sm text-neutral-600" data-testid="user-id">
|
|
27
|
+
User id: {session.user.id}
|
|
28
|
+
</p>
|
|
29
|
+
</main>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate that pending migrations apply cleanly without committing changes.
|
|
3
|
+
* Invoked by `pnpm db:check` — designed for CI pull-request gates.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: redirect Drizzle's bookkeeping table to a scratch schema, run
|
|
6
|
+
* the migrations against it, then `DROP SCHEMA ... CASCADE`. The check
|
|
7
|
+
* verifies the migration SQL parses + the bookkeeping plumbing works. For
|
|
8
|
+
* full destructive-change preview (does the SQL still apply against the
|
|
9
|
+
* current production data?), run against an ephemeral clone separately.
|
|
10
|
+
*
|
|
11
|
+
* Uses PGlite when `DATABASE_URL` is unset so CI runners can validate
|
|
12
|
+
* migrations without provisioning Postgres.
|
|
13
|
+
*/
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { dryRun } from "@raredays/postgres/migrations";
|
|
16
|
+
import * as appSchema from "./schema.js";
|
|
17
|
+
|
|
18
|
+
const migrationsFolder = resolve(import.meta.dirname, "migrations");
|
|
19
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
20
|
+
|
|
21
|
+
async function main(): Promise<void> {
|
|
22
|
+
if (databaseUrl) {
|
|
23
|
+
const { createDb } = await import("@raredays/postgres-neon");
|
|
24
|
+
const { migrate } = await import("drizzle-orm/neon-http/migrator");
|
|
25
|
+
const db = createDb(databaseUrl, { schema: appSchema });
|
|
26
|
+
await dryRun({ db, migrate, migrationsFolder });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.warn(
|
|
31
|
+
"[db:check] DATABASE_URL is not set — validating against in-process PGlite.",
|
|
32
|
+
);
|
|
33
|
+
const { createPglite } = await import("@raredays/postgres-pglite");
|
|
34
|
+
const { migrate } = await import("drizzle-orm/pglite/migrator");
|
|
35
|
+
const handle = await createPglite({ schema: appSchema });
|
|
36
|
+
try {
|
|
37
|
+
await dryRun({ db: handle.db, migrate, migrationsFolder });
|
|
38
|
+
} finally {
|
|
39
|
+
await handle.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error("[db:check] failed:", err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Database client — branches on environment.
|
|
2
|
+
//
|
|
3
|
+
// Production path: `@raredays/postgres-neon` (HTTP, edge-runtime
|
|
4
|
+
// compatible). Set `DATABASE_URL` and the lazy Proxy below resolves to a
|
|
5
|
+
// Neon client on first property access. To swap providers (Supabase /
|
|
6
|
+
// RDS / self-hosted via `@raredays/postgres-js`, or any other
|
|
7
|
+
// `@raredays/postgres-*` driver):
|
|
8
|
+
//
|
|
9
|
+
// 1. Edit `site.spec.ts` and set `deploy.database.provider` accordingly.
|
|
10
|
+
// 2. `pnpm remove @raredays/postgres-neon && pnpm add @raredays/postgres-js`
|
|
11
|
+
// 3. Replace the `@raredays/postgres-neon` static import below.
|
|
12
|
+
//
|
|
13
|
+
// Dev path (NODE_ENV !== "production" AND no DATABASE_URL): a fresh
|
|
14
|
+
// in-process `@raredays/postgres-pglite` is booted eagerly so
|
|
15
|
+
// Drizzle-backed providers (state-machine, engagement, audit, future
|
|
16
|
+
// cms-drizzle) can register their tables at composition root without
|
|
17
|
+
// provisioning Postgres. PGlite resets every time the dev server
|
|
18
|
+
// restarts; set `DATABASE_URL` to opt into a persistent database in dev.
|
|
19
|
+
//
|
|
20
|
+
// Why eager in dev and lazy in prod? Drizzle providers materialize
|
|
21
|
+
// tables at `registerMachine` / `registerTable` time — i.e. during
|
|
22
|
+
// composition root eval. The dev runtime needs the db handle ready
|
|
23
|
+
// before `createRaredaysApp` is called. The prod runtime can stay
|
|
24
|
+
// lazy: the Neon client is cheap to construct, and lazy construction
|
|
25
|
+
// means a build that never queries (e.g. SSG-only marketing routes)
|
|
26
|
+
// doesn't require DATABASE_URL.
|
|
27
|
+
import { createDb, type RaredaysDatabase as NeonDb } from "@raredays/postgres-neon";
|
|
28
|
+
import { createPglite, type RaredaysDatabase as PgliteDb } from "@raredays/postgres-pglite";
|
|
29
|
+
import * as appSchema from "./schema.js";
|
|
30
|
+
|
|
31
|
+
export type Db = NeonDb<typeof appSchema> | PgliteDb<typeof appSchema>;
|
|
32
|
+
|
|
33
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
34
|
+
const hasDatabaseUrl = Boolean(process.env.DATABASE_URL);
|
|
35
|
+
|
|
36
|
+
// Dev fallback: bootstrap PGlite eagerly so providers can register
|
|
37
|
+
// tables at composition-root eval. In prod, or when DATABASE_URL is
|
|
38
|
+
// set, leave this null and fall through to the lazy Proxy below.
|
|
39
|
+
const eagerPglite: Db | null =
|
|
40
|
+
!isProduction && !hasDatabaseUrl ? await bootstrapPglite() : null;
|
|
41
|
+
|
|
42
|
+
let cached: Db | undefined = eagerPglite ?? undefined;
|
|
43
|
+
|
|
44
|
+
function buildNeon(): Db {
|
|
45
|
+
const connectionString = process.env.DATABASE_URL;
|
|
46
|
+
if (!connectionString) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"DATABASE_URL is not set. Provision a Postgres database (Neon, Supabase, etc.) and add it to the environment before running queries.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return createDb(connectionString, { schema: appSchema }) as Db;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function bootstrapPglite(): Promise<Db> {
|
|
55
|
+
const handle = await createPglite({ schema: appSchema });
|
|
56
|
+
// Minimal CREATE TABLE statements mirroring the schemas exported by
|
|
57
|
+
// `@raredays/postgres`. Kept tight on purpose — Drizzle-provider tests
|
|
58
|
+
// exercise the real conformance suite; this is purely so a fresh
|
|
59
|
+
// `pnpm dev` doesn't crash on first query against substrate tables. If
|
|
60
|
+
// you need the full migration set in dev, point DATABASE_URL at a
|
|
61
|
+
// local Postgres and run `pnpm db:migrate`.
|
|
62
|
+
const statements: readonly string[] = [
|
|
63
|
+
`CREATE TABLE IF NOT EXISTS audit_events (
|
|
64
|
+
id text PRIMARY KEY,
|
|
65
|
+
organization_id text NOT NULL,
|
|
66
|
+
actor_id text,
|
|
67
|
+
action text NOT NULL,
|
|
68
|
+
resource_type text NOT NULL,
|
|
69
|
+
resource_id text,
|
|
70
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
71
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
72
|
+
)`,
|
|
73
|
+
`CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
|
74
|
+
id text PRIMARY KEY,
|
|
75
|
+
organization_id text NOT NULL,
|
|
76
|
+
endpoint text NOT NULL,
|
|
77
|
+
event_type text NOT NULL,
|
|
78
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
79
|
+
status text NOT NULL DEFAULT 'pending',
|
|
80
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
81
|
+
last_error text,
|
|
82
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
83
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
84
|
+
)`,
|
|
85
|
+
`CREATE TABLE IF NOT EXISTS job_executions (
|
|
86
|
+
id text PRIMARY KEY,
|
|
87
|
+
organization_id text NOT NULL,
|
|
88
|
+
job_name text NOT NULL,
|
|
89
|
+
status text NOT NULL DEFAULT 'pending',
|
|
90
|
+
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
91
|
+
result jsonb,
|
|
92
|
+
error text,
|
|
93
|
+
scheduled_at timestamptz NOT NULL DEFAULT now(),
|
|
94
|
+
started_at timestamptz,
|
|
95
|
+
completed_at timestamptz
|
|
96
|
+
)`,
|
|
97
|
+
];
|
|
98
|
+
for (const stmt of statements) {
|
|
99
|
+
await handle.client.query(stmt);
|
|
100
|
+
}
|
|
101
|
+
return handle.db as Db;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const db: Db = new Proxy({} as Db, {
|
|
105
|
+
get(_target, prop) {
|
|
106
|
+
if (!cached) cached = buildNeon();
|
|
107
|
+
return Reflect.get(cached, prop);
|
|
108
|
+
},
|
|
109
|
+
}) as Db;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply pending Drizzle migrations against the configured Postgres provider.
|
|
3
|
+
*
|
|
4
|
+
* Invoked by `pnpm db:migrate`. Branches on environment:
|
|
5
|
+
*
|
|
6
|
+
* - **Production / staging** (`DATABASE_URL` set, NODE_ENV=production): uses
|
|
7
|
+
* `@raredays/postgres-neon` by default. Swap the import if you've migrated
|
|
8
|
+
* to `@raredays/postgres-js`.
|
|
9
|
+
* - **Dev with persistent Postgres** (`DATABASE_URL` set, any NODE_ENV):
|
|
10
|
+
* same path as production — connects to the URL.
|
|
11
|
+
* - **Dev fallback** (no `DATABASE_URL`): boots PGlite in-process so the
|
|
12
|
+
* command always exits cleanly. PGlite is ephemeral, so migrations against
|
|
13
|
+
* it only validate that the SQL parses; nothing is persisted.
|
|
14
|
+
*
|
|
15
|
+
* Wraps Drizzle's `migrate()` with `@raredays/postgres/migrations`'s
|
|
16
|
+
* `safeMigrate` to acquire an advisory lock (preventing concurrent
|
|
17
|
+
* migration runs) and emit structured logs.
|
|
18
|
+
*
|
|
19
|
+
* Exit codes: 0 on success, 1 on any failure.
|
|
20
|
+
*/
|
|
21
|
+
import { resolve } from "node:path";
|
|
22
|
+
import { safeMigrate } from "@raredays/postgres/migrations";
|
|
23
|
+
import * as appSchema from "./schema.js";
|
|
24
|
+
|
|
25
|
+
const migrationsFolder = resolve(import.meta.dirname, "migrations");
|
|
26
|
+
const appKey = process.env.npm_package_name ?? "raredays-app";
|
|
27
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
28
|
+
|
|
29
|
+
async function main(): Promise<void> {
|
|
30
|
+
if (databaseUrl) {
|
|
31
|
+
// Neon HTTP path. If you've swapped to @raredays/postgres-js, change
|
|
32
|
+
// both imports below to match.
|
|
33
|
+
const { createDb } = await import("@raredays/postgres-neon");
|
|
34
|
+
const { migrate } = await import("drizzle-orm/neon-http/migrator");
|
|
35
|
+
const db = createDb(databaseUrl, { schema: appSchema });
|
|
36
|
+
await safeMigrate({ db, migrate, migrationsFolder, appKey });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Dev fallback: PGlite in-process. Migrations apply against an ephemeral
|
|
41
|
+
// database so this command never fails for missing config.
|
|
42
|
+
console.warn(
|
|
43
|
+
"[db:migrate] DATABASE_URL is not set — falling back to in-process PGlite. " +
|
|
44
|
+
"Set DATABASE_URL to persist migrations.",
|
|
45
|
+
);
|
|
46
|
+
const { createPglite } = await import("@raredays/postgres-pglite");
|
|
47
|
+
const { migrate } = await import("drizzle-orm/pglite/migrator");
|
|
48
|
+
const handle = await createPglite({ schema: appSchema });
|
|
49
|
+
try {
|
|
50
|
+
await safeMigrate({
|
|
51
|
+
db: handle.db,
|
|
52
|
+
migrate,
|
|
53
|
+
migrationsFolder,
|
|
54
|
+
appKey,
|
|
55
|
+
// PGlite supports advisory locks but they're meaningless in-process.
|
|
56
|
+
useAdvisoryLock: false,
|
|
57
|
+
});
|
|
58
|
+
} finally {
|
|
59
|
+
await handle.close();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main().catch((err) => {
|
|
64
|
+
console.error("[db:migrate] failed:", err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-side schema barrel. Re-exports the framework's substrate tables and is
|
|
3
|
+
* the place to declare app-specific tables. The drizzle-kit config in
|
|
4
|
+
* `drizzle.config.ts` points at this file, so any table declared here gets
|
|
5
|
+
* generated into a migration when you run `pnpm db:generate`.
|
|
6
|
+
*/
|
|
7
|
+
export * from "@raredays/postgres/schema";
|
|
8
|
+
|
|
9
|
+
// Declare app-specific tables here. Use the column helpers from
|
|
10
|
+
// @raredays/postgres so every table stays multi-tenancy-ready. Example:
|
|
11
|
+
//
|
|
12
|
+
// import { pgTable, text, uuid } from "drizzle-orm/pg-core";
|
|
13
|
+
// import { tenantColumns, timestampColumns } from "@raredays/postgres";
|
|
14
|
+
//
|
|
15
|
+
// export const widgets = pgTable("widgets", {
|
|
16
|
+
// id: uuid("id").primaryKey().defaultRandom(),
|
|
17
|
+
// ...tenantColumns(),
|
|
18
|
+
// ...timestampColumns(),
|
|
19
|
+
// name: text("name").notNull(),
|
|
20
|
+
// });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
|
|
3
|
+
// `drizzle-kit generate` reads the schema only and does NOT need a
|
|
4
|
+
// connection string; `drizzle-kit push` / `studio` / `migrate` do. We
|
|
5
|
+
// therefore allow the URL to be absent at config eval time and let the
|
|
6
|
+
// individual drizzle-kit commands surface their own clearer errors.
|
|
7
|
+
const url = process.env.DATABASE_URL ?? "postgres://placeholder@localhost:5432/placeholder";
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
dialect: "postgresql",
|
|
11
|
+
schema: "./db/schema.ts",
|
|
12
|
+
out: "./db/migrations",
|
|
13
|
+
dbCredentials: { url },
|
|
14
|
+
strict: true,
|
|
15
|
+
verbose: true,
|
|
16
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Next.js calls `register()` early in the server boot, before any route
|
|
2
|
+
// handler runs. The Node-runtime guard avoids starting MSW in edge
|
|
3
|
+
// runtimes where setupServer isn't compatible.
|
|
4
|
+
|
|
5
|
+
export async function register(): Promise<void> {
|
|
6
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
7
|
+
if (process.env.MOCKS !== "on") return;
|
|
8
|
+
const { mockServer } = await import("./mocks/node");
|
|
9
|
+
mockServer.start();
|
|
10
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// Browser-side mock worker. Wraps the app so that no in-page fetch fires
|
|
4
|
+
// until the Service Worker has claimed the page — otherwise the very
|
|
5
|
+
// first request races the SW registration and slips through unmocked.
|
|
6
|
+
//
|
|
7
|
+
// When `NEXT_PUBLIC_MOCKS` is unset (production), the wrapper renders
|
|
8
|
+
// children synchronously with zero overhead. Requires
|
|
9
|
+
// `public/mockServiceWorker.js` — shipped by the scaffold and pinned
|
|
10
|
+
// to the same `msw` version as this template. Regenerate with
|
|
11
|
+
// `npx msw init public/ --save` only if you bump the `msw` dep.
|
|
12
|
+
|
|
13
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
14
|
+
import { enableMockWorker } from "@raredays/mocks/browser";
|
|
15
|
+
import { handlers } from "./handlers";
|
|
16
|
+
|
|
17
|
+
const ENABLED = process.env.NEXT_PUBLIC_MOCKS === "on";
|
|
18
|
+
const worker = ENABLED ? enableMockWorker(handlers) : null;
|
|
19
|
+
|
|
20
|
+
export function StartMocks({ children }: { children: ReactNode }): ReactNode {
|
|
21
|
+
const [ready, setReady] = useState(!ENABLED);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!worker) return;
|
|
24
|
+
void worker.start().then(() => setReady(true));
|
|
25
|
+
return () => worker.stop();
|
|
26
|
+
}, []);
|
|
27
|
+
return ready ? children : null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Mock composition root. Imported by `mocks/node.ts`, `mocks/browser.tsx`,
|
|
2
|
+
// and the vitest setup file — same handlers, three environments.
|
|
3
|
+
//
|
|
4
|
+
// To enable per-vendor canonical mocks, install the corresponding
|
|
5
|
+
// `@raredays/*` packages (already in your deps if you selected the module)
|
|
6
|
+
// and uncomment the relevant import + spread below.
|
|
7
|
+
//
|
|
8
|
+
// Per-app handlers go in the trailing array; later handlers win on equal
|
|
9
|
+
// URL specificity, so app-specific overrides should come after vendor sets.
|
|
10
|
+
|
|
11
|
+
import { defineMocks, type RequestHandler } from "@raredays/mocks";
|
|
12
|
+
import { http, HttpResponse } from "msw";
|
|
13
|
+
// CRDA:mocks-imports
|
|
14
|
+
|
|
15
|
+
// Explicit annotation — TS2742 otherwise points at MSW's internal
|
|
16
|
+
// HttpResponse-*.mjs path which isn't portable across consumer apps.
|
|
17
|
+
export const handlers: RequestHandler[] = defineMocks(
|
|
18
|
+
// CRDA:mocks-spread
|
|
19
|
+
[
|
|
20
|
+
// App-local handlers — the demo at /demo/widgets calls /api/widgets,
|
|
21
|
+
// which by default returns []. With MOCKS on, MSW intercepts here and
|
|
22
|
+
// returns canned data so you can design UI against realistic responses.
|
|
23
|
+
http.get("/api/widgets", () =>
|
|
24
|
+
HttpResponse.json({
|
|
25
|
+
widgets: [
|
|
26
|
+
{ id: "w_1", name: "Mock Widget A", inventory: 12 },
|
|
27
|
+
{ id: "w_2", name: "Mock Widget B", inventory: 7 },
|
|
28
|
+
{ id: "w_3", name: "Mock Widget C", inventory: 0 },
|
|
29
|
+
],
|
|
30
|
+
total: 3,
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
);
|