@terreno/api 0.12.2 → 0.13.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.
@@ -1,13 +1,14 @@
1
1
  import {beforeEach, describe, expect, it} from "bun:test";
2
- import mongoose, {Schema} from "mongoose";
2
+ import mongoose, {type Document, type HydratedDocument, Schema} from "mongoose";
3
3
 
4
4
  import {fixMixedFields, getOpenApiSpecForModel, unpopulate} from "./populate";
5
- import {FoodModel, setupDb, UserModel} from "./tests";
5
+ import {FoodModel, setupDb, type User, UserModel} from "./tests";
6
6
 
7
7
  describe("populate functions", () => {
8
- let admin: any;
9
- let notAdmin: any;
8
+ let admin: HydratedDocument<User>;
9
+ let notAdmin: HydratedDocument<User>;
10
10
 
11
+ // noExplicitAny: typing as HydratedDocument<Food> causes cascading errors on populated field access patterns (e.g. populated.ownerId.name)
11
12
  let spinach: any;
12
13
 
13
14
  beforeEach(async () => {
@@ -44,6 +45,7 @@ describe("populate functions", () => {
44
45
  expect(populated.likesIds[1].userId.id).toBe(notAdmin.id);
45
46
  expect(populated.likesIds[1].userId.name).toBe("Not Admin");
46
47
 
48
+ // noExplicitAny: unpopulate returns Document<T> which doesn't expose model properties; would require refactoring the return type
47
49
  let unpopulated: any = unpopulate(populated, "ownerId");
48
50
  expect(spinach.ownerId.name).toBeUndefined();
49
51
  expect(unpopulated.ownerId.toString()).toBe(admin.id);
@@ -68,7 +70,7 @@ describe("populate functions", () => {
68
70
  describe("unpopulate edge cases", () => {
69
71
  it("throws error when path is empty", () => {
70
72
  const doc = {name: "test"};
71
- expect(() => unpopulate(doc as any, "")).toThrow("path is required");
73
+ expect(() => unpopulate(doc as unknown as Document<unknown>, "")).toThrow("path is required");
72
74
  });
73
75
 
74
76
  it("unpopulates single populated field", () => {
@@ -76,7 +78,9 @@ describe("unpopulate edge cases", () => {
76
78
  name: "test",
77
79
  ownerId: {_id: "owner-123", email: "owner@test.com"},
78
80
  };
79
- const result = unpopulate(doc as any, "ownerId") as any;
81
+ const result = unpopulate(doc as unknown as Document<unknown>, "ownerId") as unknown as {
82
+ ownerId: string;
83
+ };
80
84
  expect(result.ownerId).toBe("owner-123");
81
85
  });
82
86
 
@@ -85,7 +89,9 @@ describe("unpopulate edge cases", () => {
85
89
  items: [{_id: "item-1", name: "Item 1"}, {_id: "item-2", name: "Item 2"}, "item-3"],
86
90
  name: "test",
87
91
  };
88
- const result = unpopulate(doc as any, "items") as any;
92
+ const result = unpopulate(doc as unknown as Document<unknown>, "items") as unknown as {
93
+ items: string[];
94
+ };
89
95
  expect(result.items).toEqual(["item-1", "item-2", "item-3"]);
90
96
  });
91
97
 
@@ -99,13 +105,17 @@ describe("unpopulate edge cases", () => {
99
105
  ],
100
106
  },
101
107
  };
102
- const result = unpopulate(doc as any, "nested.items") as any;
108
+ const result = unpopulate(doc as unknown as Document<unknown>, "nested.items") as unknown as {
109
+ nested: {items: string[]};
110
+ };
103
111
  expect(result.nested.items).toEqual(["item-1", "item-2"]);
104
112
  });
105
113
 
106
114
  it("returns original doc when path does not exist", () => {
107
115
  const doc = {name: "test"};
108
- const result = unpopulate(doc as any, "nonexistent") as any;
116
+ const result = unpopulate(doc as unknown as Document<unknown>, "nonexistent") as unknown as {
117
+ name: string;
118
+ };
109
119
  expect(result).toEqual(doc);
110
120
  });
111
121
 
@@ -117,7 +127,10 @@ describe("unpopulate edge cases", () => {
117
127
  ],
118
128
  name: "test",
119
129
  };
120
- const result = unpopulate(doc as any, "containers.items") as any;
130
+ const result = unpopulate(
131
+ doc as unknown as Document<unknown>,
132
+ "containers.items"
133
+ ) as unknown as {containers: {items: string[]}[]};
121
134
  expect(result.containers[0].items).toEqual(["item-1", "item-2"]);
122
135
  expect(result.containers[1].items).toEqual(["item-3", "item-4"]);
123
136
  });
@@ -131,12 +144,14 @@ describe("fixMixedFields", () => {
131
144
 
132
145
  it("returns early when properties is missing", () => {
133
146
  const schema = new Schema({});
134
- expect(() => fixMixedFields(schema, null as any)).not.toThrow();
147
+ expect(() => fixMixedFields(schema, null as unknown as Record<string, unknown>)).not.toThrow();
135
148
  });
136
149
 
137
150
  it("replaces Mixed fields with only description", () => {
138
151
  const schema = new Schema({data: {description: "any data", type: Schema.Types.Mixed}});
139
- const properties: any = {data: {description: "any data", type: "object"}};
152
+ const properties: Record<string, Record<string, unknown>> = {
153
+ data: {description: "any data", type: "object"},
154
+ };
140
155
  fixMixedFields(schema, properties);
141
156
  expect(properties.data).toEqual({description: "any data"});
142
157
  });
@@ -144,14 +159,14 @@ describe("fixMixedFields", () => {
144
159
  it("recurses into arrays of sub-documents", () => {
145
160
  const subSchema = new Schema({meta: {type: Schema.Types.Mixed}});
146
161
  const schema = new Schema({items: [subSchema]});
147
- const properties: any = {
162
+ const properties = {
148
163
  items: {
149
164
  items: {
150
165
  properties: {
151
- meta: {type: "object"},
166
+ meta: {type: "object"} as Record<string, unknown>,
152
167
  },
153
168
  },
154
- type: "array",
169
+ type: "array" as const,
155
170
  },
156
171
  };
157
172
  fixMixedFields(schema, properties);
@@ -211,7 +226,7 @@ describe("getOpenApiSpecForModel edge cases", () => {
211
226
  populatePaths: [{path: "eatenBy"}],
212
227
  });
213
228
  expect(result.properties.eatenBy).toBeDefined();
214
- expect((result.properties.eatenBy as any).items).toBeDefined();
229
+ expect((result.properties.eatenBy as Record<string, unknown>).items).toBeDefined();
215
230
  });
216
231
 
217
232
  it("populates nested ref in sub-schema (likesIds.userId)", () => {
@@ -224,7 +239,7 @@ describe("getOpenApiSpecForModel edge cases", () => {
224
239
  it("includes virtuals from model schema", () => {
225
240
  const result = getOpenApiSpecForModel(FoodModel);
226
241
  expect(result.properties.description).toBeDefined();
227
- expect((result.properties.description as any).type).toBe("any");
242
+ expect((result.properties.description as Record<string, unknown>).type).toBe("any");
228
243
  });
229
244
 
230
245
  it("includes virtuals from child schemas", () => {
@@ -240,7 +255,10 @@ describe("getOpenApiSpecForModel edge cases", () => {
240
255
  mongoose.models.ParentWithChildVirtual ||
241
256
  mongoose.model("ParentWithChildVirtual", parentSchema);
242
257
  const result = getOpenApiSpecForModel(ParentModel);
243
- const detail = result.properties.detail as any;
258
+ const detail = result.properties.detail as Record<
259
+ string,
260
+ Record<string, Record<string, unknown>>
261
+ >;
244
262
  expect(detail.properties.displayAmount).toBeDefined();
245
263
  expect(detail.properties.displayAmount.type).toBe("any");
246
264
  });
@@ -251,7 +269,10 @@ describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
251
269
  const result = getOpenApiSpecForModel(FoodModel, {
252
270
  populatePaths: [{fields: ["name.nested"], path: "ownerId"}],
253
271
  });
254
- const ownerProps = (result.properties.ownerId as any).properties;
272
+ const ownerProps = (result.properties.ownerId as Record<string, unknown>).properties as Record<
273
+ string,
274
+ unknown
275
+ >;
255
276
  expect(ownerProps.name).toBeDefined();
256
277
  expect(typeof ownerProps.name).toBe("object");
257
278
  });
@@ -261,8 +282,12 @@ describe("filterKeys (via getOpenApiSpecForModel populatePaths)", () => {
261
282
  populatePaths: [{fields: ["__proto__.polluted"], path: "ownerId"}],
262
283
  });
263
284
  expect(result.properties).toBeDefined();
285
+ // noExplicitAny: testing that prototype pollution did not add a 'polluted' property to Object.prototype
264
286
  expect((Object.prototype as any).polluted).toBeUndefined();
265
- const ownerProps = (result.properties.ownerId as any).properties;
287
+ const ownerProps = (result.properties.ownerId as Record<string, unknown>).properties as Record<
288
+ string,
289
+ unknown
290
+ >;
266
291
  expect(ownerProps).toBeDefined();
267
292
  expect(Object.keys(ownerProps)).not.toContain("__proto__");
268
293
  });
@@ -86,7 +86,7 @@ const progressSchema = new Schema(
86
86
  percentage: {description: "Progress percentage from 0 to 100", max: 100, min: 0, type: Number},
87
87
  stage: {description: "Current stage of the task", type: String},
88
88
  },
89
- {_id: false}
89
+ {_id: false, strict: "throw"}
90
90
  );
91
91
 
92
92
  const logSchema = new Schema(
@@ -100,7 +100,7 @@ const logSchema = new Schema(
100
100
  message: {description: "Log message", required: true, type: String},
101
101
  timestamp: {description: "When this log entry was created", required: true, type: Date},
102
102
  },
103
- {_id: false}
103
+ {_id: false, strict: "throw"}
104
104
  );
105
105
 
106
106
  const backgroundTaskSchema = new Schema<
@@ -86,9 +86,10 @@ export class GcpSecretProvider implements SecretProvider {
86
86
  const SecretManagerServiceClient =
87
87
  mod.SecretManagerServiceClient ?? mod.default?.SecretManagerServiceClient;
88
88
  if (!SecretManagerServiceClient) {
89
- throw new Error(
90
- "SecretManagerServiceClient not found in @google-cloud/secret-manager module"
91
- );
89
+ throw new APIError({
90
+ status: 500,
91
+ title: "SecretManagerServiceClient not found in @google-cloud/secret-manager module",
92
+ });
92
93
  }
93
94
  this.client = new SecretManagerServiceClient();
94
95
  }
package/src/tests.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import express, {type Express} from "express";
2
2
  import mongoose, {type Model, model, Schema} from "mongoose";
3
- import passportLocalMongoose from "passport-local-mongoose";
3
+ import passportLocalMongoose, {type PassportLocalMongooseDocument} from "passport-local-mongoose";
4
4
  import qs from "qs";
5
5
  import supertest from "supertest";
6
6
  import type TestAgent from "supertest/lib/agent";
@@ -62,19 +62,22 @@ const userSchema = new Schema<User>({
62
62
  username: {description: "The user's username", type: String},
63
63
  });
64
64
 
65
- userSchema.plugin(passportLocalMongoose as any, {
66
- attemptsField: "attempts",
67
- interval: process.env.NODE_ENV === "test" ? 1 : 100,
68
- limitAttempts: true,
69
- maxAttempts: 3,
70
- maxInterval: process.env.NODE_ENV === "test" ? 1 : 300000,
71
- usernameCaseInsensitive: true,
72
- usernameField: "email",
73
- });
65
+ userSchema.plugin(
66
+ passportLocalMongoose as unknown as (schema: Schema, options?: Record<string, unknown>) => void,
67
+ {
68
+ attemptsField: "attempts",
69
+ interval: process.env.NODE_ENV === "test" ? 1 : 100,
70
+ limitAttempts: true,
71
+ maxAttempts: 3,
72
+ maxInterval: process.env.NODE_ENV === "test" ? 1 : 300000,
73
+ usernameCaseInsensitive: true,
74
+ usernameField: "email",
75
+ }
76
+ );
74
77
  // userSchema.plugin(tokenPlugin);
75
78
  userSchema.plugin(createdUpdatedPlugin);
76
79
  userSchema.plugin(isDisabledPlugin);
77
- userSchema.methods.postCreate = async function (body: any) {
80
+ userSchema.methods.postCreate = async function (body: {age?: number}) {
78
81
  this.age = body.age;
79
82
  return this.save();
80
83
  };
@@ -121,6 +124,7 @@ const foodSchema = new Schema<Food>(
121
124
  type: Schema.Types.ObjectId,
122
125
  },
123
126
  ],
127
+ // noExplicitAny: DateOnly is a custom SchemaType not recognized by Mongoose's built-in type definitions
124
128
  expiration: {description: "Expiration date of the food", type: DateOnly as any},
125
129
  hidden: {
126
130
  default: false,
@@ -221,13 +225,13 @@ export const setupDb = async () => {
221
225
  UserModel.create({admin: true, email: "admin@example.com", name: "Admin"}),
222
226
  UserModel.create({admin: true, email: "admin+other@example.com", name: "Admin Other"}),
223
227
  ]);
224
- await (notAdmin as any).setPassword("password");
228
+ await (notAdmin as unknown as PassportLocalMongooseDocument).setPassword("password");
225
229
  await notAdmin.save();
226
230
 
227
- await (admin as any).setPassword("securePassword");
231
+ await (admin as unknown as PassportLocalMongooseDocument).setPassword("securePassword");
228
232
  await admin.save();
229
233
 
230
- await (adminOther as any).setPassword("otherPassword");
234
+ await (adminOther as unknown as PassportLocalMongooseDocument).setPassword("otherPassword");
231
235
 
232
236
  await adminOther.save();
233
237
 
package/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import mongoose, {Types} from "mongoose";
2
2
 
3
+ import {APIError} from "./errors";
3
4
  import {logger} from "./logger";
4
5
 
5
6
  // A better version of mongoose's ObjectId.isValid,
@@ -31,17 +32,26 @@ export const checkModelsStrict = (ignoredModels: string[] = []): void => {
31
32
  const schema = mongoose.model(model).schema;
32
33
 
33
34
  if (schema.get("toObject")?.virtuals !== true) {
34
- throw new Error(`Model ${model} toObject.virtuals not set to true`);
35
+ throw new APIError({
36
+ status: 500,
37
+ title: `Model ${model} toObject.virtuals not set to true`,
38
+ });
35
39
  }
36
40
  if (schema.get("toJSON")?.virtuals !== true) {
37
- throw new Error(`Model ${model} toJSON.virtuals not set to true`);
41
+ throw new APIError({
42
+ status: 500,
43
+ title: `Model ${model} toJSON.virtuals not set to true`,
44
+ });
38
45
  }
39
46
 
40
47
  if (ignoredModels.includes(model)) {
41
48
  continue;
42
49
  }
43
50
  if (schema.get("strict") !== "throw") {
44
- throw new Error(`Model ${model} is not set to strict mode.`);
51
+ throw new APIError({
52
+ status: 500,
53
+ title: `Model ${model} is not set to strict mode.`,
54
+ });
45
55
  }
46
56
  }
47
57
  };