@terreno/api 0.20.2 → 0.22.0
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/.ai/guidelines/core.md +71 -0
- package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
- package/README.md +54 -1
- package/bunfig.toml +1 -1
- package/dist/__tests__/versionCheckPlugin.test.js +29 -7
- package/dist/actions.openApi.test.js +13 -11
- package/dist/api.js +98 -11
- package/dist/api.query.test.js +31 -1
- package/dist/api.test.js +211 -0
- package/dist/auth.test.js +418 -43
- package/dist/betterAuth.d.ts +1 -1
- package/dist/consentApp.test.js +1 -0
- package/dist/example.js +4 -4
- package/dist/expressServer.d.ts +0 -22
- package/dist/expressServer.js +1 -125
- package/dist/expressServer.test.js +90 -91
- package/dist/githubAuth.test.js +22 -22
- package/dist/logger.d.ts +154 -0
- package/dist/logger.js +445 -26
- package/dist/logger.test.js +435 -0
- package/dist/middleware.d.ts +7 -0
- package/dist/middleware.js +58 -1
- package/dist/middleware.test.js +159 -0
- package/dist/models/consentForm.js +2 -1
- package/dist/models/consentResponse.js +2 -1
- package/dist/models/versionConfig.js +2 -1
- package/dist/openApi.test.js +10 -17
- package/dist/openApiBuilder.d.ts +18 -0
- package/dist/openApiBuilder.js +21 -0
- package/dist/openApiBuilder.test.js +34 -10
- package/dist/permissions.test.js +10 -43
- package/dist/populate.test.js +10 -42
- package/dist/realtime/changeStreamWatcher.d.ts +4 -4
- package/dist/realtime/changeStreamWatcher.js +2 -4
- package/dist/realtime/queryMatcher.d.ts +1 -1
- package/dist/realtime/queryMatcher.js +39 -14
- package/dist/realtime/types.d.ts +3 -3
- package/dist/requestContext.d.ts +61 -0
- package/dist/requestContext.js +74 -0
- package/dist/secretProviders.test.js +335 -0
- package/dist/syncConsents.test.js +2 -2
- package/dist/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +66 -262
- package/dist/tests/createTestData.d.ts +9 -0
- package/dist/tests/createTestData.js +272 -0
- package/dist/tests/models.d.ts +71 -0
- package/dist/tests/models.js +134 -0
- package/dist/tests/mongoTestSetup.d.ts +7 -0
- package/dist/tests/mongoTestSetup.js +150 -0
- package/dist/tests/testEnv.d.ts +0 -0
- package/dist/tests/testEnv.js +6 -0
- package/dist/tests/testHelper.d.ts +22 -0
- package/dist/tests/testHelper.js +115 -0
- package/dist/tests/types.d.ts +29 -0
- package/dist/tests/types.js +2 -0
- package/dist/tests.d.ts +10 -78
- package/dist/tests.js +24 -241
- package/dist/transformers.test.js +14 -50
- package/package.json +18 -4
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
- package/src/__tests__/versionCheckPlugin.test.ts +43 -15
- package/src/actions.openApi.test.ts +12 -10
- package/src/api.query.test.ts +24 -1
- package/src/api.test.ts +169 -0
- package/src/api.ts +71 -0
- package/src/auth.test.ts +287 -39
- package/src/betterAuth.ts +1 -1
- package/src/consentApp.test.ts +1 -0
- package/src/example.ts +4 -4
- package/src/expressServer.test.ts +82 -85
- package/src/expressServer.ts +1 -213
- package/src/githubAuth.test.ts +22 -22
- package/src/logger.test.ts +466 -1
- package/src/logger.ts +477 -14
- package/src/middleware.test.ts +74 -2
- package/src/middleware.ts +57 -0
- package/src/models/consentForm.ts +3 -4
- package/src/models/consentResponse.ts +6 -4
- package/src/models/versionConfig.ts +3 -4
- package/src/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +27 -10
- package/src/openApiBuilder.ts +24 -0
- package/src/permissions.test.ts +8 -23
- package/src/populate.test.ts +7 -22
- package/src/realtime/changeStreamWatcher.ts +15 -10
- package/src/realtime/queryMatcher.ts +54 -27
- package/src/realtime/types.ts +4 -4
- package/src/requestContext.ts +86 -0
- package/src/secretProviders.test.ts +219 -1
- package/src/syncConsents.test.ts +1 -1
- package/src/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +22 -236
- package/src/tests/createTestData.ts +176 -0
- package/src/tests/models.ts +164 -0
- package/src/tests/mongoTestSetup.ts +69 -0
- package/src/tests/testEnv.ts +4 -0
- package/src/tests/testHelper.ts +57 -0
- package/src/tests/types.ts +35 -0
- package/src/tests.ts +40 -231
- package/src/transformers.test.ts +11 -30
- package/tsconfig.typedoc.json +4 -0
- package/dist/tests/index.d.ts +0 -1
- package/dist/tests/index.js +0 -17
- package/src/tests/index.ts +0 -1
|
@@ -3,8 +3,6 @@ import {beforeEach, describe, expect, it} from "bun:test";
|
|
|
3
3
|
import type express from "express";
|
|
4
4
|
import supertest from "supertest";
|
|
5
5
|
import {type ModelRouterOptions, modelRouter} from "./api";
|
|
6
|
-
import {addAuthRoutes, setupAuth} from "./auth";
|
|
7
|
-
import {setupServer} from "./expressServer";
|
|
8
6
|
import {Permissions} from "./permissions";
|
|
9
7
|
import {TerrenoApp} from "./terrenoApp";
|
|
10
8
|
import {FoodModel, setupDb, UserModel} from "./tests";
|
|
@@ -57,6 +55,8 @@ const primeActionOpenApiRoutes = async (
|
|
|
57
55
|
};
|
|
58
56
|
|
|
59
57
|
const assertActionOpenApiSpec = (spec: Record<string, unknown>): void => {
|
|
58
|
+
expect(spec.requestId).toBeUndefined();
|
|
59
|
+
|
|
60
60
|
const paths = spec.paths as Record<string, Record<string, unknown>>;
|
|
61
61
|
const collectionPath = paths["/food/summarize"];
|
|
62
62
|
const instancePath = paths["/food/{id}/ping"];
|
|
@@ -139,14 +139,18 @@ describe("action OpenAPI emission", () => {
|
|
|
139
139
|
|
|
140
140
|
const specRes = await server.get("/openapi.json").expect(200);
|
|
141
141
|
assertActionOpenApiSpec(specRes.body);
|
|
142
|
+
|
|
143
|
+
const pingRes = await server.get(`/food/${foodId}/ping`).expect(200);
|
|
144
|
+
expect(pingRes.body.data).toEqual({id: foodId});
|
|
145
|
+
expect(pingRes.body.requestId).toBe(pingRes.headers["x-request-id"]);
|
|
142
146
|
});
|
|
143
147
|
});
|
|
144
148
|
|
|
145
|
-
describe("
|
|
149
|
+
describe("configureApp", () => {
|
|
146
150
|
let app: express.Application;
|
|
147
151
|
|
|
148
152
|
beforeEach(() => {
|
|
149
|
-
const
|
|
153
|
+
const configureApp = (
|
|
150
154
|
router: express.Router,
|
|
151
155
|
routerOptions?: Partial<ModelRouterOptions<unknown>>
|
|
152
156
|
): void => {
|
|
@@ -156,16 +160,14 @@ describe("action OpenAPI emission", () => {
|
|
|
156
160
|
);
|
|
157
161
|
};
|
|
158
162
|
|
|
159
|
-
app =
|
|
160
|
-
|
|
163
|
+
app = new TerrenoApp({
|
|
164
|
+
configureApp,
|
|
161
165
|
skipListen: true,
|
|
162
166
|
userModel: UserModel as any,
|
|
163
|
-
});
|
|
164
|
-
setupAuth(app, UserModel as any);
|
|
165
|
-
addAuthRoutes(app, UserModel as any);
|
|
167
|
+
}).build();
|
|
166
168
|
});
|
|
167
169
|
|
|
168
|
-
it("emits the same action operations on first hit via
|
|
170
|
+
it("emits the same action operations on first hit via configureApp", async () => {
|
|
169
171
|
const server = supertest(app);
|
|
170
172
|
await primeActionOpenApiRoutes(server, foodId);
|
|
171
173
|
|
package/src/api.query.test.ts
CHANGED
|
@@ -95,7 +95,17 @@ describe("query and list methods", () => {
|
|
|
95
95
|
update: [Permissions.IsOwner],
|
|
96
96
|
},
|
|
97
97
|
populatePaths: [{path: "ownerId"}],
|
|
98
|
-
queryFields: [
|
|
98
|
+
queryFields: [
|
|
99
|
+
"hidden",
|
|
100
|
+
"name",
|
|
101
|
+
"calories",
|
|
102
|
+
"created",
|
|
103
|
+
"created_gte",
|
|
104
|
+
"created_lte",
|
|
105
|
+
"source.name",
|
|
106
|
+
"tags",
|
|
107
|
+
"eatenBy",
|
|
108
|
+
],
|
|
99
109
|
sort: {created: "descending"},
|
|
100
110
|
})
|
|
101
111
|
);
|
|
@@ -190,6 +200,19 @@ describe("query and list methods", () => {
|
|
|
190
200
|
expect(res.body.data[0].id).toBe((apple as any).id);
|
|
191
201
|
});
|
|
192
202
|
|
|
203
|
+
it("list applies created_gte and created_lte as a Date range", async () => {
|
|
204
|
+
const res = await agent
|
|
205
|
+
.get("/food")
|
|
206
|
+
.query({
|
|
207
|
+
created_gte: "2021-12-03T00:00:05.000Z",
|
|
208
|
+
created_lte: "2021-12-03T00:00:25.000Z",
|
|
209
|
+
limit: 10,
|
|
210
|
+
})
|
|
211
|
+
.expect(200);
|
|
212
|
+
const names = (res.body.data as {name: string}[]).map((d) => d.name).sort();
|
|
213
|
+
expect(names).toEqual(["Pizza", "Spinach"]);
|
|
214
|
+
});
|
|
215
|
+
|
|
193
216
|
it("list query params not in list", async () => {
|
|
194
217
|
const res = await agent.get(`/food?ownerId=${admin._id}`).expect(400);
|
|
195
218
|
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
package/src/api.test.ts
CHANGED
|
@@ -2040,5 +2040,174 @@ describe("@terreno/api", () => {
|
|
|
2040
2040
|
|
|
2041
2041
|
expect(res.body.data.name).toBe("Spinach");
|
|
2042
2042
|
});
|
|
2043
|
+
|
|
2044
|
+
it("returns 400 when X-Unmodified-Since-ISO header is an invalid date", async () => {
|
|
2045
|
+
const res = await agent
|
|
2046
|
+
.patch(`/food/${spinach._id}`)
|
|
2047
|
+
.set("X-Unmodified-Since-ISO", "not-a-date")
|
|
2048
|
+
.send({name: "Bad Date"})
|
|
2049
|
+
.expect(400);
|
|
2050
|
+
|
|
2051
|
+
expect(res.body.title).toBe("Invalid conflict-detection timestamp");
|
|
2052
|
+
expect(res.body.detail).toContain("X-Unmodified-Since-ISO");
|
|
2053
|
+
});
|
|
2054
|
+
|
|
2055
|
+
it("returns 400 when If-Unmodified-Since header is an invalid HTTP date", async () => {
|
|
2056
|
+
const res = await agent
|
|
2057
|
+
.patch(`/food/${spinach._id}`)
|
|
2058
|
+
.set("If-Unmodified-Since", "not-a-valid-http-date")
|
|
2059
|
+
.send({name: "Bad HTTP Date"})
|
|
2060
|
+
.expect(400);
|
|
2061
|
+
|
|
2062
|
+
expect(res.body.title).toBe("Invalid conflict-detection timestamp");
|
|
2063
|
+
expect(res.body.detail).toContain("If-Unmodified-Since");
|
|
2064
|
+
});
|
|
2065
|
+
|
|
2066
|
+
it("returns 400 when _updatedAt body field is an invalid date", async () => {
|
|
2067
|
+
const res = await agent
|
|
2068
|
+
.patch(`/food/${spinach._id}`)
|
|
2069
|
+
.send({_updatedAt: "garbage-date", name: "Bad Body Date"})
|
|
2070
|
+
.expect(400);
|
|
2071
|
+
|
|
2072
|
+
expect(res.body.title).toBe("Invalid conflict-detection timestamp");
|
|
2073
|
+
expect(res.body.detail).toContain("_updatedAt");
|
|
2074
|
+
});
|
|
2075
|
+
|
|
2076
|
+
it("handles doc timestamp stored as ISO string", async () => {
|
|
2077
|
+
await FoodModel.collection.updateOne(
|
|
2078
|
+
{_id: spinach._id as unknown as mongoose.Types.ObjectId},
|
|
2079
|
+
{$set: {updated: "2025-06-15T12:00:00.000Z"}}
|
|
2080
|
+
);
|
|
2081
|
+
|
|
2082
|
+
const res = await agent
|
|
2083
|
+
.patch(`/food/${spinach._id}`)
|
|
2084
|
+
.set("If-Unmodified-Since", DateTime.fromISO("2025-06-15T11:00:00.000Z").toHTTP()!)
|
|
2085
|
+
.send({name: "String Timestamp"})
|
|
2086
|
+
.expect(409);
|
|
2087
|
+
|
|
2088
|
+
expect(res.body.error).toBe("Conflict");
|
|
2089
|
+
});
|
|
2090
|
+
});
|
|
2091
|
+
|
|
2092
|
+
describe("three-arg modelRouter registration", () => {
|
|
2093
|
+
it("returns a ModelRouterRegistration with path and realtime", () => {
|
|
2094
|
+
const registration = modelRouter("/food", FoodModel, {
|
|
2095
|
+
permissions: {
|
|
2096
|
+
create: [Permissions.IsAny],
|
|
2097
|
+
delete: [Permissions.IsAny],
|
|
2098
|
+
list: [Permissions.IsAny],
|
|
2099
|
+
read: [Permissions.IsAny],
|
|
2100
|
+
update: [Permissions.IsAny],
|
|
2101
|
+
},
|
|
2102
|
+
realtime: {methods: ["create", "update", "delete"], roomStrategy: "model"},
|
|
2103
|
+
});
|
|
2104
|
+
expect(registration).toHaveProperty("__type", "modelRouter");
|
|
2105
|
+
expect(registration).toHaveProperty("path", "/food");
|
|
2106
|
+
expect(registration).toHaveProperty("router");
|
|
2107
|
+
expect(registration).toHaveProperty("_buildWithOpenApi");
|
|
2108
|
+
});
|
|
2109
|
+
|
|
2110
|
+
it("logs a warning when realtime config is used without the path form", () => {
|
|
2111
|
+
const router = modelRouter(FoodModel, {
|
|
2112
|
+
permissions: {
|
|
2113
|
+
create: [Permissions.IsAny],
|
|
2114
|
+
delete: [Permissions.IsAny],
|
|
2115
|
+
list: [Permissions.IsAny],
|
|
2116
|
+
read: [Permissions.IsAny],
|
|
2117
|
+
update: [Permissions.IsAny],
|
|
2118
|
+
},
|
|
2119
|
+
realtime: {methods: ["create", "update", "delete"], roomStrategy: "model"},
|
|
2120
|
+
});
|
|
2121
|
+
// Returns a plain Router, not a registration, because no path was given
|
|
2122
|
+
expect(router).toBeDefined();
|
|
2123
|
+
expect(router).not.toHaveProperty("__type");
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
describe("create transform error wrapping", () => {
|
|
2128
|
+
let admin: mongoose.HydratedDocument<User>;
|
|
2129
|
+
let agent: TestAgent;
|
|
2130
|
+
|
|
2131
|
+
beforeEach(async () => {
|
|
2132
|
+
[admin] = await setupDb();
|
|
2133
|
+
|
|
2134
|
+
app = getBaseServer();
|
|
2135
|
+
setupAuth(app, UserModel as unknown as Parameters<typeof setupAuth>[1]);
|
|
2136
|
+
addAuthRoutes(app, UserModel as unknown as Parameters<typeof addAuthRoutes>[1]);
|
|
2137
|
+
app.use(
|
|
2138
|
+
"/food",
|
|
2139
|
+
modelRouter(FoodModel, {
|
|
2140
|
+
permissions: {
|
|
2141
|
+
create: [Permissions.IsAny],
|
|
2142
|
+
delete: [Permissions.IsAny],
|
|
2143
|
+
list: [Permissions.IsAny],
|
|
2144
|
+
read: [Permissions.IsAny],
|
|
2145
|
+
update: [Permissions.IsAny],
|
|
2146
|
+
},
|
|
2147
|
+
transformer: {
|
|
2148
|
+
transform: () => {
|
|
2149
|
+
throw new Error("generic transform error");
|
|
2150
|
+
},
|
|
2151
|
+
},
|
|
2152
|
+
})
|
|
2153
|
+
);
|
|
2154
|
+
server = supertest(app);
|
|
2155
|
+
agent = await authAsUser(app, "admin");
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
it("wraps non-APIError transform errors in a 400 APIError on create", async () => {
|
|
2159
|
+
const res = await agent
|
|
2160
|
+
.post("/food")
|
|
2161
|
+
.send({calories: 10, name: "New Food", ownerId: admin._id})
|
|
2162
|
+
.expect(400);
|
|
2163
|
+
|
|
2164
|
+
expect(res.body.title).toContain("generic transform error");
|
|
2165
|
+
});
|
|
2166
|
+
});
|
|
2167
|
+
|
|
2168
|
+
describe("update transform error wrapping", () => {
|
|
2169
|
+
let admin: mongoose.HydratedDocument<User>;
|
|
2170
|
+
let agent: TestAgent;
|
|
2171
|
+
let spinach: Food;
|
|
2172
|
+
|
|
2173
|
+
beforeEach(async () => {
|
|
2174
|
+
[admin] = await setupDb();
|
|
2175
|
+
spinach = await FoodModel.create({
|
|
2176
|
+
calories: 10,
|
|
2177
|
+
created: new Date(),
|
|
2178
|
+
hidden: false,
|
|
2179
|
+
name: "Spinach",
|
|
2180
|
+
ownerId: admin._id,
|
|
2181
|
+
});
|
|
2182
|
+
|
|
2183
|
+
app = getBaseServer();
|
|
2184
|
+
setupAuth(app, UserModel as unknown as Parameters<typeof setupAuth>[1]);
|
|
2185
|
+
addAuthRoutes(app, UserModel as unknown as Parameters<typeof addAuthRoutes>[1]);
|
|
2186
|
+
app.use(
|
|
2187
|
+
"/food",
|
|
2188
|
+
modelRouter(FoodModel, {
|
|
2189
|
+
permissions: {
|
|
2190
|
+
create: [Permissions.IsAny],
|
|
2191
|
+
delete: [Permissions.IsAny],
|
|
2192
|
+
list: [Permissions.IsAny],
|
|
2193
|
+
read: [Permissions.IsAny],
|
|
2194
|
+
update: [Permissions.IsAny],
|
|
2195
|
+
},
|
|
2196
|
+
transformer: {
|
|
2197
|
+
transform: () => {
|
|
2198
|
+
throw new Error("update transform error");
|
|
2199
|
+
},
|
|
2200
|
+
},
|
|
2201
|
+
})
|
|
2202
|
+
);
|
|
2203
|
+
server = supertest(app);
|
|
2204
|
+
agent = await authAsUser(app, "admin");
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
it("wraps non-APIError transform errors in a 403 APIError on update", async () => {
|
|
2208
|
+
const res = await agent.patch(`/food/${spinach._id}`).send({name: "Updated"}).expect(403);
|
|
2209
|
+
|
|
2210
|
+
expect(res.body.title).toContain("update transform error");
|
|
2211
|
+
});
|
|
2043
2212
|
});
|
|
2044
2213
|
});
|
package/src/api.ts
CHANGED
|
@@ -342,6 +342,75 @@ export interface ModelRouterOptions<T> {
|
|
|
342
342
|
realtime?: RealtimeConfig;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Parses a date-range query bound from an ISO-8601 string using Luxon, throwing a 400
|
|
347
|
+
* APIError when the value cannot be parsed. Centralizes date-string parsing so the repo's
|
|
348
|
+
* Luxon convention is honored for admin changelist date-range filters.
|
|
349
|
+
*/
|
|
350
|
+
const parseDateRangeBound = (rawValue: unknown, queryKey: string): Date => {
|
|
351
|
+
const parsed = DateTime.fromISO(String(rawValue), {zone: "utc"});
|
|
352
|
+
if (!parsed.isValid) {
|
|
353
|
+
throw new APIError({
|
|
354
|
+
status: 400,
|
|
355
|
+
title: `Invalid date for query parameter ${queryKey}`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return parsed.toJSDate();
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Collapses `field_gte` / `field_lte` query pairs into `{ field: { $gte, $lte } }` for Date paths,
|
|
363
|
+
* so admin changelist date-range filters map to valid Mongoose range queries.
|
|
364
|
+
*/
|
|
365
|
+
const mergeDateRangeQueryParams = <T>(
|
|
366
|
+
model: Model<T>,
|
|
367
|
+
query: Record<string, unknown>
|
|
368
|
+
): Record<string, unknown> => {
|
|
369
|
+
const schema = model.schema;
|
|
370
|
+
const result: Record<string, unknown> = {...query};
|
|
371
|
+
const dateRangeBases = new Set<string>();
|
|
372
|
+
for (const key of Object.keys(result)) {
|
|
373
|
+
const match = /^(.+)_(gte|lte)$/.exec(key);
|
|
374
|
+
if (!match) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const baseField = match[1];
|
|
378
|
+
const path = schema.path(baseField);
|
|
379
|
+
if (!path || path.instance !== "Date") {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
dateRangeBases.add(baseField);
|
|
383
|
+
}
|
|
384
|
+
for (const baseField of dateRangeBases) {
|
|
385
|
+
const gteKey = `${baseField}_gte`;
|
|
386
|
+
const lteKey = `${baseField}_lte`;
|
|
387
|
+
const gteRaw = result[gteKey];
|
|
388
|
+
const lteRaw = result[lteKey];
|
|
389
|
+
const bounds: {$gte?: Date; $lte?: Date} = {};
|
|
390
|
+
if (gteRaw !== undefined && gteRaw !== null && String(gteRaw).trim() !== "") {
|
|
391
|
+
bounds.$gte = parseDateRangeBound(gteRaw, gteKey);
|
|
392
|
+
}
|
|
393
|
+
if (lteRaw !== undefined && lteRaw !== null && String(lteRaw).trim() !== "") {
|
|
394
|
+
bounds.$lte = parseDateRangeBound(lteRaw, lteKey);
|
|
395
|
+
}
|
|
396
|
+
if (Object.keys(bounds).length === 0) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
delete result[gteKey];
|
|
400
|
+
delete result[lteKey];
|
|
401
|
+
const direct = result[baseField];
|
|
402
|
+
if (direct !== undefined && direct !== null && typeof direct !== "object") {
|
|
403
|
+
delete result[baseField];
|
|
404
|
+
}
|
|
405
|
+
if (typeof direct === "object" && direct !== null && !Array.isArray(direct)) {
|
|
406
|
+
result[baseField] = {...(direct as Record<string, unknown>), ...bounds};
|
|
407
|
+
} else {
|
|
408
|
+
result[baseField] = bounds;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
};
|
|
413
|
+
|
|
345
414
|
// Ensures query params are allowed. Also checks nested query params when using $and/$or.
|
|
346
415
|
const checkQueryParamAllowed = (
|
|
347
416
|
queryParam: string,
|
|
@@ -711,6 +780,8 @@ function _buildModelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
711
780
|
}
|
|
712
781
|
}
|
|
713
782
|
|
|
783
|
+
query = mergeDateRangeQueryParams(model, query);
|
|
784
|
+
|
|
714
785
|
// Special operators. NOTE: these request Mongo Atlas.
|
|
715
786
|
if (req.query.$search) {
|
|
716
787
|
mongoose.connection.db?.collection(model.collection.collectionName);
|