@terreno/api 0.0.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/LICENSE +202 -0
- package/README.md +170 -0
- package/biome.jsonc +22 -0
- package/bunfig.toml +4 -0
- package/dist/api.d.ts +227 -0
- package/dist/api.js +1024 -0
- package/dist/api.test.d.ts +1 -0
- package/dist/api.test.js +2143 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +512 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +778 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.js +216 -0
- package/dist/example.d.ts +1 -0
- package/dist/example.js +118 -0
- package/dist/expressServer.d.ts +35 -0
- package/dist/expressServer.js +436 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +30 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.js +249 -0
- package/dist/middleware.d.ts +10 -0
- package/dist/middleware.js +52 -0
- package/dist/notifiers/googleChatNotifier.d.ts +5 -0
- package/dist/notifiers/googleChatNotifier.js +130 -0
- package/dist/notifiers/googleChatNotifier.test.d.ts +1 -0
- package/dist/notifiers/googleChatNotifier.test.js +260 -0
- package/dist/notifiers/index.d.ts +3 -0
- package/dist/notifiers/index.js +19 -0
- package/dist/notifiers/slackNotifier.d.ts +5 -0
- package/dist/notifiers/slackNotifier.js +130 -0
- package/dist/notifiers/slackNotifier.test.d.ts +1 -0
- package/dist/notifiers/slackNotifier.test.js +259 -0
- package/dist/notifiers/zoomNotifier.d.ts +34 -0
- package/dist/notifiers/zoomNotifier.js +181 -0
- package/dist/notifiers/zoomNotifier.test.d.ts +1 -0
- package/dist/notifiers/zoomNotifier.test.js +370 -0
- package/dist/openApi.d.ts +60 -0
- package/dist/openApi.js +441 -0
- package/dist/openApi.test.d.ts +1 -0
- package/dist/openApi.test.js +445 -0
- package/dist/openApiBuilder.d.ts +419 -0
- package/dist/openApiBuilder.js +424 -0
- package/dist/openApiBuilder.test.d.ts +1 -0
- package/dist/openApiBuilder.test.js +509 -0
- package/dist/openApiEtag.d.ts +7 -0
- package/dist/openApiEtag.js +38 -0
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +331 -0
- package/dist/permissions.test.d.ts +1 -0
- package/dist/permissions.test.js +413 -0
- package/dist/plugins.d.ts +67 -0
- package/dist/plugins.js +315 -0
- package/dist/plugins.test.d.ts +1 -0
- package/dist/plugins.test.js +639 -0
- package/dist/populate.d.ts +14 -0
- package/dist/populate.js +315 -0
- package/dist/populate.test.d.ts +1 -0
- package/dist/populate.test.js +133 -0
- package/dist/response.d.ts +0 -0
- package/dist/response.js +1 -0
- package/dist/tests/bunSetup.d.ts +1 -0
- package/dist/tests/bunSetup.js +297 -0
- package/dist/tests/index.d.ts +1 -0
- package/dist/tests/index.js +17 -0
- package/dist/tests.d.ts +99 -0
- package/dist/tests.js +273 -0
- package/dist/transformers.d.ts +25 -0
- package/dist/transformers.js +217 -0
- package/dist/transformers.test.d.ts +1 -0
- package/dist/transformers.test.js +370 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.js +143 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +14 -0
- package/index.ts +1 -0
- package/package.json +88 -0
- package/src/__snapshots__/openApi.test.ts.snap +4814 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +1485 -0
- package/src/api.test.ts +1661 -0
- package/src/api.ts +1036 -0
- package/src/auth.test.ts +550 -0
- package/src/auth.ts +408 -0
- package/src/errors.ts +225 -0
- package/src/example.ts +99 -0
- package/src/express.d.ts +5 -0
- package/src/expressServer.ts +387 -0
- package/src/index.ts +14 -0
- package/src/logger.ts +190 -0
- package/src/middleware.ts +18 -0
- package/src/notifiers/googleChatNotifier.test.ts +114 -0
- package/src/notifiers/googleChatNotifier.ts +47 -0
- package/src/notifiers/index.ts +3 -0
- package/src/notifiers/slackNotifier.test.ts +113 -0
- package/src/notifiers/slackNotifier.ts +55 -0
- package/src/notifiers/zoomNotifier.test.ts +207 -0
- package/src/notifiers/zoomNotifier.ts +111 -0
- package/src/openApi.test.ts +331 -0
- package/src/openApi.ts +494 -0
- package/src/openApiBuilder.test.ts +442 -0
- package/src/openApiBuilder.ts +636 -0
- package/src/openApiEtag.ts +40 -0
- package/src/permissions.test.ts +219 -0
- package/src/permissions.ts +228 -0
- package/src/plugins.test.ts +390 -0
- package/src/plugins.ts +289 -0
- package/src/populate.test.ts +65 -0
- package/src/populate.ts +258 -0
- package/src/response.ts +0 -0
- package/src/tests/bunSetup.ts +234 -0
- package/src/tests/index.ts +1 -0
- package/src/tests.ts +218 -0
- package/src/transformers.test.ts +202 -0
- package/src/transformers.ts +170 -0
- package/src/utils.test.ts +14 -0
- package/src/utils.ts +47 -0
- package/tsconfig.json +60 -0
- package/types.d.ts +17 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {beforeEach, describe, expect, it} from "bun:test";
|
|
2
|
+
import type express from "express";
|
|
3
|
+
import type {Router} from "express";
|
|
4
|
+
import supertest from "supertest";
|
|
5
|
+
import type TestAgent from "supertest/lib/agent";
|
|
6
|
+
|
|
7
|
+
import {modelRouter, type modelRouterOptions} from "./api";
|
|
8
|
+
import {addAuthRoutes, setupAuth} from "./auth";
|
|
9
|
+
import {setupServer} from "./expressServer";
|
|
10
|
+
import {Permissions} from "./permissions";
|
|
11
|
+
import {FoodModel, setupDb, UserModel} from "./tests";
|
|
12
|
+
|
|
13
|
+
function getMessageSummaryOpenApiMiddleware(options: Partial<modelRouterOptions<any>>): any {
|
|
14
|
+
return options.openApi.path({
|
|
15
|
+
parameters: [
|
|
16
|
+
{
|
|
17
|
+
in: "query",
|
|
18
|
+
name: "foodIds",
|
|
19
|
+
schema: {
|
|
20
|
+
type: "string",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
responses: {
|
|
25
|
+
200: {
|
|
26
|
+
content: {
|
|
27
|
+
"application/json": {
|
|
28
|
+
schema: {
|
|
29
|
+
properties: {
|
|
30
|
+
message: {
|
|
31
|
+
type: "string",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
type: "object",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
description: "Success",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
tags: ["Food"],
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function addRoutes(router: Router, options?: Partial<modelRouterOptions<any>>): void {
|
|
46
|
+
router.use(
|
|
47
|
+
"/food",
|
|
48
|
+
modelRouter(FoodModel as any, {
|
|
49
|
+
...options,
|
|
50
|
+
allowAnonymous: true,
|
|
51
|
+
openApiExtraModelProperties: {
|
|
52
|
+
foo: {
|
|
53
|
+
type: "string",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
permissions: {
|
|
57
|
+
create: [Permissions.IsAny],
|
|
58
|
+
delete: [Permissions.IsAny],
|
|
59
|
+
list: [Permissions.IsAny],
|
|
60
|
+
read: [Permissions.IsAny],
|
|
61
|
+
update: [Permissions.IsAny],
|
|
62
|
+
},
|
|
63
|
+
populatePaths: [{path: "ownerId"}, {path: "eatenBy"}],
|
|
64
|
+
queryFields: ["calories"],
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
router.use("/food/count", getMessageSummaryOpenApiMiddleware, async (_req, res) => {
|
|
68
|
+
res.json({message: "count"});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("openApi", () => {
|
|
73
|
+
let server: TestAgent;
|
|
74
|
+
let app: express.Application;
|
|
75
|
+
|
|
76
|
+
beforeEach(async () => {
|
|
77
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
78
|
+
process.env.ENABLE_SWAGGER = "true";
|
|
79
|
+
|
|
80
|
+
app = setupServer({
|
|
81
|
+
addRoutes,
|
|
82
|
+
skipListen: true,
|
|
83
|
+
userModel: UserModel as any,
|
|
84
|
+
});
|
|
85
|
+
setupAuth(app, UserModel as any);
|
|
86
|
+
addAuthRoutes(app, UserModel as any);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("gets the openapi.json", async () => {
|
|
90
|
+
server = supertest(app);
|
|
91
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
92
|
+
expect(res.body).toMatchSnapshot();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("gets the openapi.json with ETag header", async () => {
|
|
96
|
+
server = supertest(app);
|
|
97
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
98
|
+
expect(res.headers.etag).toBeDefined();
|
|
99
|
+
expect(res.headers.etag).toMatch(/^"[a-f0-9]{16}"$/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns 304 when If-None-Match matches ETag", async () => {
|
|
103
|
+
server = supertest(app);
|
|
104
|
+
|
|
105
|
+
// First request to get the ETag
|
|
106
|
+
const firstRes = await server.get("/openapi.json").expect(200);
|
|
107
|
+
const etag = firstRes.headers.etag;
|
|
108
|
+
expect(etag).toBeDefined();
|
|
109
|
+
|
|
110
|
+
// Second request with If-None-Match header
|
|
111
|
+
const secondRes = await server.get("/openapi.json").set("If-None-Match", etag).expect(304);
|
|
112
|
+
|
|
113
|
+
expect(secondRes.body).toEqual({});
|
|
114
|
+
expect(secondRes.headers.etag).toBe(etag);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns 200 when If-None-Match does not match ETag", async () => {
|
|
118
|
+
server = supertest(app);
|
|
119
|
+
|
|
120
|
+
// Request with a different ETag
|
|
121
|
+
const res = await server
|
|
122
|
+
.get("/openapi.json")
|
|
123
|
+
.set("If-None-Match", '"different-etag"')
|
|
124
|
+
.expect(200);
|
|
125
|
+
|
|
126
|
+
expect(res.body).toBeDefined();
|
|
127
|
+
expect(res.headers.etag).toBeDefined();
|
|
128
|
+
expect(res.headers.etag).not.toBe('"different-etag"');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("gets the swagger ui", async () => {
|
|
132
|
+
server = supertest(app);
|
|
133
|
+
await server.get("/swagger/").expect(200);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("gets food with populated paths", async () => {
|
|
137
|
+
server = supertest(app);
|
|
138
|
+
const [_admin, notAdmin] = await setupDb();
|
|
139
|
+
const food = await FoodModel.create({
|
|
140
|
+
name: "test",
|
|
141
|
+
ownerId: notAdmin._id,
|
|
142
|
+
});
|
|
143
|
+
const res = await server.get(`/food/${food._id}`).expect(200);
|
|
144
|
+
expect(res.body.data.ownerId._id).toEqual(notAdmin._id.toString());
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// create a test for a custom express endpoint that doesnt use modelRouter and manually adds it
|
|
148
|
+
// to openapi
|
|
149
|
+
it("gets the openapi.json with custom endpoint", async () => {
|
|
150
|
+
server = supertest(app);
|
|
151
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
152
|
+
expect(res.body).toMatchSnapshot();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("gets the openapi.json and has correct Number query fields", async () => {
|
|
156
|
+
server = supertest(app);
|
|
157
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
158
|
+
const foodQuery = res.body.paths["/food/"].get.parameters.find((p) => p.name === "calories");
|
|
159
|
+
|
|
160
|
+
// Ensure that a Number query field supports gt/gte/lt/lte and just a Number
|
|
161
|
+
expect(foodQuery.schema).toEqual({
|
|
162
|
+
oneOf: [
|
|
163
|
+
{type: "number"},
|
|
164
|
+
{
|
|
165
|
+
properties: {
|
|
166
|
+
$gt: {type: "number"},
|
|
167
|
+
$gte: {type: "number"},
|
|
168
|
+
$lt: {type: "number"},
|
|
169
|
+
$lte: {type: "number"},
|
|
170
|
+
},
|
|
171
|
+
type: "object",
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
});
|
|
175
|
+
expect(foodQuery).toMatchSnapshot();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function addRoutesPopulate(router: Router, options?: Partial<modelRouterOptions<any>>): void {
|
|
180
|
+
options?.openApi.component("schemas", "LimitedUser", {
|
|
181
|
+
properties: {
|
|
182
|
+
email: {
|
|
183
|
+
description: "LimitedUser's email",
|
|
184
|
+
type: "string",
|
|
185
|
+
},
|
|
186
|
+
name: {
|
|
187
|
+
description: "LimitedUser's name",
|
|
188
|
+
type: "string",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
type: "object",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
router.use(
|
|
195
|
+
"/food",
|
|
196
|
+
modelRouter(FoodModel as any, {
|
|
197
|
+
...options,
|
|
198
|
+
allowAnonymous: true,
|
|
199
|
+
openApiExtraModelProperties: {
|
|
200
|
+
foo: {
|
|
201
|
+
type: "string",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
permissions: {
|
|
205
|
+
create: [Permissions.IsAny],
|
|
206
|
+
delete: [Permissions.IsAny],
|
|
207
|
+
list: [Permissions.IsAny],
|
|
208
|
+
read: [Permissions.IsAny],
|
|
209
|
+
update: [Permissions.IsAny],
|
|
210
|
+
},
|
|
211
|
+
populatePaths: [
|
|
212
|
+
{fields: ["name", "email"], path: "ownerId"},
|
|
213
|
+
{
|
|
214
|
+
fields: ["name", "email"],
|
|
215
|
+
openApiComponent: "LimitedUser",
|
|
216
|
+
path: "eatenBy",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
fields: ["name", "email"],
|
|
220
|
+
openApiComponent: "LimitedUser",
|
|
221
|
+
path: "likesIds.userId",
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
describe("openApi without swagger", () => {
|
|
229
|
+
let server: TestAgent;
|
|
230
|
+
let app: express.Application;
|
|
231
|
+
|
|
232
|
+
beforeEach(async () => {
|
|
233
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
234
|
+
process.env.ENABLE_SWAGGER = "false";
|
|
235
|
+
|
|
236
|
+
app = setupServer({
|
|
237
|
+
addRoutes,
|
|
238
|
+
skipListen: true,
|
|
239
|
+
userModel: UserModel as any,
|
|
240
|
+
});
|
|
241
|
+
setupAuth(app, UserModel as any);
|
|
242
|
+
addAuthRoutes(app, UserModel as any);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does not have the swagger ui", async () => {
|
|
246
|
+
server = supertest(app);
|
|
247
|
+
await server.get("/swagger/").expect(404);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("openApi populate", () => {
|
|
252
|
+
let server: TestAgent;
|
|
253
|
+
let app: express.Application;
|
|
254
|
+
|
|
255
|
+
beforeEach(async () => {
|
|
256
|
+
process.env.REFRESH_TOKEN_SECRET = "testsecret1234";
|
|
257
|
+
|
|
258
|
+
app = setupServer({
|
|
259
|
+
addRoutes: addRoutesPopulate,
|
|
260
|
+
skipListen: true,
|
|
261
|
+
userModel: UserModel as any,
|
|
262
|
+
});
|
|
263
|
+
setupAuth(app, UserModel as any);
|
|
264
|
+
addAuthRoutes(app, UserModel as any);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("gets the openapi.json with populate", async () => {
|
|
268
|
+
server = supertest(app);
|
|
269
|
+
const res = await server.get("/openapi.json").expect(200);
|
|
270
|
+
const properties =
|
|
271
|
+
res.body.paths["/food/{id}"].get.responses["200"].content["application/json"].schema
|
|
272
|
+
.properties;
|
|
273
|
+
|
|
274
|
+
// There's no component here, so we automatically generate the limited properties.
|
|
275
|
+
expect(properties.ownerId).toEqual({
|
|
276
|
+
properties: {
|
|
277
|
+
email: {
|
|
278
|
+
type: "string",
|
|
279
|
+
},
|
|
280
|
+
name: {
|
|
281
|
+
type: "string",
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
type: "object",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// We only reference the component here, rather than listing each field each time.
|
|
288
|
+
expect(properties.eatenBy).toEqual({
|
|
289
|
+
items: {
|
|
290
|
+
$ref: "#/components/schemas/LimitedUser",
|
|
291
|
+
},
|
|
292
|
+
type: "array",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(properties.likesIds).toEqual({
|
|
296
|
+
items: {
|
|
297
|
+
properties: {
|
|
298
|
+
_id: {
|
|
299
|
+
type: "string",
|
|
300
|
+
},
|
|
301
|
+
likes: {
|
|
302
|
+
type: "boolean",
|
|
303
|
+
},
|
|
304
|
+
userId: {
|
|
305
|
+
$ref: "#/components/schemas/LimitedUser",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
required: [],
|
|
309
|
+
type: "object",
|
|
310
|
+
},
|
|
311
|
+
type: "array",
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Ensure the component is registered and used.
|
|
315
|
+
expect(res.body.components.schemas.LimitedUser).toEqual({
|
|
316
|
+
properties: {
|
|
317
|
+
email: {
|
|
318
|
+
description: "LimitedUser's email",
|
|
319
|
+
type: "string",
|
|
320
|
+
},
|
|
321
|
+
name: {
|
|
322
|
+
description: "LimitedUser's name",
|
|
323
|
+
type: "string",
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
type: "object",
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(res.body).toMatchSnapshot();
|
|
330
|
+
});
|
|
331
|
+
});
|