@terreno/api 0.11.8 → 0.11.9
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/betterAuthSetup.js +10 -3
- package/dist/githubAuth.d.ts +3 -3
- package/dist/githubAuth.js +16 -16
- package/dist/middleware.test.d.ts +1 -0
- package/dist/middleware.test.js +82 -0
- package/dist/notifiers/googleChatNotifier.js +4 -3
- package/dist/plugins.d.ts +12 -1
- package/dist/plugins.js +34 -1
- package/dist/plugins.test.js +190 -8
- package/dist/types/consentForm.d.ts +4 -2
- package/package.json +1 -1
- package/src/betterAuthSetup.ts +10 -3
- package/src/githubAuth.ts +11 -10
- package/src/middleware.test.ts +71 -0
- package/src/notifiers/googleChatNotifier.ts +4 -3
- package/src/plugins.test.ts +101 -0
- package/src/plugins.ts +35 -0
- package/src/types/consentForm.ts +6 -4
package/dist/betterAuthSetup.js
CHANGED
|
@@ -62,6 +62,7 @@ var mongodb_1 = require("better-auth/adapters/mongodb");
|
|
|
62
62
|
var node_1 = require("better-auth/node");
|
|
63
63
|
var mongoose_1 = __importDefault(require("mongoose"));
|
|
64
64
|
var logger_1 = require("./logger");
|
|
65
|
+
var plugins_1 = require("./plugins");
|
|
65
66
|
/**
|
|
66
67
|
* Creates a Better Auth instance with MongoDB adapter.
|
|
67
68
|
*/
|
|
@@ -135,7 +136,9 @@ var createBetterAuthSessionMiddleware = function (auth, userModel) {
|
|
|
135
136
|
if (!((session === null || session === void 0 ? void 0 : session.user) && (session === null || session === void 0 ? void 0 : session.session))) return [3 /*break*/, 7];
|
|
136
137
|
betterAuthUser = session.user;
|
|
137
138
|
if (!userModel) return [3 /*break*/, 6];
|
|
138
|
-
return [4 /*yield*/,
|
|
139
|
+
return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(userModel, {
|
|
140
|
+
betterAuthId: betterAuthUser.id,
|
|
141
|
+
})];
|
|
139
142
|
case 2:
|
|
140
143
|
appUser = _a.sent();
|
|
141
144
|
if (!appUser) return [3 /*break*/, 3];
|
|
@@ -185,7 +188,9 @@ var syncBetterAuthUser = function (userModel, betterAuthUser, oauthProvider) { r
|
|
|
185
188
|
switch (_a.label) {
|
|
186
189
|
case 0:
|
|
187
190
|
_a.trys.push([0, 8, , 9]);
|
|
188
|
-
return [4 /*yield*/,
|
|
191
|
+
return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(userModel, {
|
|
192
|
+
betterAuthId: betterAuthUser.id,
|
|
193
|
+
})];
|
|
189
194
|
case 1:
|
|
190
195
|
existingUser = _a.sent();
|
|
191
196
|
if (!existingUser) return [3 /*break*/, 3];
|
|
@@ -198,7 +203,9 @@ var syncBetterAuthUser = function (userModel, betterAuthUser, oauthProvider) { r
|
|
|
198
203
|
case 2:
|
|
199
204
|
_a.sent();
|
|
200
205
|
return [2 /*return*/, existingUser];
|
|
201
|
-
case 3: return [4 /*yield*/,
|
|
206
|
+
case 3: return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(userModel, {
|
|
207
|
+
email: betterAuthUser.email,
|
|
208
|
+
})];
|
|
202
209
|
case 4:
|
|
203
210
|
userByEmail = _a.sent();
|
|
204
211
|
if (!userByEmail) return [3 /*break*/, 6];
|
package/dist/githubAuth.d.ts
CHANGED
|
@@ -46,12 +46,12 @@ export interface GitHubUserFields {
|
|
|
46
46
|
* userSchema.plugin(githubUserPlugin);
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
|
-
export declare
|
|
49
|
+
export declare const githubUserPlugin: (schema: any) => void;
|
|
50
50
|
/**
|
|
51
51
|
* Sets up GitHub OAuth authentication strategy.
|
|
52
52
|
* Call this after setupAuth() in your server initialization.
|
|
53
53
|
*/
|
|
54
|
-
export declare
|
|
54
|
+
export declare const setupGitHubAuth: (_app: express.Application, userModel: UserModel, githubOptions: GitHubAuthOptions) => void;
|
|
55
55
|
/**
|
|
56
56
|
* Adds GitHub OAuth routes to the Express application.
|
|
57
57
|
*
|
|
@@ -61,4 +61,4 @@ export declare function setupGitHubAuth(_app: express.Application, userModel: Us
|
|
|
61
61
|
* - POST /auth/github/link - Links GitHub account to authenticated user (requires JWT auth)
|
|
62
62
|
* - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
|
|
63
63
|
*/
|
|
64
|
-
export declare
|
|
64
|
+
export declare const addGitHubAuthRoutes: (app: express.Application, userModel: UserModel, githubOptions: GitHubAuthOptions, authOptions?: AuthOptions) => void;
|
package/dist/githubAuth.js
CHANGED
|
@@ -39,14 +39,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.githubUserPlugin =
|
|
43
|
-
exports.setupGitHubAuth = setupGitHubAuth;
|
|
44
|
-
exports.addGitHubAuthRoutes = addGitHubAuthRoutes;
|
|
42
|
+
exports.addGitHubAuthRoutes = exports.setupGitHubAuth = exports.githubUserPlugin = void 0;
|
|
45
43
|
var passport_1 = __importDefault(require("passport"));
|
|
46
44
|
var passport_github2_1 = require("passport-github2");
|
|
47
45
|
var auth_1 = require("./auth");
|
|
48
46
|
var errors_1 = require("./errors");
|
|
49
47
|
var logger_1 = require("./logger");
|
|
48
|
+
var plugins_1 = require("./plugins");
|
|
50
49
|
/**
|
|
51
50
|
* Plugin to add GitHub authentication fields to a user schema.
|
|
52
51
|
* Apply this plugin to your User schema if you want to enable GitHub auth.
|
|
@@ -58,20 +57,20 @@ var logger_1 = require("./logger");
|
|
|
58
57
|
* userSchema.plugin(githubUserPlugin);
|
|
59
58
|
* ```
|
|
60
59
|
*/
|
|
61
|
-
function
|
|
60
|
+
var githubUserPlugin = function (schema) {
|
|
62
61
|
schema.add({
|
|
63
62
|
githubAvatarUrl: { type: String },
|
|
64
63
|
githubId: { index: true, sparse: true, type: String, unique: true },
|
|
65
64
|
githubProfileUrl: { type: String },
|
|
66
65
|
githubUsername: { type: String },
|
|
67
66
|
});
|
|
68
|
-
}
|
|
67
|
+
};
|
|
68
|
+
exports.githubUserPlugin = githubUserPlugin;
|
|
69
69
|
/**
|
|
70
70
|
* Sets up GitHub OAuth authentication strategy.
|
|
71
71
|
* Call this after setupAuth() in your server initialization.
|
|
72
72
|
*/
|
|
73
|
-
function
|
|
74
|
-
var _this = this;
|
|
73
|
+
var setupGitHubAuth = function (_app, userModel, githubOptions) {
|
|
75
74
|
var _a;
|
|
76
75
|
var scope = (_a = githubOptions.scope) !== null && _a !== void 0 ? _a : ["user:email"];
|
|
77
76
|
passport_1.default.use("github", new passport_github2_1.Strategy({
|
|
@@ -80,7 +79,7 @@ function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
|
80
79
|
clientSecret: githubOptions.clientSecret,
|
|
81
80
|
passReqToCallback: true,
|
|
82
81
|
scope: scope,
|
|
83
|
-
}, (function (req, accessToken, refreshToken, profile, done) { return __awaiter(
|
|
82
|
+
}, (function (req, accessToken, refreshToken, profile, done) { return __awaiter(void 0, void 0, void 0, function () {
|
|
84
83
|
var existingUser, user, githubId, existingGitHubUser, user, email, existingEmailUser, newUser, error_1;
|
|
85
84
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
86
85
|
return __generator(this, function (_j) {
|
|
@@ -95,7 +94,7 @@ function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
|
95
94
|
return [2 /*return*/, done(null, user)];
|
|
96
95
|
case 2:
|
|
97
96
|
githubId = profile.id;
|
|
98
|
-
return [4 /*yield*/,
|
|
97
|
+
return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(userModel, { githubId: githubId })];
|
|
99
98
|
case 3:
|
|
100
99
|
existingGitHubUser = _j.sent();
|
|
101
100
|
if (!existingUser) return [3 /*break*/, 7];
|
|
@@ -129,7 +128,7 @@ function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
|
129
128
|
}
|
|
130
129
|
email = (_d = (_c = profile.emails) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.value;
|
|
131
130
|
if (!email) return [3 /*break*/, 11];
|
|
132
|
-
return [4 /*yield*/,
|
|
131
|
+
return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(userModel, { email: email })];
|
|
133
132
|
case 8:
|
|
134
133
|
existingEmailUser = _j.sent();
|
|
135
134
|
if (!existingEmailUser) return [3 /*break*/, 11];
|
|
@@ -167,7 +166,8 @@ function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
|
167
166
|
}
|
|
168
167
|
});
|
|
169
168
|
}); })));
|
|
170
|
-
}
|
|
169
|
+
};
|
|
170
|
+
exports.setupGitHubAuth = setupGitHubAuth;
|
|
171
171
|
/**
|
|
172
172
|
* Adds GitHub OAuth routes to the Express application.
|
|
173
173
|
*
|
|
@@ -177,8 +177,7 @@ function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
|
177
177
|
* - POST /auth/github/link - Links GitHub account to authenticated user (requires JWT auth)
|
|
178
178
|
* - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
|
|
179
179
|
*/
|
|
180
|
-
function
|
|
181
|
-
var _this = this;
|
|
180
|
+
var addGitHubAuthRoutes = function (app, userModel, githubOptions, authOptions) {
|
|
182
181
|
var router = require("express").Router();
|
|
183
182
|
// Initiate GitHub OAuth flow
|
|
184
183
|
router.get("/github", function (req, _res, next) {
|
|
@@ -194,7 +193,7 @@ function addGitHubAuthRoutes(app, userModel, githubOptions, authOptions) {
|
|
|
194
193
|
router.get("/github/callback", passport_1.default.authenticate("github", {
|
|
195
194
|
failureRedirect: "/auth/github/failure",
|
|
196
195
|
session: false,
|
|
197
|
-
}), function (req, res) { return __awaiter(
|
|
196
|
+
}), function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
198
197
|
var tokens, returnTo, url, error_2;
|
|
199
198
|
var _a, _b, _c, _d;
|
|
200
199
|
return __generator(this, function (_e) {
|
|
@@ -249,7 +248,7 @@ function addGitHubAuthRoutes(app, userModel, githubOptions, authOptions) {
|
|
|
249
248
|
})(req, res, next);
|
|
250
249
|
}, passport_1.default.authenticate("github", { session: false }));
|
|
251
250
|
// Unlink GitHub from user account
|
|
252
|
-
router.delete("/github/unlink", passport_1.default.authenticate("jwt", { session: false }), function (req, res) { return __awaiter(
|
|
251
|
+
router.delete("/github/unlink", passport_1.default.authenticate("jwt", { session: false }), function (req, res) { return __awaiter(void 0, void 0, void 0, function () {
|
|
253
252
|
var user, hasPassword, error_3;
|
|
254
253
|
return __generator(this, function (_a) {
|
|
255
254
|
switch (_a.label) {
|
|
@@ -290,4 +289,5 @@ function addGitHubAuthRoutes(app, userModel, githubOptions, authOptions) {
|
|
|
290
289
|
}); });
|
|
291
290
|
}
|
|
292
291
|
app.use("/auth", router);
|
|
293
|
-
}
|
|
292
|
+
};
|
|
293
|
+
exports.addGitHubAuthRoutes = addGitHubAuthRoutes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
var bun_test_1 = require("bun:test");
|
|
37
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
38
|
+
var middleware_1 = require("./middleware");
|
|
39
|
+
var buildReq = function (headers) {
|
|
40
|
+
return {
|
|
41
|
+
get: function (name) { return headers[name]; },
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
var buildNext = function () { return (0, bun_test_1.mock)(function () { }); };
|
|
45
|
+
(0, bun_test_1.describe)("sentryAppVersionMiddleware", function () {
|
|
46
|
+
var setTagMock;
|
|
47
|
+
(0, bun_test_1.beforeEach)(function () {
|
|
48
|
+
// bunSetup.ts mocks @sentry/bun so that getCurrentScope() returns a scope
|
|
49
|
+
// with a Bun mock setTag. Clear that mock between tests so each assertion
|
|
50
|
+
// sees only its own calls.
|
|
51
|
+
setTagMock = Sentry.getCurrentScope().setTag;
|
|
52
|
+
setTagMock.mockClear();
|
|
53
|
+
});
|
|
54
|
+
(0, bun_test_1.it)("sets the app_version tag when the App-Version header is present", function () {
|
|
55
|
+
var next = buildNext();
|
|
56
|
+
var req = buildReq({ "App-Version": "1.2.3" });
|
|
57
|
+
(0, middleware_1.sentryAppVersionMiddleware)(req, {}, next);
|
|
58
|
+
(0, bun_test_1.expect)(setTagMock).toHaveBeenCalledTimes(1);
|
|
59
|
+
(0, bun_test_1.expect)(setTagMock.mock.calls[0]).toEqual(["app_version", "1.2.3"]);
|
|
60
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
(0, bun_test_1.it)("does not set a tag when the App-Version header is missing", function () {
|
|
63
|
+
var next = buildNext();
|
|
64
|
+
var req = buildReq({});
|
|
65
|
+
(0, middleware_1.sentryAppVersionMiddleware)(req, {}, next);
|
|
66
|
+
(0, bun_test_1.expect)(setTagMock).not.toHaveBeenCalled();
|
|
67
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
68
|
+
});
|
|
69
|
+
(0, bun_test_1.it)("does not set a tag when the App-Version header is an empty string", function () {
|
|
70
|
+
var next = buildNext();
|
|
71
|
+
var req = buildReq({ "App-Version": "" });
|
|
72
|
+
(0, middleware_1.sentryAppVersionMiddleware)(req, {}, next);
|
|
73
|
+
(0, bun_test_1.expect)(setTagMock).not.toHaveBeenCalled();
|
|
74
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
75
|
+
});
|
|
76
|
+
(0, bun_test_1.it)("calls next exactly once with no arguments when the header is present", function () {
|
|
77
|
+
var next = buildNext();
|
|
78
|
+
(0, middleware_1.sentryAppVersionMiddleware)(buildReq({ "App-Version": "9.9.9" }), {}, next);
|
|
79
|
+
(0, bun_test_1.expect)(next).toHaveBeenCalledTimes(1);
|
|
80
|
+
(0, bun_test_1.expect)(next.mock.calls[0]).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -108,7 +108,7 @@ var sendToGoogleChat = function (messageText_1) {
|
|
|
108
108
|
args_1[_i - 1] = arguments[_i];
|
|
109
109
|
}
|
|
110
110
|
return __awaiter(void 0, __spreadArray([messageText_1], __read(args_1), false), void 0, function (messageText, _a) {
|
|
111
|
-
var chatWebhooksString, msg, chatWebhooks, chatChannel, chatWebhookUrl, msg, formattedMessageText, error_1;
|
|
111
|
+
var chatWebhooksString, msg, chatWebhooks, chatChannel, chatWebhookUrl, msg, formattedMessageText, error_1, errorObj;
|
|
112
112
|
var _b, _c, _d;
|
|
113
113
|
var _e = _a === void 0 ? {} : _a, channel = _e.channel, _f = _e.shouldThrow, shouldThrow = _f === void 0 ? false : _f, env = _e.env;
|
|
114
114
|
return __generator(this, function (_g) {
|
|
@@ -143,12 +143,13 @@ var sendToGoogleChat = function (messageText_1) {
|
|
|
143
143
|
return [3 /*break*/, 4];
|
|
144
144
|
case 3:
|
|
145
145
|
error_1 = _g.sent();
|
|
146
|
-
|
|
146
|
+
errorObj = error_1;
|
|
147
|
+
logger_1.logger.error("Error posting to Google Chat: ".concat((_c = errorObj.text) !== null && _c !== void 0 ? _c : errorObj.message));
|
|
147
148
|
Sentry.captureException(error_1);
|
|
148
149
|
if (shouldThrow) {
|
|
149
150
|
throw new errors_1.APIError({
|
|
150
151
|
status: 500,
|
|
151
|
-
title: "Error posting to Google Chat: ".concat((_d =
|
|
152
|
+
title: "Error posting to Google Chat: ".concat((_d = errorObj.text) !== null && _d !== void 0 ? _d : errorObj.message),
|
|
152
153
|
});
|
|
153
154
|
}
|
|
154
155
|
return [3 /*break*/, 4];
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Document, type Schema, SchemaType, type SchemaTypeOptions } from "mongoose";
|
|
1
|
+
import mongoose, { type Document, type FilterQuery, type Schema, SchemaType, type SchemaTypeOptions } from "mongoose";
|
|
2
2
|
import { type APIErrorConstructor } from "./errors";
|
|
3
3
|
export interface BaseUser {
|
|
4
4
|
admin: boolean;
|
|
@@ -31,6 +31,17 @@ export declare const firebaseJWTPlugin: (schema: Schema) => void;
|
|
|
31
31
|
* @param schema Mongoose Schema
|
|
32
32
|
*/
|
|
33
33
|
export declare const findOneOrNone: <T>(schema: Schema<T>) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Helper that performs a `findOneOrNone` lookup against any Mongoose model. Returns the matching
|
|
36
|
+
* document, `null` if none match, or throws if more than one matches. If the model's schema has
|
|
37
|
+
* the {@link findOneOrNone} plugin applied, the plugin static is used; otherwise the lookup is
|
|
38
|
+
* performed directly via `model.find(...)`. Prefer this helper from framework code where the
|
|
39
|
+
* consumer's model may or may not have the plugin installed.
|
|
40
|
+
* @param model Mongoose Model
|
|
41
|
+
* @param query Mongoose query object
|
|
42
|
+
* @param errorArgs Optional overrides for the thrown {@link APIError} when multiple match
|
|
43
|
+
*/
|
|
44
|
+
export declare const findOneOrNoneFor: <T>(model: mongoose.Model<T>, query: FilterQuery<T>, errorArgs?: Partial<APIErrorConstructor>) => Promise<(Document & T) | null>;
|
|
34
45
|
/**
|
|
35
46
|
* This adds a static method `Model.findExactlyOne` to the schema. This or findOneOrNone should replace `Model.findOne`
|
|
36
47
|
* in most instances.
|
package/dist/plugins.js
CHANGED
|
@@ -95,7 +95,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
95
95
|
}
|
|
96
96
|
};
|
|
97
97
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
98
|
-
exports.DateOnly = exports.upsertPlugin = exports.findExactlyOne = exports.findOneOrNone = exports.firebaseJWTPlugin = exports.createdUpdatedPlugin = exports.isDisabledPlugin = exports.isDeletedPlugin = exports.baseUserPlugin = void 0;
|
|
98
|
+
exports.DateOnly = exports.upsertPlugin = exports.findExactlyOne = exports.findOneOrNoneFor = exports.findOneOrNone = exports.firebaseJWTPlugin = exports.createdUpdatedPlugin = exports.isDisabledPlugin = exports.isDeletedPlugin = exports.baseUserPlugin = void 0;
|
|
99
99
|
var luxon_1 = require("luxon");
|
|
100
100
|
var mongoose_1 = __importStar(require("mongoose"));
|
|
101
101
|
var errors_1 = require("./errors");
|
|
@@ -199,6 +199,39 @@ var findOneOrNone = function (schema) {
|
|
|
199
199
|
};
|
|
200
200
|
};
|
|
201
201
|
exports.findOneOrNone = findOneOrNone;
|
|
202
|
+
/**
|
|
203
|
+
* Helper that performs a `findOneOrNone` lookup against any Mongoose model. Returns the matching
|
|
204
|
+
* document, `null` if none match, or throws if more than one matches. If the model's schema has
|
|
205
|
+
* the {@link findOneOrNone} plugin applied, the plugin static is used; otherwise the lookup is
|
|
206
|
+
* performed directly via `model.find(...)`. Prefer this helper from framework code where the
|
|
207
|
+
* consumer's model may or may not have the plugin installed.
|
|
208
|
+
* @param model Mongoose Model
|
|
209
|
+
* @param query Mongoose query object
|
|
210
|
+
* @param errorArgs Optional overrides for the thrown {@link APIError} when multiple match
|
|
211
|
+
*/
|
|
212
|
+
var findOneOrNoneFor = function (model, query, errorArgs) { return __awaiter(void 0, void 0, void 0, function () {
|
|
213
|
+
var withStatic, results;
|
|
214
|
+
return __generator(this, function (_a) {
|
|
215
|
+
switch (_a.label) {
|
|
216
|
+
case 0:
|
|
217
|
+
withStatic = model;
|
|
218
|
+
if (typeof withStatic.findOneOrNone === "function") {
|
|
219
|
+
return [2 /*return*/, withStatic.findOneOrNone(query, errorArgs)];
|
|
220
|
+
}
|
|
221
|
+
return [4 /*yield*/, model.find(query)];
|
|
222
|
+
case 1:
|
|
223
|
+
results = _a.sent();
|
|
224
|
+
if (results.length === 0) {
|
|
225
|
+
return [2 /*return*/, null];
|
|
226
|
+
}
|
|
227
|
+
if (results.length > 1) {
|
|
228
|
+
throw new errors_1.APIError(__assign({ detail: "query: ".concat(JSON.stringify(query)), status: 500, title: "".concat(model.modelName, ".findOne query returned multiple documents") }, errorArgs));
|
|
229
|
+
}
|
|
230
|
+
return [2 /*return*/, results[0]];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}); };
|
|
234
|
+
exports.findOneOrNoneFor = findOneOrNoneFor;
|
|
202
235
|
/**
|
|
203
236
|
* This adds a static method `Model.findExactlyOne` to the schema. This or findOneOrNone should replace `Model.findOne`
|
|
204
237
|
* in most instances.
|
package/dist/plugins.test.js
CHANGED
|
@@ -278,6 +278,188 @@ var StuffModel = (0, mongoose_1.model)("Stuff", stuffSchema);
|
|
|
278
278
|
});
|
|
279
279
|
}); });
|
|
280
280
|
});
|
|
281
|
+
var bareThingSchema = new mongoose_1.Schema({
|
|
282
|
+
name: { description: "The name of the bare item", type: String },
|
|
283
|
+
ownerId: { description: "The owner of the bare item", type: String },
|
|
284
|
+
});
|
|
285
|
+
var BareThingModel = (0, mongoose_1.model)("BareThing", bareThingSchema);
|
|
286
|
+
(0, bun_test_1.describe)("findOneOrNoneFor", function () {
|
|
287
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
288
|
+
return __generator(this, function (_a) {
|
|
289
|
+
switch (_a.label) {
|
|
290
|
+
case 0: return [4 /*yield*/, StuffModel.deleteMany({})];
|
|
291
|
+
case 1:
|
|
292
|
+
_a.sent();
|
|
293
|
+
return [4 /*yield*/, BareThingModel.deleteMany({})];
|
|
294
|
+
case 2:
|
|
295
|
+
_a.sent();
|
|
296
|
+
return [4 /*yield*/, (0, tests_1.setupDb)()];
|
|
297
|
+
case 3:
|
|
298
|
+
_a.sent();
|
|
299
|
+
return [2 /*return*/];
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}); });
|
|
303
|
+
(0, bun_test_1.describe)("when the schema has the findOneOrNone plugin", function () {
|
|
304
|
+
var things;
|
|
305
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
306
|
+
var _a;
|
|
307
|
+
return __generator(this, function (_b) {
|
|
308
|
+
switch (_b.label) {
|
|
309
|
+
case 0: return [4 /*yield*/, Promise.all([
|
|
310
|
+
StuffModel.create({ name: "Things", ownerId: "123" }),
|
|
311
|
+
StuffModel.create({ name: "StuffNThings", ownerId: "123" }),
|
|
312
|
+
])];
|
|
313
|
+
case 1:
|
|
314
|
+
_a = __read.apply(void 0, [_b.sent(), 1]), things = _a[0];
|
|
315
|
+
return [2 /*return*/];
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
}); });
|
|
319
|
+
(0, bun_test_1.it)("returns null with no matches", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
320
|
+
var result;
|
|
321
|
+
return __generator(this, function (_a) {
|
|
322
|
+
switch (_a.label) {
|
|
323
|
+
case 0: return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(StuffModel, { name: "OtherStuff" })];
|
|
324
|
+
case 1:
|
|
325
|
+
result = _a.sent();
|
|
326
|
+
(0, bun_test_1.expect)(result).toBeNull();
|
|
327
|
+
return [2 /*return*/];
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}); });
|
|
331
|
+
(0, bun_test_1.it)("returns a single match", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
332
|
+
var result;
|
|
333
|
+
return __generator(this, function (_a) {
|
|
334
|
+
switch (_a.label) {
|
|
335
|
+
case 0: return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(StuffModel, { name: "Things" })];
|
|
336
|
+
case 1:
|
|
337
|
+
result = _a.sent();
|
|
338
|
+
(0, bun_test_1.expect)(result).not.toBeNull();
|
|
339
|
+
(0, bun_test_1.expect)(result === null || result === void 0 ? void 0 : result._id.toString()).toBe(things._id.toString());
|
|
340
|
+
return [2 /*return*/];
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}); });
|
|
344
|
+
(0, bun_test_1.it)("throws when multiple documents match", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
345
|
+
var fn;
|
|
346
|
+
return __generator(this, function (_a) {
|
|
347
|
+
switch (_a.label) {
|
|
348
|
+
case 0:
|
|
349
|
+
fn = function () { return (0, plugins_1.findOneOrNoneFor)(StuffModel, { ownerId: "123" }); };
|
|
350
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(fn()).rejects.toThrow(/Stuff\.findOne query returned multiple documents/)];
|
|
351
|
+
case 1:
|
|
352
|
+
_a.sent();
|
|
353
|
+
return [2 /*return*/];
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}); });
|
|
357
|
+
(0, bun_test_1.it)("forwards errorArgs to the thrown APIError", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
358
|
+
var fn, error_2;
|
|
359
|
+
return __generator(this, function (_a) {
|
|
360
|
+
switch (_a.label) {
|
|
361
|
+
case 0:
|
|
362
|
+
fn = function () {
|
|
363
|
+
return (0, plugins_1.findOneOrNoneFor)(StuffModel, { ownerId: "123" }, { status: 400, title: "Oh no!" });
|
|
364
|
+
};
|
|
365
|
+
_a.label = 1;
|
|
366
|
+
case 1:
|
|
367
|
+
_a.trys.push([1, 3, , 4]);
|
|
368
|
+
return [4 /*yield*/, fn()];
|
|
369
|
+
case 2:
|
|
370
|
+
_a.sent();
|
|
371
|
+
throw new Error("Expected promise to reject");
|
|
372
|
+
case 3:
|
|
373
|
+
error_2 = _a.sent();
|
|
374
|
+
(0, bun_test_1.expect)(error_2.title).toBe("Oh no!");
|
|
375
|
+
(0, bun_test_1.expect)(error_2.status).toBe(400);
|
|
376
|
+
return [3 /*break*/, 4];
|
|
377
|
+
case 4: return [2 /*return*/];
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}); });
|
|
381
|
+
});
|
|
382
|
+
(0, bun_test_1.describe)("when the schema does NOT have the findOneOrNone plugin", function () {
|
|
383
|
+
var bareThings;
|
|
384
|
+
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
385
|
+
var _a;
|
|
386
|
+
return __generator(this, function (_b) {
|
|
387
|
+
switch (_b.label) {
|
|
388
|
+
case 0: return [4 /*yield*/, Promise.all([
|
|
389
|
+
BareThingModel.create({ name: "Things", ownerId: "123" }),
|
|
390
|
+
BareThingModel.create({ name: "StuffNThings", ownerId: "123" }),
|
|
391
|
+
])];
|
|
392
|
+
case 1:
|
|
393
|
+
_a = __read.apply(void 0, [_b.sent(), 1]), bareThings = _a[0];
|
|
394
|
+
return [2 /*return*/];
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
}); });
|
|
398
|
+
(0, bun_test_1.it)("returns null with no matches", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
399
|
+
var result;
|
|
400
|
+
return __generator(this, function (_a) {
|
|
401
|
+
switch (_a.label) {
|
|
402
|
+
case 0: return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(BareThingModel, { name: "OtherStuff" })];
|
|
403
|
+
case 1:
|
|
404
|
+
result = _a.sent();
|
|
405
|
+
(0, bun_test_1.expect)(result).toBeNull();
|
|
406
|
+
return [2 /*return*/];
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}); });
|
|
410
|
+
(0, bun_test_1.it)("returns a single match", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
411
|
+
var result;
|
|
412
|
+
var _a, _b;
|
|
413
|
+
return __generator(this, function (_c) {
|
|
414
|
+
switch (_c.label) {
|
|
415
|
+
case 0: return [4 /*yield*/, (0, plugins_1.findOneOrNoneFor)(BareThingModel, { name: "Things" })];
|
|
416
|
+
case 1:
|
|
417
|
+
result = _c.sent();
|
|
418
|
+
(0, bun_test_1.expect)(result).not.toBeNull();
|
|
419
|
+
(0, bun_test_1.expect)((_a = result === null || result === void 0 ? void 0 : result._id) === null || _a === void 0 ? void 0 : _a.toString()).toBe((_b = bareThings._id) === null || _b === void 0 ? void 0 : _b.toString());
|
|
420
|
+
return [2 /*return*/];
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}); });
|
|
424
|
+
(0, bun_test_1.it)("throws when multiple documents match", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
425
|
+
var fn;
|
|
426
|
+
return __generator(this, function (_a) {
|
|
427
|
+
switch (_a.label) {
|
|
428
|
+
case 0:
|
|
429
|
+
fn = function () { return (0, plugins_1.findOneOrNoneFor)(BareThingModel, { ownerId: "123" }); };
|
|
430
|
+
return [4 /*yield*/, (0, bun_test_1.expect)(fn()).rejects.toThrow(/BareThing\.findOne query returned multiple documents/)];
|
|
431
|
+
case 1:
|
|
432
|
+
_a.sent();
|
|
433
|
+
return [2 /*return*/];
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}); });
|
|
437
|
+
(0, bun_test_1.it)("forwards errorArgs to the thrown APIError", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
438
|
+
var fn, error_3;
|
|
439
|
+
return __generator(this, function (_a) {
|
|
440
|
+
switch (_a.label) {
|
|
441
|
+
case 0:
|
|
442
|
+
fn = function () {
|
|
443
|
+
return (0, plugins_1.findOneOrNoneFor)(BareThingModel, { ownerId: "123" }, { status: 400, title: "Oh no!" });
|
|
444
|
+
};
|
|
445
|
+
_a.label = 1;
|
|
446
|
+
case 1:
|
|
447
|
+
_a.trys.push([1, 3, , 4]);
|
|
448
|
+
return [4 /*yield*/, fn()];
|
|
449
|
+
case 2:
|
|
450
|
+
_a.sent();
|
|
451
|
+
throw new Error("Expected promise to reject");
|
|
452
|
+
case 3:
|
|
453
|
+
error_3 = _a.sent();
|
|
454
|
+
(0, bun_test_1.expect)(error_3.title).toBe("Oh no!");
|
|
455
|
+
(0, bun_test_1.expect)(error_3.status).toBe(400);
|
|
456
|
+
return [3 /*break*/, 4];
|
|
457
|
+
case 4: return [2 /*return*/];
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}); });
|
|
461
|
+
});
|
|
462
|
+
});
|
|
281
463
|
(0, bun_test_1.describe)("findExactlyOne", function () {
|
|
282
464
|
var things;
|
|
283
465
|
(0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
@@ -345,7 +527,7 @@ var StuffModel = (0, mongoose_1.model)("Stuff", stuffSchema);
|
|
|
345
527
|
});
|
|
346
528
|
}); });
|
|
347
529
|
(0, bun_test_1.it)("throws custom error with two matches.", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
348
|
-
var fn,
|
|
530
|
+
var fn, error_4;
|
|
349
531
|
return __generator(this, function (_a) {
|
|
350
532
|
switch (_a.label) {
|
|
351
533
|
case 0:
|
|
@@ -359,11 +541,11 @@ var StuffModel = (0, mongoose_1.model)("Stuff", stuffSchema);
|
|
|
359
541
|
// If the promise doesn't reject, the test should fail
|
|
360
542
|
throw new Error("Expected promise to reject");
|
|
361
543
|
case 3:
|
|
362
|
-
|
|
544
|
+
error_4 = _a.sent();
|
|
363
545
|
// Check if the error has title and status properties
|
|
364
|
-
(0, bun_test_1.expect)(
|
|
365
|
-
(0, bun_test_1.expect)(
|
|
366
|
-
(0, bun_test_1.expect)(
|
|
546
|
+
(0, bun_test_1.expect)(error_4.title).toBe("Oh no!");
|
|
547
|
+
(0, bun_test_1.expect)(error_4.status).toBe(400);
|
|
548
|
+
(0, bun_test_1.expect)(error_4.detail).toBe('query: {"ownerId":"123"}');
|
|
367
549
|
return [3 /*break*/, 4];
|
|
368
550
|
case 4: return [2 /*return*/];
|
|
369
551
|
}
|
|
@@ -526,7 +708,7 @@ var StuffModel = (0, mongoose_1.model)("Stuff", stuffSchema);
|
|
|
526
708
|
});
|
|
527
709
|
(0, bun_test_1.describe)("DateOnly", function () {
|
|
528
710
|
(0, bun_test_1.it)("throws error with invalid date", function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
529
|
-
var
|
|
711
|
+
var error_5;
|
|
530
712
|
return __generator(this, function (_a) {
|
|
531
713
|
switch (_a.label) {
|
|
532
714
|
case 0:
|
|
@@ -540,8 +722,8 @@ var StuffModel = (0, mongoose_1.model)("Stuff", stuffSchema);
|
|
|
540
722
|
_a.sent();
|
|
541
723
|
return [3 /*break*/, 3];
|
|
542
724
|
case 2:
|
|
543
|
-
|
|
544
|
-
(0, bun_test_1.expect)(
|
|
725
|
+
error_5 = _a.sent();
|
|
726
|
+
(0, bun_test_1.expect)(error_5.message).toMatch(/Cast to DateOnly failed/);
|
|
545
727
|
return [2 /*return*/];
|
|
546
728
|
case 3: throw new Error("Expected error was not thrown");
|
|
547
729
|
}
|
|
@@ -7,8 +7,10 @@ export interface ConsentFormCheckbox {
|
|
|
7
7
|
}
|
|
8
8
|
export type ConsentFormType = "agreement" | "privacy" | "hipaa" | "research" | "terms" | "custom";
|
|
9
9
|
export type ConsentFormMethods = {};
|
|
10
|
-
export
|
|
11
|
-
|
|
10
|
+
export interface ConsentFormStatics extends FindExactlyOnePlugin<ConsentFormDocument>, FindOneOrNonePlugin<ConsentFormDocument> {
|
|
11
|
+
}
|
|
12
|
+
export interface ConsentFormModel extends mongoose.Model<ConsentFormDocument, object, ConsentFormMethods>, ConsentFormStatics {
|
|
13
|
+
}
|
|
12
14
|
export interface ConsentFormDocument extends mongoose.Document {
|
|
13
15
|
_id: mongoose.Types.ObjectId;
|
|
14
16
|
title: string;
|
package/package.json
CHANGED
package/src/betterAuthSetup.ts
CHANGED
|
@@ -13,6 +13,7 @@ import mongoose from "mongoose";
|
|
|
13
13
|
import type {UserModel} from "./auth";
|
|
14
14
|
import type {BetterAuthConfig, BetterAuthSessionData, BetterAuthUser} from "./betterAuth";
|
|
15
15
|
import {logger} from "./logger";
|
|
16
|
+
import {findOneOrNoneFor} from "./plugins";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* The Better Auth instance type.
|
|
@@ -109,7 +110,9 @@ export const createBetterAuthSessionMiddleware = (
|
|
|
109
110
|
|
|
110
111
|
if (userModel) {
|
|
111
112
|
// Look up the application user by betterAuthId
|
|
112
|
-
const appUser = await userModel
|
|
113
|
+
const appUser = await findOneOrNoneFor(userModel, {
|
|
114
|
+
betterAuthId: betterAuthUser.id,
|
|
115
|
+
});
|
|
113
116
|
if (appUser) {
|
|
114
117
|
(req as any).user = appUser;
|
|
115
118
|
(req as any).betterAuthSession = session;
|
|
@@ -151,7 +154,9 @@ export const syncBetterAuthUser = async (
|
|
|
151
154
|
oauthProvider?: string
|
|
152
155
|
): Promise<any> => {
|
|
153
156
|
try {
|
|
154
|
-
const existingUser: any = await userModel
|
|
157
|
+
const existingUser: any = await findOneOrNoneFor(userModel, {
|
|
158
|
+
betterAuthId: betterAuthUser.id,
|
|
159
|
+
});
|
|
155
160
|
|
|
156
161
|
if (existingUser) {
|
|
157
162
|
// Update existing user if needed
|
|
@@ -164,7 +169,9 @@ export const syncBetterAuthUser = async (
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
// Check if user exists by email (migration case)
|
|
167
|
-
const userByEmail: any = await userModel
|
|
172
|
+
const userByEmail: any = await findOneOrNoneFor(userModel, {
|
|
173
|
+
email: betterAuthUser.email,
|
|
174
|
+
});
|
|
168
175
|
if (userByEmail) {
|
|
169
176
|
// Link existing user to Better Auth
|
|
170
177
|
userByEmail.betterAuthId = betterAuthUser.id;
|
package/src/githubAuth.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {generateTokens, type UserModel} from "./auth";
|
|
|
5
5
|
import {APIError} from "./errors";
|
|
6
6
|
import type {AuthOptions} from "./expressServer";
|
|
7
7
|
import {logger} from "./logger";
|
|
8
|
+
import {findOneOrNoneFor} from "./plugins";
|
|
8
9
|
|
|
9
10
|
/** Options for configuring GitHub OAuth authentication */
|
|
10
11
|
export interface GitHubAuthOptions {
|
|
@@ -57,24 +58,24 @@ export interface GitHubUserFields {
|
|
|
57
58
|
* userSchema.plugin(githubUserPlugin);
|
|
58
59
|
* ```
|
|
59
60
|
*/
|
|
60
|
-
export
|
|
61
|
+
export const githubUserPlugin = (schema: any): void => {
|
|
61
62
|
schema.add({
|
|
62
63
|
githubAvatarUrl: {type: String},
|
|
63
64
|
githubId: {index: true, sparse: true, type: String, unique: true},
|
|
64
65
|
githubProfileUrl: {type: String},
|
|
65
66
|
githubUsername: {type: String},
|
|
66
67
|
});
|
|
67
|
-
}
|
|
68
|
+
};
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* Sets up GitHub OAuth authentication strategy.
|
|
71
72
|
* Call this after setupAuth() in your server initialization.
|
|
72
73
|
*/
|
|
73
|
-
export
|
|
74
|
+
export const setupGitHubAuth = (
|
|
74
75
|
_app: express.Application,
|
|
75
76
|
userModel: UserModel,
|
|
76
77
|
githubOptions: GitHubAuthOptions
|
|
77
|
-
) {
|
|
78
|
+
): void => {
|
|
78
79
|
const scope = githubOptions.scope ?? ["user:email"];
|
|
79
80
|
|
|
80
81
|
passport.use(
|
|
@@ -112,7 +113,7 @@ export function setupGitHubAuth(
|
|
|
112
113
|
const githubId = profile.id;
|
|
113
114
|
|
|
114
115
|
// Check if user with this GitHub ID already exists
|
|
115
|
-
const existingGitHubUser = await userModel
|
|
116
|
+
const existingGitHubUser = await findOneOrNoneFor(userModel, {githubId});
|
|
116
117
|
|
|
117
118
|
// Case 1: User is authenticated and wants to link GitHub account
|
|
118
119
|
if (existingUser) {
|
|
@@ -155,7 +156,7 @@ export function setupGitHubAuth(
|
|
|
155
156
|
|
|
156
157
|
// Check if user with this email already exists
|
|
157
158
|
if (email) {
|
|
158
|
-
const existingEmailUser = await userModel
|
|
159
|
+
const existingEmailUser = await findOneOrNoneFor(userModel, {email});
|
|
159
160
|
if (existingEmailUser) {
|
|
160
161
|
// If account linking is allowed, link GitHub to existing email account
|
|
161
162
|
if (githubOptions.allowAccountLinking !== false) {
|
|
@@ -195,7 +196,7 @@ export function setupGitHubAuth(
|
|
|
195
196
|
}) as any
|
|
196
197
|
) as passport.Strategy
|
|
197
198
|
);
|
|
198
|
-
}
|
|
199
|
+
};
|
|
199
200
|
|
|
200
201
|
/**
|
|
201
202
|
* Adds GitHub OAuth routes to the Express application.
|
|
@@ -206,12 +207,12 @@ export function setupGitHubAuth(
|
|
|
206
207
|
* - POST /auth/github/link - Links GitHub account to authenticated user (requires JWT auth)
|
|
207
208
|
* - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
|
|
208
209
|
*/
|
|
209
|
-
export
|
|
210
|
+
export const addGitHubAuthRoutes = (
|
|
210
211
|
app: express.Application,
|
|
211
212
|
userModel: UserModel,
|
|
212
213
|
githubOptions: GitHubAuthOptions,
|
|
213
214
|
authOptions?: AuthOptions
|
|
214
|
-
): void {
|
|
215
|
+
): void => {
|
|
215
216
|
const router = require("express").Router();
|
|
216
217
|
|
|
217
218
|
// Initiate GitHub OAuth flow
|
|
@@ -332,4 +333,4 @@ export function addGitHubAuthRoutes(
|
|
|
332
333
|
}
|
|
333
334
|
|
|
334
335
|
app.use("/auth", router);
|
|
335
|
-
}
|
|
336
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it, type Mock, mock} from "bun:test";
|
|
2
|
+
import * as Sentry from "@sentry/bun";
|
|
3
|
+
import type {NextFunction, Request, Response} from "express";
|
|
4
|
+
|
|
5
|
+
import {sentryAppVersionMiddleware} from "./middleware";
|
|
6
|
+
|
|
7
|
+
const buildReq = (headers: Record<string, string | undefined>): Request => {
|
|
8
|
+
return {
|
|
9
|
+
get: (name: string) => headers[name],
|
|
10
|
+
} as unknown as Request;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const buildNext = (): Mock<() => void> => mock(() => {});
|
|
14
|
+
|
|
15
|
+
describe("sentryAppVersionMiddleware", () => {
|
|
16
|
+
let setTagMock: Mock<(key: string, value: string) => void>;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
// bunSetup.ts mocks @sentry/bun so that getCurrentScope() returns a scope
|
|
20
|
+
// with a Bun mock setTag. Clear that mock between tests so each assertion
|
|
21
|
+
// sees only its own calls.
|
|
22
|
+
setTagMock = Sentry.getCurrentScope().setTag as unknown as Mock<
|
|
23
|
+
(key: string, value: string) => void
|
|
24
|
+
>;
|
|
25
|
+
setTagMock.mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("sets the app_version tag when the App-Version header is present", () => {
|
|
29
|
+
const next = buildNext();
|
|
30
|
+
const req = buildReq({"App-Version": "1.2.3"});
|
|
31
|
+
|
|
32
|
+
sentryAppVersionMiddleware(req, {} as Response, next as unknown as NextFunction);
|
|
33
|
+
|
|
34
|
+
expect(setTagMock).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(setTagMock.mock.calls[0]).toEqual(["app_version", "1.2.3"]);
|
|
36
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not set a tag when the App-Version header is missing", () => {
|
|
40
|
+
const next = buildNext();
|
|
41
|
+
const req = buildReq({});
|
|
42
|
+
|
|
43
|
+
sentryAppVersionMiddleware(req, {} as Response, next as unknown as NextFunction);
|
|
44
|
+
|
|
45
|
+
expect(setTagMock).not.toHaveBeenCalled();
|
|
46
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not set a tag when the App-Version header is an empty string", () => {
|
|
50
|
+
const next = buildNext();
|
|
51
|
+
const req = buildReq({"App-Version": ""});
|
|
52
|
+
|
|
53
|
+
sentryAppVersionMiddleware(req, {} as Response, next as unknown as NextFunction);
|
|
54
|
+
|
|
55
|
+
expect(setTagMock).not.toHaveBeenCalled();
|
|
56
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("calls next exactly once with no arguments when the header is present", () => {
|
|
60
|
+
const next = buildNext();
|
|
61
|
+
|
|
62
|
+
sentryAppVersionMiddleware(
|
|
63
|
+
buildReq({"App-Version": "9.9.9"}),
|
|
64
|
+
{} as Response,
|
|
65
|
+
next as unknown as NextFunction
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(next.mock.calls[0]).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -34,13 +34,14 @@ export const sendToGoogleChat = async (
|
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
36
|
await axios.post(chatWebhookUrl, {text: formattedMessageText});
|
|
37
|
-
} catch (error:
|
|
38
|
-
|
|
37
|
+
} catch (error: unknown) {
|
|
38
|
+
const errorObj = error as {text?: string; message?: string};
|
|
39
|
+
logger.error(`Error posting to Google Chat: ${errorObj.text ?? errorObj.message}`);
|
|
39
40
|
Sentry.captureException(error);
|
|
40
41
|
if (shouldThrow) {
|
|
41
42
|
throw new APIError({
|
|
42
43
|
status: 500,
|
|
43
|
-
title: `Error posting to Google Chat: ${
|
|
44
|
+
title: `Error posting to Google Chat: ${errorObj.text ?? errorObj.message}`,
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
}
|
package/src/plugins.test.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
DateOnly,
|
|
14
14
|
findExactlyOne,
|
|
15
15
|
findOneOrNone,
|
|
16
|
+
findOneOrNoneFor,
|
|
16
17
|
firebaseJWTPlugin,
|
|
17
18
|
type IsDeleted,
|
|
18
19
|
isDeletedPlugin,
|
|
@@ -187,6 +188,106 @@ describe("findOneOrNone", () => {
|
|
|
187
188
|
});
|
|
188
189
|
});
|
|
189
190
|
|
|
191
|
+
interface BareThing {
|
|
192
|
+
name: string;
|
|
193
|
+
ownerId: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const bareThingSchema = new Schema<BareThing>({
|
|
197
|
+
name: {description: "The name of the bare item", type: String},
|
|
198
|
+
ownerId: {description: "The owner of the bare item", type: String},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const BareThingModel = model<BareThing>("BareThing", bareThingSchema);
|
|
202
|
+
|
|
203
|
+
describe("findOneOrNoneFor", () => {
|
|
204
|
+
beforeEach(async () => {
|
|
205
|
+
await StuffModel.deleteMany({});
|
|
206
|
+
await BareThingModel.deleteMany({});
|
|
207
|
+
await setupDb();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("when the schema has the findOneOrNone plugin", () => {
|
|
211
|
+
let things: any;
|
|
212
|
+
|
|
213
|
+
beforeEach(async () => {
|
|
214
|
+
[things] = await Promise.all([
|
|
215
|
+
StuffModel.create({name: "Things", ownerId: "123"}),
|
|
216
|
+
StuffModel.create({name: "StuffNThings", ownerId: "123"}),
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("returns null with no matches", async () => {
|
|
221
|
+
const result = await findOneOrNoneFor(StuffModel, {name: "OtherStuff"});
|
|
222
|
+
expect(result).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("returns a single match", async () => {
|
|
226
|
+
const result = await findOneOrNoneFor(StuffModel, {name: "Things"});
|
|
227
|
+
expect(result).not.toBeNull();
|
|
228
|
+
expect(result?._id.toString()).toBe(things._id.toString());
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("throws when multiple documents match", async () => {
|
|
232
|
+
const fn = () => findOneOrNoneFor(StuffModel, {ownerId: "123"});
|
|
233
|
+
await expect(fn()).rejects.toThrow(/Stuff\.findOne query returned multiple documents/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("forwards errorArgs to the thrown APIError", async () => {
|
|
237
|
+
const fn = () =>
|
|
238
|
+
findOneOrNoneFor(StuffModel, {ownerId: "123"}, {status: 400, title: "Oh no!"});
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await fn();
|
|
242
|
+
throw new Error("Expected promise to reject");
|
|
243
|
+
} catch (error: any) {
|
|
244
|
+
expect(error.title).toBe("Oh no!");
|
|
245
|
+
expect(error.status).toBe(400);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("when the schema does NOT have the findOneOrNone plugin", () => {
|
|
251
|
+
let bareThings: any;
|
|
252
|
+
|
|
253
|
+
beforeEach(async () => {
|
|
254
|
+
[bareThings] = await Promise.all([
|
|
255
|
+
BareThingModel.create({name: "Things", ownerId: "123"}),
|
|
256
|
+
BareThingModel.create({name: "StuffNThings", ownerId: "123"}),
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("returns null with no matches", async () => {
|
|
261
|
+
const result = await findOneOrNoneFor(BareThingModel, {name: "OtherStuff"});
|
|
262
|
+
expect(result).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns a single match", async () => {
|
|
266
|
+
const result = await findOneOrNoneFor(BareThingModel, {name: "Things"});
|
|
267
|
+
expect(result).not.toBeNull();
|
|
268
|
+
expect((result as any)?._id?.toString()).toBe(bareThings._id?.toString());
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("throws when multiple documents match", async () => {
|
|
272
|
+
const fn = () => findOneOrNoneFor(BareThingModel, {ownerId: "123"});
|
|
273
|
+
await expect(fn()).rejects.toThrow(/BareThing\.findOne query returned multiple documents/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("forwards errorArgs to the thrown APIError", async () => {
|
|
277
|
+
const fn = () =>
|
|
278
|
+
findOneOrNoneFor(BareThingModel, {ownerId: "123"}, {status: 400, title: "Oh no!"});
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await fn();
|
|
282
|
+
throw new Error("Expected promise to reject");
|
|
283
|
+
} catch (error: any) {
|
|
284
|
+
expect(error.title).toBe("Oh no!");
|
|
285
|
+
expect(error.status).toBe(400);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
190
291
|
describe("findExactlyOne", () => {
|
|
191
292
|
let things: any;
|
|
192
293
|
|
package/src/plugins.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {DateTime} from "luxon";
|
|
2
2
|
import mongoose, {
|
|
3
3
|
type Document,
|
|
4
|
+
type FilterQuery,
|
|
4
5
|
Error as MongooseError,
|
|
5
6
|
type Query,
|
|
6
7
|
type Schema,
|
|
@@ -129,6 +130,40 @@ export const findOneOrNone = <T>(schema: Schema<T>): void => {
|
|
|
129
130
|
};
|
|
130
131
|
};
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Helper that performs a `findOneOrNone` lookup against any Mongoose model. Returns the matching
|
|
135
|
+
* document, `null` if none match, or throws if more than one matches. If the model's schema has
|
|
136
|
+
* the {@link findOneOrNone} plugin applied, the plugin static is used; otherwise the lookup is
|
|
137
|
+
* performed directly via `model.find(...)`. Prefer this helper from framework code where the
|
|
138
|
+
* consumer's model may or may not have the plugin installed.
|
|
139
|
+
* @param model Mongoose Model
|
|
140
|
+
* @param query Mongoose query object
|
|
141
|
+
* @param errorArgs Optional overrides for the thrown {@link APIError} when multiple match
|
|
142
|
+
*/
|
|
143
|
+
export const findOneOrNoneFor = async <T>(
|
|
144
|
+
model: mongoose.Model<T>,
|
|
145
|
+
query: FilterQuery<T>,
|
|
146
|
+
errorArgs?: Partial<APIErrorConstructor>
|
|
147
|
+
): Promise<(Document & T) | null> => {
|
|
148
|
+
const withStatic = model as mongoose.Model<T> & Partial<FindOneOrNonePlugin<T>>;
|
|
149
|
+
if (typeof withStatic.findOneOrNone === "function") {
|
|
150
|
+
return withStatic.findOneOrNone(query, errorArgs);
|
|
151
|
+
}
|
|
152
|
+
const results = await model.find(query);
|
|
153
|
+
if (results.length === 0) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (results.length > 1) {
|
|
157
|
+
throw new APIError({
|
|
158
|
+
detail: `query: ${JSON.stringify(query)}`,
|
|
159
|
+
status: 500,
|
|
160
|
+
title: `${model.modelName}.findOne query returned multiple documents`,
|
|
161
|
+
...errorArgs,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return results[0] as unknown as Document & T;
|
|
165
|
+
};
|
|
166
|
+
|
|
132
167
|
/**
|
|
133
168
|
* This adds a static method `Model.findExactlyOne` to the schema. This or findOneOrNone should replace `Model.findOne`
|
|
134
169
|
* in most instances.
|
package/src/types/consentForm.ts
CHANGED
|
@@ -12,11 +12,13 @@ export type ConsentFormType = "agreement" | "privacy" | "hipaa" | "research" | "
|
|
|
12
12
|
// biome-ignore lint/complexity/noBannedTypes: No methods.
|
|
13
13
|
export type ConsentFormMethods = {};
|
|
14
14
|
|
|
15
|
-
export
|
|
16
|
-
|
|
15
|
+
export interface ConsentFormStatics
|
|
16
|
+
extends FindExactlyOnePlugin<ConsentFormDocument>,
|
|
17
|
+
FindOneOrNonePlugin<ConsentFormDocument> {}
|
|
17
18
|
|
|
18
|
-
export
|
|
19
|
-
|
|
19
|
+
export interface ConsentFormModel
|
|
20
|
+
extends mongoose.Model<ConsentFormDocument, object, ConsentFormMethods>,
|
|
21
|
+
ConsentFormStatics {}
|
|
20
22
|
|
|
21
23
|
export interface ConsentFormDocument extends mongoose.Document {
|
|
22
24
|
_id: mongoose.Types.ObjectId;
|