@terreno/api 0.11.0 → 0.11.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/dist/api.d.ts CHANGED
@@ -8,11 +8,11 @@ import { type TerrenoTransformer } from "./transformers";
8
8
  export type JSONPrimitive = string | number | boolean | null;
9
9
  export interface JSONArray extends Array<JSONValue> {
10
10
  }
11
- export type JSONObject = {
11
+ export interface JSONObject {
12
12
  [member: string]: JSONValue;
13
- };
13
+ }
14
14
  export type JSONValue = JSONPrimitive | JSONObject | JSONArray;
15
- export declare function addPopulateToQuery(builtQuery: mongoose.Query<any[], any, Record<string, never>, any>, populatePaths?: PopulatePath[]): mongoose.Query<any[], any, Record<string, never>, any, "find", Record<string, never>>;
15
+ export declare const addPopulateToQuery: (builtQuery: mongoose.Query<any[], any, Record<string, never>, any>, populatePaths?: PopulatePath[]) => mongoose.Query<any[], any, Record<string, never>, any, "find", Record<string, never>>;
16
16
  /**
17
17
  * @param a - the first number
18
18
  * @param b - the second number
package/dist/api.js CHANGED
@@ -119,8 +119,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
119
119
  return (mod && mod.__esModule) ? mod : { "default": mod };
120
120
  };
121
121
  Object.defineProperty(exports, "__esModule", { value: true });
122
- exports.gooseRestRouter = exports.asyncHandler = void 0;
123
- exports.addPopulateToQuery = addPopulateToQuery;
122
+ exports.gooseRestRouter = exports.asyncHandler = exports.addPopulateToQuery = void 0;
124
123
  exports.modelRouter = modelRouter;
125
124
  /**
126
125
  * This is the doc comment for api.ts
@@ -139,7 +138,7 @@ var openApiValidator_1 = require("./openApiValidator");
139
138
  var permissions_1 = require("./permissions");
140
139
  var transformers_1 = require("./transformers");
141
140
  var utils_1 = require("./utils");
142
- function addPopulateToQuery(builtQuery, populatePaths) {
141
+ var addPopulateToQuery = function (builtQuery, populatePaths) {
143
142
  var e_1, _a;
144
143
  var paths = populatePaths !== null && populatePaths !== void 0 ? populatePaths : [];
145
144
  var query = builtQuery;
@@ -159,7 +158,8 @@ function addPopulateToQuery(builtQuery, populatePaths) {
159
158
  finally { if (e_1) throw e_1.error; }
160
159
  }
161
160
  return query;
162
- }
161
+ };
162
+ exports.addPopulateToQuery = addPopulateToQuery;
163
163
  // TODOS:
164
164
  // Support bulk actions
165
165
  // Support more complex query fields
@@ -169,7 +169,7 @@ var PAGINATION_QUERY_PARAMS = ["limit", "page", "sort"];
169
169
  // Add support for more complex queries.
170
170
  var COMPLEX_QUERY_PARAMS = ["$and", "$or"];
171
171
  // Ensures query params are allowed. Also checks nested query params when using $and/$or.
172
- function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
172
+ var checkQueryParamAllowed = function (queryParam, queryParamValue, queryFields) {
173
173
  var e_2, _a, e_3, _b;
174
174
  if (queryFields === void 0) { queryFields = []; }
175
175
  // Check the values of each of the complex query params. We don't support recursive queries here,
@@ -209,7 +209,7 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
209
209
  title: "".concat(queryParam, " is not allowed as a query param."),
210
210
  });
211
211
  }
212
- }
212
+ };
213
213
  // Handles dot notation patches, creates a normal object to be used for updates.
214
214
  // function flattenDotNotationPatch(data: any) {
215
215
  // const result = {};
@@ -231,7 +231,7 @@ function checkQueryParamAllowed(queryParam, queryParamValue, queryFields) {
231
231
  // Helper to determine if validation should be enabled for a specific operation.
232
232
  // When options.validation is not set, returns true — the middleware's own
233
233
  // isConfigured check will decide whether to actually validate.
234
- function shouldValidate(options, operation) {
234
+ var shouldValidate = function (options, operation) {
235
235
  var _a, _b, _c;
236
236
  // Check route-specific validation option first
237
237
  if (options.validation !== undefined) {
@@ -248,9 +248,9 @@ function shouldValidate(options, operation) {
248
248
  }
249
249
  // Default: let middleware's isConfigured check decide
250
250
  return true;
251
- }
251
+ };
252
252
  // Get body validation middleware if validation is enabled
253
- function getBodyValidationMiddleware(model, options, operation) {
253
+ var getBodyValidationMiddleware = function (model, options, operation) {
254
254
  var validationOptions = {};
255
255
  if (!shouldValidate(options, operation)) {
256
256
  validationOptions.enabled = false;
@@ -271,9 +271,9 @@ function getBodyValidationMiddleware(model, options, operation) {
271
271
  }
272
272
  }
273
273
  return (0, openApiValidator_1.validateModelRequestBody)(model, validationOptions);
274
- }
274
+ };
275
275
  // Get query validation middleware if validation is enabled
276
- function getQueryValidationMiddleware(model, options) {
276
+ var getQueryValidationMiddleware = function (model, options) {
277
277
  var querySchema = (0, openApiValidator_1.buildQuerySchemaFromFields)(model, options.queryFields);
278
278
  var validationOptions = {};
279
279
  if (!shouldValidate(options, "query")) {
@@ -283,7 +283,7 @@ function getQueryValidationMiddleware(model, options) {
283
283
  validationOptions.onError = options.validation.onError;
284
284
  }
285
285
  return (0, openApiValidator_1.validateQueryParams)(querySchema, validationOptions);
286
- }
286
+ };
287
287
  function modelRouter(pathOrModel, modelOrOptions, maybeOptions) {
288
288
  var model;
289
289
  var options;
@@ -406,7 +406,7 @@ function _buildModelRouter(model, options) {
406
406
  case 10:
407
407
  _a.trys.push([10, 12, , 13]);
408
408
  populateQuery = model.findById(data._id);
409
- populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
409
+ populateQuery = (0, exports.addPopulateToQuery)(populateQuery, options.populatePaths);
410
410
  return [4 /*yield*/, populateQuery.exec()];
411
411
  case 11:
412
412
  data = _a.sent();
@@ -568,7 +568,7 @@ function _buildModelRouter(model, options) {
568
568
  else if (options.sort) {
569
569
  builtQuery = builtQuery.sort(options.sort);
570
570
  }
571
- populatedQuery = addPopulateToQuery(builtQuery, options.populatePaths);
571
+ populatedQuery = (0, exports.addPopulateToQuery)(builtQuery, options.populatePaths);
572
572
  _o.label = 7;
573
573
  case 7:
574
574
  _o.trys.push([7, 9, , 10]);
@@ -754,7 +754,7 @@ function _buildModelRouter(model, options) {
754
754
  case 9:
755
755
  if (!options.populatePaths) return [3 /*break*/, 11];
756
756
  populateQuery = model.findById(doc._id);
757
- populateQuery = addPopulateToQuery(populateQuery, options.populatePaths);
757
+ populateQuery = (0, exports.addPopulateToQuery)(populateQuery, options.populatePaths);
758
758
  return [4 /*yield*/, populateQuery.exec()];
759
759
  case 10:
760
760
  doc = _b.sent();
@@ -281,4 +281,66 @@ var tests_1 = require("./tests");
281
281
  });
282
282
  (0, bun_test_1.expect)(result.properties).toBeDefined();
283
283
  });
284
+ (0, bun_test_1.it)("uses openApiComponent $ref when provided", function () {
285
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
286
+ populatePaths: [{ openApiComponent: "UserComponent", path: "ownerId" }],
287
+ });
288
+ (0, bun_test_1.expect)(result.properties.ownerId).toEqual({
289
+ $ref: "#/components/schemas/UserComponent",
290
+ });
291
+ });
292
+ (0, bun_test_1.it)("populates array ref fields (eatenBy)", function () {
293
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
294
+ populatePaths: [{ path: "eatenBy" }],
295
+ });
296
+ (0, bun_test_1.expect)(result.properties.eatenBy).toBeDefined();
297
+ (0, bun_test_1.expect)(result.properties.eatenBy.items).toBeDefined();
298
+ });
299
+ (0, bun_test_1.it)("populates nested ref in sub-schema (likesIds.userId)", function () {
300
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
301
+ populatePaths: [{ path: "likesIds.userId" }],
302
+ });
303
+ (0, bun_test_1.expect)(result.properties.likesIds).toBeDefined();
304
+ });
305
+ (0, bun_test_1.it)("includes virtuals from model schema", function () {
306
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel);
307
+ (0, bun_test_1.expect)(result.properties.description).toBeDefined();
308
+ (0, bun_test_1.expect)(result.properties.description.type).toBe("any");
309
+ });
310
+ (0, bun_test_1.it)("includes virtuals from child schemas", function () {
311
+ var childSub = new mongoose_1.Schema({ amount: { description: "Amount", type: Number } });
312
+ childSub.virtual("displayAmount").get(function () {
313
+ return "".concat(this.amount, " units");
314
+ });
315
+ var parentSchema = new mongoose_1.Schema({
316
+ detail: { description: "Single embedded detail", type: childSub },
317
+ title: { description: "Title", type: String },
318
+ });
319
+ var ParentModel = mongoose_1.default.models.ParentWithChildVirtual ||
320
+ mongoose_1.default.model("ParentWithChildVirtual", parentSchema);
321
+ var result = (0, populate_1.getOpenApiSpecForModel)(ParentModel);
322
+ var detail = result.properties.detail;
323
+ (0, bun_test_1.expect)(detail.properties.displayAmount).toBeDefined();
324
+ (0, bun_test_1.expect)(detail.properties.displayAmount.type).toBe("any");
325
+ });
326
+ });
327
+ (0, bun_test_1.describe)("filterKeys (via getOpenApiSpecForModel populatePaths)", function () {
328
+ (0, bun_test_1.it)("filters populated fields using dot-notation keys", function () {
329
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
330
+ populatePaths: [{ fields: ["name.nested"], path: "ownerId" }],
331
+ });
332
+ var ownerProps = result.properties.ownerId.properties;
333
+ (0, bun_test_1.expect)(ownerProps.name).toBeDefined();
334
+ (0, bun_test_1.expect)(typeof ownerProps.name).toBe("object");
335
+ });
336
+ (0, bun_test_1.it)("rejects prototype pollution keys in nested dot-notation", function () {
337
+ var result = (0, populate_1.getOpenApiSpecForModel)(tests_1.FoodModel, {
338
+ populatePaths: [{ fields: ["__proto__.polluted"], path: "ownerId" }],
339
+ });
340
+ (0, bun_test_1.expect)(result.properties).toBeDefined();
341
+ (0, bun_test_1.expect)(Object.prototype.polluted).toBeUndefined();
342
+ var ownerProps = result.properties.ownerId.properties;
343
+ (0, bun_test_1.expect)(ownerProps).toBeDefined();
344
+ (0, bun_test_1.expect)(Object.keys(ownerProps)).not.toContain("__proto__");
345
+ });
284
346
  });
package/package.json CHANGED
@@ -104,5 +104,5 @@
104
104
  "updateSnapshot": "bun test --update-snapshots"
105
105
  },
106
106
  "types": "dist/index.d.ts",
107
- "version": "0.11.0"
107
+ "version": "0.11.2"
108
108
  }
package/src/api.ts CHANGED
@@ -36,13 +36,15 @@ import {isValidObjectId} from "./utils";
36
36
 
37
37
  export type JSONPrimitive = string | number | boolean | null;
38
38
  export interface JSONArray extends Array<JSONValue> {}
39
- export type JSONObject = {[member: string]: JSONValue};
39
+ export interface JSONObject {
40
+ [member: string]: JSONValue;
41
+ }
40
42
  export type JSONValue = JSONPrimitive | JSONObject | JSONArray;
41
43
 
42
- export function addPopulateToQuery(
44
+ export const addPopulateToQuery = (
43
45
  builtQuery: mongoose.Query<any[], any, Record<string, never>, any>,
44
46
  populatePaths?: PopulatePath[]
45
- ) {
47
+ ) => {
46
48
  const paths = populatePaths ?? [];
47
49
  let query = builtQuery;
48
50
 
@@ -52,7 +54,7 @@ export function addPopulateToQuery(
52
54
  query = builtQuery.populate({path, select});
53
55
  }
54
56
  return query;
55
- }
57
+ };
56
58
 
57
59
  // TODOS:
58
60
  // Support bulk actions
@@ -312,11 +314,11 @@ export interface ModelRouterOptions<T> {
312
314
  }
313
315
 
314
316
  // Ensures query params are allowed. Also checks nested query params when using $and/$or.
315
- function checkQueryParamAllowed(
317
+ const checkQueryParamAllowed = (
316
318
  queryParam: string,
317
319
  queryParamValue: any,
318
320
  queryFields: string[] = []
319
- ) {
321
+ ) => {
320
322
  // Check the values of each of the complex query params. We don't support recursive queries here,
321
323
  // just one level of and/or
322
324
  if (COMPLEX_QUERY_PARAMS.includes(queryParam)) {
@@ -334,7 +336,7 @@ function checkQueryParamAllowed(
334
336
  title: `${queryParam} is not allowed as a query param.`,
335
337
  });
336
338
  }
337
- }
339
+ };
338
340
 
339
341
  // Handles dot notation patches, creates a normal object to be used for updates.
340
342
  // function flattenDotNotationPatch(data: any) {
@@ -358,10 +360,10 @@ function checkQueryParamAllowed(
358
360
  // Helper to determine if validation should be enabled for a specific operation.
359
361
  // When options.validation is not set, returns true — the middleware's own
360
362
  // isConfigured check will decide whether to actually validate.
361
- function shouldValidate(
363
+ const shouldValidate = (
362
364
  options: ModelRouterOptions<any>,
363
365
  operation: "create" | "update" | "query"
364
- ): boolean {
366
+ ): boolean => {
365
367
  // Check route-specific validation option first
366
368
  if (options.validation !== undefined) {
367
369
  if (typeof options.validation === "boolean") {
@@ -378,14 +380,14 @@ function shouldValidate(
378
380
 
379
381
  // Default: let middleware's isConfigured check decide
380
382
  return true;
381
- }
383
+ };
382
384
 
383
385
  // Get body validation middleware if validation is enabled
384
- function getBodyValidationMiddleware<T>(
386
+ const getBodyValidationMiddleware = <T>(
385
387
  model: Model<T>,
386
388
  options: ModelRouterOptions<T>,
387
389
  operation: "create" | "update"
388
- ): (req: Request, res: Response, next: NextFunction) => void {
390
+ ): ((req: Request, res: Response, next: NextFunction) => void) => {
389
391
  const validationOptions: import("./openApiValidator").RequestBodyValidatorOptions = {};
390
392
  if (!shouldValidate(options, operation)) {
391
393
  validationOptions.enabled = false;
@@ -408,13 +410,13 @@ function getBodyValidationMiddleware<T>(
408
410
  }
409
411
 
410
412
  return validateModelRequestBody(model, validationOptions);
411
- }
413
+ };
412
414
 
413
415
  // Get query validation middleware if validation is enabled
414
- function getQueryValidationMiddleware<T>(
416
+ const getQueryValidationMiddleware = <T>(
415
417
  model: Model<T>,
416
418
  options: ModelRouterOptions<T>
417
- ): (req: Request, res: Response, next: NextFunction) => void {
419
+ ): ((req: Request, res: Response, next: NextFunction) => void) => {
418
420
  const querySchema = buildQuerySchemaFromFields(model, options.queryFields);
419
421
  const validationOptions: import("./openApiValidator").QueryValidatorOptions = {};
420
422
  if (!shouldValidate(options, "query")) {
@@ -425,7 +427,7 @@ function getQueryValidationMiddleware<T>(
425
427
  }
426
428
 
427
429
  return validateQueryParams(querySchema, validationOptions);
428
- }
430
+ };
429
431
 
430
432
  /**
431
433
  * Registration object returned by modelRouter when called with a path.
@@ -196,4 +196,74 @@ describe("getOpenApiSpecForModel edge cases", () => {
196
196
  });
197
197
  expect(result.properties).toBeDefined();
198
198
  });
199
+
200
+ it("uses openApiComponent $ref when provided", () => {
201
+ const result = getOpenApiSpecForModel(FoodModel, {
202
+ populatePaths: [{openApiComponent: "UserComponent", path: "ownerId"}],
203
+ });
204
+ expect(result.properties.ownerId).toEqual({
205
+ $ref: "#/components/schemas/UserComponent",
206
+ });
207
+ });
208
+
209
+ it("populates array ref fields (eatenBy)", () => {
210
+ const result = getOpenApiSpecForModel(FoodModel, {
211
+ populatePaths: [{path: "eatenBy"}],
212
+ });
213
+ expect(result.properties.eatenBy).toBeDefined();
214
+ expect((result.properties.eatenBy as any).items).toBeDefined();
215
+ });
216
+
217
+ it("populates nested ref in sub-schema (likesIds.userId)", () => {
218
+ const result = getOpenApiSpecForModel(FoodModel, {
219
+ populatePaths: [{path: "likesIds.userId"}],
220
+ });
221
+ expect(result.properties.likesIds).toBeDefined();
222
+ });
223
+
224
+ it("includes virtuals from model schema", () => {
225
+ const result = getOpenApiSpecForModel(FoodModel);
226
+ expect(result.properties.description).toBeDefined();
227
+ expect((result.properties.description as any).type).toBe("any");
228
+ });
229
+
230
+ it("includes virtuals from child schemas", () => {
231
+ const childSub = new Schema({amount: {description: "Amount", type: Number}});
232
+ childSub.virtual("displayAmount").get(function () {
233
+ return `${this.amount} units`;
234
+ });
235
+ const parentSchema = new Schema({
236
+ detail: {description: "Single embedded detail", type: childSub},
237
+ title: {description: "Title", type: String},
238
+ });
239
+ const ParentModel =
240
+ mongoose.models.ParentWithChildVirtual ||
241
+ mongoose.model("ParentWithChildVirtual", parentSchema);
242
+ const result = getOpenApiSpecForModel(ParentModel);
243
+ const detail = result.properties.detail as any;
244
+ expect(detail.properties.displayAmount).toBeDefined();
245
+ expect(detail.properties.displayAmount.type).toBe("any");
246
+ });
247
+ });
248
+
249
+ describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
250
+ it("filters populated fields using dot-notation keys", () => {
251
+ const result = getOpenApiSpecForModel(FoodModel, {
252
+ populatePaths: [{fields: ["name.nested"], path: "ownerId"}],
253
+ });
254
+ const ownerProps = (result.properties.ownerId as any).properties;
255
+ expect(ownerProps.name).toBeDefined();
256
+ expect(typeof ownerProps.name).toBe("object");
257
+ });
258
+
259
+ it("rejects prototype pollution keys in nested dot-notation", () => {
260
+ const result = getOpenApiSpecForModel(FoodModel, {
261
+ populatePaths: [{fields: ["__proto__.polluted"], path: "ownerId"}],
262
+ });
263
+ expect(result.properties).toBeDefined();
264
+ expect((Object.prototype as any).polluted).toBeUndefined();
265
+ const ownerProps = (result.properties.ownerId as any).properties;
266
+ expect(ownerProps).toBeDefined();
267
+ expect(Object.keys(ownerProps)).not.toContain("__proto__");
268
+ });
199
269
  });