@rebasepro/cli 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e
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/cli.test.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 +1 -1
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/index.cjs +188 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.es.js +188 -62
- package/dist/index.es.js.map +1 -1
- package/dist/utils/project.test.d.ts +1 -0
- package/package.json +67 -66
- package/templates/template/.env.template +22 -1
- package/templates/template/README.md +2 -2
- package/templates/template/backend/Dockerfile +6 -6
- package/templates/template/backend/functions/hello.ts +24 -6
- package/templates/template/backend/package.json +2 -2
- package/templates/template/backend/src/env.ts +52 -0
- package/templates/template/backend/src/index.ts +86 -34
- 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 +39 -1
- package/templates/template/config/collections/tags.ts +27 -0
- package/templates/template/{shared → config}/package.json +1 -1
- package/templates/template/docker-compose.yml +12 -0
- package/templates/template/frontend/Dockerfile +3 -3
- package/templates/template/frontend/package.json +2 -2
- package/templates/template/frontend/src/App.tsx +11 -12
- package/templates/template/frontend/src/main.tsx +2 -2
- package/templates/template/frontend/vite.config.ts +1 -1
- package/templates/template/pnpm-workspace.yaml +1 -1
- package/templates/template/scripts/example.ts +91 -0
- package/templates/template/shared/collections/index.ts +0 -3
- /package/templates/template/{shared → config}/index.ts +0 -0
- /package/templates/template/{shared → config}/tsconfig.json +0 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# docker run -p 3001:3001 --env-file .env my-app-backend
|
|
8
8
|
|
|
9
9
|
# ── Stage 1: Install + Build ─────────────────────────────────────────
|
|
10
|
-
FROM node:
|
|
10
|
+
FROM node:22-alpine AS builder
|
|
11
11
|
|
|
12
12
|
ENV PNPM_HOME="/pnpm"
|
|
13
13
|
ENV PATH="$PNPM_HOME:$PATH"
|
|
@@ -23,20 +23,20 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
|
23
23
|
|
|
24
24
|
# Copy workspace packages
|
|
25
25
|
COPY backend ./backend
|
|
26
|
-
COPY
|
|
26
|
+
COPY config ./config
|
|
27
27
|
|
|
28
28
|
# Install all deps (including devDependencies for build)
|
|
29
29
|
RUN pnpm install --frozen-lockfile
|
|
30
30
|
|
|
31
|
-
# Build
|
|
32
|
-
RUN pnpm --filter "*-
|
|
31
|
+
# Build config first, then backend
|
|
32
|
+
RUN pnpm --filter "*-config" run build
|
|
33
33
|
RUN pnpm --filter "*-backend" run build
|
|
34
34
|
|
|
35
35
|
# Prune dev dependencies for a smaller runtime
|
|
36
36
|
RUN pnpm install --frozen-lockfile --prod
|
|
37
37
|
|
|
38
38
|
# ── Stage 2: Production Runtime ──────────────────────────────────────
|
|
39
|
-
FROM node:
|
|
39
|
+
FROM node:22-alpine AS runtime
|
|
40
40
|
|
|
41
41
|
ENV PNPM_HOME="/pnpm"
|
|
42
42
|
ENV PATH="$PNPM_HOME:$PATH"
|
|
@@ -53,7 +53,7 @@ WORKDIR /app
|
|
|
53
53
|
COPY --from=builder /app/package.json /app/pnpm-lock.yaml /app/pnpm-workspace.yaml ./
|
|
54
54
|
COPY --from=builder /app/node_modules ./node_modules
|
|
55
55
|
COPY --from=builder /app/backend ./backend
|
|
56
|
-
COPY --from=builder /app/
|
|
56
|
+
COPY --from=builder /app/config ./config
|
|
57
57
|
|
|
58
58
|
# Create uploads directory
|
|
59
59
|
RUN mkdir -p /app/backend/uploads && chown -R rebase:rebase /app
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import type { HonoEnv } from "@rebasepro/server-core";
|
|
3
|
+
import { rebase } from "@rebasepro/server-core";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Example custom function route.
|
|
@@ -12,23 +14,39 @@ import { Hono } from "hono";
|
|
|
12
14
|
*
|
|
13
15
|
* This is a standard Hono app — use any Hono middleware,
|
|
14
16
|
* define any HTTP methods, access the request/response directly.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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").
|
|
17
21
|
*/
|
|
18
|
-
const app = new Hono();
|
|
22
|
+
const app = new Hono<HonoEnv>();
|
|
19
23
|
|
|
20
24
|
app.post("/", async (c) => {
|
|
21
25
|
const body = await c.req.json().catch(() => ({}));
|
|
22
|
-
const user = c.get("user")
|
|
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 });
|
|
23
40
|
|
|
24
41
|
return c.json({
|
|
25
42
|
message: `Hello, ${body.name || "World"}!`,
|
|
26
|
-
user:
|
|
43
|
+
user: userId
|
|
27
44
|
});
|
|
28
45
|
});
|
|
29
46
|
|
|
30
47
|
app.get("/", (c) => {
|
|
31
|
-
return c.json({ status: "ok",
|
|
48
|
+
return c.json({ status: "ok",
|
|
49
|
+
endpoint: "hello" });
|
|
32
50
|
});
|
|
33
51
|
|
|
34
52
|
export default app;
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"dev": "tsx watch --watch=\"../
|
|
8
|
+
"dev": "tsx watch --watch=\"../config/**/*\" src/index.ts",
|
|
9
9
|
"build": "tsc --noEmit",
|
|
10
10
|
"start": "tsx src/index.ts"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"{{PROJECT_NAME}}-
|
|
13
|
+
"{{PROJECT_NAME}}-config": "workspace:*",
|
|
14
14
|
"@rebasepro/server-core": "workspace:*",
|
|
15
15
|
"@rebasepro/server-postgresql": "workspace:*",
|
|
16
16
|
"drizzle-orm": "^0.44.4",
|
|
@@ -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);
|
|
@@ -8,88 +8,140 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
import {
|
|
9
9
|
initializeRebaseBackend,
|
|
10
10
|
serveSPA,
|
|
11
|
-
HonoEnv
|
|
11
|
+
HonoEnv,
|
|
12
|
+
listenWithPortRetry,
|
|
13
|
+
cleanupDevPortFile,
|
|
14
|
+
logger
|
|
12
15
|
} from "@rebasepro/server-core";
|
|
13
16
|
import { createPostgresDatabaseConnection, createPostgresBootstrapper } from "@rebasepro/server-postgresql";
|
|
14
17
|
import { enums, relations, tables } from "./schema.generated";
|
|
15
|
-
import
|
|
18
|
+
import { env } from "./env";
|
|
16
19
|
|
|
17
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
21
|
const __dirname = path.dirname(__filename);
|
|
19
22
|
|
|
20
|
-
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
|
21
|
-
|
|
22
23
|
// ─── App ─────────────────────────────────────────────────────────────
|
|
23
24
|
const app = new Hono<HonoEnv>();
|
|
24
25
|
|
|
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
|
+
: [];
|
|
30
|
+
|
|
25
31
|
app.use("/*", cors({
|
|
26
|
-
origin:
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
origin: (origin) => {
|
|
33
|
+
if (!isProduction) return origin || "*";
|
|
34
|
+
return allowedOrigins.includes(origin) ? origin : null;
|
|
35
|
+
},
|
|
29
36
|
credentials: true
|
|
30
37
|
}));
|
|
31
38
|
|
|
32
39
|
app.use("/*", secureHeaders());
|
|
33
40
|
|
|
34
41
|
// ─── Database ────────────────────────────────────────────────────────
|
|
35
|
-
const databaseUrl =
|
|
36
|
-
if (!databaseUrl) throw new Error("DATABASE_URL is not set");
|
|
42
|
+
const databaseUrl = env.DATABASE_URL;
|
|
37
43
|
|
|
38
|
-
const { db, connectionString } = createPostgresDatabaseConnection(databaseUrl);
|
|
44
|
+
const { db, pool, connectionString } = createPostgresDatabaseConnection(databaseUrl);
|
|
39
45
|
|
|
40
46
|
// ─── Start ───────────────────────────────────────────────────────────
|
|
41
47
|
async function startServer() {
|
|
42
|
-
const jwtSecret =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const PORT = parseInt(process.env.PORT || "3001", 10);
|
|
48
|
+
const jwtSecret = env.JWT_SECRET;
|
|
49
|
+
const PORT = env.PORT;
|
|
46
50
|
const server = createServer(getRequestListener(app.fetch));
|
|
47
51
|
|
|
48
52
|
const backend = await initializeRebaseBackend({
|
|
49
|
-
collectionsDir: path.resolve(__dirname, "../../
|
|
53
|
+
collectionsDir: path.resolve(__dirname, "../../config/collections"),
|
|
50
54
|
functionsDir: path.resolve(__dirname, "../functions"),
|
|
51
55
|
server,
|
|
52
56
|
app,
|
|
53
57
|
bootstrappers: [
|
|
54
58
|
createPostgresBootstrapper({
|
|
55
59
|
connection: db,
|
|
56
|
-
schema: { tables,
|
|
57
|
-
|
|
60
|
+
schema: { tables,
|
|
61
|
+
enums,
|
|
62
|
+
relations },
|
|
63
|
+
adminConnectionString: env.ADMIN_CONNECTION_STRING || databaseUrl,
|
|
58
64
|
connectionString
|
|
59
65
|
})
|
|
60
66
|
],
|
|
61
67
|
auth: {
|
|
62
68
|
jwtSecret,
|
|
63
|
-
accessExpiresIn:
|
|
64
|
-
refreshExpiresIn:
|
|
65
|
-
|
|
66
|
-
|
|
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 }
|
|
67
74
|
: undefined,
|
|
68
75
|
seedDefaultRoles: true,
|
|
69
|
-
allowRegistration:
|
|
76
|
+
allowRegistration: env.ALLOW_REGISTRATION
|
|
70
77
|
},
|
|
71
|
-
storage:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
93
|
});
|
|
77
94
|
|
|
78
|
-
// ───
|
|
79
|
-
app.get("/health", (c) =>
|
|
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);
|
|
104
|
+
});
|
|
80
105
|
|
|
81
106
|
// Serve the frontend in production
|
|
82
|
-
if (
|
|
107
|
+
if (isProduction) {
|
|
83
108
|
serveSPA(app, { frontendPath: path.join(__dirname, "../../frontend/dist") });
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
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"));
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
startServer().catch(err => {
|
|
92
|
-
|
|
144
|
+
logger.error("Failed to start server", { error: err instanceof Error ? err : new Error(String(err)) });
|
|
93
145
|
process.exit(1);
|
|
94
146
|
});
|
|
95
147
|
|
|
@@ -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,4 +1,6 @@
|
|
|
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",
|
|
@@ -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;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { EntityCollection } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
const tagsCollection: EntityCollection = {
|
|
4
|
+
name: "Tags",
|
|
5
|
+
singularName: "Tag",
|
|
6
|
+
slug: "tags",
|
|
7
|
+
table: "tags",
|
|
8
|
+
icon: "Tag",
|
|
9
|
+
properties: {
|
|
10
|
+
id: {
|
|
11
|
+
name: "ID",
|
|
12
|
+
type: "number",
|
|
13
|
+
validation: {
|
|
14
|
+
required: true
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
name: {
|
|
18
|
+
name: "Tag Name",
|
|
19
|
+
type: "string",
|
|
20
|
+
validation: {
|
|
21
|
+
required: true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default tagsCollection;
|
|
@@ -63,6 +63,7 @@ services:
|
|
|
63
63
|
PORT: "3001"
|
|
64
64
|
ALLOW_REGISTRATION: ${ALLOW_REGISTRATION:-false}
|
|
65
65
|
FRONTEND_URL: ${FRONTEND_URL:-http://localhost}
|
|
66
|
+
CORS_ORIGINS: ${CORS_ORIGINS:-}
|
|
66
67
|
# Optional Google OAuth
|
|
67
68
|
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
|
|
68
69
|
# Optional SMTP
|
|
@@ -73,6 +74,17 @@ services:
|
|
|
73
74
|
SMTP_PASS: ${SMTP_PASS:-}
|
|
74
75
|
SMTP_FROM: ${SMTP_FROM:-}
|
|
75
76
|
APP_NAME: ${APP_NAME:-Rebase}
|
|
77
|
+
# Storage
|
|
78
|
+
STORAGE_TYPE: ${STORAGE_TYPE:-local}
|
|
79
|
+
STORAGE_PATH: ${STORAGE_PATH:-/app/backend/uploads}
|
|
80
|
+
S3_BUCKET: ${S3_BUCKET:-}
|
|
81
|
+
S3_REGION: ${S3_REGION:-}
|
|
82
|
+
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID:-}
|
|
83
|
+
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY:-}
|
|
84
|
+
S3_ENDPOINT: ${S3_ENDPOINT:-}
|
|
85
|
+
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
|
|
86
|
+
# Service key for scripts & server-to-server admin access
|
|
87
|
+
REBASE_SERVICE_KEY: ${REBASE_SERVICE_KEY:-}
|
|
76
88
|
depends_on:
|
|
77
89
|
db:
|
|
78
90
|
condition: service_healthy
|
|
@@ -21,13 +21,13 @@ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
|
21
21
|
|
|
22
22
|
# Copy workspace packages
|
|
23
23
|
COPY frontend ./frontend
|
|
24
|
-
COPY
|
|
24
|
+
COPY config ./config
|
|
25
25
|
|
|
26
26
|
# Install dependencies
|
|
27
27
|
RUN pnpm install --frozen-lockfile
|
|
28
28
|
|
|
29
|
-
# Build
|
|
30
|
-
RUN pnpm --filter "*-
|
|
29
|
+
# Build config first, then frontend
|
|
30
|
+
RUN pnpm --filter "*-config" run build
|
|
31
31
|
RUN pnpm --filter "*-frontend" run build
|
|
32
32
|
|
|
33
33
|
# ── Stage 2: Serve with nginx ────────────────────────────────────────
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"@rebasepro/core": "workspace:*",
|
|
9
9
|
"@rebasepro/plugin-data-enhancement": "workspace:*",
|
|
10
10
|
"@rebasepro/client": "workspace:*",
|
|
11
|
-
"@rebasepro/
|
|
11
|
+
"@rebasepro/admin": "workspace:*",
|
|
12
12
|
"@rebasepro/studio": "workspace:*",
|
|
13
13
|
"@rebasepro/types": "workspace:*",
|
|
14
14
|
"@rebasepro/ui": "workspace:*",
|
|
15
15
|
"@fontsource/jetbrains-mono": "^5.2.5",
|
|
16
|
-
"{{PROJECT_NAME}}-
|
|
16
|
+
"{{PROJECT_NAME}}-config": "workspace:*",
|
|
17
17
|
"react": "^19.0.0",
|
|
18
18
|
"react-dom": "^19.0.0",
|
|
19
19
|
"react-router": "^7.0.0",
|
|
@@ -25,14 +25,14 @@ import {
|
|
|
25
25
|
useBuildModeController,
|
|
26
26
|
useBuildNavigationStateController
|
|
27
27
|
} from "@rebasepro/core";
|
|
28
|
-
import { RebaseRoute } from "@rebasepro/
|
|
28
|
+
import { RebaseRoute } from "@rebasepro/admin";
|
|
29
29
|
import { CircularProgressCenter } from "@rebasepro/ui";
|
|
30
30
|
import { collections } from "virtual:rebase-collections";
|
|
31
31
|
import { Route, Outlet } from "react-router-dom";
|
|
32
32
|
import { createRebaseClient } from "@rebasepro/client";
|
|
33
33
|
|
|
34
34
|
// Configuration from environment
|
|
35
|
-
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";
|
|
35
|
+
const API_URL = import.meta.env.VITE_API_URL || (import.meta.env.DEV ? "http://localhost:3001" : undefined);
|
|
36
36
|
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
|
|
37
37
|
|
|
38
38
|
export function App() {
|
|
@@ -40,8 +40,7 @@ export function App() {
|
|
|
40
40
|
const userConfigPersistence = useBuildLocalConfigurationPersistence();
|
|
41
41
|
|
|
42
42
|
const rebaseClient = React.useMemo(() => createRebaseClient({
|
|
43
|
-
baseUrl: API_URL
|
|
44
|
-
websocketUrl: API_URL.replace(/^http/, "ws")
|
|
43
|
+
baseUrl: API_URL
|
|
45
44
|
}), [API_URL]);
|
|
46
45
|
|
|
47
46
|
const authController = useRebaseAuthController({
|
|
@@ -89,7 +88,7 @@ export function App() {
|
|
|
89
88
|
>
|
|
90
89
|
{({ loading }) => {
|
|
91
90
|
if (loading || authController.initialLoading) {
|
|
92
|
-
return <CircularProgressCenter
|
|
91
|
+
return <CircularProgressCenter/>;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
if (!authController.user) {
|
|
@@ -106,15 +105,15 @@ export function App() {
|
|
|
106
105
|
<RebaseRoutes>
|
|
107
106
|
<Route element={
|
|
108
107
|
<Scaffold autoOpenDrawer={false}>
|
|
109
|
-
<AppBar
|
|
110
|
-
<Drawer
|
|
111
|
-
<Outlet
|
|
112
|
-
<SideDialogs
|
|
108
|
+
<AppBar/>
|
|
109
|
+
<Drawer/>
|
|
110
|
+
<Outlet/>
|
|
111
|
+
<SideDialogs/>
|
|
113
112
|
</Scaffold>
|
|
114
113
|
}>
|
|
115
|
-
<Route path={"/"} element={<ContentHomePage
|
|
116
|
-
<Route path={"/c/*"} element={<RebaseRoute
|
|
117
|
-
<Route path={"*"} element={<NotFoundPage
|
|
114
|
+
<Route path={"/"} element={<ContentHomePage/>}/>
|
|
115
|
+
<Route path={"/c/*"} element={<RebaseRoute/>}/>
|
|
116
|
+
<Route path={"*"} element={<NotFoundPage/>}/>
|
|
118
117
|
</Route>
|
|
119
118
|
</RebaseRoutes>
|
|
120
119
|
);
|
|
@@ -7,12 +7,12 @@ import "./index.css";
|
|
|
7
7
|
const router = createBrowserRouter([
|
|
8
8
|
{
|
|
9
9
|
path: "/*",
|
|
10
|
-
element: <App
|
|
10
|
+
element: <App/>
|
|
11
11
|
}
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
14
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
15
15
|
<React.StrictMode>
|
|
16
|
-
<RouterProvider router={router}
|
|
16
|
+
<RouterProvider router={router}/>
|
|
17
17
|
</React.StrictMode>
|
|
18
18
|
);
|