@rebasepro/cli 0.0.1-canary.0 → 0.0.1-canary.09e5ec5
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/dist/commands/auth.d.ts +1 -0
- package/dist/commands/cli.test.d.ts +1 -0
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.test.d.ts +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/generate_sdk.d.ts +18 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/schema.d.ts +1 -0
- package/dist/index.cjs +1058 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1060 -29
- package/dist/index.es.js.map +1 -1
- package/dist/utils/project.d.ts +45 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/package.json +20 -11
- package/templates/template/.dockerignore +28 -0
- package/templates/template/.env.template +42 -11
- package/templates/template/README.md +74 -17
- package/templates/template/backend/Dockerfile +55 -10
- package/templates/template/backend/drizzle.config.ts +15 -3
- package/templates/template/backend/functions/hello.ts +52 -0
- package/templates/template/backend/package.json +30 -34
- package/templates/template/backend/src/env.ts +52 -0
- package/templates/template/backend/src/index.ts +113 -99
- package/templates/template/backend/tsconfig.json +1 -1
- package/templates/template/config/collections/authors.ts +45 -0
- package/templates/template/config/collections/index.ts +5 -0
- package/templates/template/{shared → config}/collections/posts.ts +40 -2
- package/templates/template/config/collections/tags.ts +27 -0
- package/templates/template/config/package.json +28 -0
- package/templates/template/docker-compose.yml +78 -17
- package/templates/template/frontend/Dockerfile +36 -14
- package/templates/template/frontend/nginx.conf +40 -0
- package/templates/template/frontend/package.json +9 -10
- package/templates/template/frontend/src/App.tsx +24 -110
- package/templates/template/frontend/src/index.css +15 -1
- package/templates/template/frontend/src/main.tsx +2 -2
- package/templates/template/frontend/vite.config.ts +32 -3
- package/templates/template/package.json +12 -16
- package/templates/template/pnpm-workspace.yaml +4 -0
- package/templates/template/scripts/example.ts +91 -0
- package/templates/template/backend/scripts/db-generate.ts +0 -60
- package/templates/template/shared/collections/index.ts +0 -3
- package/templates/template/shared/package.json +0 -28
- /package/templates/template/{shared → config}/index.ts +0 -0
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
|
@@ -1,25 +1,70 @@
|
|
|
1
|
-
|
|
1
|
+
# ─── Multi-stage production Dockerfile for the Rebase backend ─────────
|
|
2
|
+
# Produces a minimal image (~150MB) with only the runtime needed.
|
|
3
|
+
#
|
|
4
|
+
# Build context: the project root (where pnpm-workspace.yaml lives)
|
|
5
|
+
# Usage:
|
|
6
|
+
# docker build -t my-app-backend -f backend/Dockerfile .
|
|
7
|
+
# docker run -p 3001:3001 --env-file .env my-app-backend
|
|
8
|
+
|
|
9
|
+
# ── Stage 1: Install + Build ─────────────────────────────────────────
|
|
10
|
+
FROM node:22-alpine AS builder
|
|
11
|
+
|
|
2
12
|
ENV PNPM_HOME="/pnpm"
|
|
3
13
|
ENV PATH="$PNPM_HOME:$PATH"
|
|
4
14
|
RUN corepack enable
|
|
15
|
+
|
|
16
|
+
# Native dependencies for bcrypt, pg, etc.
|
|
5
17
|
RUN apk add --no-cache python3 make g++
|
|
6
18
|
|
|
7
19
|
WORKDIR /app
|
|
8
20
|
|
|
9
|
-
# Copy root
|
|
10
|
-
COPY package.json ./
|
|
21
|
+
# Copy workspace root files first (cache-friendly layer)
|
|
22
|
+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
11
23
|
|
|
12
|
-
# Copy
|
|
24
|
+
# Copy workspace packages
|
|
13
25
|
COPY backend ./backend
|
|
14
|
-
COPY
|
|
26
|
+
COPY config ./config
|
|
27
|
+
|
|
28
|
+
# Install all deps (including devDependencies for build)
|
|
29
|
+
RUN pnpm install --frozen-lockfile
|
|
30
|
+
|
|
31
|
+
# Build config first, then backend
|
|
32
|
+
RUN pnpm --filter "*-config" run build
|
|
33
|
+
RUN pnpm --filter "*-backend" run build
|
|
34
|
+
|
|
35
|
+
# Prune dev dependencies for a smaller runtime
|
|
36
|
+
RUN pnpm install --frozen-lockfile --prod
|
|
37
|
+
|
|
38
|
+
# ── Stage 2: Production Runtime ──────────────────────────────────────
|
|
39
|
+
FROM node:22-alpine AS runtime
|
|
40
|
+
|
|
41
|
+
ENV PNPM_HOME="/pnpm"
|
|
42
|
+
ENV PATH="$PNPM_HOME:$PATH"
|
|
43
|
+
ENV NODE_ENV=production
|
|
44
|
+
|
|
45
|
+
RUN corepack enable
|
|
15
46
|
|
|
16
|
-
#
|
|
17
|
-
RUN
|
|
47
|
+
# Security: run as non-root
|
|
48
|
+
RUN addgroup -g 1001 rebase && adduser -u 1001 -G rebase -s /bin/sh -D rebase
|
|
49
|
+
|
|
50
|
+
WORKDIR /app
|
|
18
51
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
# Copy only production artifacts
|
|
53
|
+
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
|
|
54
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
55
|
+
COPY --from=builder /app/backend ./backend
|
|
56
|
+
COPY --from=builder /app/config ./config
|
|
57
|
+
|
|
58
|
+
# Create uploads directory
|
|
59
|
+
RUN mkdir -p /app/backend/uploads && chown -R rebase:rebase /app
|
|
60
|
+
|
|
61
|
+
USER rebase
|
|
22
62
|
|
|
23
63
|
WORKDIR /app/backend
|
|
24
64
|
EXPOSE 3001
|
|
65
|
+
|
|
66
|
+
# Health check for orchestrators (Docker Compose, ECS, k8s)
|
|
67
|
+
HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \
|
|
68
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
|
69
|
+
|
|
25
70
|
CMD ["pnpm", "start"]
|
|
@@ -7,8 +7,9 @@ if (!process.env.DATABASE_URL) {
|
|
|
7
7
|
throw new Error("DATABASE_URL is not set. Make sure .env file exists in the project root and contains DATABASE_URL");
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// Extract table names from the generated schema
|
|
11
|
-
// This ensures drizzle-kit ONLY manages tables defined in the schema
|
|
10
|
+
// Extract table names from the generated schema.
|
|
11
|
+
// This ensures drizzle-kit ONLY manages tables defined in the schema.
|
|
12
|
+
// Any tables in the database that are NOT part of the Rebase schema are left untouched.
|
|
12
13
|
const tableNames = Object.values(tables).map(table => getTableName(table as Table));
|
|
13
14
|
|
|
14
15
|
export default defineConfig({
|
|
@@ -18,5 +19,16 @@ export default defineConfig({
|
|
|
18
19
|
dbCredentials: {
|
|
19
20
|
url: process.env.DATABASE_URL
|
|
20
21
|
},
|
|
21
|
-
|
|
22
|
+
// Only manage tables defined in the generated schema.
|
|
23
|
+
// Unmapped tables in the database are completely ignored.
|
|
24
|
+
tablesFilter: tableNames,
|
|
25
|
+
// Restrict drizzle-kit to the public schema only — tables in other schemas
|
|
26
|
+
// (e.g. extensions, custom schemas) are never touched.
|
|
27
|
+
schemaFilter: ["public"],
|
|
28
|
+
// Prevent drizzle-kit from managing roles not defined in the schema
|
|
29
|
+
entities: {
|
|
30
|
+
roles: false
|
|
31
|
+
},
|
|
32
|
+
// If PostGIS or other extensions create helper tables, ignore them
|
|
33
|
+
extensionsFilters: ["postgis"]
|
|
22
34
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { HonoEnv } from "@rebasepro/server-core";
|
|
3
|
+
import { rebase } from "@rebasepro/server-core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Example custom function route.
|
|
7
|
+
*
|
|
8
|
+
* This file is auto-discovered by Rebase and mounted at:
|
|
9
|
+
* POST /api/functions/hello
|
|
10
|
+
* GET /api/functions/hello
|
|
11
|
+
*
|
|
12
|
+
* Call from the client SDK:
|
|
13
|
+
* const result = await client.call("functions/hello", { name: "World" });
|
|
14
|
+
*
|
|
15
|
+
* This is a standard Hono app — use any Hono middleware,
|
|
16
|
+
* define any HTTP methods, access the request/response directly.
|
|
17
|
+
*
|
|
18
|
+
* The `rebase` singleton gives you admin-level access to all
|
|
19
|
+
* app-scoped services (data, auth, storage, email) from anywhere.
|
|
20
|
+
* For request-scoped / RLS-scoped access, use c.get("user") and c.get("driver").
|
|
21
|
+
*/
|
|
22
|
+
const app = new Hono<HonoEnv>();
|
|
23
|
+
|
|
24
|
+
app.post("/", async (c) => {
|
|
25
|
+
const body = await c.req.json().catch(() => ({}));
|
|
26
|
+
const user = c.get("user");
|
|
27
|
+
|
|
28
|
+
const userId = (user && typeof user === "object" && "userId" in user)
|
|
29
|
+
? user.userId
|
|
30
|
+
: "anonymous";
|
|
31
|
+
|
|
32
|
+
// Access any Rebase service — just import and use:
|
|
33
|
+
// await rebase.email?.send({
|
|
34
|
+
// to: "admin@example.com",
|
|
35
|
+
// subject: "Function called",
|
|
36
|
+
// html: `<p>Hello from ${userId}!</p>`,
|
|
37
|
+
// });
|
|
38
|
+
//
|
|
39
|
+
// const authors = await rebase.data.authors.find({ limit: 5 });
|
|
40
|
+
|
|
41
|
+
return c.json({
|
|
42
|
+
message: `Hello, ${body.name || "World"}!`,
|
|
43
|
+
user: userId
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.get("/", (c) => {
|
|
48
|
+
return c.json({ status: "ok",
|
|
49
|
+
endpoint: "hello" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export default app;
|
|
@@ -1,36 +1,32 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"drizzle-kit": "^0.31.4",
|
|
33
|
-
"tsx": "^4.20.6",
|
|
34
|
-
"typescript": "^5.9.2"
|
|
35
|
-
}
|
|
2
|
+
"name": "{{PROJECT_NAME}}-backend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Rebase backend with PostgreSQL",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "tsx watch --watch=\"../config/**/*\" src/index.ts",
|
|
9
|
+
"build": "tsc --noEmit",
|
|
10
|
+
"start": "tsx src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"{{PROJECT_NAME}}-config": "workspace:*",
|
|
14
|
+
"@rebasepro/server-core": "workspace:*",
|
|
15
|
+
"@rebasepro/server-postgresql": "workspace:*",
|
|
16
|
+
"drizzle-orm": "^0.44.4",
|
|
17
|
+
"hono": "^4.2.0",
|
|
18
|
+
"@hono/node-server": "^1.11.0",
|
|
19
|
+
"pg": "^8.11.3",
|
|
20
|
+
"ws": "^8.16.0",
|
|
21
|
+
"zod": "^3.22.4",
|
|
22
|
+
"dotenv": "^16.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/pg": "^8.6.5",
|
|
26
|
+
"@types/node": "^20.10.5",
|
|
27
|
+
"@types/ws": "^8.5.10",
|
|
28
|
+
"drizzle-kit": "^0.31.4",
|
|
29
|
+
"tsx": "^4.20.6",
|
|
30
|
+
"typescript": "^5.9.2"
|
|
31
|
+
}
|
|
36
32
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
|
10
|
+
|
|
11
|
+
const envSchema = z.object({
|
|
12
|
+
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
13
|
+
PORT: z.string().default("3001").transform(Number),
|
|
14
|
+
DATABASE_URL: z.string().url("DATABASE_URL must be a valid URL"),
|
|
15
|
+
ADMIN_CONNECTION_STRING: z.string().url().optional(),
|
|
16
|
+
JWT_SECRET: z.string().min(32, "JWT_SECRET must be at least 32 characters long"),
|
|
17
|
+
JWT_ACCESS_EXPIRES_IN: z.string().default("1h"),
|
|
18
|
+
JWT_REFRESH_EXPIRES_IN: z.string().default("30d"),
|
|
19
|
+
GOOGLE_CLIENT_ID: z.string().optional(),
|
|
20
|
+
ALLOW_REGISTRATION: z.enum(["true", "false", ""]).default("false").transform(v => v === "true"),
|
|
21
|
+
CORS_ORIGINS: z.string().optional(),
|
|
22
|
+
FRONTEND_URL: z.string().optional(),
|
|
23
|
+
DB_POOL_MAX: z.string().default("20").transform(Number),
|
|
24
|
+
DB_POOL_IDLE_TIMEOUT: z.string().default("30000").transform(Number),
|
|
25
|
+
DB_POOL_CONNECT_TIMEOUT: z.string().default("10000").transform(Number),
|
|
26
|
+
FORCE_LOCAL_STORAGE: z.enum(["true", "false", ""]).optional().transform(v => v === "true"),
|
|
27
|
+
STORAGE_TYPE: z.enum(["local", "s3"]).default("local"),
|
|
28
|
+
STORAGE_PATH: z.string().optional(),
|
|
29
|
+
S3_BUCKET: z.string().optional(),
|
|
30
|
+
S3_REGION: z.string().optional(),
|
|
31
|
+
S3_ACCESS_KEY_ID: z.string().optional(),
|
|
32
|
+
S3_SECRET_ACCESS_KEY: z.string().optional(),
|
|
33
|
+
S3_ENDPOINT: z.string().url().optional(),
|
|
34
|
+
S3_FORCE_PATH_STYLE: z.enum(["true", "false", ""]).optional().transform(v => v === "true"),
|
|
35
|
+
/**
|
|
36
|
+
* Service key for admin-level script / server-to-server authentication.
|
|
37
|
+
* When set, scripts can send `Authorization: Bearer <key>` to get admin access.
|
|
38
|
+
* Generate with: node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"
|
|
39
|
+
*/
|
|
40
|
+
REBASE_SERVICE_KEY: z.string().min(32, "REBASE_SERVICE_KEY must be at least 32 characters").optional()
|
|
41
|
+
}).superRefine((data, ctx) => {
|
|
42
|
+
if (data.NODE_ENV === "production" && !data.CORS_ORIGINS && !data.FRONTEND_URL) {
|
|
43
|
+
ctx.addIssue({
|
|
44
|
+
code: z.ZodIssueCode.custom,
|
|
45
|
+
message: "CORS_ORIGINS or FRONTEND_URL must be set in production to secure the API.",
|
|
46
|
+
path: ["CORS_ORIGINS"]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Parse and export
|
|
52
|
+
export const env = envSchema.parse(process.env);
|
|
@@ -1,134 +1,148 @@
|
|
|
1
|
-
import
|
|
2
|
-
import cors from "cors";
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { cors } from "hono/cors";
|
|
3
|
+
import { secureHeaders } from "hono/secure-headers";
|
|
4
|
+
import { getRequestListener } from "@hono/node-server";
|
|
5
|
+
import { createServer } from "http";
|
|
3
6
|
import path from "path";
|
|
4
7
|
import { fileURLToPath } from "url";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
import {
|
|
9
|
+
initializeRebaseBackend,
|
|
10
|
+
serveSPA,
|
|
11
|
+
HonoEnv,
|
|
12
|
+
listenWithPortRetry,
|
|
13
|
+
cleanupDevPortFile,
|
|
14
|
+
logger
|
|
15
|
+
} from "@rebasepro/server-core";
|
|
16
|
+
import { createPostgresDatabaseConnection, createPostgresBootstrapper } from "@rebasepro/server-postgresql";
|
|
8
17
|
import { enums, relations, tables } from "./schema.generated";
|
|
9
|
-
|
|
10
|
-
import * as dotenv from "dotenv";
|
|
18
|
+
import { env } from "./env";
|
|
11
19
|
|
|
12
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
21
|
const __dirname = path.dirname(__filename);
|
|
14
22
|
|
|
15
|
-
//
|
|
16
|
-
|
|
23
|
+
// ─── App ─────────────────────────────────────────────────────────────
|
|
24
|
+
const app = new Hono<HonoEnv>();
|
|
17
25
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
26
|
+
const isProduction = env.NODE_ENV === "production";
|
|
27
|
+
const allowedOrigins = isProduction
|
|
28
|
+
? (env.CORS_ORIGINS || env.FRONTEND_URL || "https://yourdomain.com").split(",").map(s => s.trim())
|
|
29
|
+
: [];
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
31
|
+
app.use("/*", cors({
|
|
32
|
+
origin: (origin) => {
|
|
33
|
+
if (!isProduction) return origin || "*";
|
|
34
|
+
return allowedOrigins.includes(origin) ? origin : null;
|
|
35
|
+
},
|
|
36
|
+
credentials: true
|
|
37
|
+
}));
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
app.use(cors());
|
|
30
|
-
app.use(express.json({ limit: "10mb" }));
|
|
39
|
+
app.use("/*", secureHeaders());
|
|
31
40
|
|
|
41
|
+
// ─── Database ────────────────────────────────────────────────────────
|
|
42
|
+
const databaseUrl = env.DATABASE_URL;
|
|
43
|
+
|
|
44
|
+
const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
|
|
45
|
+
|
|
46
|
+
// ─── Start ───────────────────────────────────────────────────────────
|
|
32
47
|
async function startServer() {
|
|
33
|
-
const jwtSecret =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
48
|
+
const jwtSecret = env.JWT_SECRET;
|
|
49
|
+
const PORT = env.PORT;
|
|
50
|
+
const server = createServer(getRequestListener(app.fetch));
|
|
37
51
|
|
|
38
|
-
// Initialize Rebase Backend with auth
|
|
39
52
|
const backend = await initializeRebaseBackend({
|
|
40
|
-
collectionsDir: path.resolve(__dirname, "../../
|
|
53
|
+
collectionsDir: path.resolve(__dirname, "../../config/collections"),
|
|
54
|
+
functionsDir: path.resolve(__dirname, "../functions"),
|
|
41
55
|
server,
|
|
42
56
|
app,
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
bootstrappers: [
|
|
58
|
+
createPostgresBootstrapper({
|
|
45
59
|
connection: db,
|
|
46
|
-
schema: { tables,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
schema: { tables,
|
|
61
|
+
enums,
|
|
62
|
+
relations },
|
|
63
|
+
adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
|
|
64
|
+
connectionString
|
|
65
|
+
})
|
|
66
|
+
],
|
|
50
67
|
auth: {
|
|
51
68
|
jwtSecret,
|
|
52
|
-
accessExpiresIn:
|
|
53
|
-
refreshExpiresIn:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
accessExpiresIn: env.JWT_ACCESS_EXPIRES_IN,
|
|
70
|
+
refreshExpiresIn: env.JWT_REFRESH_EXPIRES_IN,
|
|
71
|
+
serviceKey: env.REBASE_SERVICE_KEY,
|
|
72
|
+
google: env.GOOGLE_CLIENT_ID
|
|
73
|
+
? { clientId: env.GOOGLE_CLIENT_ID }
|
|
74
|
+
: undefined,
|
|
57
75
|
seedDefaultRoles: true,
|
|
58
|
-
allowRegistration:
|
|
59
|
-
email: process.env.SMTP_HOST ? {
|
|
60
|
-
from: process.env.SMTP_FROM || "noreply@example.com",
|
|
61
|
-
appName: process.env.APP_NAME || "Rebase",
|
|
62
|
-
resetPasswordUrl: process.env.FRONTEND_URL || "http://localhost:5173",
|
|
63
|
-
verifyEmailUrl: process.env.FRONTEND_URL || "http://localhost:5173",
|
|
64
|
-
smtp: {
|
|
65
|
-
host: process.env.SMTP_HOST,
|
|
66
|
-
port: parseInt(process.env.SMTP_PORT || "587"),
|
|
67
|
-
secure: process.env.SMTP_SECURE === "true",
|
|
68
|
-
auth: process.env.SMTP_USER ? {
|
|
69
|
-
user: process.env.SMTP_USER,
|
|
70
|
-
pass: process.env.SMTP_PASS || ""
|
|
71
|
-
} : undefined
|
|
72
|
-
}
|
|
73
|
-
} : undefined
|
|
76
|
+
allowRegistration: env.ALLOW_REGISTRATION
|
|
74
77
|
},
|
|
75
|
-
storage:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
storage: env.STORAGE_TYPE === "s3"
|
|
79
|
+
? {
|
|
80
|
+
type: "s3",
|
|
81
|
+
bucket: env.S3_BUCKET!,
|
|
82
|
+
region: env.S3_REGION || "auto",
|
|
83
|
+
accessKeyId: env.S3_ACCESS_KEY_ID || "",
|
|
84
|
+
secretAccessKey: env.S3_SECRET_ACCESS_KEY || "",
|
|
85
|
+
endpoint: env.S3_ENDPOINT,
|
|
86
|
+
forcePathStyle: env.S3_FORCE_PATH_STYLE
|
|
87
|
+
}
|
|
88
|
+
: {
|
|
89
|
+
type: "local",
|
|
90
|
+
basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
|
|
91
|
+
},
|
|
92
|
+
history: true
|
|
79
93
|
});
|
|
80
94
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
95
|
+
// ─── Health check ─────────────────────────────────────────────
|
|
96
|
+
app.get("/health", async (c) => {
|
|
97
|
+
const result = await backend.healthCheck();
|
|
98
|
+
const status = result.healthy ? 200 : 503;
|
|
99
|
+
return c.json({
|
|
100
|
+
status: result.healthy ? "ok" : "degraded",
|
|
101
|
+
latencyMs: result.latencyMs,
|
|
102
|
+
...(result.details ? { details: result.details } : {})
|
|
103
|
+
}, status);
|
|
91
104
|
});
|
|
92
105
|
|
|
93
|
-
// Serve
|
|
94
|
-
if (
|
|
95
|
-
serveSPA(app, {
|
|
96
|
-
frontendPath: path.join(__dirname, "../../frontend/dist"),
|
|
97
|
-
excludePaths: ["/health"]
|
|
98
|
-
});
|
|
106
|
+
// Serve the frontend in production
|
|
107
|
+
if (isProduction) {
|
|
108
|
+
serveSPA(app, { frontendPath: path.join(__dirname, "../../frontend/dist") });
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
if (!isProduction) {
|
|
112
|
+
// Dev mode: retry the next port if the current one is in use
|
|
113
|
+
const projectRoot = path.resolve(__dirname, "../..");
|
|
114
|
+
const actualPort = await listenWithPortRetry(server, PORT, { portFileDir: projectRoot });
|
|
115
|
+
|
|
116
|
+
// Clean up port file on exit
|
|
117
|
+
const cleanup = () => cleanupDevPortFile(projectRoot);
|
|
118
|
+
process.on("SIGINT", cleanup);
|
|
119
|
+
process.on("SIGTERM", cleanup);
|
|
120
|
+
process.on("exit", cleanup);
|
|
121
|
+
|
|
122
|
+
logger.info(`Server running at http://localhost:${actualPort}`);
|
|
123
|
+
} else {
|
|
124
|
+
server.listen(PORT, () => {
|
|
125
|
+
logger.info(`Server running at http://localhost:${PORT}`);
|
|
115
126
|
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const PORT = process.env.PORT || 3001;
|
|
127
|
+
}
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
129
|
+
// ─── Graceful Shutdown ───────────────────────────────────────────────
|
|
130
|
+
// Uses the framework's built-in shutdown() which drains connections,
|
|
131
|
+
// stops the cron scheduler, and force-exits after 15s timeout.
|
|
132
|
+
const gracefulShutdown = async (signal: string) => {
|
|
133
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
134
|
+
await backend.shutdown();
|
|
135
|
+
await pool.end();
|
|
136
|
+
logger.info("Database pool closed");
|
|
137
|
+
process.exit(0);
|
|
138
|
+
};
|
|
139
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
140
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
startServer().catch(err => {
|
|
130
|
-
|
|
144
|
+
logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
|
|
131
145
|
process.exit(1);
|
|
132
146
|
});
|
|
133
147
|
|
|
134
|
-
export { app
|
|
148
|
+
export { app };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
const authorsCollection: EntityCollection = {
|
|
4
|
+
name: "Authors",
|
|
5
|
+
singularName: "Author",
|
|
6
|
+
slug: "authors",
|
|
7
|
+
table: "authors",
|
|
8
|
+
icon: "Person",
|
|
9
|
+
properties: {
|
|
10
|
+
id: {
|
|
11
|
+
name: "ID",
|
|
12
|
+
type: "number",
|
|
13
|
+
isId: "increment"
|
|
14
|
+
},
|
|
15
|
+
name: {
|
|
16
|
+
name: "Name",
|
|
17
|
+
type: "string",
|
|
18
|
+
validation: {
|
|
19
|
+
required: true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
email: {
|
|
23
|
+
name: "Email",
|
|
24
|
+
type: "string",
|
|
25
|
+
validation: {
|
|
26
|
+
required: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
picture: {
|
|
30
|
+
name: "Picture",
|
|
31
|
+
type: "string",
|
|
32
|
+
storage: {
|
|
33
|
+
storagePath: "author_pictures/"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
propertiesOrder: [
|
|
38
|
+
"id",
|
|
39
|
+
"name",
|
|
40
|
+
"email",
|
|
41
|
+
"picture"
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default authorsCollection;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { EntityCollection } from "@rebasepro/types";
|
|
2
|
+
import authorsCollection from "./authors";
|
|
3
|
+
import tagsCollection from "./tags";
|
|
2
4
|
|
|
3
5
|
const postsCollection: EntityCollection = {
|
|
4
6
|
name: "Posts",
|
|
5
7
|
singularName: "Post",
|
|
6
8
|
slug: "posts",
|
|
7
|
-
|
|
9
|
+
table: "posts",
|
|
8
10
|
icon: "Article",
|
|
9
11
|
properties: {
|
|
10
12
|
id: {
|
|
@@ -46,8 +48,44 @@ const postsCollection: EntityCollection = {
|
|
|
46
48
|
color: "greenLight"
|
|
47
49
|
}
|
|
48
50
|
]
|
|
51
|
+
},
|
|
52
|
+
author: {
|
|
53
|
+
name: "Author",
|
|
54
|
+
type: "relation",
|
|
55
|
+
relationName: "author",
|
|
56
|
+
relation: {
|
|
57
|
+
relationName: "author",
|
|
58
|
+
cardinality: "one",
|
|
59
|
+
direction: "owning",
|
|
60
|
+
target: () => authorsCollection
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
tags: {
|
|
64
|
+
name: "Tags",
|
|
65
|
+
type: "relation",
|
|
66
|
+
relationName: "tags",
|
|
67
|
+
relation: {
|
|
68
|
+
relationName: "tags",
|
|
69
|
+
cardinality: "many",
|
|
70
|
+
direction: "owning",
|
|
71
|
+
target: () => tagsCollection
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
relations: [
|
|
76
|
+
{
|
|
77
|
+
relationName: "author",
|
|
78
|
+
target: () => authorsCollection,
|
|
79
|
+
cardinality: "one",
|
|
80
|
+
direction: "owning"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
relationName: "tags",
|
|
84
|
+
target: () => tagsCollection,
|
|
85
|
+
cardinality: "many",
|
|
86
|
+
direction: "owning"
|
|
49
87
|
}
|
|
50
|
-
|
|
88
|
+
]
|
|
51
89
|
};
|
|
52
90
|
|
|
53
91
|
export default postsCollection;
|