@terreno/api 0.15.1 → 0.15.2
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/CHANGELOG.md +21 -0
- package/dist/actions.d.ts +55 -0
- package/dist/actions.js +472 -0
- package/dist/actions.openApi.test.d.ts +1 -0
- package/dist/actions.openApi.test.js +252 -0
- package/dist/actions.test.d.ts +1 -0
- package/dist/actions.test.js +946 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.js +4 -1
- package/dist/consentApp.js +118 -102
- package/dist/docLoader.d.ts +7 -0
- package/dist/docLoader.js +154 -0
- package/dist/docLoader.test.d.ts +1 -0
- package/dist/docLoader.test.js +137 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/permissions.d.ts +2 -2
- package/dist/permissions.js +11 -107
- package/dist/zodOpenApi.d.ts +2 -0
- package/dist/zodOpenApi.js +7 -0
- package/package.json +6 -3
- package/src/actions.openApi.test.ts +176 -0
- package/src/actions.test.ts +636 -0
- package/src/actions.ts +441 -0
- package/src/api.ts +14 -1
- package/src/consentApp.ts +80 -81
- package/src/docLoader.test.ts +58 -0
- package/src/docLoader.ts +77 -0
- package/src/index.ts +2 -0
- package/src/permissions.ts +4 -62
- package/src/zodOpenApi.ts +6 -0
package/src/consentApp.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import {type Application, Router} from "express";
|
|
10
10
|
import {DateTime} from "luxon";
|
|
11
|
+
import type {CollectionActionConfig} from "./actions";
|
|
11
12
|
import {asyncHandler, modelRouter} from "./api";
|
|
12
13
|
import type {User} from "./auth";
|
|
13
14
|
import {authenticateMiddleware} from "./auth";
|
|
@@ -40,6 +41,12 @@ export interface ConsentAppOptions {
|
|
|
40
41
|
supportedLocales?: string[];
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
const requireAdmin = (user: User | undefined): void => {
|
|
45
|
+
if (!user?.admin) {
|
|
46
|
+
throw new APIError({status: 403, title: "Admin access required"});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
43
50
|
export class ConsentApp implements TerrenoPlugin {
|
|
44
51
|
private options: ConsentAppOptions;
|
|
45
52
|
|
|
@@ -50,79 +57,76 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
50
57
|
register(app: Application): void {
|
|
51
58
|
const {auditTrail, resolveConsentForms, aiConfig} = this.options;
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
app.use(
|
|
55
|
-
"/consent-forms",
|
|
56
|
-
modelRouter(ConsentForm, {
|
|
57
|
-
endpoints: (router) => {
|
|
58
|
-
if (aiConfig) {
|
|
59
|
-
// POST /consent-forms/generate - generate consent form content with AI
|
|
60
|
-
router.post(
|
|
61
|
-
"/generate",
|
|
62
|
-
authenticateMiddleware(),
|
|
63
|
-
asyncHandler(async (req, res) => {
|
|
64
|
-
const user = req.user as User | undefined;
|
|
65
|
-
if (!user?.admin) {
|
|
66
|
-
throw new APIError({status: 403, title: "Admin access required"});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const {type, description, locale = "en"} = req.body;
|
|
70
|
-
if (!type) {
|
|
71
|
-
throw new APIError({status: 400, title: "type is required"});
|
|
72
|
-
}
|
|
73
|
-
if (!description) {
|
|
74
|
-
throw new APIError({status: 400, title: "description is required"});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const content = await aiConfig.generateContent({description, locale, type});
|
|
78
|
-
|
|
79
|
-
logger.info("ConsentForm content generated", {locale, type});
|
|
80
|
-
|
|
81
|
-
return res.json({data: {content}});
|
|
82
|
-
})
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// POST /consent-forms/translate - translate consent form content with AI
|
|
86
|
-
router.post(
|
|
87
|
-
"/translate",
|
|
88
|
-
authenticateMiddleware(),
|
|
89
|
-
asyncHandler(async (req, res) => {
|
|
90
|
-
const user = req.user as User | undefined;
|
|
91
|
-
if (!user?.admin) {
|
|
92
|
-
throw new APIError({status: 403, title: "Admin access required"});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const {content, fromLocale, toLocale} = req.body;
|
|
96
|
-
if (!content) {
|
|
97
|
-
throw new APIError({status: 400, title: "content is required"});
|
|
98
|
-
}
|
|
99
|
-
if (!fromLocale) {
|
|
100
|
-
throw new APIError({status: 400, title: "fromLocale is required"});
|
|
101
|
-
}
|
|
102
|
-
if (!toLocale) {
|
|
103
|
-
throw new APIError({status: 400, title: "toLocale is required"});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const translated = await aiConfig.translateContent({content, fromLocale, toLocale});
|
|
60
|
+
const collectionActions: Record<string, CollectionActionConfig<unknown, unknown, unknown>> = {};
|
|
107
61
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
62
|
+
if (aiConfig) {
|
|
63
|
+
collectionActions.generate = {
|
|
64
|
+
handler: async ({body, user}) => {
|
|
65
|
+
requireAdmin(user);
|
|
66
|
+
const typedBody = body as {type?: string; description?: string; locale?: string};
|
|
67
|
+
if (!typedBody.type) {
|
|
68
|
+
throw new APIError({status: 400, title: "type is required"});
|
|
113
69
|
}
|
|
70
|
+
if (!typedBody.description) {
|
|
71
|
+
throw new APIError({status: 400, title: "description is required"});
|
|
72
|
+
}
|
|
73
|
+
const locale = typedBody.locale ?? "en";
|
|
74
|
+
const content = await aiConfig.generateContent({
|
|
75
|
+
description: typedBody.description,
|
|
76
|
+
locale,
|
|
77
|
+
type: typedBody.type,
|
|
78
|
+
});
|
|
79
|
+
logger.info("ConsentForm content generated", {locale, type: typedBody.type});
|
|
80
|
+
return {content};
|
|
81
|
+
},
|
|
82
|
+
method: "POST",
|
|
83
|
+
permissions: [Permissions.IsAuthenticated],
|
|
84
|
+
summary: "Generate consent form content with AI",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
collectionActions.translate = {
|
|
88
|
+
handler: async ({body, user}) => {
|
|
89
|
+
requireAdmin(user);
|
|
90
|
+
const typedBody = body as {
|
|
91
|
+
content?: string;
|
|
92
|
+
fromLocale?: string;
|
|
93
|
+
toLocale?: string;
|
|
94
|
+
};
|
|
95
|
+
if (!typedBody.content) {
|
|
96
|
+
throw new APIError({status: 400, title: "content is required"});
|
|
97
|
+
}
|
|
98
|
+
if (!typedBody.fromLocale) {
|
|
99
|
+
throw new APIError({status: 400, title: "fromLocale is required"});
|
|
100
|
+
}
|
|
101
|
+
if (!typedBody.toLocale) {
|
|
102
|
+
throw new APIError({status: 400, title: "toLocale is required"});
|
|
103
|
+
}
|
|
104
|
+
const translated = await aiConfig.translateContent({
|
|
105
|
+
content: typedBody.content,
|
|
106
|
+
fromLocale: typedBody.fromLocale,
|
|
107
|
+
toLocale: typedBody.toLocale,
|
|
108
|
+
});
|
|
109
|
+
logger.info("ConsentForm content translated", {
|
|
110
|
+
fromLocale: typedBody.fromLocale,
|
|
111
|
+
toLocale: typedBody.toLocale,
|
|
112
|
+
});
|
|
113
|
+
return {content: translated};
|
|
114
|
+
},
|
|
115
|
+
method: "POST",
|
|
116
|
+
permissions: [Permissions.IsAuthenticated],
|
|
117
|
+
summary: "Translate consent form content with AI",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
114
120
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const form = await ConsentForm.findExactlyOne({_id: req.params.id});
|
|
121
|
+
app.use(
|
|
122
|
+
"/consent-forms",
|
|
123
|
+
modelRouter(ConsentForm, {
|
|
124
|
+
collectionActions,
|
|
125
|
+
instanceActions: {
|
|
126
|
+
publish: {
|
|
127
|
+
handler: async ({doc, user}) => {
|
|
128
|
+
requireAdmin(user);
|
|
129
|
+
const form = doc as ConsentFormDocument;
|
|
126
130
|
|
|
127
131
|
const newFormData = {
|
|
128
132
|
active: true,
|
|
@@ -144,7 +148,6 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
144
148
|
|
|
145
149
|
const newForm = await ConsentForm.create(newFormData);
|
|
146
150
|
|
|
147
|
-
// Deactivate all other versions of the same slug
|
|
148
151
|
await ConsentForm.updateMany(
|
|
149
152
|
{_id: {$ne: newForm._id}, slug: form.slug},
|
|
150
153
|
{active: false}
|
|
@@ -155,9 +158,12 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
155
158
|
slug: newForm.slug,
|
|
156
159
|
});
|
|
157
160
|
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
+
return newForm;
|
|
162
|
+
},
|
|
163
|
+
method: "POST",
|
|
164
|
+
permissions: [Permissions.IsAuthenticated],
|
|
165
|
+
summary: "Publish a new version of a consent form",
|
|
166
|
+
},
|
|
161
167
|
},
|
|
162
168
|
permissions: {
|
|
163
169
|
create: [Permissions.IsAdmin],
|
|
@@ -229,7 +235,6 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
229
235
|
respondedFormVersions.set(formId, response.formVersionSnapshot ?? 0);
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
// Fetch the forms corresponding to existing responses to check version matches
|
|
233
238
|
const respondedFormIds = existingResponses.map((r) => r.consentFormId);
|
|
234
239
|
const respondedForms = await ConsentForm.find({_id: {$in: respondedFormIds}});
|
|
235
240
|
const formVersionByFormId = new Map<string, number>();
|
|
@@ -237,17 +242,14 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
237
242
|
formVersionByFormId.set(form._id.toString(), form.version);
|
|
238
243
|
}
|
|
239
244
|
|
|
240
|
-
// Filter out forms where the user already has a response matching the current version
|
|
241
245
|
const pendingForms = resolvedForms.filter((form) => {
|
|
242
246
|
const formId = form._id.toString();
|
|
243
|
-
// Find responses for this form
|
|
244
247
|
const matchingResponses = existingResponses.filter(
|
|
245
248
|
(r) => r.consentFormId.toString() === formId
|
|
246
249
|
);
|
|
247
250
|
if (matchingResponses.length === 0) {
|
|
248
251
|
return true;
|
|
249
252
|
}
|
|
250
|
-
// Check if any response matches the current form version
|
|
251
253
|
return !matchingResponses.some((r) => r.formVersionSnapshot === form.version);
|
|
252
254
|
});
|
|
253
255
|
|
|
@@ -300,7 +302,6 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
300
302
|
throw new APIError({status: 400, title: "Consent form is not active"});
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
// Validate signature requirement
|
|
304
305
|
if (form.captureSignature && agreed && !signature) {
|
|
305
306
|
throw new APIError({
|
|
306
307
|
status: 400,
|
|
@@ -308,7 +309,6 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
308
309
|
});
|
|
309
310
|
}
|
|
310
311
|
|
|
311
|
-
// Validate required checkboxes
|
|
312
312
|
if (agreed && form.checkboxes.length > 0) {
|
|
313
313
|
const values = checkboxValues ?? {};
|
|
314
314
|
for (let i = 0; i < form.checkboxes.length; i++) {
|
|
@@ -409,7 +409,6 @@ export class ConsentApp implements TerrenoPlugin {
|
|
|
409
409
|
})
|
|
410
410
|
);
|
|
411
411
|
|
|
412
|
-
// GET /consents/audit/:userId - admin audit trail for a specific user
|
|
413
412
|
if (auditTrail) {
|
|
414
413
|
router.get(
|
|
415
414
|
"/audit/:userId",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
|
|
2
|
+
import {describe, expect, it, mock} from "bun:test";
|
|
3
|
+
import {loadDocOr404} from "./docLoader";
|
|
4
|
+
import {APIError} from "./errors";
|
|
5
|
+
|
|
6
|
+
describe("loadDocOr404", () => {
|
|
7
|
+
it("returns hidden reason metadata when document is deleted", async () => {
|
|
8
|
+
const model = {
|
|
9
|
+
collection: {findOne: mock(async () => ({deleted: true}))},
|
|
10
|
+
findById: mock(() => ({exec: mock(async () => null)})),
|
|
11
|
+
modelName: "MockModel",
|
|
12
|
+
} as any;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await loadDocOr404(model, "507f1f77bcf86cd799439011");
|
|
16
|
+
expect.unreachable("expected loadDocOr404 to throw");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
expect(error).toBeInstanceOf(APIError);
|
|
19
|
+
const apiError = error as APIError;
|
|
20
|
+
expect(apiError.status).toBe(404);
|
|
21
|
+
expect(apiError.meta).toEqual({deleted: "true"});
|
|
22
|
+
expect(apiError.disableExternalErrorTracking).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rethrows APIError from query execution without wrapping", async () => {
|
|
27
|
+
const original = new APIError({status: 400, title: "validation failed"});
|
|
28
|
+
const model = {
|
|
29
|
+
collection: {findOne: mock(async () => null)},
|
|
30
|
+
findById: mock(() => ({
|
|
31
|
+
exec: mock(async () => {
|
|
32
|
+
throw original;
|
|
33
|
+
}),
|
|
34
|
+
})),
|
|
35
|
+
modelName: "MockModel",
|
|
36
|
+
} as any;
|
|
37
|
+
|
|
38
|
+
await expect(loadDocOr404(model, "507f1f77bcf86cd799439011")).rejects.toBe(original);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns plain not found when document does not exist", async () => {
|
|
42
|
+
const model = {
|
|
43
|
+
collection: {findOne: mock(async () => null)},
|
|
44
|
+
findById: mock(() => ({exec: mock(async () => null)})),
|
|
45
|
+
modelName: "MockModel",
|
|
46
|
+
} as any;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await loadDocOr404(model, "507f1f77bcf86cd799439011");
|
|
50
|
+
expect.unreachable("expected loadDocOr404 to throw");
|
|
51
|
+
} catch (error) {
|
|
52
|
+
expect(error).toBeInstanceOf(APIError);
|
|
53
|
+
const apiError = error as APIError;
|
|
54
|
+
expect(apiError.status).toBe(404);
|
|
55
|
+
expect(apiError.meta).toBeUndefined();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
package/src/docLoader.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as Sentry from "@sentry/bun";
|
|
2
|
+
import mongoose, {type Model} from "mongoose";
|
|
3
|
+
|
|
4
|
+
import {addPopulateToQuery} from "./api";
|
|
5
|
+
import {APIError, isAPIError} from "./errors";
|
|
6
|
+
import type {PopulatePath} from "./populate";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loads a document by id or throws a 404 APIError.
|
|
10
|
+
* Matches permission middleware behavior including soft-delete metadata.
|
|
11
|
+
*/
|
|
12
|
+
export const loadDocOr404 = async <T>(
|
|
13
|
+
model: Model<T>,
|
|
14
|
+
id: string,
|
|
15
|
+
populatePaths?: PopulatePath[]
|
|
16
|
+
): Promise<T> => {
|
|
17
|
+
const builtQuery = model.findById(id);
|
|
18
|
+
const populatedQuery = addPopulateToQuery(
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: Query types vary based on populate paths
|
|
20
|
+
builtQuery as any,
|
|
21
|
+
populatePaths
|
|
22
|
+
);
|
|
23
|
+
let data: T | null;
|
|
24
|
+
try {
|
|
25
|
+
data = (await populatedQuery.exec()) as T | null;
|
|
26
|
+
} catch (error: unknown) {
|
|
27
|
+
if (isAPIError(error)) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
throw new APIError({
|
|
31
|
+
error: error as Error,
|
|
32
|
+
status: 500,
|
|
33
|
+
title: `GET failed on ${id}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!data) {
|
|
37
|
+
const hiddenDoc = await model.collection.findOne({
|
|
38
|
+
_id: new mongoose.Types.ObjectId(id),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!hiddenDoc) {
|
|
42
|
+
Sentry.captureMessage(`Document ${id} not found for model ${model.modelName}`);
|
|
43
|
+
const error = new APIError({
|
|
44
|
+
status: 404,
|
|
45
|
+
title: `Document ${id} not found for model ${model.modelName}`,
|
|
46
|
+
});
|
|
47
|
+
error.meta = undefined;
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let reason: {[key: string]: string} | null = null;
|
|
52
|
+
if (hiddenDoc.deleted) {
|
|
53
|
+
reason = {deleted: "true"};
|
|
54
|
+
} else if (hiddenDoc.disabled) {
|
|
55
|
+
reason = {disabled: "true"};
|
|
56
|
+
} else if (hiddenDoc.archived) {
|
|
57
|
+
reason = {archived: "true"};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!reason) {
|
|
61
|
+
const error = new APIError({
|
|
62
|
+
status: 404,
|
|
63
|
+
title: `Document ${id} not found for model ${model.modelName}`,
|
|
64
|
+
});
|
|
65
|
+
error.meta = undefined;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
throw new APIError({
|
|
69
|
+
disableExternalErrorTracking: true,
|
|
70
|
+
meta: reason,
|
|
71
|
+
status: 404,
|
|
72
|
+
title: `Document ${id} not found for model ${model.modelName}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return data;
|
|
77
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./actions";
|
|
1
2
|
export * from "./api";
|
|
2
3
|
export * from "./auth";
|
|
3
4
|
export * from "./betterAuth";
|
|
@@ -36,3 +37,4 @@ export * from "./types/consentForm";
|
|
|
36
37
|
export * from "./types/consentResponse";
|
|
37
38
|
export * from "./utils";
|
|
38
39
|
export * from "./versionCheckPlugin";
|
|
40
|
+
export {z} from "./zodOpenApi";
|
package/src/permissions.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import * as Sentry from "@sentry/bun";
|
|
2
1
|
import type express from "express";
|
|
3
2
|
import type {NextFunction} from "express";
|
|
4
|
-
import
|
|
3
|
+
import type {Model} from "mongoose";
|
|
5
4
|
|
|
6
|
-
import
|
|
5
|
+
import type {ModelRouterOptions, RESTMethod} from "./api";
|
|
7
6
|
import type {User} from "./auth";
|
|
7
|
+
import {loadDocOr404} from "./docLoader";
|
|
8
8
|
import {APIError} from "./errors";
|
|
9
9
|
import {logger} from "./logger";
|
|
10
10
|
|
|
@@ -145,65 +145,7 @@ export const permissionMiddleware = <T>(
|
|
|
145
145
|
return next();
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const
|
|
149
|
-
const populatedQuery = addPopulateToQuery(
|
|
150
|
-
// biome-ignore lint/suspicious/noExplicitAny: Query types vary based on populate paths
|
|
151
|
-
builtQuery as any,
|
|
152
|
-
options.populatePaths
|
|
153
|
-
);
|
|
154
|
-
let data: T | null;
|
|
155
|
-
try {
|
|
156
|
-
data = (await populatedQuery.exec()) as T | null;
|
|
157
|
-
} catch (error: unknown) {
|
|
158
|
-
throw new APIError({
|
|
159
|
-
error: error as Error,
|
|
160
|
-
status: 500,
|
|
161
|
-
title: `GET failed on ${req.params.id}`,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
if (!data) {
|
|
165
|
-
// Check if document exists but is hidden. Completely skip plugins.
|
|
166
|
-
const hiddenDoc = await model.collection.findOne({
|
|
167
|
-
_id: new mongoose.Types.ObjectId(req.params.id as string),
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
if (!hiddenDoc) {
|
|
171
|
-
Sentry.captureMessage(`Document ${req.params.id} not found for model ${model.modelName}`);
|
|
172
|
-
const error = new APIError({
|
|
173
|
-
status: 404,
|
|
174
|
-
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
175
|
-
});
|
|
176
|
-
error.meta = undefined;
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Document exists but is hidden
|
|
181
|
-
let reason: {[key: string]: string} | null = null;
|
|
182
|
-
if (hiddenDoc.deleted) {
|
|
183
|
-
reason = {deleted: "true"};
|
|
184
|
-
} else if (hiddenDoc.disabled) {
|
|
185
|
-
reason = {disabled: "true"};
|
|
186
|
-
} else if (hiddenDoc.archived) {
|
|
187
|
-
reason = {archived: "true"};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// If no reason found, treat as not found
|
|
191
|
-
if (!reason) {
|
|
192
|
-
const error = new APIError({
|
|
193
|
-
status: 404,
|
|
194
|
-
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
195
|
-
});
|
|
196
|
-
error.meta = undefined;
|
|
197
|
-
throw error;
|
|
198
|
-
}
|
|
199
|
-
throw new APIError({
|
|
200
|
-
// We don't want to send this to Sentry because it's expected behavior.
|
|
201
|
-
disableExternalErrorTracking: true,
|
|
202
|
-
meta: reason,
|
|
203
|
-
status: 404,
|
|
204
|
-
title: `Document ${req.params.id} not found for model ${model.modelName}`,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
148
|
+
const data = await loadDocOr404<T>(model, req.params.id as string, options.populatePaths);
|
|
207
149
|
|
|
208
150
|
if (!(await checkPermissions(method, options.permissions[method], req.user, data))) {
|
|
209
151
|
throw new APIError({
|