@terreno/api 0.0.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -3
- package/dist/api.d.ts +96 -3
- package/dist/api.js +159 -11
- package/dist/api.test.js +906 -2
- package/dist/auth.js +3 -1
- 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/errors.js +14 -11
- package/dist/example.js +7 -7
- package/dist/expressServer.js +2 -2
- package/dist/githubAuth.test.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/plugins.test.js +3 -3
- 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 +38 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests.js +34 -24
- package/package.json +8 -2
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.test.ts +743 -2
- package/src/api.ts +270 -6
- package/src/auth.ts +3 -1
- 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/errors.ts +14 -11
- package/src/example.ts +7 -7
- package/src/expressServer.ts +4 -5
- package/src/githubAuth.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/plugins.test.ts +3 -3
- package/src/terrenoApp.test.ts +201 -0
- package/src/terrenoApp.ts +347 -0
- package/src/terrenoPlugin.ts +39 -0
- package/src/tests.ts +34 -24
- package/.cursorrules +0 -107
- package/.windsurfrules +0 -107
- package/AGENTS.md +0 -313
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __values = (this && this.__values) || function(o) {
|
|
36
|
+
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
|
37
|
+
if (m) return m.call(o);
|
|
38
|
+
if (o && typeof o.length === "number") return {
|
|
39
|
+
next: function () {
|
|
40
|
+
if (o && i >= o.length) o = void 0;
|
|
41
|
+
return { value: o && o[i++], done: !o };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
|
45
|
+
};
|
|
46
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
47
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.TerrenoApp = void 0;
|
|
51
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
52
|
+
var openapi_1 = __importDefault(require("@wesleytodd/openapi"));
|
|
53
|
+
var cors_1 = __importDefault(require("cors"));
|
|
54
|
+
var express_1 = __importDefault(require("express"));
|
|
55
|
+
var qs_1 = __importDefault(require("qs"));
|
|
56
|
+
var auth_1 = require("./auth");
|
|
57
|
+
var errors_1 = require("./errors");
|
|
58
|
+
var expressServer_1 = require("./expressServer");
|
|
59
|
+
var githubAuth_1 = require("./githubAuth");
|
|
60
|
+
var logger_1 = require("./logger");
|
|
61
|
+
var openApiEtag_1 = require("./openApiEtag");
|
|
62
|
+
/**
|
|
63
|
+
* Fluent API for building Express applications with Terreno framework.
|
|
64
|
+
*
|
|
65
|
+
* TerrenoApp provides an alternative to `setupServer` using a registration
|
|
66
|
+
* pattern instead of callbacks. Build applications by registering model
|
|
67
|
+
* routers and plugins, then calling `start()` to begin listening.
|
|
68
|
+
*
|
|
69
|
+
* The middleware stack is configured in this order:
|
|
70
|
+
* 1. CORS
|
|
71
|
+
* 2. Custom middleware (via addMiddleware)
|
|
72
|
+
* 3. JSON body parser
|
|
73
|
+
* 4. Auth routes (/auth/login, /auth/signup, etc.)
|
|
74
|
+
* 5. JWT authentication setup
|
|
75
|
+
* 6. Request logging
|
|
76
|
+
* 7. Sentry scopes
|
|
77
|
+
* 8. OpenAPI middleware
|
|
78
|
+
* 9. /auth/me routes
|
|
79
|
+
* 10. GitHub OAuth routes (if enabled)
|
|
80
|
+
* 11. Registered model routers and plugins
|
|
81
|
+
* 12. Error handling middleware
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Basic usage with model routers
|
|
86
|
+
* const todoRouter = modelRouter("/todos", Todo, {
|
|
87
|
+
* permissions: { list: [Permissions.IsAuthenticated], ... },
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
91
|
+
* .register(todoRouter)
|
|
92
|
+
* .register(new HealthApp())
|
|
93
|
+
* .start();
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* // With custom middleware
|
|
99
|
+
* const app = new TerrenoApp({
|
|
100
|
+
* userModel: User,
|
|
101
|
+
* corsOrigin: ["https://app.example.com"],
|
|
102
|
+
* loggingOptions: { logRequests: true },
|
|
103
|
+
* githubAuth: {
|
|
104
|
+
* clientId: process.env.GITHUB_CLIENT_ID!,
|
|
105
|
+
* clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
106
|
+
* callbackURL: process.env.GITHUB_CALLBACK_URL!,
|
|
107
|
+
* },
|
|
108
|
+
* })
|
|
109
|
+
* .addMiddleware((req, res, next) => {
|
|
110
|
+
* res.setHeader("X-Custom-Header", "value");
|
|
111
|
+
* next();
|
|
112
|
+
* })
|
|
113
|
+
* .register(todoRouter)
|
|
114
|
+
* .register(userRouter)
|
|
115
|
+
* .start();
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @see setupServer for the callback-based alternative
|
|
119
|
+
* @see TerrenoPlugin for creating reusable plugins
|
|
120
|
+
* @see modelRouter for creating CRUD route registrations
|
|
121
|
+
*/
|
|
122
|
+
var TerrenoApp = /** @class */ (function () {
|
|
123
|
+
/**
|
|
124
|
+
* Create a new TerrenoApp builder.
|
|
125
|
+
*
|
|
126
|
+
* @param options - Application configuration options including user model and auth settings
|
|
127
|
+
*/
|
|
128
|
+
function TerrenoApp(options) {
|
|
129
|
+
this.registrations = [];
|
|
130
|
+
this.middlewareFns = [];
|
|
131
|
+
this.options = options;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Register a model router or plugin with the application.
|
|
135
|
+
*
|
|
136
|
+
* Model routers are created with `modelRouter("/path", Model, options)` and
|
|
137
|
+
* provide CRUD endpoints. Plugins implement `TerrenoPlugin` interface and
|
|
138
|
+
* can register custom routes and middleware.
|
|
139
|
+
*
|
|
140
|
+
* Registrations are mounted in the order they are added.
|
|
141
|
+
*
|
|
142
|
+
* @param registration - A ModelRouterRegistration from modelRouter() or a TerrenoPlugin instance
|
|
143
|
+
* @returns This TerrenoApp instance for method chaining
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const todoRouter = modelRouter("/todos", Todo, options);
|
|
148
|
+
* const healthPlugin = new HealthApp({ path: "/health" });
|
|
149
|
+
*
|
|
150
|
+
* app.register(todoRouter).register(healthPlugin);
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
TerrenoApp.prototype.register = function (registration) {
|
|
154
|
+
this.registrations.push(registration);
|
|
155
|
+
return this;
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Add custom Express middleware to the application.
|
|
159
|
+
*
|
|
160
|
+
* Middleware is added BEFORE JSON body parsing and authentication setup,
|
|
161
|
+
* allowing you to modify incoming requests early in the middleware stack.
|
|
162
|
+
*
|
|
163
|
+
* @param fn - Express middleware function or a function that configures the app
|
|
164
|
+
* @returns This TerrenoApp instance for method chaining
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* app.addMiddleware((req, res, next) => {
|
|
169
|
+
* res.setHeader("X-Request-ID", req.id);
|
|
170
|
+
* next();
|
|
171
|
+
* });
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
TerrenoApp.prototype.addMiddleware = function (fn) {
|
|
175
|
+
this.middlewareFns.push(fn);
|
|
176
|
+
return this;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Build the Express application without starting the server.
|
|
180
|
+
*
|
|
181
|
+
* Configures the complete middleware stack including:
|
|
182
|
+
* - CORS, JSON parsing, authentication, logging, Sentry, OpenAPI
|
|
183
|
+
* - All registered model routers and plugins
|
|
184
|
+
* - Error handling middleware
|
|
185
|
+
*
|
|
186
|
+
* Use this method when you need the Express app instance for testing
|
|
187
|
+
* or custom server setup. For normal use, call `start()` instead.
|
|
188
|
+
*
|
|
189
|
+
* @returns Configured Express application instance
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
194
|
+
* .register(todoRouter)
|
|
195
|
+
* .build();
|
|
196
|
+
*
|
|
197
|
+
* // Use app for testing with supertest
|
|
198
|
+
* await request(app).get("/todos").expect(200);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
TerrenoApp.prototype.build = function () {
|
|
202
|
+
var e_1, _a, e_2, _b;
|
|
203
|
+
var _c;
|
|
204
|
+
(0, logger_1.setupLogging)(this.options.loggingOptions);
|
|
205
|
+
var app = (0, express_1.default)();
|
|
206
|
+
var options = this.options;
|
|
207
|
+
app.set("query parser", function (str) { var _a; return qs_1.default.parse(str, { arrayLimit: (_a = options.arrayLimit) !== null && _a !== void 0 ? _a : 200 }); });
|
|
208
|
+
app.use((0, cors_1.default)({ origin: (_c = options.corsOrigin) !== null && _c !== void 0 ? _c : "*" }));
|
|
209
|
+
try {
|
|
210
|
+
// Apply custom middleware before JSON parsing
|
|
211
|
+
for (var _d = __values(this.middlewareFns), _e = _d.next(); !_e.done; _e = _d.next()) {
|
|
212
|
+
var fn = _e.value;
|
|
213
|
+
if (fn.length <= 3) {
|
|
214
|
+
// express.RequestHandler (req, res, next)
|
|
215
|
+
app.use(fn);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Function that receives the app
|
|
219
|
+
fn(app);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
224
|
+
finally {
|
|
225
|
+
try {
|
|
226
|
+
if (_e && !_e.done && (_a = _d.return)) _a.call(_d);
|
|
227
|
+
}
|
|
228
|
+
finally { if (e_1) throw e_1.error; }
|
|
229
|
+
}
|
|
230
|
+
app.use(express_1.default.json());
|
|
231
|
+
// Auth routes (login/signup/refresh_token) before JWT middleware
|
|
232
|
+
(0, auth_1.addAuthRoutes)(app, options.userModel, options.authOptions);
|
|
233
|
+
(0, auth_1.setupAuth)(app, options.userModel);
|
|
234
|
+
if (options.logRequests !== false) {
|
|
235
|
+
app.use(expressServer_1.logRequests);
|
|
236
|
+
}
|
|
237
|
+
// Store logging options on the response locals
|
|
238
|
+
app.use(function (_req, res, next) {
|
|
239
|
+
res.locals.loggingOptions = options.loggingOptions;
|
|
240
|
+
next();
|
|
241
|
+
});
|
|
242
|
+
// Sentry scopes
|
|
243
|
+
app.all("*", function (req, _res, next) {
|
|
244
|
+
var _a;
|
|
245
|
+
var transactionId = req.header("X-Transaction-ID");
|
|
246
|
+
var sessionId = req.header("X-Session-ID");
|
|
247
|
+
if (transactionId) {
|
|
248
|
+
Sentry.getCurrentScope().setTag("transaction_id", transactionId);
|
|
249
|
+
}
|
|
250
|
+
if (sessionId) {
|
|
251
|
+
Sentry.getCurrentScope().setTag("session_id", sessionId);
|
|
252
|
+
}
|
|
253
|
+
if ((_a = req.user) === null || _a === void 0 ? void 0 : _a._id) {
|
|
254
|
+
Sentry.getCurrentScope().setTag("user", req.user._id);
|
|
255
|
+
}
|
|
256
|
+
next();
|
|
257
|
+
});
|
|
258
|
+
// OpenAPI
|
|
259
|
+
app.use(openApiEtag_1.openApiEtagMiddleware);
|
|
260
|
+
var oapi = (0, openapi_1.default)({
|
|
261
|
+
info: {
|
|
262
|
+
description: "Generated docs from an Express api",
|
|
263
|
+
title: "Express Application",
|
|
264
|
+
version: "1.0.0",
|
|
265
|
+
},
|
|
266
|
+
openapi: "3.0.0",
|
|
267
|
+
});
|
|
268
|
+
app.use(oapi);
|
|
269
|
+
if (process.env.ENABLE_SWAGGER === "true") {
|
|
270
|
+
app.use("/swagger", oapi.swaggerui());
|
|
271
|
+
}
|
|
272
|
+
(0, auth_1.addMeRoutes)(app, options.userModel, options.authOptions);
|
|
273
|
+
// GitHub OAuth
|
|
274
|
+
if (options.githubAuth) {
|
|
275
|
+
(0, githubAuth_1.setupGitHubAuth)(app, options.userModel, options.githubAuth);
|
|
276
|
+
(0, githubAuth_1.addGitHubAuthRoutes)(app, options.userModel, options.githubAuth, options.authOptions);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
// Mount registered model routers and plugins
|
|
280
|
+
for (var _f = __values(this.registrations), _g = _f.next(); !_g.done; _g = _f.next()) {
|
|
281
|
+
var registration = _g.value;
|
|
282
|
+
if (this.isModelRouterRegistration(registration)) {
|
|
283
|
+
app.use(registration.path, registration.router);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
registration.register(app);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
291
|
+
finally {
|
|
292
|
+
try {
|
|
293
|
+
if (_g && !_g.done && (_b = _f.return)) _b.call(_f);
|
|
294
|
+
}
|
|
295
|
+
finally { if (e_2) throw e_2.error; }
|
|
296
|
+
}
|
|
297
|
+
// Inject openApi into model router options for registered routers
|
|
298
|
+
// The openApi middleware handles this via the oapi instance already mounted on the app
|
|
299
|
+
Sentry.setupExpressErrorHandler(app);
|
|
300
|
+
// Error middleware
|
|
301
|
+
app.use(errors_1.apiUnauthorizedMiddleware);
|
|
302
|
+
app.use(errors_1.apiErrorMiddleware);
|
|
303
|
+
app.use(function onError(err, _req, res, _next) {
|
|
304
|
+
logger_1.logger.error("Fallthrough error: ".concat(err).concat((err === null || err === void 0 ? void 0 : err.stack) ? "\n".concat(err.stack) : "", "}"));
|
|
305
|
+
Sentry.captureException(err);
|
|
306
|
+
res.statusCode = 500;
|
|
307
|
+
res.end("".concat(res.sentry, "\n"));
|
|
308
|
+
});
|
|
309
|
+
return app;
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* Build the Express application and start listening on the configured port.
|
|
313
|
+
*
|
|
314
|
+
* Calls `build()` to configure the application, then starts an HTTP server
|
|
315
|
+
* listening on the port specified by the `PORT` environment variable (default: 9000).
|
|
316
|
+
* If `skipListen` option is true, the app is built but the server is not started.
|
|
317
|
+
*
|
|
318
|
+
* @returns Configured Express application instance
|
|
319
|
+
*
|
|
320
|
+
* @throws Process exits with code 1 if the server fails to start
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* ```typescript
|
|
324
|
+
* // Start server on port 3000
|
|
325
|
+
* process.env.PORT = "3000";
|
|
326
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
327
|
+
* .register(todoRouter)
|
|
328
|
+
* .start();
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
TerrenoApp.prototype.start = function () {
|
|
332
|
+
var app = this.build();
|
|
333
|
+
if (!this.options.skipListen) {
|
|
334
|
+
var port_1 = process.env.PORT || "9000";
|
|
335
|
+
try {
|
|
336
|
+
app.listen(port_1, function () {
|
|
337
|
+
logger_1.logger.info("Listening on port ".concat(port_1));
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
logger_1.logger.error("Error trying to start HTTP server: ".concat(error, "\n").concat(error.stack));
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return app;
|
|
346
|
+
};
|
|
347
|
+
TerrenoApp.prototype.isModelRouterRegistration = function (registration) {
|
|
348
|
+
return registration.__type === "modelRouter";
|
|
349
|
+
};
|
|
350
|
+
return TerrenoApp;
|
|
351
|
+
}());
|
|
352
|
+
exports.TerrenoApp = TerrenoApp;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
24
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __read = (this && this.__read) || function (o, n) {
|
|
50
|
+
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
|
51
|
+
if (!m) return o;
|
|
52
|
+
var i = m.call(o), r, ar = [], e;
|
|
53
|
+
try {
|
|
54
|
+
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) { e = { error: error }; }
|
|
57
|
+
finally {
|
|
58
|
+
try {
|
|
59
|
+
if (r && !r.done && (m = i["return"])) m.call(i);
|
|
60
|
+
}
|
|
61
|
+
finally { if (e) throw e.error; }
|
|
62
|
+
}
|
|
63
|
+
return ar;
|
|
64
|
+
};
|
|
65
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
66
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
67
|
+
};
|
|
68
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
69
|
+
var bun_test_1 = require("bun:test");
|
|
70
|
+
var supertest_1 = __importDefault(require("supertest"));
|
|
71
|
+
var api_1 = require("./api");
|
|
72
|
+
var permissions_1 = require("./permissions");
|
|
73
|
+
var terrenoApp_1 = require("./terrenoApp");
|
|
74
|
+
var tests_1 = require("./tests");
|
|
75
|
+
(0, bun_test_1.describe)("TerrenoApp", function () {
|
|
76
|
+
var originalEnv = process.env;
|
|
77
|
+
(0, bun_test_1.beforeEach)(function () {
|
|
78
|
+
process.env = __assign(__assign({}, originalEnv), { REFRESH_TOKEN_SECRET: "test-refresh-secret", SESSION_SECRET: "test-session-secret", TOKEN_EXPIRES_IN: "1h", TOKEN_ISSUER: "test-issuer", TOKEN_SECRET: "test-secret" });
|
|
79
|
+
});
|
|
80
|
+
(0, bun_test_1.afterEach)(function () {
|
|
81
|
+
process.env = originalEnv;
|
|
82
|
+
});
|
|
83
|
+
(0, bun_test_1.describe)("build", function () {
|
|
84
|
+
(0, bun_test_1.it)("returns an express application without listening", function () {
|
|
85
|
+
var app = new terrenoApp_1.TerrenoApp({
|
|
86
|
+
skipListen: true,
|
|
87
|
+
userModel: tests_1.UserModel,
|
|
88
|
+
}).build();
|
|
89
|
+
(0, bun_test_1.expect)(app).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
(0, bun_test_1.it)("creates server with custom corsOrigin", function () {
|
|
92
|
+
var app = new terrenoApp_1.TerrenoApp({
|
|
93
|
+
corsOrigin: "https://example.com",
|
|
94
|
+
skipListen: true,
|
|
95
|
+
userModel: tests_1.UserModel,
|
|
96
|
+
}).build();
|
|
97
|
+
(0, bun_test_1.expect)(app).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
(0, bun_test_1.describe)("start", function () {
|
|
101
|
+
(0, bun_test_1.it)("returns an express application with skipListen", function () {
|
|
102
|
+
var app = new terrenoApp_1.TerrenoApp({
|
|
103
|
+
skipListen: true,
|
|
104
|
+
userModel: tests_1.UserModel,
|
|
105
|
+
}).start();
|
|
106
|
+
(0, bun_test_1.expect)(app).toBeDefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
(0, bun_test_1.describe)("register with modelRouter", function () {
|
|
110
|
+
var admin;
|
|
111
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
112
|
+
var _a;
|
|
113
|
+
return __generator(this, function (_b) {
|
|
114
|
+
switch (_b.label) {
|
|
115
|
+
case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
|
|
116
|
+
case 1:
|
|
117
|
+
_a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
|
|
118
|
+
return [2 /*return*/];
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}); });
|
|
122
|
+
(0, bun_test_1.it)("mounts model router at the specified path", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
123
|
+
var foodRegistration, app, agent, res;
|
|
124
|
+
return __generator(this, function (_a) {
|
|
125
|
+
switch (_a.label) {
|
|
126
|
+
case 0:
|
|
127
|
+
foodRegistration = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
|
|
128
|
+
allowAnonymous: true,
|
|
129
|
+
permissions: {
|
|
130
|
+
create: [permissions_1.Permissions.IsAny],
|
|
131
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
132
|
+
list: [permissions_1.Permissions.IsAny],
|
|
133
|
+
read: [permissions_1.Permissions.IsAny],
|
|
134
|
+
update: [permissions_1.Permissions.IsAny],
|
|
135
|
+
},
|
|
136
|
+
sort: "-created",
|
|
137
|
+
});
|
|
138
|
+
(0, bun_test_1.expect)(foodRegistration.__type).toBe("modelRouter");
|
|
139
|
+
(0, bun_test_1.expect)(foodRegistration.path).toBe("/food");
|
|
140
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
141
|
+
skipListen: true,
|
|
142
|
+
userModel: tests_1.UserModel,
|
|
143
|
+
})
|
|
144
|
+
.register(foodRegistration)
|
|
145
|
+
.build();
|
|
146
|
+
return [4 /*yield*/, tests_1.FoodModel.create({
|
|
147
|
+
calories: 100,
|
|
148
|
+
name: "Apple",
|
|
149
|
+
ownerId: admin._id,
|
|
150
|
+
source: { name: "Nature" },
|
|
151
|
+
})];
|
|
152
|
+
case 1:
|
|
153
|
+
_a.sent();
|
|
154
|
+
return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
|
|
155
|
+
case 2:
|
|
156
|
+
agent = _a.sent();
|
|
157
|
+
return [4 /*yield*/, agent.get("/food").expect(200)];
|
|
158
|
+
case 3:
|
|
159
|
+
res = _a.sent();
|
|
160
|
+
(0, bun_test_1.expect)(res.body.data).toHaveLength(1);
|
|
161
|
+
(0, bun_test_1.expect)(res.body.data[0].name).toBe("Apple");
|
|
162
|
+
return [2 /*return*/];
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}); });
|
|
166
|
+
(0, bun_test_1.it)("supports chaining multiple registrations", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
167
|
+
var foodRegistration, app;
|
|
168
|
+
return __generator(this, function (_a) {
|
|
169
|
+
foodRegistration = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
|
|
170
|
+
allowAnonymous: true,
|
|
171
|
+
permissions: {
|
|
172
|
+
create: [permissions_1.Permissions.IsAny],
|
|
173
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
174
|
+
list: [permissions_1.Permissions.IsAny],
|
|
175
|
+
read: [permissions_1.Permissions.IsAny],
|
|
176
|
+
update: [permissions_1.Permissions.IsAny],
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
180
|
+
skipListen: true,
|
|
181
|
+
userModel: tests_1.UserModel,
|
|
182
|
+
})
|
|
183
|
+
.register(foodRegistration)
|
|
184
|
+
.build();
|
|
185
|
+
(0, bun_test_1.expect)(app).toBeDefined();
|
|
186
|
+
return [2 /*return*/];
|
|
187
|
+
});
|
|
188
|
+
}); });
|
|
189
|
+
});
|
|
190
|
+
(0, bun_test_1.describe)("register with plugin", function () {
|
|
191
|
+
(0, bun_test_1.it)("calls plugin.register with the express app", function () {
|
|
192
|
+
var registerFn = (0, bun_test_1.mock)(function () { });
|
|
193
|
+
var plugin = {
|
|
194
|
+
register: registerFn,
|
|
195
|
+
};
|
|
196
|
+
var app = new terrenoApp_1.TerrenoApp({
|
|
197
|
+
skipListen: true,
|
|
198
|
+
userModel: tests_1.UserModel,
|
|
199
|
+
})
|
|
200
|
+
.register(plugin)
|
|
201
|
+
.build();
|
|
202
|
+
(0, bun_test_1.expect)(registerFn).toHaveBeenCalledTimes(1);
|
|
203
|
+
// Verify the plugin received the express app
|
|
204
|
+
var calledWith = registerFn.mock.calls[0][0];
|
|
205
|
+
(0, bun_test_1.expect)(calledWith).toBe(app);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
(0, bun_test_1.describe)("addMiddleware", function () {
|
|
209
|
+
(0, bun_test_1.it)("runs request handler middleware", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
210
|
+
var middlewareCalled, middleware, app;
|
|
211
|
+
return __generator(this, function (_a) {
|
|
212
|
+
switch (_a.label) {
|
|
213
|
+
case 0:
|
|
214
|
+
middlewareCalled = false;
|
|
215
|
+
middleware = function (_req, _res, next) {
|
|
216
|
+
middlewareCalled = true;
|
|
217
|
+
next();
|
|
218
|
+
};
|
|
219
|
+
app = new terrenoApp_1.TerrenoApp({
|
|
220
|
+
skipListen: true,
|
|
221
|
+
userModel: tests_1.UserModel,
|
|
222
|
+
})
|
|
223
|
+
.addMiddleware(middleware)
|
|
224
|
+
.build();
|
|
225
|
+
return [4 /*yield*/, (0, supertest_1.default)(app).get("/nonexistent").expect(404)];
|
|
226
|
+
case 1:
|
|
227
|
+
_a.sent();
|
|
228
|
+
(0, bun_test_1.expect)(middlewareCalled).toBe(true);
|
|
229
|
+
return [2 /*return*/];
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}); });
|
|
233
|
+
});
|
|
234
|
+
(0, bun_test_1.describe)("modelRouter overload", function () {
|
|
235
|
+
(0, bun_test_1.it)("returns ModelRouterRegistration when path is provided", function () {
|
|
236
|
+
var result = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
|
|
237
|
+
permissions: {
|
|
238
|
+
create: [permissions_1.Permissions.IsAny],
|
|
239
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
240
|
+
list: [permissions_1.Permissions.IsAny],
|
|
241
|
+
read: [permissions_1.Permissions.IsAny],
|
|
242
|
+
update: [permissions_1.Permissions.IsAny],
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
(0, bun_test_1.expect)(result.__type).toBe("modelRouter");
|
|
246
|
+
(0, bun_test_1.expect)(result.path).toBe("/food");
|
|
247
|
+
(0, bun_test_1.expect)(result.router).toBeDefined();
|
|
248
|
+
});
|
|
249
|
+
(0, bun_test_1.it)("returns express.Router when no path is provided", function () {
|
|
250
|
+
var result = (0, api_1.modelRouter)(tests_1.FoodModel, {
|
|
251
|
+
permissions: {
|
|
252
|
+
create: [permissions_1.Permissions.IsAny],
|
|
253
|
+
delete: [permissions_1.Permissions.IsAny],
|
|
254
|
+
list: [permissions_1.Permissions.IsAny],
|
|
255
|
+
read: [permissions_1.Permissions.IsAny],
|
|
256
|
+
update: [permissions_1.Permissions.IsAny],
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
// Should be a regular router (function), not a ModelRouterRegistration
|
|
260
|
+
(0, bun_test_1.expect)(typeof result).toBe("function");
|
|
261
|
+
(0, bun_test_1.expect)(result.__type).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type express from "express";
|
|
2
|
+
/**
|
|
3
|
+
* Interface for plugins that can be registered with TerrenoApp.
|
|
4
|
+
*
|
|
5
|
+
* Implement this interface to create reusable plugins that encapsulate
|
|
6
|
+
* routes, middleware, or other Express application setup. Plugins are
|
|
7
|
+
* registered via `TerrenoApp.register()` and are mounted after core
|
|
8
|
+
* authentication and OpenAPI middleware.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* class MyPlugin implements TerrenoPlugin {
|
|
13
|
+
* register(app: express.Application): void {
|
|
14
|
+
* app.get("/my-route", (req, res) => {
|
|
15
|
+
* res.json({ status: "ok" });
|
|
16
|
+
* });
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* const app = new TerrenoApp({ userModel: User })
|
|
21
|
+
* .register(new MyPlugin())
|
|
22
|
+
* .start();
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @see TerrenoApp for the application builder that consumes plugins
|
|
26
|
+
* @see HealthApp for a built-in plugin example
|
|
27
|
+
*/
|
|
28
|
+
export interface TerrenoPlugin {
|
|
29
|
+
/**
|
|
30
|
+
* Register routes and middleware with the Express application.
|
|
31
|
+
*
|
|
32
|
+
* Called during `TerrenoApp.build()` after core middleware has been
|
|
33
|
+
* configured but before error handling middleware is added.
|
|
34
|
+
*
|
|
35
|
+
* @param app - The Express application instance to register with
|
|
36
|
+
*/
|
|
37
|
+
register(app: express.Application): void;
|
|
38
|
+
}
|