create-pyro 0.1.0 → 0.2.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 +32 -11
- package/dist/index.js +150 -30
- package/dist/scaffold.js +106 -0
- package/package.json +7 -7
- package/template/.dockerignore +30 -0
- package/template/.env.example +23 -0
- package/template/.gitattributes +2 -0
- package/template/.github/workflows/ci.yml +43 -0
- package/template/.vscode/extensions.json +3 -0
- package/template/.vscode/settings.json +8 -0
- package/template/README.md +57 -4
- package/template/apps/api/Dockerfile +46 -0
- package/template/apps/api/package.json +43 -0
- package/template/apps/api/src/app.test.ts +44 -0
- package/template/apps/api/src/app.ts +54 -0
- package/template/apps/api/src/index.ts +13 -0
- package/template/apps/api/src/lib/auth.ts +46 -0
- package/template/apps/api/src/lib/response.ts +28 -0
- package/template/apps/api/src/lib/session.ts +22 -0
- package/template/apps/api/src/middleware/auth.ts +34 -0
- package/template/apps/api/src/routes/auth.ts +9 -0
- package/template/apps/api/src/routes/index.ts +8 -0
- package/template/apps/api/src/routes/me.test.ts +45 -0
- package/template/apps/api/src/routes/me.ts +29 -0
- package/template/apps/api/src/test/database-url.ts +23 -0
- package/template/apps/api/src/test/global-setup.ts +36 -0
- package/template/apps/api/src/test/setup.ts +8 -0
- package/template/apps/api/src/types/context.ts +8 -0
- package/template/apps/api/tsconfig.build.json +8 -0
- package/template/apps/api/tsconfig.json +9 -0
- package/template/apps/api/tsdown.config.ts +13 -0
- package/template/apps/api/vitest.config.ts +11 -0
- package/template/apps/web/Dockerfile +49 -0
- package/template/apps/web/app/app.css +29 -0
- package/template/apps/web/app/lib/api.server.ts +13 -0
- package/template/apps/web/app/lib/api.test.ts +28 -0
- package/template/apps/web/app/lib/api.ts +47 -0
- package/template/apps/web/app/lib/auth-client.ts +13 -0
- package/template/apps/web/app/lib/env.server.ts +11 -0
- package/template/apps/web/app/lib/public-env.ts +17 -0
- package/template/apps/web/app/lib/session.server.ts +31 -0
- package/template/apps/web/app/lib/utils.ts +6 -0
- package/template/apps/web/app/root.tsx +86 -0
- package/template/apps/web/app/routes/dashboard.tsx +81 -0
- package/template/apps/web/app/routes/home.tsx +161 -0
- package/template/apps/web/app/routes.ts +6 -0
- package/template/apps/web/package.json +41 -0
- package/template/apps/web/public/favicon.svg +5 -0
- package/template/apps/web/react-router.config.ts +6 -0
- package/template/apps/web/tsconfig.json +23 -0
- package/template/apps/web/vite.config.ts +19 -0
- package/template/apps/web/vitest.config.ts +13 -0
- package/template/biome.json +53 -0
- package/template/compose.dev.yml +26 -0
- package/template/compose.yml +75 -0
- package/template/gitignore +32 -1
- package/template/infra/nginx/default.conf +33 -0
- package/template/package.json +23 -1
- package/template/packages/config/package.json +31 -0
- package/template/packages/config/src/index.test.ts +100 -0
- package/template/packages/config/src/index.ts +100 -0
- package/template/packages/config/tsconfig.build.json +8 -0
- package/template/packages/config/tsconfig.json +9 -0
- package/template/packages/config/tsdown.config.ts +12 -0
- package/template/packages/db/Dockerfile +22 -0
- package/template/packages/db/drizzle/0000_init.sql +53 -0
- package/template/packages/db/drizzle/meta/0000_snapshot.json +376 -0
- package/template/packages/db/drizzle/meta/_journal.json +13 -0
- package/template/packages/db/drizzle.config.ts +12 -0
- package/template/packages/db/package.json +38 -0
- package/template/packages/db/src/client.ts +12 -0
- package/template/packages/db/src/index.ts +2 -0
- package/template/packages/db/src/schema/auth.ts +96 -0
- package/template/packages/db/src/schema/index.ts +1 -0
- package/template/packages/db/tsconfig.build.json +7 -0
- package/template/packages/db/tsconfig.json +9 -0
- package/template/packages/db/tsdown.config.ts +12 -0
- package/template/packages/shared/package.json +32 -0
- package/template/packages/shared/src/index.ts +2 -0
- package/template/packages/shared/src/schemas/auth.test.ts +32 -0
- package/template/packages/shared/src/schemas/auth.ts +22 -0
- package/template/packages/shared/src/schemas/index.ts +1 -0
- package/template/packages/shared/src/types/api.ts +15 -0
- package/template/packages/shared/src/types/index.ts +2 -0
- package/template/packages/shared/src/types/user.ts +10 -0
- package/template/packages/shared/tsconfig.build.json +8 -0
- package/template/packages/shared/tsconfig.json +9 -0
- package/template/packages/shared/tsdown.config.ts +12 -0
- package/template/pnpm-workspace.yaml +25 -0
- package/template/tsconfig.json +19 -0
- package/template/turbo.json +27 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: my-pyro-app
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
postgres:
|
|
5
|
+
image: postgres:18-alpine
|
|
6
|
+
restart: unless-stopped
|
|
7
|
+
environment:
|
|
8
|
+
POSTGRES_USER: ${POSTGRES_USER:?see .env.example}
|
|
9
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?see .env.example}
|
|
10
|
+
POSTGRES_DB: ${POSTGRES_DB:?see .env.example}
|
|
11
|
+
volumes:
|
|
12
|
+
- postgres_data:/var/lib/postgresql
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
|
|
15
|
+
interval: 10s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 5
|
|
18
|
+
|
|
19
|
+
migrator:
|
|
20
|
+
build:
|
|
21
|
+
context: .
|
|
22
|
+
dockerfile: packages/db/Dockerfile
|
|
23
|
+
environment:
|
|
24
|
+
NODE_ENV: production
|
|
25
|
+
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
26
|
+
depends_on:
|
|
27
|
+
postgres:
|
|
28
|
+
condition: service_healthy
|
|
29
|
+
restart: "no"
|
|
30
|
+
|
|
31
|
+
api:
|
|
32
|
+
build:
|
|
33
|
+
context: .
|
|
34
|
+
dockerfile: apps/api/Dockerfile
|
|
35
|
+
environment:
|
|
36
|
+
NODE_ENV: production
|
|
37
|
+
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
38
|
+
API_PORT: 3002
|
|
39
|
+
BETTER_AUTH_URL: ${PUBLIC_ORIGIN:-http://localhost}
|
|
40
|
+
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?see .env.example}
|
|
41
|
+
CORS_ORIGIN: ${PUBLIC_ORIGIN:-http://localhost}
|
|
42
|
+
depends_on:
|
|
43
|
+
migrator:
|
|
44
|
+
condition: service_completed_successfully
|
|
45
|
+
restart: unless-stopped
|
|
46
|
+
|
|
47
|
+
web:
|
|
48
|
+
build:
|
|
49
|
+
context: .
|
|
50
|
+
dockerfile: apps/web/Dockerfile
|
|
51
|
+
environment:
|
|
52
|
+
NODE_ENV: production
|
|
53
|
+
PORT: 3000
|
|
54
|
+
PUBLIC_API_URL: ${PUBLIC_ORIGIN:-http://localhost}
|
|
55
|
+
INTERNAL_API_URL: http://api:3002
|
|
56
|
+
depends_on:
|
|
57
|
+
api:
|
|
58
|
+
condition: service_healthy
|
|
59
|
+
restart: unless-stopped
|
|
60
|
+
|
|
61
|
+
nginx:
|
|
62
|
+
image: nginx:1.30-alpine
|
|
63
|
+
ports:
|
|
64
|
+
- "80:80"
|
|
65
|
+
volumes:
|
|
66
|
+
- ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
|
67
|
+
depends_on:
|
|
68
|
+
web:
|
|
69
|
+
condition: service_healthy
|
|
70
|
+
api:
|
|
71
|
+
condition: service_healthy
|
|
72
|
+
restart: unless-stopped
|
|
73
|
+
|
|
74
|
+
volumes:
|
|
75
|
+
postgres_data:
|
package/template/gitignore
CHANGED
|
@@ -1,2 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# deps
|
|
2
2
|
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# output
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# react router
|
|
9
|
+
.react-router/
|
|
10
|
+
|
|
11
|
+
# env
|
|
12
|
+
.env*
|
|
13
|
+
!.env.example
|
|
14
|
+
|
|
15
|
+
# logs
|
|
16
|
+
logs/
|
|
17
|
+
*.log
|
|
18
|
+
npm-debug.log*
|
|
19
|
+
pnpm-debug.log*
|
|
20
|
+
|
|
21
|
+
# editor
|
|
22
|
+
.vscode/*
|
|
23
|
+
!.vscode/extensions.json
|
|
24
|
+
!.vscode/settings.json
|
|
25
|
+
|
|
26
|
+
# turbo
|
|
27
|
+
.turbo
|
|
28
|
+
|
|
29
|
+
# misc
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
|
32
|
+
*.local
|
|
33
|
+
*.tsbuildinfo
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 80 default_server;
|
|
3
|
+
server_name _;
|
|
4
|
+
|
|
5
|
+
client_max_body_size 10m;
|
|
6
|
+
|
|
7
|
+
# Docker's embedded DNS. Resolving per request (via the variables below)
|
|
8
|
+
# lets nginx start before the upstreams and survive their restarts.
|
|
9
|
+
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
10
|
+
|
|
11
|
+
set $api_upstream http://api:3002;
|
|
12
|
+
set $web_upstream http://web:3000;
|
|
13
|
+
|
|
14
|
+
location /api/ {
|
|
15
|
+
proxy_pass $api_upstream;
|
|
16
|
+
proxy_http_version 1.1;
|
|
17
|
+
proxy_set_header Host $host;
|
|
18
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
19
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
20
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
location / {
|
|
24
|
+
proxy_pass $web_upstream;
|
|
25
|
+
proxy_http_version 1.1;
|
|
26
|
+
proxy_set_header Host $host;
|
|
27
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
28
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
29
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
30
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
31
|
+
proxy_set_header Connection "upgrade";
|
|
32
|
+
}
|
|
33
|
+
}
|
package/template/package.json
CHANGED
|
@@ -2,5 +2,27 @@
|
|
|
2
2
|
"name": "my-pyro-app",
|
|
3
3
|
"private": true,
|
|
4
4
|
"version": "0.1.0",
|
|
5
|
-
"type": "module"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "pnpm@12.15.0",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "turbo dev",
|
|
9
|
+
"build": "turbo build",
|
|
10
|
+
"start": "turbo start",
|
|
11
|
+
"lint": "biome check --write",
|
|
12
|
+
"lint:ci": "biome ci",
|
|
13
|
+
"typecheck": "turbo typecheck",
|
|
14
|
+
"test": "turbo test",
|
|
15
|
+
"db:generate": "turbo build --filter=./packages/config --output-logs=errors-only && pnpm -F db generate",
|
|
16
|
+
"db:migrate": "turbo build --filter=./packages/config --output-logs=errors-only && pnpm -F db migrate",
|
|
17
|
+
"db:push": "turbo build --filter=./packages/config --output-logs=errors-only && pnpm -F db push",
|
|
18
|
+
"db:studio": "turbo build --filter=./packages/config --output-logs=errors-only && pnpm -F db studio"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@biomejs/biome": "^2.6.3",
|
|
22
|
+
"turbo": "^2.11.10"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=24",
|
|
26
|
+
"pnpm": ">=12"
|
|
27
|
+
}
|
|
6
28
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@repo/config",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsdown",
|
|
8
|
+
"dev": "tsdown --watch --no-clean",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"test:watch": "vitest",
|
|
11
|
+
"typecheck": "tsc"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"zod": "catalog:"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "catalog:",
|
|
18
|
+
"tsdown": "catalog:",
|
|
19
|
+
"typescript": "catalog:",
|
|
20
|
+
"vitest": "catalog:"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"import": "./dist/index.mjs",
|
|
26
|
+
"default": "./dist/index.mjs"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.mts"
|
|
31
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
beforeEach(() => {
|
|
4
|
+
vi.resetModules();
|
|
5
|
+
vi.unstubAllEnvs();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const DB_URL = "postgresql://app:password@localhost:15432/app";
|
|
9
|
+
const SECRET = "a".repeat(32);
|
|
10
|
+
|
|
11
|
+
describe("laziness", () => {
|
|
12
|
+
it("importing the module never throws, even with an empty environment", async () => {
|
|
13
|
+
await expect(import("./index")).resolves.toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("dbEnv", () => {
|
|
18
|
+
it("reads DATABASE_URL", async () => {
|
|
19
|
+
vi.stubEnv("DATABASE_URL", DB_URL);
|
|
20
|
+
const { dbEnv } = await import("./index");
|
|
21
|
+
expect(dbEnv.DATABASE_URL).toBe(DB_URL);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("does not require variables from other scopes", async () => {
|
|
25
|
+
vi.stubEnv("DATABASE_URL", DB_URL);
|
|
26
|
+
vi.stubEnv("BETTER_AUTH_SECRET", "");
|
|
27
|
+
vi.stubEnv("CORS_ORIGIN", "");
|
|
28
|
+
const { dbEnv } = await import("./index");
|
|
29
|
+
expect(() => dbEnv.DATABASE_URL).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("explains an absent variable rather than reporting a type error", async () => {
|
|
33
|
+
vi.stubEnv("DATABASE_URL", undefined);
|
|
34
|
+
const { dbEnv } = await import("./index");
|
|
35
|
+
expect(() => dbEnv.DATABASE_URL).toThrow(/DATABASE_URL: required/);
|
|
36
|
+
expect(() => dbEnv.DATABASE_URL).not.toThrow(/received undefined/);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("authEnv", () => {
|
|
41
|
+
it("reports every problem in the scope at once", async () => {
|
|
42
|
+
vi.stubEnv("BETTER_AUTH_URL", "not-a-url");
|
|
43
|
+
vi.stubEnv("BETTER_AUTH_SECRET", "too-short");
|
|
44
|
+
const { authEnv } = await import("./index");
|
|
45
|
+
let message = "";
|
|
46
|
+
try {
|
|
47
|
+
void authEnv.BETTER_AUTH_URL;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
message = err instanceof Error ? err.message : "";
|
|
50
|
+
}
|
|
51
|
+
expect(message).toContain("BETTER_AUTH_URL");
|
|
52
|
+
expect(message).toContain("BETTER_AUTH_SECRET");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("accepts a valid scope", async () => {
|
|
56
|
+
vi.stubEnv("BETTER_AUTH_URL", "http://localhost:3002/");
|
|
57
|
+
vi.stubEnv("BETTER_AUTH_SECRET", SECRET);
|
|
58
|
+
const { authEnv } = await import("./index");
|
|
59
|
+
expect(authEnv.BETTER_AUTH_URL).toBe("http://localhost:3002");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("apiEnv", () => {
|
|
64
|
+
it("applies defaults for the port and NODE_ENV", async () => {
|
|
65
|
+
vi.stubEnv("CORS_ORIGIN", "http://localhost:3000");
|
|
66
|
+
vi.stubEnv("NODE_ENV", undefined);
|
|
67
|
+
vi.stubEnv("API_PORT", undefined);
|
|
68
|
+
const { apiEnv } = await import("./index");
|
|
69
|
+
expect(apiEnv.API_PORT).toBe(3002);
|
|
70
|
+
expect(apiEnv.NODE_ENV).toBe("development");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("coerces a port from a string", async () => {
|
|
74
|
+
vi.stubEnv("CORS_ORIGIN", "http://localhost:3000");
|
|
75
|
+
vi.stubEnv("API_PORT", "4000");
|
|
76
|
+
const { apiEnv } = await import("./index");
|
|
77
|
+
expect(apiEnv.API_PORT).toBe(4000);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects a CORS_ORIGIN that is not an absolute URL", async () => {
|
|
81
|
+
vi.stubEnv("CORS_ORIGIN", "localhost:3000");
|
|
82
|
+
const { apiEnv } = await import("./index");
|
|
83
|
+
expect(() => apiEnv.CORS_ORIGIN).toThrow(/CORS_ORIGIN/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("webEnv", () => {
|
|
88
|
+
it("rejects a PUBLIC_API_URL with a trailing /api", async () => {
|
|
89
|
+
vi.stubEnv("PUBLIC_API_URL", "http://localhost/api");
|
|
90
|
+
const { webEnv } = await import("./index");
|
|
91
|
+
expect(() => webEnv.PUBLIC_API_URL).toThrow(/PUBLIC_API_URL/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("defaults INTERNAL_API_URL for the non-Docker dev loop", async () => {
|
|
95
|
+
vi.stubEnv("PUBLIC_API_URL", "http://localhost:3002");
|
|
96
|
+
vi.stubEnv("INTERNAL_API_URL", undefined);
|
|
97
|
+
const { webEnv } = await import("./index");
|
|
98
|
+
expect(webEnv.INTERNAL_API_URL).toBe("http://localhost:3002");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const nodeEnv = z
|
|
4
|
+
.enum(["development", "production", "test"])
|
|
5
|
+
.default("development");
|
|
6
|
+
|
|
7
|
+
const required = (hint: string) =>
|
|
8
|
+
z.string(`required — ${hint}`).min(1, `required — ${hint}`);
|
|
9
|
+
|
|
10
|
+
const port = (fallback: number) =>
|
|
11
|
+
z.coerce.number().int().min(1).max(65535).default(fallback);
|
|
12
|
+
|
|
13
|
+
const origin = (label: string) =>
|
|
14
|
+
required(`${label}, as an absolute http(s) URL`)
|
|
15
|
+
.refine((value) => {
|
|
16
|
+
try {
|
|
17
|
+
const { protocol } = new URL(value);
|
|
18
|
+
return protocol === "http:" || protocol === "https:";
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}, `${label} — must be an absolute http(s) URL, e.g. http://localhost:3000`)
|
|
23
|
+
.refine(
|
|
24
|
+
(value) => !/\/api\/?$/.test(value),
|
|
25
|
+
`${label} — must be an origin only; drop the trailing "/api" (call sites append their own /api/... path)`,
|
|
26
|
+
)
|
|
27
|
+
.transform((value) => value.replace(/\/+$/, ""));
|
|
28
|
+
|
|
29
|
+
const dbSchema = z.object({
|
|
30
|
+
DATABASE_URL: required("e.g. postgresql://app:password@localhost:15432/app"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const authSchema = z.object({
|
|
34
|
+
BETTER_AUTH_URL: origin("origin of the API"),
|
|
35
|
+
BETTER_AUTH_SECRET: required("generate with `openssl rand -base64 32`").min(
|
|
36
|
+
32,
|
|
37
|
+
"must be at least 32 chars — generate with `openssl rand -base64 32`",
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const apiSchema = z.object({
|
|
42
|
+
NODE_ENV: nodeEnv,
|
|
43
|
+
API_PORT: port(3002),
|
|
44
|
+
CORS_ORIGIN: origin("origin of the web app"),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const webSchema = z.object({
|
|
48
|
+
NODE_ENV: nodeEnv,
|
|
49
|
+
PUBLIC_API_URL: origin("public origin of the API"),
|
|
50
|
+
INTERNAL_API_URL: origin("internal origin of the API").default(
|
|
51
|
+
"http://localhost:3002",
|
|
52
|
+
),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function formatIssues(scope: string, error: z.ZodError): string {
|
|
56
|
+
const lines = error.issues.map((issue) => {
|
|
57
|
+
const key = issue.path.join(".") || "(root)";
|
|
58
|
+
return ` ${key}: ${issue.message}`;
|
|
59
|
+
});
|
|
60
|
+
return `Invalid environment for "${scope}":\n${lines.join("\n")}\n\nSee .env.example at the repo root.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates `process.env` against `schema` on first property access, not on
|
|
65
|
+
* import, so a process only needs the variables of the scopes it reads.
|
|
66
|
+
*/
|
|
67
|
+
function lazyEnv<T extends z.ZodType<object>>(
|
|
68
|
+
scope: string,
|
|
69
|
+
schema: T,
|
|
70
|
+
): z.infer<T> {
|
|
71
|
+
let cached: z.infer<T> | undefined;
|
|
72
|
+
|
|
73
|
+
const load = (): z.infer<T> => {
|
|
74
|
+
if (cached) return cached;
|
|
75
|
+
const parsed = schema.safeParse(process.env);
|
|
76
|
+
if (!parsed.success) throw new Error(formatIssues(scope, parsed.error));
|
|
77
|
+
cached = parsed.data;
|
|
78
|
+
return cached;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return new Proxy({} as z.infer<T>, {
|
|
82
|
+
get: (_target, prop) => load()[prop as keyof z.infer<T>],
|
|
83
|
+
has: (_target, prop) => prop in load(),
|
|
84
|
+
ownKeys: () => Reflect.ownKeys(load()),
|
|
85
|
+
getOwnPropertyDescriptor: (_target, prop) => ({
|
|
86
|
+
...Object.getOwnPropertyDescriptor(load(), prop),
|
|
87
|
+
configurable: true,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const dbEnv = lazyEnv("db", dbSchema);
|
|
93
|
+
export const authEnv = lazyEnv("auth", authSchema);
|
|
94
|
+
export const apiEnv = lazyEnv("api", apiSchema);
|
|
95
|
+
export const webEnv = lazyEnv("web", webSchema);
|
|
96
|
+
|
|
97
|
+
export type DbEnv = z.infer<typeof dbSchema>;
|
|
98
|
+
export type AuthEnv = z.infer<typeof authSchema>;
|
|
99
|
+
export type ApiEnv = z.infer<typeof apiSchema>;
|
|
100
|
+
export type WebEnv = z.infer<typeof webSchema>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# One-shot migrator: applies packages/db/drizzle against DATABASE_URL, then exits.
|
|
2
|
+
FROM node:24-alpine
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.json ./
|
|
6
|
+
COPY apps/api/package.json ./apps/api/
|
|
7
|
+
COPY apps/web/package.json ./apps/web/
|
|
8
|
+
COPY packages/config/package.json ./packages/config/
|
|
9
|
+
COPY packages/db/package.json ./packages/db/
|
|
10
|
+
COPY packages/shared/package.json ./packages/shared/
|
|
11
|
+
|
|
12
|
+
RUN corepack enable && pnpm install --frozen-lockfile --filter @repo/db...
|
|
13
|
+
|
|
14
|
+
COPY packages/config ./packages/config
|
|
15
|
+
COPY packages/db ./packages/db
|
|
16
|
+
|
|
17
|
+
RUN pnpm --filter @repo/config build
|
|
18
|
+
|
|
19
|
+
USER node
|
|
20
|
+
WORKDIR /app/packages/db
|
|
21
|
+
|
|
22
|
+
CMD ["node_modules/.bin/drizzle-kit", "migrate", "--config", "drizzle.config.ts"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
CREATE TABLE "accounts" (
|
|
2
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
3
|
+
"account_id" text NOT NULL,
|
|
4
|
+
"provider_id" text NOT NULL,
|
|
5
|
+
"user_id" text NOT NULL,
|
|
6
|
+
"access_token" text,
|
|
7
|
+
"refresh_token" text,
|
|
8
|
+
"id_token" text,
|
|
9
|
+
"access_token_expires_at" timestamp,
|
|
10
|
+
"refresh_token_expires_at" timestamp,
|
|
11
|
+
"scope" text,
|
|
12
|
+
"password" text,
|
|
13
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
14
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE "sessions" (
|
|
18
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
19
|
+
"expires_at" timestamp NOT NULL,
|
|
20
|
+
"token" text NOT NULL,
|
|
21
|
+
"ip_address" text,
|
|
22
|
+
"user_agent" text,
|
|
23
|
+
"user_id" text NOT NULL,
|
|
24
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
25
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
26
|
+
CONSTRAINT "sessions_token_unique" UNIQUE("token")
|
|
27
|
+
);
|
|
28
|
+
--> statement-breakpoint
|
|
29
|
+
CREATE TABLE "users" (
|
|
30
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
31
|
+
"name" text NOT NULL,
|
|
32
|
+
"email" text NOT NULL,
|
|
33
|
+
"email_verified" boolean DEFAULT false NOT NULL,
|
|
34
|
+
"image" text,
|
|
35
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
36
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
37
|
+
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
38
|
+
);
|
|
39
|
+
--> statement-breakpoint
|
|
40
|
+
CREATE TABLE "verifications" (
|
|
41
|
+
"id" text PRIMARY KEY NOT NULL,
|
|
42
|
+
"identifier" text NOT NULL,
|
|
43
|
+
"value" text NOT NULL,
|
|
44
|
+
"expires_at" timestamp NOT NULL,
|
|
45
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
46
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
47
|
+
);
|
|
48
|
+
--> statement-breakpoint
|
|
49
|
+
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
50
|
+
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
51
|
+
CREATE INDEX "accounts_user_id_idx" ON "accounts" USING btree ("user_id");--> statement-breakpoint
|
|
52
|
+
CREATE INDEX "sessions_user_id_idx" ON "sessions" USING btree ("user_id");--> statement-breakpoint
|
|
53
|
+
CREATE INDEX "verifications_identifier_idx" ON "verifications" USING btree ("identifier");
|