@terreno/api 0.0.4 → 0.0.11-beta.1

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
@@ -70,7 +70,7 @@ export type RESTMethod = "list" | "create" | "read" | "update" | "delete";
70
70
  * This is the main configuration.
71
71
  * @param T - the base document type. This should not include Mongoose models, just the types of the object.
72
72
  */
73
- export interface modelRouterOptions<T> {
73
+ export interface ModelRouterOptions<T> {
74
74
  /**
75
75
  * A group of method-level (create/read/update/delete/list) permissions.
76
76
  * Determine if the user can perform the operation at all, and for read/update/delete methods,
@@ -239,18 +239,8 @@ export interface modelRouterOptions<T> {
239
239
  value: (Document<any, any, any> & T) | (Document<any, any, any> & T)[],
240
240
  method: "list" | "create" | "read" | "update" | "delete",
241
241
  request: express.Request,
242
- options: modelRouterOptions<T>
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),
@@ -1033,4 +991,4 @@ export const asyncHandler = (fn: any) => (req: Request, res: Response, next: Nex
1033
991
 
1034
992
  // For backwards compatibility with the old names.
1035
993
  export const gooseRestRouter = modelRouter;
1036
- export type GooseRESTOptions<T> = modelRouterOptions<T>;
994
+ export type GooseRESTOptions<T> = ModelRouterOptions<T>;
package/src/example.ts CHANGED
@@ -2,7 +2,7 @@ import express from "express";
2
2
  import mongoose, {model, Schema} from "mongoose";
3
3
  import passportLocalMongoose from "passport-local-mongoose";
4
4
 
5
- import {modelRouter, type modelRouterOptions} from "./api";
5
+ import {type ModelRouterOptions, modelRouter} from "./api";
6
6
  import {addAuthRoutes, setupAuth} from "./auth";
7
7
  import {setupServer} from "./expressServer";
8
8
  import {logger} from "./logger";
@@ -68,7 +68,7 @@ function getBaseServer() {
68
68
  setupAuth(app, UserModel as any);
69
69
  addAuthRoutes(app, UserModel as any);
70
70
 
71
- function addRoutes(router: express.Router, options?: Partial<modelRouterOptions<any>>): void {
71
+ function addRoutes(router: express.Router, options?: Partial<ModelRouterOptions<any>>): void {
72
72
  router.use(
73
73
  "/food",
74
74
  modelRouter(FoodModel, {
@@ -9,7 +9,7 @@ import onFinished from "on-finished";
9
9
  import passport from "passport";
10
10
  import qs from "qs";
11
11
 
12
- import type {modelRouterOptions} from "./api";
12
+ import type {ModelRouterOptions} from "./api";
13
13
  import {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
14
14
  import {apiErrorMiddleware, apiUnauthorizedMiddleware} from "./errors";
15
15
  import {type LoggingOptions, logger, setupLogging} from "./logger";
@@ -41,7 +41,7 @@ export function setupEnvironment(): void {
41
41
  }
42
42
  }
43
43
 
44
- export type AddRoutes = (router: Router, options?: Partial<modelRouterOptions<any>>) => void;
44
+ export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<any>>) => void;
45
45
 
46
46
  const logRequestsFinished = (req: any, res: any, startTime: bigint) => {
47
47
  const options = (res.locals.loggingOptions ?? {}) as LoggingOptions;
@@ -4,13 +4,13 @@ import type {Router} from "express";
4
4
  import supertest from "supertest";
5
5
  import type TestAgent from "supertest/lib/agent";
6
6
 
7
- import {modelRouter, type modelRouterOptions} from "./api";
7
+ import {type ModelRouterOptions, modelRouter} from "./api";
8
8
  import {addAuthRoutes, setupAuth} from "./auth";
9
9
  import {setupServer} from "./expressServer";
10
10
  import {Permissions} from "./permissions";
11
11
  import {FoodModel, setupDb, UserModel} from "./tests";
12
12
 
13
- function getMessageSummaryOpenApiMiddleware(options: Partial<modelRouterOptions<any>>): any {
13
+ function getMessageSummaryOpenApiMiddleware(options: Partial<ModelRouterOptions<any>>): any {
14
14
  return options.openApi.path({
15
15
  parameters: [
16
16
  {
@@ -42,7 +42,7 @@ function getMessageSummaryOpenApiMiddleware(options: Partial<modelRouterOptions<
42
42
  });
43
43
  }
44
44
 
45
- function addRoutes(router: Router, options?: Partial<modelRouterOptions<any>>): void {
45
+ function addRoutes(router: Router, options?: Partial<ModelRouterOptions<any>>): void {
46
46
  router.use(
47
47
  "/food",
48
48
  modelRouter(FoodModel as any, {
@@ -176,7 +176,7 @@ describe("openApi", () => {
176
176
  });
177
177
  });
178
178
 
179
- function addRoutesPopulate(router: Router, options?: Partial<modelRouterOptions<any>>): void {
179
+ function addRoutesPopulate(router: Router, options?: Partial<ModelRouterOptions<any>>): void {
180
180
  options?.openApi.component("schemas", "LimitedUser", {
181
181
  properties: {
182
182
  email: {
package/src/openApi.ts CHANGED
@@ -3,7 +3,7 @@ import merge from "lodash/merge";
3
3
  import type {Model} from "mongoose";
4
4
  import m2s from "mongoose-to-swagger";
5
5
 
6
- import type {modelRouterOptions} from "./api";
6
+ import type {ModelRouterOptions} from "./api";
7
7
  import {logger} from "./logger";
8
8
  import {getOpenApiSpecForModel} from "./populate";
9
9
 
@@ -112,7 +112,7 @@ function createAPIErrorComponent(openApi: any) {
112
112
  });
113
113
  }
114
114
 
115
- export function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<modelRouterOptions<T>>) {
115
+ export function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) {
116
116
  createAPIErrorComponent(options.openApi);
117
117
  if (!options.openApi?.path) {
118
118
  // Just log this once rather than for each middleware.
@@ -154,7 +154,7 @@ export function getOpenApiMiddleware<T>(model: Model<T>, options: Partial<modelR
154
154
  );
155
155
  }
156
156
 
157
- export function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<modelRouterOptions<T>>) {
157
+ export function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<ModelRouterOptions<T>>) {
158
158
  if (!options.openApi?.path) {
159
159
  return noop;
160
160
  }
@@ -319,7 +319,7 @@ export function listOpenApiMiddleware<T>(model: Model<T>, options: Partial<model
319
319
 
320
320
  export function createOpenApiMiddleware<T>(
321
321
  model: Model<T>,
322
- options: Partial<modelRouterOptions<T>>
322
+ options: Partial<ModelRouterOptions<T>>
323
323
  ) {
324
324
  if (!options.openApi?.path) {
325
325
  return noop;
@@ -371,7 +371,7 @@ export function createOpenApiMiddleware<T>(
371
371
 
372
372
  export function patchOpenApiMiddleware<T>(
373
373
  model: Model<T>,
374
- options: Partial<modelRouterOptions<T>>
374
+ options: Partial<ModelRouterOptions<T>>
375
375
  ) {
376
376
  if (!options.openApi?.path) {
377
377
  return noop;
@@ -423,7 +423,7 @@ export function patchOpenApiMiddleware<T>(
423
423
 
424
424
  export function deleteOpenApiMiddleware<T>(
425
425
  model: Model<T>,
426
- options: Partial<modelRouterOptions<T>>
426
+ options: Partial<ModelRouterOptions<T>>
427
427
  ) {
428
428
  if (!options.openApi?.path) {
429
429
  return noop;
@@ -452,7 +452,7 @@ export function deleteOpenApiMiddleware<T>(
452
452
  // This is a generic OpenAPI wrapper for a read that returns any object described by `properties`.
453
453
  // Useful for endpoints that don't directly map to a model.
454
454
  export function readOpenApiMiddleware<T>(
455
- options: Partial<modelRouterOptions<T>>,
455
+ options: Partial<ModelRouterOptions<T>>,
456
456
  properties: any,
457
457
  required: string[],
458
458
  queryParameters: any
@@ -4,14 +4,14 @@ import type {Router} from "express";
4
4
  import supertest from "supertest";
5
5
  import type TestAgent from "supertest/lib/agent";
6
6
 
7
- import {modelRouter, type modelRouterOptions} from "./api";
7
+ import {type ModelRouterOptions, modelRouter} from "./api";
8
8
  import {addAuthRoutes, setupAuth} from "./auth";
9
9
  import {setupServer} from "./expressServer";
10
10
  import {createOpenApiBuilder, OpenApiMiddlewareBuilder} from "./openApiBuilder";
11
11
  import {Permissions} from "./permissions";
12
12
  import {FoodModel, UserModel} from "./tests";
13
13
 
14
- function addRoutesWithBuilder(router: Router, options?: Partial<modelRouterOptions<any>>): void {
14
+ function addRoutesWithBuilder(router: Router, options?: Partial<ModelRouterOptions<any>>): void {
15
15
  // Add a custom endpoint using the OpenApiMiddlewareBuilder
16
16
  const statsMiddleware = createOpenApiBuilder(options ?? {})
17
17
  .withTags(["Stats"])
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import merge from "lodash/merge";
33
33
 
34
- import type {modelRouterOptions} from "./api";
34
+ import type {ModelRouterOptions} from "./api";
35
35
  import {logger} from "./logger";
36
36
  import {defaultOpenApiErrorResponses} from "./openApi";
37
37
 
@@ -250,7 +250,7 @@ interface OpenApiConfig {
250
250
  */
251
251
  export class OpenApiMiddlewareBuilder {
252
252
  /** Router options containing OpenAPI configuration */
253
- private options: Partial<modelRouterOptions<any>>;
253
+ private options: Partial<ModelRouterOptions<any>>;
254
254
 
255
255
  /** Accumulated OpenAPI configuration from builder methods */
256
256
  private config: OpenApiConfig;
@@ -260,7 +260,7 @@ export class OpenApiMiddlewareBuilder {
260
260
  *
261
261
  * @param options - Router options containing the OpenAPI path configuration
262
262
  */
263
- constructor(options: Partial<modelRouterOptions<any>>) {
263
+ constructor(options: Partial<ModelRouterOptions<any>>) {
264
264
  this.options = options;
265
265
  this.config = {
266
266
  responses: {},
@@ -630,7 +630,7 @@ export class OpenApiMiddlewareBuilder {
630
630
  * ```
631
631
  */
632
632
  export function createOpenApiBuilder(
633
- options: Partial<modelRouterOptions<any>>
633
+ options: Partial<ModelRouterOptions<any>>
634
634
  ): OpenApiMiddlewareBuilder {
635
635
  return new OpenApiMiddlewareBuilder(options);
636
636
  }
@@ -4,7 +4,7 @@ import type express from "express";
4
4
  import type {NextFunction} from "express";
5
5
  import mongoose, {type Model} from "mongoose";
6
6
 
7
- import {addPopulateToQuery, getModel, type modelRouterOptions, type RESTMethod} from "./api";
7
+ import {addPopulateToQuery, type ModelRouterOptions, type RESTMethod} from "./api";
8
8
  import type {User} from "./auth";
9
9
  import {APIError} from "./errors";
10
10
  import {logger} from "./logger";
@@ -101,8 +101,8 @@ export async function checkPermissions<T>(
101
101
  // finds the relevant object, checks the permissions, and attaches the object to the request as
102
102
  // req.obj.
103
103
  export function permissionMiddleware<T>(
104
- baseModel: Model<T>,
105
- options: Pick<modelRouterOptions<T>, "permissions" | "populatePaths" | "discriminatorKey">
104
+ model: Model<T>,
105
+ options: Pick<ModelRouterOptions<T>, "permissions" | "populatePaths">
106
106
  ) {
107
107
  return async (req: express.Request, _res: express.Response, next: NextFunction) => {
108
108
  if (req.method === "OPTIONS") {
@@ -131,8 +131,6 @@ export function permissionMiddleware<T>(
131
131
  });
132
132
  }
133
133
 
134
- const model = getModel(baseModel, req.body, options);
135
-
136
134
  // All methods check for permissions.
137
135
  if (!(await checkPermissions(method, options.permissions[method], req.user))) {
138
136
  throw new APIError({
@@ -159,15 +157,7 @@ export function permissionMiddleware<T>(
159
157
  title: `GET failed on ${req.params.id}`,
160
158
  });
161
159
  }
162
- if (!data || (["update", "delete"].includes(method) && data?.__t && !req.body?.__t)) {
163
- // For discriminated models, return 404 without checking hidden state
164
- if (["update", "delete"].includes(method) && data?.__t && !req.body?.__t) {
165
- throw new APIError({
166
- status: 404,
167
- title: `Document ${req.params.id} not found for model ${model.modelName}`,
168
- });
169
- }
170
-
160
+ if (!data) {
171
161
  // Check if document exists but is hidden. Completely skip plugins.
172
162
  const hiddenDoc = await model.collection.findOne({
173
163
  _id: new mongoose.Types.ObjectId(req.params.id),
@@ -1,7 +1,7 @@
1
1
  import type express from "express";
2
2
  import type {Document} from "mongoose";
3
3
 
4
- import type {modelRouterOptions} from "./api";
4
+ import type {ModelRouterOptions} from "./api";
5
5
  import type {User} from "./auth";
6
6
  import {APIError} from "./errors";
7
7
  import {logger} from "./logger";
@@ -88,7 +88,7 @@ export function AdminOwnerTransformer<T>(options: {
88
88
  }
89
89
 
90
90
  export function transform<T>(
91
- options: modelRouterOptions<T>,
91
+ options: ModelRouterOptions<T>,
92
92
  data: Partial<T> | Partial<T>[],
93
93
  method: "create" | "update",
94
94
  user?: User
@@ -112,7 +112,7 @@ export function transform<T>(
112
112
 
113
113
  export function serialize<T>(
114
114
  req: express.Request,
115
- options: modelRouterOptions<T>,
115
+ options: ModelRouterOptions<T>,
116
116
  data: (Document<any, any, any> & T) | (Document<any, any, any> & T)[]
117
117
  ) {
118
118
  const serializeFn = (serializeData: Document<any, any, any> & T, serializeUser?: User) => {
@@ -153,7 +153,7 @@ export async function defaultResponseHandler<T>(
153
153
  doc: (Document<any, any, any> & T) | (Document<any, any, any> & T)[] | null,
154
154
  method: "list" | "create" | "read" | "update",
155
155
  request: express.Request,
156
- options: modelRouterOptions<T>
156
+ options: ModelRouterOptions<T>
157
157
  ) {
158
158
  if (!doc) {
159
159
  return null;