@terreno/api 0.0.11 → 0.0.12
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/CLAUDE.md +107 -0
- package/biome.jsonc +1 -1
- package/bunfig.toml +3 -2
- package/dist/api.arrayOperations.test.d.ts +1 -0
- package/dist/api.arrayOperations.test.js +868 -0
- package/dist/api.d.ts +3 -14
- package/dist/api.errors.test.d.ts +1 -0
- package/dist/api.errors.test.js +175 -0
- package/dist/api.hooks.test.d.ts +1 -0
- package/dist/api.hooks.test.js +891 -0
- package/dist/api.js +44 -68
- package/dist/api.query.test.d.ts +1 -0
- package/dist/api.query.test.js +805 -0
- package/dist/api.test.js +691 -1678
- package/dist/auth.test.js +135 -0
- package/dist/expressServer.test.d.ts +1 -0
- package/dist/expressServer.test.js +669 -0
- package/dist/notifiers/slackNotifier.d.ts +2 -1
- package/dist/notifiers/slackNotifier.js +20 -13
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.js +17 -25
- package/dist/permissions.test.js +57 -0
- package/dist/populate.test.js +52 -0
- package/dist/tests.d.ts +9 -27
- package/dist/utils.test.js +235 -7
- package/package.json +3 -2
- package/src/api.arrayOperations.test.ts +690 -0
- package/src/api.errors.test.ts +156 -0
- package/src/api.hooks.test.ts +704 -0
- package/src/api.query.test.ts +538 -0
- package/src/api.test.ts +510 -1301
- package/src/api.ts +19 -61
- package/src/auth.test.ts +72 -0
- package/src/expressServer.test.ts +579 -0
- package/src/notifiers/slackNotifier.ts +28 -17
- package/src/permissions.test.ts +70 -1
- package/src/permissions.ts +4 -14
- package/src/populate.test.ts +58 -0
- package/src/utils.test.ts +214 -9
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import * as Sentry from "@sentry/node";
|
|
3
|
+
import type express from "express";
|
|
4
|
+
import qs from "qs";
|
|
5
|
+
import supertest from "supertest";
|
|
6
|
+
import type TestAgent from "supertest/lib/agent";
|
|
7
|
+
|
|
8
|
+
import {modelRouter} from "./api";
|
|
9
|
+
import {addAuthRoutes, setupAuth} from "./auth";
|
|
10
|
+
import {logRequests} from "./expressServer";
|
|
11
|
+
import {Permissions} from "./permissions";
|
|
12
|
+
import {authAsUser, type Food, FoodModel, getBaseServer, setupDb, UserModel} from "./tests";
|
|
13
|
+
|
|
14
|
+
describe("query and list methods", () => {
|
|
15
|
+
let server: TestAgent;
|
|
16
|
+
let app: express.Application;
|
|
17
|
+
let notAdmin: any;
|
|
18
|
+
let admin: any;
|
|
19
|
+
let adminOther: any;
|
|
20
|
+
let agent: TestAgent;
|
|
21
|
+
|
|
22
|
+
let spinach: Food;
|
|
23
|
+
let apple: Food;
|
|
24
|
+
let carrots: Food;
|
|
25
|
+
let pizza: Food;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
[admin, notAdmin, adminOther] = await setupDb();
|
|
29
|
+
|
|
30
|
+
const results = (await Promise.all([
|
|
31
|
+
FoodModel.create({
|
|
32
|
+
calories: 1,
|
|
33
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
34
|
+
eatenBy: [admin._id],
|
|
35
|
+
hidden: false,
|
|
36
|
+
lastEatenWith: {
|
|
37
|
+
dressing: new Date("2021-12-03T19:00:30.000Z"),
|
|
38
|
+
},
|
|
39
|
+
name: "Spinach",
|
|
40
|
+
ownerId: notAdmin._id,
|
|
41
|
+
source: {
|
|
42
|
+
dateAdded: "2023-12-13T12:30:00.000Z",
|
|
43
|
+
href: "https://www.google.com",
|
|
44
|
+
name: "Brand",
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
FoodModel.create({
|
|
48
|
+
calories: 100,
|
|
49
|
+
created: new Date("2021-12-03T00:00:30.000Z"),
|
|
50
|
+
hidden: true,
|
|
51
|
+
name: "Apple",
|
|
52
|
+
ownerId: admin._id,
|
|
53
|
+
tags: ["healthy"],
|
|
54
|
+
}),
|
|
55
|
+
FoodModel.create({
|
|
56
|
+
calories: 100,
|
|
57
|
+
created: new Date("2021-12-03T00:00:00.000Z"),
|
|
58
|
+
eatenBy: [admin._id, notAdmin._id],
|
|
59
|
+
hidden: false,
|
|
60
|
+
name: "Carrots",
|
|
61
|
+
ownerId: admin._id,
|
|
62
|
+
source: {
|
|
63
|
+
name: "USDA",
|
|
64
|
+
},
|
|
65
|
+
tags: ["healthy", "cheap"],
|
|
66
|
+
}),
|
|
67
|
+
FoodModel.create({
|
|
68
|
+
calories: 400,
|
|
69
|
+
created: new Date("2021-12-03T00:00:10.000Z"),
|
|
70
|
+
eatenBy: [adminOther._id],
|
|
71
|
+
hidden: false,
|
|
72
|
+
name: "Pizza",
|
|
73
|
+
ownerId: admin._id,
|
|
74
|
+
tags: ["cheap"],
|
|
75
|
+
}),
|
|
76
|
+
])) as [Food, Food, Food, Food];
|
|
77
|
+
[spinach, apple, carrots, pizza] = results;
|
|
78
|
+
app = getBaseServer();
|
|
79
|
+
setupAuth(app, UserModel as any);
|
|
80
|
+
addAuthRoutes(app, UserModel as any);
|
|
81
|
+
app.use(logRequests);
|
|
82
|
+
app.use(
|
|
83
|
+
"/food",
|
|
84
|
+
modelRouter(FoodModel, {
|
|
85
|
+
allowAnonymous: true,
|
|
86
|
+
defaultLimit: 2,
|
|
87
|
+
defaultQueryParams: {hidden: false},
|
|
88
|
+
maxLimit: 3,
|
|
89
|
+
permissions: {
|
|
90
|
+
create: [Permissions.IsAuthenticated],
|
|
91
|
+
delete: [Permissions.IsAdmin],
|
|
92
|
+
list: [Permissions.IsAny],
|
|
93
|
+
read: [Permissions.IsAny],
|
|
94
|
+
update: [Permissions.IsOwner],
|
|
95
|
+
},
|
|
96
|
+
populatePaths: [{path: "ownerId"}],
|
|
97
|
+
queryFields: ["hidden", "name", "calories", "created", "source.name", "tags", "eatenBy"],
|
|
98
|
+
sort: {created: "descending"},
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
server = supertest(app);
|
|
102
|
+
agent = await authAsUser(app, "notAdmin");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("read default", async () => {
|
|
106
|
+
const res = await agent.get(`/food/${spinach._id}`).expect(200);
|
|
107
|
+
expect(res.body.data._id).toBe(spinach._id.toString());
|
|
108
|
+
expect(res.body.data.ownerId._id).toBe(notAdmin.id);
|
|
109
|
+
expect(res.body.data.lastEatenWith).toEqual({
|
|
110
|
+
dressing: "2021-12-03T19:00:30.000Z",
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("list default", async () => {
|
|
115
|
+
const res = await agent.get("/food").expect(200);
|
|
116
|
+
expect(res.body.data).toHaveLength(2);
|
|
117
|
+
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
118
|
+
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
119
|
+
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
120
|
+
expect(res.body.data[1].ownerId._id).toBe(admin.id);
|
|
121
|
+
expect(res.body.data[0].lastEatenWith).toEqual({
|
|
122
|
+
dressing: "2021-12-03T19:00:30.000Z",
|
|
123
|
+
});
|
|
124
|
+
expect(res.body.data[1].lastEatenWith).toEqual(undefined);
|
|
125
|
+
expect(res.body.more).toBe(true);
|
|
126
|
+
expect(res.body.total).toBe(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("list limit", async () => {
|
|
130
|
+
const res = await agent.get("/food?limit=1").expect(200);
|
|
131
|
+
expect(res.body.data).toHaveLength(1);
|
|
132
|
+
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
133
|
+
expect(res.body.data[0].ownerId._id).toBe(notAdmin.id);
|
|
134
|
+
expect(res.body.more).toBe(true);
|
|
135
|
+
expect(res.body.total).toBe(3);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("list limit over", async () => {
|
|
139
|
+
await FoodModel.create({
|
|
140
|
+
calories: 400,
|
|
141
|
+
created: new Date("2021-12-02T00:00:10.000Z"),
|
|
142
|
+
hidden: false,
|
|
143
|
+
name: "Pizza",
|
|
144
|
+
ownerId: admin._id,
|
|
145
|
+
});
|
|
146
|
+
const res = await agent.get("/food?limit=4").expect(200);
|
|
147
|
+
expect(res.body.data).toHaveLength(3);
|
|
148
|
+
expect(res.body.more).toBe(true);
|
|
149
|
+
expect(res.body.total).toBe(4);
|
|
150
|
+
expect(res.body.data[0].id).toBe((spinach as any).id);
|
|
151
|
+
expect(res.body.data[1].id).toBe((pizza as any).id);
|
|
152
|
+
expect(res.body.data[2].id).toBe((carrots as any).id);
|
|
153
|
+
|
|
154
|
+
expect(Sentry.captureMessage).toHaveBeenCalledWith(
|
|
155
|
+
'More than 3 results returned for foods without pagination, data may be silently truncated. req.query: {"limit":"4"}'
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("list page", async () => {
|
|
160
|
+
const res = await agent.get("/food?limit=1&page=2").expect(200);
|
|
161
|
+
expect(res.body.data).toHaveLength(1);
|
|
162
|
+
expect(res.body.more).toBe(true);
|
|
163
|
+
expect(res.body.total).toBe(3);
|
|
164
|
+
expect(res.body.data[0].id).toBe((pizza as any).id);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("list page 0 ", async () => {
|
|
168
|
+
const res = await agent.get("/food?limit=1&page=0").expect(400);
|
|
169
|
+
expect(res.body.title).toBe("Invalid page: 0");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("list page with garbage ", async () => {
|
|
173
|
+
const res = await agent.get("/food?limit=1&page=abc").expect(400);
|
|
174
|
+
expect(res.body.title).toBe("Invalid page: abc");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("list page over", async () => {
|
|
178
|
+
const res = await agent.get("/food?limit=1&page=5").expect(200);
|
|
179
|
+
expect(res.body.data).toHaveLength(0);
|
|
180
|
+
expect(res.body.more).toBe(false);
|
|
181
|
+
expect(res.body.total).toBe(3);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("list query params", async () => {
|
|
185
|
+
const res = await agent.get("/food?hidden=true").expect(200);
|
|
186
|
+
expect(res.body.data).toHaveLength(1);
|
|
187
|
+
expect(res.body.more).toBe(false);
|
|
188
|
+
expect(res.body.total).toBe(1);
|
|
189
|
+
expect(res.body.data[0].id).toBe((apple as any).id);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("list query params not in list", async () => {
|
|
193
|
+
const res = await agent.get(`/food?ownerId=${admin._id}`).expect(400);
|
|
194
|
+
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("list query by nested param", async () => {
|
|
198
|
+
const res = await agent.get("/food?source.name=USDA").expect(200);
|
|
199
|
+
expect(res.body.data).toHaveLength(1);
|
|
200
|
+
expect(res.body.total).toBe(1);
|
|
201
|
+
expect(res.body.data[0].id).toBe((carrots as any).id);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("query by date", async () => {
|
|
205
|
+
const authRes = await server
|
|
206
|
+
.post("/auth/login")
|
|
207
|
+
.send({email: "admin@example.com", password: "securePassword"})
|
|
208
|
+
.expect(200);
|
|
209
|
+
const token = authRes.body.data.token;
|
|
210
|
+
|
|
211
|
+
let res = await server
|
|
212
|
+
.get(
|
|
213
|
+
`/food?limit=3&${qs.stringify({
|
|
214
|
+
created: {
|
|
215
|
+
$gte: "2021-12-03T00:00:00.000Z",
|
|
216
|
+
$lte: "2021-12-03T00:00:20.000Z",
|
|
217
|
+
},
|
|
218
|
+
})}`
|
|
219
|
+
)
|
|
220
|
+
.set("authorization", `Bearer ${token}`)
|
|
221
|
+
.expect(200);
|
|
222
|
+
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
223
|
+
expect.arrayContaining([
|
|
224
|
+
"2021-12-03T00:00:20.000Z",
|
|
225
|
+
"2021-12-03T00:00:10.000Z",
|
|
226
|
+
"2021-12-03T00:00:00.000Z",
|
|
227
|
+
])
|
|
228
|
+
);
|
|
229
|
+
expect(res.body.data.map((d: any) => d.created)).toHaveLength(3);
|
|
230
|
+
|
|
231
|
+
res = await server
|
|
232
|
+
.get(
|
|
233
|
+
`/food?limit=3&${qs.stringify({
|
|
234
|
+
created: {
|
|
235
|
+
$gte: "2021-12-03T00:00:00.000Z",
|
|
236
|
+
$lt: "2021-12-03T00:00:20.000Z",
|
|
237
|
+
},
|
|
238
|
+
})}`
|
|
239
|
+
)
|
|
240
|
+
.set("authorization", `Bearer ${token}`)
|
|
241
|
+
.expect(200);
|
|
242
|
+
expect(res.body.data.map((d: any) => d.created)).toEqual(
|
|
243
|
+
expect.arrayContaining(["2021-12-03T00:00:10.000Z", "2021-12-03T00:00:00.000Z"])
|
|
244
|
+
);
|
|
245
|
+
expect(res.body.data.map((d: any) => d.created)).toHaveLength(2);
|
|
246
|
+
|
|
247
|
+
res = await server
|
|
248
|
+
.get(
|
|
249
|
+
`/food?limit=3&${qs.stringify({
|
|
250
|
+
created: {
|
|
251
|
+
$gt: "2021-12-03T00:00:00.000Z",
|
|
252
|
+
$lt: "2021-12-03T00:00:20.000Z",
|
|
253
|
+
},
|
|
254
|
+
})}`
|
|
255
|
+
)
|
|
256
|
+
.set("authorization", `Bearer ${token}`)
|
|
257
|
+
.expect(200);
|
|
258
|
+
const createdDates = res.body.data.map((d: any) => d.created);
|
|
259
|
+
expect(createdDates).toEqual(expect.arrayContaining(["2021-12-03T00:00:10.000Z"]));
|
|
260
|
+
expect(createdDates).toHaveLength(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("query with a space", async () => {
|
|
264
|
+
const greenBeans = await FoodModel.create({
|
|
265
|
+
calories: 102,
|
|
266
|
+
created: Date.now() - 10,
|
|
267
|
+
name: "Green Beans",
|
|
268
|
+
ownerId: admin?._id,
|
|
269
|
+
});
|
|
270
|
+
const res = await agent.get(`/food?${qs.stringify({name: "Green Beans"})}`).expect(200);
|
|
271
|
+
expect(res.body.data).toHaveLength(1);
|
|
272
|
+
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
273
|
+
expect(res.body.data[0].name).toBe("Green Beans");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("query with a regex", async () => {
|
|
277
|
+
const greenBeans = await FoodModel.create({
|
|
278
|
+
calories: 102,
|
|
279
|
+
created: Date.now() - 10,
|
|
280
|
+
name: "Green Beans",
|
|
281
|
+
ownerId: admin?._id,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
let res = await agent.get(`/food?${qs.stringify({name: {$regex: "Green"}})}`).expect(200);
|
|
285
|
+
expect(res.body.data).toHaveLength(1);
|
|
286
|
+
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
287
|
+
expect(res.body.data[0].name).toBe("Green Beans");
|
|
288
|
+
|
|
289
|
+
res = await agent.get(`/food?${qs.stringify({name: {$regex: "green"}})}`).expect(200);
|
|
290
|
+
expect(res.body.data).toHaveLength(0);
|
|
291
|
+
|
|
292
|
+
res = await agent
|
|
293
|
+
.get(`/food?${qs.stringify({name: {$options: "i", $regex: "green"}})}`)
|
|
294
|
+
.expect(200);
|
|
295
|
+
expect(res.body.data).toHaveLength(1);
|
|
296
|
+
expect(res.body.data[0].id).toBe(greenBeans?.id);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("query with an $in operator", async () => {
|
|
300
|
+
let res = await server
|
|
301
|
+
.get(
|
|
302
|
+
`/food?${qs.stringify({
|
|
303
|
+
name: {
|
|
304
|
+
$in: ["Apple", "Spinach"],
|
|
305
|
+
},
|
|
306
|
+
})}`
|
|
307
|
+
)
|
|
308
|
+
.expect(200);
|
|
309
|
+
const names1 = res.body.data.map((d: any) => d.name);
|
|
310
|
+
expect(names1).toEqual(expect.arrayContaining(["Spinach"]));
|
|
311
|
+
expect(names1).toHaveLength(1);
|
|
312
|
+
|
|
313
|
+
res = await server
|
|
314
|
+
.get(
|
|
315
|
+
`/food?${qs.stringify({
|
|
316
|
+
name: {
|
|
317
|
+
$in: ["Carrots", "Spinach"],
|
|
318
|
+
},
|
|
319
|
+
})}`
|
|
320
|
+
)
|
|
321
|
+
.expect(200);
|
|
322
|
+
const names2 = res.body.data.map((d: any) => d.name);
|
|
323
|
+
expect(names2).toEqual(expect.arrayContaining(["Spinach", "Carrots"]));
|
|
324
|
+
expect(names2).toHaveLength(2);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("query with an $in for _ids in nested object", async () => {
|
|
328
|
+
const res = await server
|
|
329
|
+
.get(
|
|
330
|
+
`/food?${qs.stringify({
|
|
331
|
+
eatenBy: {
|
|
332
|
+
$in: [notAdmin._id.toString(), adminOther._id.toString()],
|
|
333
|
+
},
|
|
334
|
+
})}`
|
|
335
|
+
)
|
|
336
|
+
.expect(200);
|
|
337
|
+
expect(res.body.more).toBe(false);
|
|
338
|
+
expect(res.body.total).toBe(2);
|
|
339
|
+
expect(res.body.data).toHaveLength(2);
|
|
340
|
+
const names3 = res.body.data.map((d: any) => d.name);
|
|
341
|
+
expect(names3).toEqual(expect.arrayContaining(["Carrots", "Pizza"]));
|
|
342
|
+
expect(names3).toHaveLength(2);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("query $and operator on same field", async () => {
|
|
346
|
+
const res = await agent
|
|
347
|
+
.get(`/food?${qs.stringify({$and: [{tags: "healthy"}, {tags: "cheap"}]})}`)
|
|
348
|
+
.expect(200);
|
|
349
|
+
expect(res.body.data).toHaveLength(1);
|
|
350
|
+
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("query $and operator on same field, nested objects", async () => {
|
|
354
|
+
const res = await agent
|
|
355
|
+
.get(
|
|
356
|
+
`/food?${qs.stringify({
|
|
357
|
+
$and: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
358
|
+
})}`
|
|
359
|
+
)
|
|
360
|
+
.expect(200);
|
|
361
|
+
expect(res.body.data).toHaveLength(1);
|
|
362
|
+
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("query $or operator on same field", async () => {
|
|
366
|
+
const res = await agent
|
|
367
|
+
.get(`/food?${qs.stringify({$or: [{name: "Carrots"}, {name: "Pizza"}]})}`)
|
|
368
|
+
.expect(200);
|
|
369
|
+
expect(res.body.data).toHaveLength(2);
|
|
370
|
+
const ids1 = res.body.data.map((d: any) => d.id);
|
|
371
|
+
expect(ids1).toEqual(expect.arrayContaining([carrots?._id.toString(), pizza?._id.toString()]));
|
|
372
|
+
expect(ids1).toHaveLength(2);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("query $and operator on same field, nested objects for $or", async () => {
|
|
376
|
+
const res = await agent
|
|
377
|
+
.get(
|
|
378
|
+
`/food?${qs.stringify({
|
|
379
|
+
$or: [{eatenBy: admin.id}, {eatenBy: notAdmin.id}],
|
|
380
|
+
limit: 3,
|
|
381
|
+
})}`
|
|
382
|
+
)
|
|
383
|
+
.expect(200);
|
|
384
|
+
expect(res.body.data).toHaveLength(2);
|
|
385
|
+
const ids2 = res.body.data.map((d: any) => d.id);
|
|
386
|
+
expect(ids2).toEqual(
|
|
387
|
+
expect.arrayContaining([carrots?._id.toString(), spinach?._id.toString()])
|
|
388
|
+
);
|
|
389
|
+
expect(ids2).toHaveLength(2);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("query $and and $or are rejected if field is not in queryFields", async () => {
|
|
393
|
+
let res = await agent
|
|
394
|
+
.get(`/food?${qs.stringify({$and: [{ownerId: "healthy"}, {tags: "cheap"}]})}`)
|
|
395
|
+
.expect(400);
|
|
396
|
+
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
397
|
+
res = await agent
|
|
398
|
+
.get(`/food?${qs.stringify({$and: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
399
|
+
.expect(400);
|
|
400
|
+
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
401
|
+
|
|
402
|
+
res = await agent
|
|
403
|
+
.get(`/food?${qs.stringify({$or: [{tags: "cheap"}, {ownerId: "healthy"}]})}`)
|
|
404
|
+
.expect(400);
|
|
405
|
+
expect(res.body.title).toBe("ownerId is not allowed as a query param.");
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("query with a number", async () => {
|
|
409
|
+
const res = await agent.get("/food?calories=100").expect(200);
|
|
410
|
+
expect(res.body.data).toHaveLength(1);
|
|
411
|
+
expect(res.body.data[0].id).toBe(carrots?._id.toString());
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("update", async () => {
|
|
415
|
+
let res = await agent.patch(`/food/${spinach._id}`).send({name: "Kale"}).expect(200);
|
|
416
|
+
expect(res.body.data.name).toBe("Kale");
|
|
417
|
+
expect(res.body.data.calories).toBe(1);
|
|
418
|
+
expect(res.body.data.hidden).toBe(false);
|
|
419
|
+
|
|
420
|
+
res = await agent
|
|
421
|
+
.patch(`/food/${spinach._id}`)
|
|
422
|
+
.send({lastEatenWith: {dressing: "2023-12-03T00:00:20.000Z"}})
|
|
423
|
+
.expect(200);
|
|
424
|
+
expect(res.body.data.name).toBe("Kale");
|
|
425
|
+
expect(res.body.data.calories).toBe(1);
|
|
426
|
+
expect(res.body.data.hidden).toBe(false);
|
|
427
|
+
expect(res.body.data.lastEatenWith).toEqual({
|
|
428
|
+
dressing: "2023-12-03T00:00:20.000Z",
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
res = await agent
|
|
432
|
+
.patch(`/food/${spinach._id}`)
|
|
433
|
+
.send({
|
|
434
|
+
lastEatenWith: {
|
|
435
|
+
cucumber: "2023-12-04T12:00:20.000Z",
|
|
436
|
+
dressing: "2023-12-03T00:00:20.000Z",
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
.expect(200);
|
|
440
|
+
expect(res.body.data.lastEatenWith).toEqual({
|
|
441
|
+
cucumber: "2023-12-04T12:00:20.000Z",
|
|
442
|
+
dressing: "2023-12-03T00:00:20.000Z",
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("update using dot notation", async () => {
|
|
447
|
+
const res = await agent
|
|
448
|
+
.patch(`/food/${spinach._id}`)
|
|
449
|
+
.send({"source.href": "https://food.com"})
|
|
450
|
+
.expect(200);
|
|
451
|
+
expect(res.body.data.source.href).toBe("https://food.com");
|
|
452
|
+
expect(res.body.data.source.name).toBe("Brand");
|
|
453
|
+
expect(res.body.data.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
454
|
+
|
|
455
|
+
const dbSpinach = await FoodModel.findById(spinach._id);
|
|
456
|
+
expect(dbSpinach?.source.href).toBe("https://food.com");
|
|
457
|
+
expect(dbSpinach?.source.name).toBe("Brand");
|
|
458
|
+
expect(dbSpinach?.source.dateAdded).toBe("2023-12-13T12:30:00.000Z");
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe("special query params", () => {
|
|
463
|
+
let server: TestAgent;
|
|
464
|
+
let app: express.Application;
|
|
465
|
+
let admin: any;
|
|
466
|
+
|
|
467
|
+
beforeEach(async () => {
|
|
468
|
+
[admin] = await setupDb();
|
|
469
|
+
|
|
470
|
+
await FoodModel.create({
|
|
471
|
+
calories: 1,
|
|
472
|
+
created: new Date("2021-12-03T00:00:20.000Z"),
|
|
473
|
+
hidden: false,
|
|
474
|
+
name: "Spinach",
|
|
475
|
+
ownerId: admin._id,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
app = getBaseServer();
|
|
479
|
+
setupAuth(app, UserModel as any);
|
|
480
|
+
addAuthRoutes(app, UserModel as any);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("period query param is stripped from query", async () => {
|
|
484
|
+
app.use(
|
|
485
|
+
"/food",
|
|
486
|
+
modelRouter(FoodModel, {
|
|
487
|
+
allowAnonymous: true,
|
|
488
|
+
permissions: {
|
|
489
|
+
create: [Permissions.IsAny],
|
|
490
|
+
delete: [Permissions.IsAny],
|
|
491
|
+
list: [Permissions.IsAny],
|
|
492
|
+
read: [Permissions.IsAny],
|
|
493
|
+
update: [Permissions.IsAny],
|
|
494
|
+
},
|
|
495
|
+
queryFields: ["name", "period"],
|
|
496
|
+
queryFilter: (_user, query) => {
|
|
497
|
+
if (query?.period) {
|
|
498
|
+
return query;
|
|
499
|
+
}
|
|
500
|
+
return query ?? {};
|
|
501
|
+
},
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
server = supertest(app);
|
|
505
|
+
|
|
506
|
+
const res = await server.get("/food?period=weekly").expect(200);
|
|
507
|
+
expect(res.body.data).toBeDefined();
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("query with false value", async () => {
|
|
511
|
+
await FoodModel.create({
|
|
512
|
+
calories: 50,
|
|
513
|
+
created: new Date("2021-12-04T00:00:20.000Z"),
|
|
514
|
+
hidden: true,
|
|
515
|
+
name: "HiddenFood",
|
|
516
|
+
ownerId: admin._id,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
app.use(
|
|
520
|
+
"/food",
|
|
521
|
+
modelRouter(FoodModel, {
|
|
522
|
+
allowAnonymous: true,
|
|
523
|
+
permissions: {
|
|
524
|
+
create: [Permissions.IsAny],
|
|
525
|
+
delete: [Permissions.IsAny],
|
|
526
|
+
list: [Permissions.IsAny],
|
|
527
|
+
read: [Permissions.IsAny],
|
|
528
|
+
update: [Permissions.IsAny],
|
|
529
|
+
},
|
|
530
|
+
queryFields: ["name", "hidden"],
|
|
531
|
+
})
|
|
532
|
+
);
|
|
533
|
+
server = supertest(app);
|
|
534
|
+
|
|
535
|
+
const res = await server.get("/food?hidden=false").expect(200);
|
|
536
|
+
expect(res.body.data.every((f: any) => f.hidden === false)).toBe(true);
|
|
537
|
+
});
|
|
538
|
+
});
|