@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.
Files changed (49) hide show
  1. package/dist/commands/auth.d.ts +1 -0
  2. package/dist/commands/cli.test.d.ts +1 -0
  3. package/dist/commands/db.d.ts +1 -0
  4. package/dist/commands/dev.d.ts +1 -0
  5. package/dist/commands/dev.test.d.ts +1 -0
  6. package/dist/commands/doctor.d.ts +1 -0
  7. package/dist/commands/generate_sdk.d.ts +18 -0
  8. package/dist/commands/init.d.ts +3 -0
  9. package/dist/commands/init.test.d.ts +1 -0
  10. package/dist/commands/schema.d.ts +1 -0
  11. package/dist/index.cjs +1058 -30
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +5 -0
  14. package/dist/index.es.js +1060 -29
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/utils/project.d.ts +45 -0
  17. package/dist/utils/project.test.d.ts +1 -0
  18. package/package.json +20 -11
  19. package/templates/template/.dockerignore +28 -0
  20. package/templates/template/.env.template +42 -11
  21. package/templates/template/README.md +74 -17
  22. package/templates/template/backend/Dockerfile +55 -10
  23. package/templates/template/backend/drizzle.config.ts +15 -3
  24. package/templates/template/backend/functions/hello.ts +52 -0
  25. package/templates/template/backend/package.json +30 -34
  26. package/templates/template/backend/src/env.ts +52 -0
  27. package/templates/template/backend/src/index.ts +113 -99
  28. package/templates/template/backend/tsconfig.json +1 -1
  29. package/templates/template/config/collections/authors.ts +45 -0
  30. package/templates/template/config/collections/index.ts +5 -0
  31. package/templates/template/{shared → config}/collections/posts.ts +40 -2
  32. package/templates/template/config/collections/tags.ts +27 -0
  33. package/templates/template/config/package.json +28 -0
  34. package/templates/template/docker-compose.yml +78 -17
  35. package/templates/template/frontend/Dockerfile +36 -14
  36. package/templates/template/frontend/nginx.conf +40 -0
  37. package/templates/template/frontend/package.json +9 -10
  38. package/templates/template/frontend/src/App.tsx +24 -110
  39. package/templates/template/frontend/src/index.css +15 -1
  40. package/templates/template/frontend/src/main.tsx +2 -2
  41. package/templates/template/frontend/vite.config.ts +32 -3
  42. package/templates/template/package.json +12 -16
  43. package/templates/template/pnpm-workspace.yaml +4 -0
  44. package/templates/template/scripts/example.ts +91 -0
  45. package/templates/template/backend/scripts/db-generate.ts +0 -60
  46. package/templates/template/shared/collections/index.ts +0 -3
  47. package/templates/template/shared/package.json +0 -28
  48. /package/templates/template/{shared → config}/index.ts +0 -0
  49. /package/templates/template/{shared → config}/tsconfig.json +0 -0
@@ -1,25 +1,70 @@
1
- FROM node:24-alpine AS base
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 configurations
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 internal workspaces
24
+ # Copy workspace packages
13
25
  COPY backend ./backend
14
- COPY shared ./shared
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
- # Install dependencies
17
- RUN pnpm install
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
- # Build shared and backend
20
- RUN pnpm run build:shared
21
- RUN pnpm run build:backend
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
- tablesFilter: tableNames
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
- "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=\"../shared/**/*\" src/index.ts",
9
- "build": "tsc --noEmit",
10
- "start": "tsx src/index.ts",
11
- "generate:schema": "tsx node_modules/@rebasepro/backend/src/generate-drizzle-schema.ts --collections=../shared/collections --output=src/schema.generated.ts",
12
- "db:generate": "pnpm generate:schema && tsx scripts/db-generate.ts",
13
- "db:migrate": "DOTENV_CONFIG_PATH=../.env drizzle-kit migrate",
14
- "db:push": "DOTENV_CONFIG_PATH=../.env drizzle-kit push",
15
- "db:studio": "DOTENV_CONFIG_PATH=../.env drizzle-kit studio"
16
- },
17
- "dependencies": {
18
- "{{PROJECT_NAME}}-shared": "workspace:*",
19
- "@rebasepro/backend": "^4.0.0",
20
- "cors": "^2.8.5",
21
- "drizzle-orm": "^0.44.4",
22
- "express": "^5.1.0",
23
- "pg": "^8.11.3",
24
- "ws": "^8.16.0",
25
- "zod": "^3.22.4",
26
- "dotenv": "^16.0.0"
27
- },
28
- "devDependencies": {
29
- "@types/pg": "^8.6.5",
30
- "@types/node": "^20.10.5",
31
- "@types/ws": "^8.5.10",
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 express from "express";
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 { createServer } from "http";
6
- import { createPostgresDatabaseConnection, initializeRebaseAPI, initializeRebaseBackend, serveSPA } from "@rebasepro/backend";
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
- // Load environment from project root
16
- dotenv.config({ path: path.resolve(__dirname, "../../.env") });
23
+ // ─── App ─────────────────────────────────────────────────────────────
24
+ const app = new Hono<HonoEnv>();
17
25
 
18
- const app = express();
19
- const server = createServer(app);
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
- // PostgreSQL connection
22
- const databaseUrl = process.env.DATABASE_URL;
23
- if (!databaseUrl) {
24
- throw new Error("DATABASE_URL environment variable is not set");
25
- }
26
- const db = createPostgresDatabaseConnection(databaseUrl);
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
- // Middleware
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 = process.env.JWT_SECRET;
34
- if (!jwtSecret) {
35
- throw new Error("JWT_SECRET environment variable is not set");
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, "../../shared/collections"),
53
+ collectionsDir: path.resolve(__dirname, "../../config/collections"),
54
+ functionsDir: path.resolve(__dirname, "../functions"),
41
55
  server,
42
56
  app,
43
- datasource: {
44
- "(default)": {
57
+ bootstrappers: [
58
+ createPostgresBootstrapper({
45
59
  connection: db,
46
- schema: { tables, enums, relations },
47
- adminConnectionString: process.env.ADMIN_CONNECTION_STRING || process.env.DATABASE_URL
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: process.env.JWT_ACCESS_EXPIRES_IN || "1h",
53
- refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || "30d",
54
- google: process.env.GOOGLE_CLIENT_ID ? {
55
- clientId: process.env.GOOGLE_CLIENT_ID
56
- } : undefined,
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: process.env.ALLOW_REGISTRATION === "true",
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
- type: "local",
77
- basePath: path.resolve(__dirname, "../../uploads")
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
- // Initialize REST/GraphQL API
82
- await initializeRebaseAPI(app, backend, {
83
- basePath: "/api",
84
- collectionsDir: path.resolve(__dirname, "../../shared/collections"),
85
- enableGraphQL: true,
86
- enableREST: true,
87
- cors: {
88
- origin: true,
89
- credentials: true
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 SPA in production
94
- if (process.env.NODE_ENV === "production") {
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
- app.get("/health", (req, res) => {
102
- res.json({
103
- status: "ok",
104
- timestamp: new Date().toISOString(),
105
- environment: process.env.NODE_ENV
106
- });
107
- });
108
-
109
- // Error handling
110
- app.use((error: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
111
- console.error("Unhandled error:", error);
112
- res.status(500).json({
113
- error: "Internal server error",
114
- message: error.message
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
- server.listen(PORT, () => {
121
- console.log(`🚀 Server running at http://localhost:${PORT}`);
122
- console.log(` • REST API: http://localhost:${PORT}/api`);
123
- console.log(` • GraphQL: http://localhost:${PORT}/api/graphql`);
124
- console.log(` Swagger docs: http://localhost:${PORT}/api/swagger`);
125
- console.log(` • Health: http://localhost:${PORT}/health`);
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
- console.error("Failed to start server:", err);
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, server };
148
+ export { app };
@@ -15,5 +15,5 @@
15
15
  "sourceMap": true,
16
16
  "jsx": "preserve"
17
17
  },
18
- "include": ["src/**/*", "../shared/**/*", "drizzle.config.ts"]
18
+ "include": ["src/**/*", "../config/**/*", "drizzle.config.ts"]
19
19
  }
@@ -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;
@@ -0,0 +1,5 @@
1
+ import postsCollection from "./posts";
2
+ import authorsCollection from "./authors";
3
+ import tagsCollection from "./tags";
4
+
5
+ export const collections = [postsCollection, authorsCollection, tagsCollection];
@@ -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
- dbPath: "posts",
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;