@terreno/api 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/dist/api.d.ts +28 -2
- package/dist/api.js +20 -7
- package/dist/betterAuth.d.ts +91 -0
- package/dist/betterAuth.js +8 -0
- package/dist/betterAuth.test.d.ts +1 -0
- package/dist/betterAuth.test.js +181 -0
- package/dist/betterAuthApp.d.ts +22 -0
- package/dist/betterAuthApp.js +38 -0
- package/dist/betterAuthApp.test.d.ts +1 -0
- package/dist/betterAuthApp.test.js +242 -0
- package/dist/betterAuthSetup.d.ts +60 -0
- package/dist/betterAuthSetup.js +278 -0
- package/dist/betterAuthSetup.test.d.ts +1 -0
- package/dist/betterAuthSetup.test.js +684 -0
- package/dist/expressServer.js +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/terrenoApp.d.ts +189 -0
- package/dist/terrenoApp.js +352 -0
- package/dist/terrenoApp.test.d.ts +1 -0
- package/dist/terrenoApp.test.js +264 -0
- package/dist/terrenoPlugin.d.ts +34 -0
- package/package.json +6 -3
- package/src/api.ts +61 -3
- package/src/betterAuth.test.ts +160 -0
- package/src/betterAuth.ts +104 -0
- package/src/betterAuthApp.test.ts +114 -0
- package/src/betterAuthApp.ts +60 -0
- package/src/betterAuthSetup.test.ts +485 -0
- package/src/betterAuthSetup.ts +251 -0
- package/src/expressServer.ts +4 -5
- package/src/index.ts +4 -0
- package/src/openApiValidator.ts +1 -1
- package/src/terrenoApp.test.ts +201 -0
- package/src/terrenoApp.ts +347 -0
- package/src/terrenoPlugin.ts +34 -0
- package/.claude/CLAUDE.local.md +0 -204
- package/.cursor/rules/00-root.mdc +0 -338
- package/.github/copilot-instructions.md +0 -333
- package/AGENTS.md +0 -333
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/bun";
|
|
2
|
+
import openapi from "@wesleytodd/openapi";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import qs from "qs";
|
|
6
|
+
|
|
7
|
+
import type {ModelRouterRegistration} from "./api";
|
|
8
|
+
import {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
|
|
9
|
+
import {apiErrorMiddleware, apiUnauthorizedMiddleware} from "./errors";
|
|
10
|
+
import {type AuthOptions, logRequests} from "./expressServer";
|
|
11
|
+
import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
|
|
12
|
+
import {type LoggingOptions, logger, setupLogging} from "./logger";
|
|
13
|
+
import {openApiEtagMiddleware} from "./openApiEtag";
|
|
14
|
+
import type {TerrenoPlugin} from "./terrenoPlugin";
|
|
15
|
+
|
|
16
|
+
type CorsOrigin =
|
|
17
|
+
| string
|
|
18
|
+
| boolean
|
|
19
|
+
| RegExp
|
|
20
|
+
| Array<boolean | string | RegExp>
|
|
21
|
+
| ((
|
|
22
|
+
requestOrigin: string | undefined,
|
|
23
|
+
callback: (
|
|
24
|
+
err: Error | null,
|
|
25
|
+
origin?: boolean | string | RegExp | Array<boolean | string | RegExp>
|
|
26
|
+
) => void
|
|
27
|
+
) => void);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Configuration options for TerrenoApp.
|
|
31
|
+
*/
|
|
32
|
+
export interface TerrenoAppOptions {
|
|
33
|
+
/** Mongoose User model with passport-local-mongoose plugin */
|
|
34
|
+
userModel: UserMongooseModel;
|
|
35
|
+
/** CORS origin configuration (default: "*") */
|
|
36
|
+
corsOrigin?: CorsOrigin;
|
|
37
|
+
/** Logging configuration options */
|
|
38
|
+
loggingOptions?: LoggingOptions;
|
|
39
|
+
/** Authentication configuration options */
|
|
40
|
+
authOptions?: AuthOptions;
|
|
41
|
+
/** GitHub OAuth configuration (enables GitHub authentication if provided) */
|
|
42
|
+
githubAuth?: GitHubAuthOptions;
|
|
43
|
+
/** Skip calling app.listen() in start() method (useful for testing) */
|
|
44
|
+
skipListen?: boolean;
|
|
45
|
+
/** Sentry configuration options */
|
|
46
|
+
sentryOptions?: Sentry.BunOptions;
|
|
47
|
+
/** Maximum number of array items in query parameters (default: 200) */
|
|
48
|
+
arrayLimit?: number;
|
|
49
|
+
/** Whether to log all incoming requests (default: true) */
|
|
50
|
+
logRequests?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fluent API for building Express applications with Terreno framework.
|
|
55
|
+
*
|
|
56
|
+
* TerrenoApp provides an alternative to `setupServer` using a registration
|
|
57
|
+
* pattern instead of callbacks. Build applications by registering model
|
|
58
|
+
* routers and plugins, then calling `start()` to begin listening.
|
|
59
|
+
*
|
|
60
|
+
* The middleware stack is configured in this order:
|
|
61
|
+
* 1. CORS
|
|
62
|
+
* 2. Custom middleware (via addMiddleware)
|
|
63
|
+
* 3. JSON body parser
|
|
64
|
+
* 4. Auth routes (/auth/login, /auth/signup, etc.)
|
|
65
|
+
* 5. JWT authentication setup
|
|
66
|
+
* 6. Request logging
|
|
67
|
+
* 7. Sentry scopes
|
|
68
|
+
* 8. OpenAPI middleware
|
|
69
|
+
* 9. /auth/me routes
|
|
70
|
+
* 10. GitHub OAuth routes (if enabled)
|
|
71
|
+
* 11. Registered model routers and plugins
|
|
72
|
+
* 12. Error handling middleware
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* // Basic usage with model routers
|
|
77
|
+
* const todoRouter = modelRouter("/todos", Todo, {
|
|
78
|
+
* permissions: { list: [Permissions.IsAuthenticated], ... },
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
82
|
+
* .register(todoRouter)
|
|
83
|
+
* .register(new HealthApp())
|
|
84
|
+
* .start();
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // With custom middleware
|
|
90
|
+
* const app = new TerrenoApp({
|
|
91
|
+
* userModel: User,
|
|
92
|
+
* corsOrigin: ["https://app.example.com"],
|
|
93
|
+
* loggingOptions: { logRequests: true },
|
|
94
|
+
* githubAuth: {
|
|
95
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
96
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
97
|
+
* callbackURL: process.env.GITHUB_CALLBACK_URL!,
|
|
98
|
+
* },
|
|
99
|
+
* })
|
|
100
|
+
* .addMiddleware((req, res, next) => {
|
|
101
|
+
* res.setHeader("X-Custom-Header", "value");
|
|
102
|
+
* next();
|
|
103
|
+
* })
|
|
104
|
+
* .register(todoRouter)
|
|
105
|
+
* .register(userRouter)
|
|
106
|
+
* .start();
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @see setupServer for the callback-based alternative
|
|
110
|
+
* @see TerrenoPlugin for creating reusable plugins
|
|
111
|
+
* @see modelRouter for creating CRUD route registrations
|
|
112
|
+
*/
|
|
113
|
+
export class TerrenoApp {
|
|
114
|
+
private options: TerrenoAppOptions;
|
|
115
|
+
private registrations: (ModelRouterRegistration | TerrenoPlugin)[] = [];
|
|
116
|
+
private middlewareFns: (express.RequestHandler | ((app: express.Application) => void))[] = [];
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a new TerrenoApp builder.
|
|
120
|
+
*
|
|
121
|
+
* @param options - Application configuration options including user model and auth settings
|
|
122
|
+
*/
|
|
123
|
+
constructor(options: TerrenoAppOptions) {
|
|
124
|
+
this.options = options;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Register a model router or plugin with the application.
|
|
129
|
+
*
|
|
130
|
+
* Model routers are created with `modelRouter("/path", Model, options)` and
|
|
131
|
+
* provide CRUD endpoints. Plugins implement `TerrenoPlugin` interface and
|
|
132
|
+
* can register custom routes and middleware.
|
|
133
|
+
*
|
|
134
|
+
* Registrations are mounted in the order they are added.
|
|
135
|
+
*
|
|
136
|
+
* @param registration - A ModelRouterRegistration from modelRouter() or a TerrenoPlugin instance
|
|
137
|
+
* @returns This TerrenoApp instance for method chaining
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const todoRouter = modelRouter("/todos", Todo, options);
|
|
142
|
+
* const healthPlugin = new HealthApp({ path: "/health" });
|
|
143
|
+
*
|
|
144
|
+
* app.register(todoRouter).register(healthPlugin);
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
register(registration: ModelRouterRegistration | TerrenoPlugin): this {
|
|
148
|
+
this.registrations.push(registration);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Add custom Express middleware to the application.
|
|
154
|
+
*
|
|
155
|
+
* Middleware is added BEFORE JSON body parsing and authentication setup,
|
|
156
|
+
* allowing you to modify incoming requests early in the middleware stack.
|
|
157
|
+
*
|
|
158
|
+
* @param fn - Express middleware function or a function that configures the app
|
|
159
|
+
* @returns This TerrenoApp instance for method chaining
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* app.addMiddleware((req, res, next) => {
|
|
164
|
+
* res.setHeader("X-Request-ID", req.id);
|
|
165
|
+
* next();
|
|
166
|
+
* });
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
addMiddleware(fn: express.RequestHandler | ((app: express.Application) => void)): this {
|
|
170
|
+
this.middlewareFns.push(fn);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build the Express application without starting the server.
|
|
176
|
+
*
|
|
177
|
+
* Configures the complete middleware stack including:
|
|
178
|
+
* - CORS, JSON parsing, authentication, logging, Sentry, OpenAPI
|
|
179
|
+
* - All registered model routers and plugins
|
|
180
|
+
* - Error handling middleware
|
|
181
|
+
*
|
|
182
|
+
* Use this method when you need the Express app instance for testing
|
|
183
|
+
* or custom server setup. For normal use, call `start()` instead.
|
|
184
|
+
*
|
|
185
|
+
* @returns Configured Express application instance
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
190
|
+
* .register(todoRouter)
|
|
191
|
+
* .build();
|
|
192
|
+
*
|
|
193
|
+
* // Use app for testing with supertest
|
|
194
|
+
* await request(app).get("/todos").expect(200);
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
build(): express.Application {
|
|
198
|
+
setupLogging(this.options.loggingOptions);
|
|
199
|
+
|
|
200
|
+
const app = express();
|
|
201
|
+
const options = this.options;
|
|
202
|
+
|
|
203
|
+
app.set("query parser", (str: string) =>
|
|
204
|
+
qs.parse(str, {arrayLimit: options.arrayLimit ?? 200})
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
app.use(cors({origin: options.corsOrigin ?? "*"}));
|
|
208
|
+
|
|
209
|
+
// Apply custom middleware before JSON parsing
|
|
210
|
+
for (const fn of this.middlewareFns) {
|
|
211
|
+
if (fn.length <= 3) {
|
|
212
|
+
// express.RequestHandler (req, res, next)
|
|
213
|
+
app.use(fn as express.RequestHandler);
|
|
214
|
+
} else {
|
|
215
|
+
// Function that receives the app
|
|
216
|
+
(fn as (app: express.Application) => void)(app);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
app.use(express.json());
|
|
221
|
+
|
|
222
|
+
// Auth routes (login/signup/refresh_token) before JWT middleware
|
|
223
|
+
addAuthRoutes(app, options.userModel as any, options.authOptions);
|
|
224
|
+
setupAuth(app as any, options.userModel as any);
|
|
225
|
+
|
|
226
|
+
if (options.logRequests !== false) {
|
|
227
|
+
app.use(logRequests);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Store logging options on the response locals
|
|
231
|
+
app.use((_req, res, next) => {
|
|
232
|
+
res.locals.loggingOptions = options.loggingOptions;
|
|
233
|
+
next();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Sentry scopes
|
|
237
|
+
app.all("*", (req: any, _res: any, next: any) => {
|
|
238
|
+
const transactionId = req.header("X-Transaction-ID");
|
|
239
|
+
const sessionId = req.header("X-Session-ID");
|
|
240
|
+
if (transactionId) {
|
|
241
|
+
Sentry.getCurrentScope().setTag("transaction_id", transactionId);
|
|
242
|
+
}
|
|
243
|
+
if (sessionId) {
|
|
244
|
+
Sentry.getCurrentScope().setTag("session_id", sessionId);
|
|
245
|
+
}
|
|
246
|
+
if (req.user?._id) {
|
|
247
|
+
Sentry.getCurrentScope().setTag("user", req.user._id);
|
|
248
|
+
}
|
|
249
|
+
next();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// OpenAPI
|
|
253
|
+
app.use(openApiEtagMiddleware);
|
|
254
|
+
const oapi = openapi({
|
|
255
|
+
info: {
|
|
256
|
+
description: "Generated docs from an Express api",
|
|
257
|
+
title: "Express Application",
|
|
258
|
+
version: "1.0.0",
|
|
259
|
+
},
|
|
260
|
+
openapi: "3.0.0",
|
|
261
|
+
});
|
|
262
|
+
app.use(oapi);
|
|
263
|
+
|
|
264
|
+
if (process.env.ENABLE_SWAGGER === "true") {
|
|
265
|
+
app.use("/swagger", oapi.swaggerui());
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
addMeRoutes(app, options.userModel as any, options.authOptions);
|
|
269
|
+
|
|
270
|
+
// GitHub OAuth
|
|
271
|
+
if (options.githubAuth) {
|
|
272
|
+
setupGitHubAuth(app, options.userModel as any, options.githubAuth);
|
|
273
|
+
addGitHubAuthRoutes(app, options.userModel as any, options.githubAuth, options.authOptions);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Mount registered model routers and plugins
|
|
277
|
+
for (const registration of this.registrations) {
|
|
278
|
+
if (this.isModelRouterRegistration(registration)) {
|
|
279
|
+
app.use(registration.path, registration.router);
|
|
280
|
+
} else {
|
|
281
|
+
registration.register(app);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Inject openApi into model router options for registered routers
|
|
286
|
+
// The openApi middleware handles this via the oapi instance already mounted on the app
|
|
287
|
+
|
|
288
|
+
Sentry.setupExpressErrorHandler(app);
|
|
289
|
+
|
|
290
|
+
// Error middleware
|
|
291
|
+
app.use(apiUnauthorizedMiddleware);
|
|
292
|
+
app.use(apiErrorMiddleware);
|
|
293
|
+
|
|
294
|
+
app.use(function onError(err: any, _req: any, res: any, _next: any) {
|
|
295
|
+
logger.error(`Fallthrough error: ${err}${err?.stack ? `\n${err.stack}` : ""}}`);
|
|
296
|
+
Sentry.captureException(err);
|
|
297
|
+
res.statusCode = 500;
|
|
298
|
+
res.end(`${res.sentry}\n`);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return app;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Build the Express application and start listening on the configured port.
|
|
306
|
+
*
|
|
307
|
+
* Calls `build()` to configure the application, then starts an HTTP server
|
|
308
|
+
* listening on the port specified by the `PORT` environment variable (default: 9000).
|
|
309
|
+
* If `skipListen` option is true, the app is built but the server is not started.
|
|
310
|
+
*
|
|
311
|
+
* @returns Configured Express application instance
|
|
312
|
+
*
|
|
313
|
+
* @throws Process exits with code 1 if the server fails to start
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```typescript
|
|
317
|
+
* // Start server on port 3000
|
|
318
|
+
* process.env.PORT = "3000";
|
|
319
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
320
|
+
* .register(todoRouter)
|
|
321
|
+
* .start();
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
start(): express.Application {
|
|
325
|
+
const app = this.build();
|
|
326
|
+
|
|
327
|
+
if (!this.options.skipListen) {
|
|
328
|
+
const port = process.env.PORT || "9000";
|
|
329
|
+
try {
|
|
330
|
+
app.listen(port, () => {
|
|
331
|
+
logger.info(`Listening on port ${port}`);
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
logger.error(`Error trying to start HTTP server: ${error}\n${(error as any).stack}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return app;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private isModelRouterRegistration(
|
|
343
|
+
registration: ModelRouterRegistration | TerrenoPlugin
|
|
344
|
+
): registration is ModelRouterRegistration {
|
|
345
|
+
return (registration as ModelRouterRegistration).__type === "modelRouter";
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/terrenoPlugin.ts
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
import type express from "express";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Interface for plugins that can be registered with TerrenoApp.
|
|
5
|
+
*
|
|
6
|
+
* Implement this interface to create reusable plugins that encapsulate
|
|
7
|
+
* routes, middleware, or other Express application setup. Plugins are
|
|
8
|
+
* registered via `TerrenoApp.register()` and are mounted after core
|
|
9
|
+
* authentication and OpenAPI middleware.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* class MyPlugin implements TerrenoPlugin {
|
|
14
|
+
* register(app: express.Application): void {
|
|
15
|
+
* app.get("/my-route", (req, res) => {
|
|
16
|
+
* res.json({ status: "ok" });
|
|
17
|
+
* });
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
22
|
+
* .register(new MyPlugin())
|
|
23
|
+
* .start();
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @see TerrenoApp for the application builder that consumes plugins
|
|
27
|
+
* @see HealthApp for a built-in plugin example
|
|
28
|
+
*/
|
|
3
29
|
export interface TerrenoPlugin {
|
|
30
|
+
/**
|
|
31
|
+
* Register routes and middleware with the Express application.
|
|
32
|
+
*
|
|
33
|
+
* Called during `TerrenoApp.build()` after core middleware has been
|
|
34
|
+
* configured but before error handling middleware is added.
|
|
35
|
+
*
|
|
36
|
+
* @param app - The Express application instance to register with
|
|
37
|
+
*/
|
|
4
38
|
register(app: express.Application): void;
|
|
5
39
|
}
|
package/.claude/CLAUDE.local.md
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
# Terreno
|
|
2
|
-
|
|
3
|
-
A monorepo containing shared packages for building full-stack applications with React Native and Express/Mongoose.
|
|
4
|
-
|
|
5
|
-
## Packages
|
|
6
|
-
|
|
7
|
-
- **api/** - REST API framework built on Express/Mongoose (`@terreno/api`)
|
|
8
|
-
- **ui/** - React Native UI component library (`@terreno/ui`)
|
|
9
|
-
- **rtk/** - Redux Toolkit Query utilities for API backends (`@terreno/rtk`)
|
|
10
|
-
- **demo/** - Demo app for showcasing and testing UI components
|
|
11
|
-
- **example-frontend/** - Example Expo app demonstrating full stack usage
|
|
12
|
-
- **example-backend/** - Example Express backend using @terreno/api
|
|
13
|
-
|
|
14
|
-
## Development
|
|
15
|
-
|
|
16
|
-
Uses [Bun](https://bun.sh/) as the package manager.
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
bun install # Install dependencies
|
|
20
|
-
bun run compile # Compile all packages
|
|
21
|
-
bun run lint # Lint all packages
|
|
22
|
-
bun run lint:fix # Fix lint issues
|
|
23
|
-
bun run test # Run tests in api and ui
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
### Package-specific commands
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
bun run api:test # Test API package
|
|
30
|
-
bun run ui:test # Test UI package
|
|
31
|
-
bun run demo:start # Start demo app
|
|
32
|
-
bun run frontend:web # Start frontend example
|
|
33
|
-
bun run backend:dev # Start backend example
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## How the Packages Work Together
|
|
37
|
-
|
|
38
|
-
The three core packages form a complete full-stack framework:
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
BACKEND
|
|
42
|
-
@terreno/api
|
|
43
|
-
- Mongoose models with modelRouter -> CRUD endpoints
|
|
44
|
-
- Built-in auth (JWT + Passport)
|
|
45
|
-
- Automatic OpenAPI spec generation
|
|
46
|
-
|
|
|
47
|
-
/openapi.json
|
|
48
|
-
|
|
|
49
|
-
RTK Query SDK Codegen
|
|
50
|
-
|
|
|
51
|
-
FRONTEND
|
|
52
|
-
@terreno/rtk
|
|
53
|
-
- Generated hooks from OpenAPI spec
|
|
54
|
-
- Auth slice with JWT token management
|
|
55
|
-
- Automatic token refresh
|
|
56
|
-
+
|
|
57
|
-
@terreno/ui
|
|
58
|
-
- React Native components (Box, Button, TextField, etc.)
|
|
59
|
-
- TerrenoProvider for theming
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Integration Flow
|
|
63
|
-
|
|
64
|
-
1. **Backend (api)**: Define Mongoose models, use `modelRouter` to create CRUD endpoints with permissions
|
|
65
|
-
2. **OpenAPI Generation**: `setupServer` automatically generates `/openapi.json`
|
|
66
|
-
3. **SDK Codegen**: Frontend runs `bun run sdk` to generate RTK Query hooks from OpenAPI spec
|
|
67
|
-
4. **Frontend (rtk + ui)**: Use generated hooks with UI components for type-safe API calls
|
|
68
|
-
|
|
69
|
-
## Example Apps (Keep These Updated!)
|
|
70
|
-
|
|
71
|
-
The `example-frontend/` and `example-backend/` directories serve as both documentation and integration tests. When adding features to api, ui, or rtk:
|
|
72
|
-
|
|
73
|
-
1. **Add examples** demonstrating new features
|
|
74
|
-
2. **Update SDK** after backend changes: `cd example-frontend && bun run sdk`
|
|
75
|
-
3. **Verify integration** by running both examples together
|
|
76
|
-
|
|
77
|
-
### Running the Full Stack
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
# Terminal 1: Start backend
|
|
81
|
-
bun run backend:dev
|
|
82
|
-
|
|
83
|
-
# Terminal 2: Start frontend
|
|
84
|
-
bun run frontend:web
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Code Style
|
|
88
|
-
|
|
89
|
-
### TypeScript/JavaScript
|
|
90
|
-
- Use ES module syntax and TypeScript for all code
|
|
91
|
-
- Prefer interfaces over types; avoid enums, use maps
|
|
92
|
-
- Prefer const arrow functions over `function` keyword
|
|
93
|
-
- Use descriptive variable names with auxiliary verbs (e.g., `isLoading`)
|
|
94
|
-
- Use camelCase directories (e.g., `components/authWizard`)
|
|
95
|
-
- Favor named exports
|
|
96
|
-
- Use the RORO pattern (Receive an Object, Return an Object)
|
|
97
|
-
|
|
98
|
-
### Dates and Time
|
|
99
|
-
- Always use Luxon instead of Date or dayjs
|
|
100
|
-
|
|
101
|
-
### Error Handling
|
|
102
|
-
- Check error conditions at start of functions and return early
|
|
103
|
-
- Limit nested if statements
|
|
104
|
-
- Use multiline syntax with curly braces for all conditionals
|
|
105
|
-
|
|
106
|
-
### Testing
|
|
107
|
-
- Use bun test with expect for testing
|
|
108
|
-
|
|
109
|
-
### Logging
|
|
110
|
-
- Frontend: Use `console.info`, `console.debug`, `console.warn`, or `console.error` for permanent logs
|
|
111
|
-
- Backend: Use `logger.info/warn/error/debug` for permanent logs
|
|
112
|
-
- Use `console.log` only for debugging (to be removed)
|
|
113
|
-
|
|
114
|
-
### Development Practices
|
|
115
|
-
- Don't apologize for errors: fix them
|
|
116
|
-
- Prioritize modularity, DRY, performance, and security
|
|
117
|
-
- Focus on readability over performance
|
|
118
|
-
- Write complete, functional code without TODOs when possible
|
|
119
|
-
- Comments should describe purpose, not effect
|
|
120
|
-
|
|
121
|
-
## Package Reference
|
|
122
|
-
|
|
123
|
-
### @terreno/api
|
|
124
|
-
|
|
125
|
-
REST API framework providing:
|
|
126
|
-
|
|
127
|
-
- **modelRouter**: Auto-generates CRUD endpoints for Mongoose models
|
|
128
|
-
- **Permissions**: `IsAuthenticated`, `IsOwner`, `IsAdmin`, `IsAuthenticatedOrReadOnly`
|
|
129
|
-
- **Query Filters**: `OwnerQueryFilter` for filtering list queries by owner
|
|
130
|
-
- **setupServer**: Express server setup with auth, OpenAPI, and middleware
|
|
131
|
-
- **APIError**: Standardized error handling
|
|
132
|
-
- **logger**: Winston-based logging
|
|
133
|
-
|
|
134
|
-
Key imports:
|
|
135
|
-
```typescript
|
|
136
|
-
import {
|
|
137
|
-
modelRouter,
|
|
138
|
-
setupServer,
|
|
139
|
-
Permissions,
|
|
140
|
-
OwnerQueryFilter,
|
|
141
|
-
APIError,
|
|
142
|
-
logger,
|
|
143
|
-
asyncHandler,
|
|
144
|
-
authenticateMiddleware,
|
|
145
|
-
} from "@terreno/api";
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### @terreno/ui
|
|
149
|
-
|
|
150
|
-
React Native component library with 88+ components:
|
|
151
|
-
|
|
152
|
-
- **Layout**: Box, Page, SplitPage, Card
|
|
153
|
-
- **Forms**: TextField, SelectField, DateTimeField, CheckBox
|
|
154
|
-
- **Display**: Text, Heading, Badge, DataTable
|
|
155
|
-
- **Actions**: Button, IconButton, Link
|
|
156
|
-
- **Feedback**: Spinner, Modal, Toast
|
|
157
|
-
- **Theming**: TerrenoProvider, useTheme
|
|
158
|
-
|
|
159
|
-
Key imports:
|
|
160
|
-
```typescript
|
|
161
|
-
import {
|
|
162
|
-
Box,
|
|
163
|
-
Button,
|
|
164
|
-
Card,
|
|
165
|
-
Page,
|
|
166
|
-
Text,
|
|
167
|
-
TextField,
|
|
168
|
-
TerrenoProvider,
|
|
169
|
-
} from "@terreno/ui";
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### @terreno/rtk
|
|
173
|
-
|
|
174
|
-
Redux Toolkit Query integration:
|
|
175
|
-
|
|
176
|
-
- **generateAuthSlice**: Creates auth reducer and middleware with JWT handling
|
|
177
|
-
- **emptyApi**: Base RTK Query API for code generation
|
|
178
|
-
- **Platform utilities**: Secure token storage (expo-secure-store for native, AsyncStorage for web)
|
|
179
|
-
|
|
180
|
-
Key imports:
|
|
181
|
-
```typescript
|
|
182
|
-
import {generateAuthSlice} from "@terreno/rtk";
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## CI/CD Workflows
|
|
186
|
-
|
|
187
|
-
### Required Secret Validation
|
|
188
|
-
|
|
189
|
-
GitHub Actions workflows that use secrets or environment variables must validate all required variables are set before using them. Add a validation step early in the job that fails fast with a clear error message listing any missing variables.
|
|
190
|
-
|
|
191
|
-
```yaml
|
|
192
|
-
- name: Validate required secrets
|
|
193
|
-
run: |
|
|
194
|
-
missing=()
|
|
195
|
-
if [ -z "$VAR_NAME" ]; then missing+=("VAR_NAME"); fi
|
|
196
|
-
if [ ${#missing[@]} -ne 0 ]; then
|
|
197
|
-
echo "::error::Missing required secrets: ${missing[*]}"
|
|
198
|
-
exit 1
|
|
199
|
-
fi
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
## Dependency Management
|
|
203
|
-
|
|
204
|
-
Uses [Bun Catalogs](https://bun.sh/docs/install/catalogs) - shared versions defined in root `package.json` under `catalog`. Reference with `catalog:` in workspace packages.
|