@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/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 $baseModel and configuration options.
313
+ * Create a set of CRUD routes given a Mongoose model and configuration options.
341
314
  *
342
- * @param baseModel A Mongoose Model
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(baseModel, options),
363
- permissionMiddleware(baseModel, options),
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(baseModel, options),
473
- listOpenApiMiddleware(baseModel, options),
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(baseModel, options),
628
- permissionMiddleware(baseModel, options),
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(baseModel, options),
662
- permissionMiddleware(baseModel, options),
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(baseModel, options),
770
- permissionMiddleware(baseModel, options),
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
- // We fail here because we might fetch the document without the __t but we'd be missing all the
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(baseModel.schema.paths).find((config: any) => config.instance === "Array")) {
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
+ });