@terreno/api 0.0.11 → 0.0.13
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/CLAUDE.md +107 -0
- package/biome.jsonc +1 -1
- package/bunfig.toml +3 -2
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- package/dist/api.d.ts +3 -14
- package/dist/api.errors.test.d.ts +1 -0
- package/dist/api.errors.test.js +175 -0
- package/dist/api.hooks.test.d.ts +1 -0
- package/dist/api.hooks.test.js +891 -0
- package/dist/api.js +44 -68
- package/dist/api.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +691 -1678
- package/dist/auth.test.js +135 -0
- package/dist/expressServer.test.d.ts +1 -0
- package/dist/expressServer.test.js +669 -0
- package/dist/notifiers/slackNotifier.d.ts +2 -1
- package/dist/notifiers/slackNotifier.js +20 -13
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.js +17 -25
- package/dist/permissions.test.js +57 -0
- package/dist/populate.test.js +52 -0
- package/dist/tests.d.ts +9 -27
- package/dist/utils.test.js +235 -7
- package/package.json +3 -2
- package/src/api.arrayOperations.test.ts +690 -0
- package/src/api.errors.test.ts +156 -0
- package/src/api.hooks.test.ts +704 -0
- package/src/api.query.test.ts +538 -0
- package/src/api.test.ts +510 -1301
- package/src/api.ts +19 -61
- package/src/auth.test.ts +72 -0
- package/src/expressServer.test.ts +579 -0
- package/src/notifiers/slackNotifier.ts +28 -17
- package/src/permissions.test.ts +70 -1
- package/src/permissions.ts +4 -14
- package/src/populate.test.ts +58 -0
- package/src/utils.test.ts +214 -9
package/src/api.ts
CHANGED
|
@@ -241,16 +241,6 @@ export interface ModelRouterOptions<T> {
|
|
|
241
241
|
request: express.Request,
|
|
242
242
|
options: ModelRouterOptions<T>
|
|
243
243
|
) => Promise<JSONValue>;
|
|
244
|
-
/**
|
|
245
|
-
* The discriminatorKey that you passed when creating the Mongoose models. Defaults to __t. See:
|
|
246
|
-
* https://mongoosejs.com/docs/discriminators.html If this key is provided,
|
|
247
|
-
* you must provide the same key as part of the top level of the body when making performing
|
|
248
|
-
* update or delete operations on this model.
|
|
249
|
-
* \{discriminatorKey: "__t"\}
|
|
250
|
-
*
|
|
251
|
-
* PATCH \{__t: "SuperUser", name: "Foo"\} // __t is required or there will be a 404 error.
|
|
252
|
-
*/
|
|
253
|
-
discriminatorKey?: string;
|
|
254
244
|
/**
|
|
255
245
|
* The OpenAPI generator for this server. This is used to generate the OpenAPI documentation.
|
|
256
246
|
*/
|
|
@@ -275,23 +265,6 @@ export interface ModelRouterOptions<T> {
|
|
|
275
265
|
openApiExtraModelProperties?: any;
|
|
276
266
|
}
|
|
277
267
|
|
|
278
|
-
// A function to decide which model to use. If no discriminators are provided,
|
|
279
|
-
// just returns the base model. If
|
|
280
|
-
export function getModel(baseModel: Model<any>, body?: any, options?: ModelRouterOptions<any>) {
|
|
281
|
-
const discriminatorKey = options?.discriminatorKey ?? "__t";
|
|
282
|
-
const modelName = body?.[discriminatorKey];
|
|
283
|
-
if (!modelName) {
|
|
284
|
-
return baseModel;
|
|
285
|
-
}
|
|
286
|
-
const model = baseModel.discriminators?.[modelName];
|
|
287
|
-
if (!model) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
`Could not find discriminator model for key ${modelName}, baseModel: ${baseModel}`
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
return model;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
268
|
// Ensures query params are allowed. Also checks nested query params when using $and/$or.
|
|
296
269
|
function checkQueryParamAllowed(
|
|
297
270
|
queryParam: string,
|
|
@@ -337,15 +310,12 @@ function checkQueryParamAllowed(
|
|
|
337
310
|
// }
|
|
338
311
|
|
|
339
312
|
/**
|
|
340
|
-
* Create a set of CRUD routes given a Mongoose model
|
|
313
|
+
* Create a set of CRUD routes given a Mongoose model and configuration options.
|
|
341
314
|
*
|
|
342
|
-
* @param
|
|
315
|
+
* @param model A Mongoose Model
|
|
343
316
|
* @param options Options for configuring the REST API, such as permissions, transformers, and hooks.
|
|
344
317
|
*/
|
|
345
|
-
export function modelRouter<T>(
|
|
346
|
-
baseModel: Model<T>,
|
|
347
|
-
options: ModelRouterOptions<T>
|
|
348
|
-
): express.Router {
|
|
318
|
+
export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>): express.Router {
|
|
349
319
|
const router = express.Router();
|
|
350
320
|
|
|
351
321
|
// Do before the other router options so endpoints take priority.
|
|
@@ -359,12 +329,10 @@ export function modelRouter<T>(
|
|
|
359
329
|
"/",
|
|
360
330
|
[
|
|
361
331
|
authenticateMiddleware(options.allowAnonymous),
|
|
362
|
-
createOpenApiMiddleware(
|
|
363
|
-
permissionMiddleware(
|
|
332
|
+
createOpenApiMiddleware(model, options),
|
|
333
|
+
permissionMiddleware(model, options),
|
|
364
334
|
],
|
|
365
335
|
asyncHandler(async (req: Request, res: Response) => {
|
|
366
|
-
const model = getModel(baseModel, req.body?.__t, options);
|
|
367
|
-
|
|
368
336
|
let body: Partial<T> | (Partial<T> | undefined)[] | null | undefined;
|
|
369
337
|
try {
|
|
370
338
|
body = transform<T>(options, req.body, "create", req.user);
|
|
@@ -426,7 +394,7 @@ export function modelRouter<T>(
|
|
|
426
394
|
|
|
427
395
|
if (options.populatePaths) {
|
|
428
396
|
try {
|
|
429
|
-
let populateQuery = model.findById(data._id);
|
|
397
|
+
let populateQuery: any = model.findById(data._id);
|
|
430
398
|
populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
|
|
431
399
|
data = await populateQuery.exec();
|
|
432
400
|
} catch (error: any) {
|
|
@@ -469,13 +437,10 @@ export function modelRouter<T>(
|
|
|
469
437
|
"/",
|
|
470
438
|
[
|
|
471
439
|
authenticateMiddleware(options.allowAnonymous),
|
|
472
|
-
permissionMiddleware(
|
|
473
|
-
listOpenApiMiddleware(
|
|
440
|
+
permissionMiddleware(model, options),
|
|
441
|
+
listOpenApiMiddleware(model, options),
|
|
474
442
|
],
|
|
475
443
|
asyncHandler(async (req: Request, res: Response) => {
|
|
476
|
-
// For pure read queries, Mongoose will return the correct data with just the base model.
|
|
477
|
-
const model = baseModel;
|
|
478
|
-
|
|
479
444
|
let query: any = {};
|
|
480
445
|
for (const queryParam of Object.keys(options.defaultQueryParams ?? [])) {
|
|
481
446
|
query[queryParam] = options.defaultQueryParams?.[queryParam];
|
|
@@ -624,8 +589,8 @@ export function modelRouter<T>(
|
|
|
624
589
|
"/:id",
|
|
625
590
|
[
|
|
626
591
|
authenticateMiddleware(options.allowAnonymous),
|
|
627
|
-
getOpenApiMiddleware(
|
|
628
|
-
permissionMiddleware(
|
|
592
|
+
getOpenApiMiddleware(model, options),
|
|
593
|
+
permissionMiddleware(model, options),
|
|
629
594
|
],
|
|
630
595
|
asyncHandler(async (req: Request, res: Response) => {
|
|
631
596
|
const data: mongoose.Document & T = (req as any).obj;
|
|
@@ -658,12 +623,10 @@ export function modelRouter<T>(
|
|
|
658
623
|
"/:id",
|
|
659
624
|
[
|
|
660
625
|
authenticateMiddleware(options.allowAnonymous),
|
|
661
|
-
patchOpenApiMiddleware(
|
|
662
|
-
permissionMiddleware(
|
|
626
|
+
patchOpenApiMiddleware(model, options),
|
|
627
|
+
permissionMiddleware(model, options),
|
|
663
628
|
],
|
|
664
629
|
asyncHandler(async (req: Request, res: Response) => {
|
|
665
|
-
const model = getModel(baseModel, req.body, options);
|
|
666
|
-
|
|
667
630
|
let doc: mongoose.Document & T = (req as any).obj;
|
|
668
631
|
|
|
669
632
|
let body;
|
|
@@ -731,7 +694,7 @@ export function modelRouter<T>(
|
|
|
731
694
|
}
|
|
732
695
|
|
|
733
696
|
if (options.populatePaths) {
|
|
734
|
-
let populateQuery = model.findById(doc._id);
|
|
697
|
+
let populateQuery: any = model.findById(doc._id);
|
|
735
698
|
populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
|
|
736
699
|
doc = await populateQuery.exec();
|
|
737
700
|
}
|
|
@@ -766,12 +729,10 @@ export function modelRouter<T>(
|
|
|
766
729
|
"/:id",
|
|
767
730
|
[
|
|
768
731
|
authenticateMiddleware(options.allowAnonymous),
|
|
769
|
-
deleteOpenApiMiddleware(
|
|
770
|
-
permissionMiddleware(
|
|
732
|
+
deleteOpenApiMiddleware(model, options),
|
|
733
|
+
permissionMiddleware(model, options),
|
|
771
734
|
],
|
|
772
735
|
asyncHandler(async (req: Request, res: Response) => {
|
|
773
|
-
const model = getModel(baseModel, req.body, options);
|
|
774
|
-
|
|
775
736
|
const doc: mongoose.Document & T & {deleted?: boolean} = (req as any).obj;
|
|
776
737
|
|
|
777
738
|
if (options.preDelete) {
|
|
@@ -849,7 +810,6 @@ export function modelRouter<T>(
|
|
|
849
810
|
operation: "POST" | "PATCH" | "DELETE"
|
|
850
811
|
) {
|
|
851
812
|
// TODO Combine array operations and .patch(), as they are very similar.
|
|
852
|
-
const model = getModel(baseModel, req.body, options);
|
|
853
813
|
|
|
854
814
|
if (!(await checkPermissions("update", options.permissions.update, req.user))) {
|
|
855
815
|
throw new APIError({
|
|
@@ -861,9 +821,7 @@ export function modelRouter<T>(
|
|
|
861
821
|
const doc = await model.findById(req.params.id);
|
|
862
822
|
// Make a copy for passing pre-saved values to hooks.
|
|
863
823
|
const prevDoc = cloneDeep(doc);
|
|
864
|
-
|
|
865
|
-
// hooks.
|
|
866
|
-
if (!doc || (doc.__t && !req.body.__t)) {
|
|
824
|
+
if (!doc) {
|
|
867
825
|
throw new APIError({
|
|
868
826
|
status: 404,
|
|
869
827
|
title: `Could not find document to PATCH: ${req.params.id}`,
|
|
@@ -979,7 +937,7 @@ export function modelRouter<T>(
|
|
|
979
937
|
|
|
980
938
|
if (options.postUpdate) {
|
|
981
939
|
try {
|
|
982
|
-
await options.postUpdate(doc, body, req, prevDoc);
|
|
940
|
+
await options.postUpdate(doc as any, body, req, prevDoc as any);
|
|
983
941
|
} catch (error: any) {
|
|
984
942
|
throw new APIError({
|
|
985
943
|
disableExternalErrorTracking: getDisableExternalErrorTracking(error),
|
|
@@ -989,7 +947,7 @@ export function modelRouter<T>(
|
|
|
989
947
|
});
|
|
990
948
|
}
|
|
991
949
|
}
|
|
992
|
-
return res.json({data: serialize<T>(req, options, doc)});
|
|
950
|
+
return res.json({data: serialize<T>(req, options, doc as any)});
|
|
993
951
|
}
|
|
994
952
|
|
|
995
953
|
async function arrayPost(req: Request, res: Response) {
|
|
@@ -1004,7 +962,7 @@ export function modelRouter<T>(
|
|
|
1004
962
|
return arrayOperation(req, res, "DELETE");
|
|
1005
963
|
}
|
|
1006
964
|
// Set up routes for managing array fields. Check if there any array fields to add this for.
|
|
1007
|
-
if (Object.values(
|
|
965
|
+
if (Object.values(model.schema.paths).find((config: any) => config.instance === "Array")) {
|
|
1008
966
|
router.post(
|
|
1009
967
|
"/:id/:field",
|
|
1010
968
|
authenticateMiddleware(options.allowAnonymous),
|
package/src/auth.test.ts
CHANGED
|
@@ -548,3 +548,75 @@ describe("generateTokens", () => {
|
|
|
548
548
|
expect(refreshToken).toBeDefined();
|
|
549
549
|
});
|
|
550
550
|
});
|
|
551
|
+
|
|
552
|
+
describe("generateTokens edge cases", () => {
|
|
553
|
+
const OLD_ENV = process.env;
|
|
554
|
+
|
|
555
|
+
beforeEach(() => {
|
|
556
|
+
process.env = {...OLD_ENV};
|
|
557
|
+
process.env.TOKEN_SECRET = "secret";
|
|
558
|
+
process.env.REFRESH_TOKEN_SECRET = "refresh_secret";
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
afterEach(() => {
|
|
562
|
+
process.env = OLD_ENV;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it("returns null tokens when user is missing", async () => {
|
|
566
|
+
const result = await generateTokens(null);
|
|
567
|
+
expect(result.token).toBeNull();
|
|
568
|
+
expect(result.refreshToken).toBeNull();
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("returns null tokens when user has no _id", async () => {
|
|
572
|
+
const result = await generateTokens({email: "test@test.com"});
|
|
573
|
+
expect(result.token).toBeNull();
|
|
574
|
+
expect(result.refreshToken).toBeNull();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it("includes custom payload from generateJWTPayload option", async () => {
|
|
578
|
+
const jwtLib = await import("jsonwebtoken");
|
|
579
|
+
|
|
580
|
+
const user = {_id: "user-123"};
|
|
581
|
+
const result = await generateTokens(user, {
|
|
582
|
+
generateJWTPayload: (u) => ({customField: "customValue", userId: u._id}),
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(result.token).toBeDefined();
|
|
586
|
+
const decoded = jwtLib.decode(result.token as string) as any;
|
|
587
|
+
expect(decoded.customField).toBe("customValue");
|
|
588
|
+
expect(decoded.id).toBe("user-123");
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("uses custom token expiration from generateTokenExpiration option", async () => {
|
|
592
|
+
const jwtLib = await import("jsonwebtoken");
|
|
593
|
+
|
|
594
|
+
const user = {_id: "user-123"};
|
|
595
|
+
const result = await generateTokens(user, {
|
|
596
|
+
generateTokenExpiration: () => "1h",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
expect(result.token).toBeDefined();
|
|
600
|
+
const decoded = jwtLib.decode(result.token as string) as any;
|
|
601
|
+
// Check that exp is roughly 1 hour from now (within 5 seconds tolerance)
|
|
602
|
+
const expectedExp = Math.floor(Date.now() / 1000) + 3600;
|
|
603
|
+
expect(decoded.exp).toBeGreaterThan(expectedExp - 5);
|
|
604
|
+
expect(decoded.exp).toBeLessThan(expectedExp + 5);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("uses custom refresh token expiration from generateRefreshTokenExpiration option", async () => {
|
|
608
|
+
const jwtLib = await import("jsonwebtoken");
|
|
609
|
+
|
|
610
|
+
const user = {_id: "user-123"};
|
|
611
|
+
const result = await generateTokens(user, {
|
|
612
|
+
generateRefreshTokenExpiration: () => "7d",
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
expect(result.refreshToken).toBeDefined();
|
|
616
|
+
const decoded = jwtLib.decode(result.refreshToken as string) as any;
|
|
617
|
+
// Check that exp is roughly 7 days from now
|
|
618
|
+
const expectedExp = Math.floor(Date.now() / 1000) + 7 * 24 * 3600;
|
|
619
|
+
expect(decoded.exp).toBeGreaterThan(expectedExp - 10);
|
|
620
|
+
expect(decoded.exp).toBeLessThan(expectedExp + 10);
|
|
621
|
+
});
|
|
622
|
+
});
|