@terreno/api 0.1.0 → 0.3.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 +3 -3
- 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 +5 -6
- 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,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth setup and initialization for @terreno/api.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to initialize Better Auth with MongoDB,
|
|
5
|
+
* create session middleware, and sync users with the application User model.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {betterAuth} from "better-auth";
|
|
9
|
+
import {mongodbAdapter} from "better-auth/adapters/mongodb";
|
|
10
|
+
import {toNodeHandler} from "better-auth/node";
|
|
11
|
+
import type {Application, NextFunction, Request, Response} from "express";
|
|
12
|
+
import mongoose from "mongoose";
|
|
13
|
+
import type {UserModel} from "./auth";
|
|
14
|
+
import type {BetterAuthConfig, BetterAuthSessionData, BetterAuthUser} from "./betterAuth";
|
|
15
|
+
import {logger} from "./logger";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The Better Auth instance type.
|
|
19
|
+
*/
|
|
20
|
+
export type BetterAuthInstance = ReturnType<typeof betterAuth>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for creating a Better Auth instance.
|
|
24
|
+
*/
|
|
25
|
+
export interface CreateBetterAuthOptions {
|
|
26
|
+
config: BetterAuthConfig;
|
|
27
|
+
mongoClient: any;
|
|
28
|
+
userModel?: UserModel;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a Better Auth instance with MongoDB adapter.
|
|
33
|
+
*/
|
|
34
|
+
export const createBetterAuth = (options: CreateBetterAuthOptions): BetterAuthInstance => {
|
|
35
|
+
const {config, mongoClient} = options;
|
|
36
|
+
|
|
37
|
+
const secret = config.secret || process.env.BETTER_AUTH_SECRET;
|
|
38
|
+
if (!secret) {
|
|
39
|
+
throw new Error("BETTER_AUTH_SECRET must be set in env or config.secret must be provided.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const baseURL = config.baseURL || process.env.BETTER_AUTH_URL;
|
|
43
|
+
if (!baseURL) {
|
|
44
|
+
throw new Error("BETTER_AUTH_URL must be set in env or config.baseURL must be provided.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const basePath = config.basePath ?? "/api/auth";
|
|
48
|
+
|
|
49
|
+
const socialProviders: Record<string, {clientId: string; clientSecret: string}> = {};
|
|
50
|
+
|
|
51
|
+
if (config.googleOAuth) {
|
|
52
|
+
socialProviders.google = {
|
|
53
|
+
clientId: config.googleOAuth.clientId,
|
|
54
|
+
clientSecret: config.googleOAuth.clientSecret,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (config.appleOAuth) {
|
|
59
|
+
socialProviders.apple = {
|
|
60
|
+
clientId: config.appleOAuth.clientId,
|
|
61
|
+
clientSecret: config.appleOAuth.clientSecret,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.githubOAuth) {
|
|
66
|
+
socialProviders.github = {
|
|
67
|
+
clientId: config.githubOAuth.clientId,
|
|
68
|
+
clientSecret: config.githubOAuth.clientSecret,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const auth = betterAuth({
|
|
73
|
+
basePath,
|
|
74
|
+
baseURL,
|
|
75
|
+
database: mongodbAdapter(mongoClient.db()),
|
|
76
|
+
emailAndPassword: {
|
|
77
|
+
enabled: true,
|
|
78
|
+
},
|
|
79
|
+
secret,
|
|
80
|
+
session: {
|
|
81
|
+
cookieCache: {
|
|
82
|
+
enabled: true,
|
|
83
|
+
maxAge: 5 * 60, // 5 minutes
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
socialProviders: Object.keys(socialProviders).length > 0 ? socialProviders : undefined,
|
|
87
|
+
trustedOrigins: config.trustedOrigins ?? [],
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return auth;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates Express middleware that extracts the Better Auth session
|
|
95
|
+
* and populates req.user with the application User model.
|
|
96
|
+
*/
|
|
97
|
+
export const createBetterAuthSessionMiddleware = (
|
|
98
|
+
auth: BetterAuthInstance,
|
|
99
|
+
userModel?: UserModel
|
|
100
|
+
) => {
|
|
101
|
+
return async (req: Request, _res: Response, next: NextFunction): Promise<void> => {
|
|
102
|
+
try {
|
|
103
|
+
const session = await auth.api.getSession({
|
|
104
|
+
headers: req.headers as Record<string, string>,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (session?.user && session?.session) {
|
|
108
|
+
const betterAuthUser = session.user as BetterAuthUser;
|
|
109
|
+
|
|
110
|
+
if (userModel) {
|
|
111
|
+
// Look up the application user by betterAuthId
|
|
112
|
+
const appUser = await userModel.findOne({betterAuthId: betterAuthUser.id});
|
|
113
|
+
if (appUser) {
|
|
114
|
+
(req as any).user = appUser;
|
|
115
|
+
(req as any).betterAuthSession = session;
|
|
116
|
+
} else {
|
|
117
|
+
// User exists in Better Auth but not synced yet - create them
|
|
118
|
+
const newUser = await syncBetterAuthUser(userModel, betterAuthUser);
|
|
119
|
+
(req as any).user = newUser;
|
|
120
|
+
(req as any).betterAuthSession = session;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// No user model - just attach the Better Auth user directly
|
|
124
|
+
(req as any).user = {
|
|
125
|
+
_id: betterAuthUser.id,
|
|
126
|
+
admin: false,
|
|
127
|
+
betterAuthId: betterAuthUser.id,
|
|
128
|
+
email: betterAuthUser.email,
|
|
129
|
+
id: betterAuthUser.id,
|
|
130
|
+
name: betterAuthUser.name,
|
|
131
|
+
};
|
|
132
|
+
(req as any).betterAuthSession = session;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
next();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.debug(`Better Auth session extraction error: ${error}`);
|
|
139
|
+
next();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Syncs a Better Auth user to the application User model.
|
|
146
|
+
* Creates or updates the user as needed.
|
|
147
|
+
*/
|
|
148
|
+
export const syncBetterAuthUser = async (
|
|
149
|
+
userModel: UserModel,
|
|
150
|
+
betterAuthUser: BetterAuthUser,
|
|
151
|
+
oauthProvider?: string
|
|
152
|
+
): Promise<any> => {
|
|
153
|
+
try {
|
|
154
|
+
const existingUser: any = await userModel.findOne({betterAuthId: betterAuthUser.id});
|
|
155
|
+
|
|
156
|
+
if (existingUser) {
|
|
157
|
+
// Update existing user if needed
|
|
158
|
+
existingUser.email = betterAuthUser.email;
|
|
159
|
+
if (betterAuthUser.name) {
|
|
160
|
+
existingUser.name = betterAuthUser.name;
|
|
161
|
+
}
|
|
162
|
+
await existingUser.save();
|
|
163
|
+
return existingUser;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check if user exists by email (migration case)
|
|
167
|
+
const userByEmail: any = await userModel.findOne({email: betterAuthUser.email});
|
|
168
|
+
if (userByEmail) {
|
|
169
|
+
// Link existing user to Better Auth
|
|
170
|
+
userByEmail.betterAuthId = betterAuthUser.id;
|
|
171
|
+
if (oauthProvider) {
|
|
172
|
+
userByEmail.oauthProvider = oauthProvider;
|
|
173
|
+
}
|
|
174
|
+
await userByEmail.save();
|
|
175
|
+
return userByEmail;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Create new user
|
|
179
|
+
const newUser: any = new (userModel as any)({
|
|
180
|
+
admin: false,
|
|
181
|
+
betterAuthId: betterAuthUser.id,
|
|
182
|
+
email: betterAuthUser.email,
|
|
183
|
+
name: betterAuthUser.name || betterAuthUser.email.split("@")[0],
|
|
184
|
+
oauthProvider: oauthProvider || null,
|
|
185
|
+
});
|
|
186
|
+
await newUser.save();
|
|
187
|
+
logger.info(`Created new user from Better Auth: ${newUser.id}`);
|
|
188
|
+
return newUser;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
logger.error(`Error syncing Better Auth user: ${error}`);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Mounts Better Auth routes on the Express app.
|
|
197
|
+
*/
|
|
198
|
+
export const mountBetterAuthRoutes = (
|
|
199
|
+
app: Application,
|
|
200
|
+
auth: BetterAuthInstance,
|
|
201
|
+
basePath = "/api/auth"
|
|
202
|
+
): void => {
|
|
203
|
+
const handler = toNodeHandler(auth);
|
|
204
|
+
|
|
205
|
+
// Mount at the base path with wildcard
|
|
206
|
+
app.all(`${basePath}/*`, (req, res) => {
|
|
207
|
+
return handler(req, res);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
logger.info(`Better Auth routes mounted at ${basePath}/*`);
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Gets the MongoDB client from the mongoose connection.
|
|
215
|
+
*/
|
|
216
|
+
export const getMongoClientFromMongoose = (): any => {
|
|
217
|
+
const connection = mongoose.connection;
|
|
218
|
+
const client = (connection as any).client;
|
|
219
|
+
if (!client) {
|
|
220
|
+
throw new Error("Mongoose is not connected. Ensure MongoDB connection is established first.");
|
|
221
|
+
}
|
|
222
|
+
return client;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Sets up Better Auth user sync hooks.
|
|
227
|
+
* This ensures users created/updated in Better Auth are synced to the application User model.
|
|
228
|
+
*
|
|
229
|
+
* Note: Better Auth doesn't have built-in event hooks, so we rely on the session middleware
|
|
230
|
+
* to create users on first session access.
|
|
231
|
+
*/
|
|
232
|
+
export const setupBetterAuthUserSync = (_auth: BetterAuthInstance, _userModel: UserModel): void => {
|
|
233
|
+
// Better Auth v1.x doesn't expose event hooks for user creation.
|
|
234
|
+
// User sync is handled in createBetterAuthSessionMiddleware when a session is accessed.
|
|
235
|
+
// This function is a placeholder for future versions that may support hooks.
|
|
236
|
+
logger.debug("Better Auth user sync configured (via session middleware)");
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extracts Better Auth session data from the request.
|
|
241
|
+
*/
|
|
242
|
+
export const getBetterAuthSession = (req: Request): BetterAuthSessionData | null => {
|
|
243
|
+
return (req as any).betterAuthSession ?? null;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Checks if the request has a valid Better Auth session.
|
|
248
|
+
*/
|
|
249
|
+
export const hasBetterAuthSession = (req: Request): boolean => {
|
|
250
|
+
return Boolean((req as any).betterAuthSession);
|
|
251
|
+
};
|
package/src/expressServer.ts
CHANGED
|
@@ -177,7 +177,7 @@ function initializeRoutes(
|
|
|
177
177
|
UserModel: UserMongooseModel,
|
|
178
178
|
addRoutes: AddRoutes,
|
|
179
179
|
options: InitializeRoutesOptions = {}
|
|
180
|
-
) {
|
|
180
|
+
): express.Application {
|
|
181
181
|
const app = express();
|
|
182
182
|
|
|
183
183
|
// TODO: Log a warning when we hit the array limit.
|
|
@@ -193,11 +193,10 @@ function initializeRoutes(
|
|
|
193
193
|
options.addMiddleware(app);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
app.use(express.json());
|
|
196
|
+
app.use(express.json({limit: "50mb"}));
|
|
197
197
|
|
|
198
198
|
// Add login/signup/refresh_token before the JWT/auth middlewares
|
|
199
199
|
addAuthRoutes(app, UserModel as any, options?.authOptions);
|
|
200
|
-
|
|
201
200
|
setupAuth(app as any, UserModel as any);
|
|
202
201
|
|
|
203
202
|
if (options.logRequests !== false) {
|
|
@@ -245,7 +244,7 @@ function initializeRoutes(
|
|
|
245
244
|
|
|
246
245
|
addMeRoutes(app, UserModel as any, options?.authOptions);
|
|
247
246
|
|
|
248
|
-
// Set up GitHub OAuth if configured
|
|
247
|
+
// Set up GitHub OAuth if configured (works with JWT auth)
|
|
249
248
|
if (options.githubAuth) {
|
|
250
249
|
setupGitHubAuth(app, UserModel as any, options.githubAuth);
|
|
251
250
|
addGitHubAuthRoutes(app, UserModel as any, options.githubAuth, options.authOptions);
|
|
@@ -297,8 +296,8 @@ export interface SetupServerOptions {
|
|
|
297
296
|
sentryOptions?: Sentry.BunOptions;
|
|
298
297
|
}
|
|
299
298
|
|
|
300
|
-
// Sets up the routes and returns
|
|
301
|
-
export function setupServer(options: SetupServerOptions) {
|
|
299
|
+
// Sets up the routes and returns the app.
|
|
300
|
+
export function setupServer(options: SetupServerOptions): express.Application {
|
|
302
301
|
const UserModel = options.userModel;
|
|
303
302
|
const addRoutes = options.addRoutes;
|
|
304
303
|
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from "./api";
|
|
2
2
|
export * from "./auth";
|
|
3
|
+
export * from "./betterAuth";
|
|
4
|
+
export * from "./betterAuthApp";
|
|
5
|
+
export * from "./betterAuthSetup";
|
|
3
6
|
export * from "./errors";
|
|
4
7
|
export * from "./expressServer";
|
|
5
8
|
export * from "./githubAuth";
|
|
@@ -12,6 +15,7 @@ export * from "./openApiValidator";
|
|
|
12
15
|
export * from "./permissions";
|
|
13
16
|
export * from "./plugins";
|
|
14
17
|
export * from "./populate";
|
|
18
|
+
export * from "./terrenoApp";
|
|
15
19
|
export * from "./terrenoPlugin";
|
|
16
20
|
export * from "./transformers";
|
|
17
21
|
export * from "./utils";
|
package/src/openApiValidator.ts
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {afterEach, beforeEach, describe, expect, it, mock} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import supertest from "supertest";
|
|
4
|
+
|
|
5
|
+
import {modelRouter} from "./api";
|
|
6
|
+
import {Permissions} from "./permissions";
|
|
7
|
+
import {TerrenoApp} from "./terrenoApp";
|
|
8
|
+
import type {TerrenoPlugin} from "./terrenoPlugin";
|
|
9
|
+
import {authAsUser, FoodModel, setupDb, UserModel} from "./tests";
|
|
10
|
+
|
|
11
|
+
describe("TerrenoApp", () => {
|
|
12
|
+
const originalEnv = process.env;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
process.env = {
|
|
16
|
+
...originalEnv,
|
|
17
|
+
REFRESH_TOKEN_SECRET: "test-refresh-secret",
|
|
18
|
+
SESSION_SECRET: "test-session-secret",
|
|
19
|
+
TOKEN_EXPIRES_IN: "1h",
|
|
20
|
+
TOKEN_ISSUER: "test-issuer",
|
|
21
|
+
TOKEN_SECRET: "test-secret",
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.env = originalEnv;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("build", () => {
|
|
30
|
+
it("returns an express application without listening", () => {
|
|
31
|
+
const app = new TerrenoApp({
|
|
32
|
+
skipListen: true,
|
|
33
|
+
userModel: UserModel as any,
|
|
34
|
+
}).build();
|
|
35
|
+
|
|
36
|
+
expect(app).toBeDefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("creates server with custom corsOrigin", () => {
|
|
40
|
+
const app = new TerrenoApp({
|
|
41
|
+
corsOrigin: "https://example.com",
|
|
42
|
+
skipListen: true,
|
|
43
|
+
userModel: UserModel as any,
|
|
44
|
+
}).build();
|
|
45
|
+
|
|
46
|
+
expect(app).toBeDefined();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("start", () => {
|
|
51
|
+
it("returns an express application with skipListen", () => {
|
|
52
|
+
const app = new TerrenoApp({
|
|
53
|
+
skipListen: true,
|
|
54
|
+
userModel: UserModel as any,
|
|
55
|
+
}).start();
|
|
56
|
+
|
|
57
|
+
expect(app).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("register with modelRouter", () => {
|
|
62
|
+
let admin: any;
|
|
63
|
+
|
|
64
|
+
beforeEach(async () => {
|
|
65
|
+
[admin] = await setupDb();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("mounts model router at the specified path", async () => {
|
|
69
|
+
const foodRegistration = modelRouter("/food", FoodModel, {
|
|
70
|
+
allowAnonymous: true,
|
|
71
|
+
permissions: {
|
|
72
|
+
create: [Permissions.IsAny],
|
|
73
|
+
delete: [Permissions.IsAny],
|
|
74
|
+
list: [Permissions.IsAny],
|
|
75
|
+
read: [Permissions.IsAny],
|
|
76
|
+
update: [Permissions.IsAny],
|
|
77
|
+
},
|
|
78
|
+
sort: "-created",
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(foodRegistration.__type).toBe("modelRouter");
|
|
82
|
+
expect(foodRegistration.path).toBe("/food");
|
|
83
|
+
|
|
84
|
+
const app = new TerrenoApp({
|
|
85
|
+
skipListen: true,
|
|
86
|
+
userModel: UserModel as any,
|
|
87
|
+
})
|
|
88
|
+
.register(foodRegistration)
|
|
89
|
+
.build();
|
|
90
|
+
|
|
91
|
+
await FoodModel.create({
|
|
92
|
+
calories: 100,
|
|
93
|
+
name: "Apple",
|
|
94
|
+
ownerId: admin._id,
|
|
95
|
+
source: {name: "Nature"},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const agent = await authAsUser(app, "admin");
|
|
99
|
+
const res = await agent.get("/food").expect(200);
|
|
100
|
+
expect(res.body.data).toHaveLength(1);
|
|
101
|
+
expect(res.body.data[0].name).toBe("Apple");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("supports chaining multiple registrations", async () => {
|
|
105
|
+
const foodRegistration = modelRouter("/food", FoodModel, {
|
|
106
|
+
allowAnonymous: true,
|
|
107
|
+
permissions: {
|
|
108
|
+
create: [Permissions.IsAny],
|
|
109
|
+
delete: [Permissions.IsAny],
|
|
110
|
+
list: [Permissions.IsAny],
|
|
111
|
+
read: [Permissions.IsAny],
|
|
112
|
+
update: [Permissions.IsAny],
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const app = new TerrenoApp({
|
|
117
|
+
skipListen: true,
|
|
118
|
+
userModel: UserModel as any,
|
|
119
|
+
})
|
|
120
|
+
.register(foodRegistration)
|
|
121
|
+
.build();
|
|
122
|
+
|
|
123
|
+
expect(app).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("register with plugin", () => {
|
|
128
|
+
it("calls plugin.register with the express app", () => {
|
|
129
|
+
const registerFn = mock(() => {});
|
|
130
|
+
const plugin: TerrenoPlugin = {
|
|
131
|
+
register: registerFn,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const app = new TerrenoApp({
|
|
135
|
+
skipListen: true,
|
|
136
|
+
userModel: UserModel as any,
|
|
137
|
+
})
|
|
138
|
+
.register(plugin)
|
|
139
|
+
.build();
|
|
140
|
+
|
|
141
|
+
expect(registerFn).toHaveBeenCalledTimes(1);
|
|
142
|
+
// Verify the plugin received the express app
|
|
143
|
+
const calledWith = (registerFn.mock.calls as any[][])[0][0];
|
|
144
|
+
expect(calledWith).toBe(app);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("addMiddleware", () => {
|
|
149
|
+
it("runs request handler middleware", async () => {
|
|
150
|
+
let middlewareCalled = false;
|
|
151
|
+
const middleware: express.RequestHandler = (_req, _res, next) => {
|
|
152
|
+
middlewareCalled = true;
|
|
153
|
+
next();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const app = new TerrenoApp({
|
|
157
|
+
skipListen: true,
|
|
158
|
+
userModel: UserModel as any,
|
|
159
|
+
})
|
|
160
|
+
.addMiddleware(middleware)
|
|
161
|
+
.build();
|
|
162
|
+
|
|
163
|
+
await supertest(app).get("/nonexistent").expect(404);
|
|
164
|
+
expect(middlewareCalled).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("modelRouter overload", () => {
|
|
169
|
+
it("returns ModelRouterRegistration when path is provided", () => {
|
|
170
|
+
const result = modelRouter("/food", FoodModel, {
|
|
171
|
+
permissions: {
|
|
172
|
+
create: [Permissions.IsAny],
|
|
173
|
+
delete: [Permissions.IsAny],
|
|
174
|
+
list: [Permissions.IsAny],
|
|
175
|
+
read: [Permissions.IsAny],
|
|
176
|
+
update: [Permissions.IsAny],
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(result.__type).toBe("modelRouter");
|
|
181
|
+
expect(result.path).toBe("/food");
|
|
182
|
+
expect(result.router).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns express.Router when no path is provided", () => {
|
|
186
|
+
const result = modelRouter(FoodModel, {
|
|
187
|
+
permissions: {
|
|
188
|
+
create: [Permissions.IsAny],
|
|
189
|
+
delete: [Permissions.IsAny],
|
|
190
|
+
list: [Permissions.IsAny],
|
|
191
|
+
read: [Permissions.IsAny],
|
|
192
|
+
update: [Permissions.IsAny],
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Should be a regular router (function), not a ModelRouterRegistration
|
|
197
|
+
expect(typeof result).toBe("function");
|
|
198
|
+
expect((result as any).__type).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|