@terreno/api 0.20.2 → 0.21.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/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 +10 -10
- 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/openApi.test.js +10 -17
- package/dist/openApiBuilder.test.js +18 -10
- 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/terrenoApp.d.ts +27 -15
- package/dist/terrenoApp.js +24 -14
- package/dist/terrenoApp.test.js +52 -0
- package/dist/tests/bunSetup.js +61 -7
- package/dist/tests.js +27 -4
- package/package.json +1 -1
- 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 +10 -10
- 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/openApi.test.ts +10 -17
- package/src/openApiBuilder.test.ts +18 -10
- 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/terrenoApp.test.ts +38 -0
- package/src/terrenoApp.ts +37 -15
- package/src/tests/bunSetup.ts +16 -3
- package/src/tests.ts +17 -4
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
cronjob,
|
|
12
12
|
logRequests,
|
|
13
13
|
setupEnvironment,
|
|
14
|
-
setupServer,
|
|
15
14
|
wrapScript,
|
|
16
15
|
} from "./expressServer";
|
|
17
16
|
import {logger, winstonLogger} from "./logger";
|
|
17
|
+
import {TerrenoApp} from "./terrenoApp";
|
|
18
18
|
import {UserModel} from "./tests";
|
|
19
19
|
|
|
20
20
|
describe("expressServer", () => {
|
|
@@ -75,8 +75,8 @@ describe("expressServer", () => {
|
|
|
75
75
|
stream: logStream,
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
const app =
|
|
79
|
-
|
|
78
|
+
const app = new TerrenoApp({
|
|
79
|
+
configureApp: (router) => {
|
|
80
80
|
router.get("/context-test", (req, res) => {
|
|
81
81
|
logger.info("context route log");
|
|
82
82
|
return res.json({requestId: req.requestId, sessionId: req.sessionId});
|
|
@@ -85,7 +85,7 @@ describe("expressServer", () => {
|
|
|
85
85
|
logRequests: false,
|
|
86
86
|
skipListen: true,
|
|
87
87
|
userModel: UserModel as any,
|
|
88
|
-
});
|
|
88
|
+
}).build();
|
|
89
89
|
winstonLogger.add(transport);
|
|
90
90
|
|
|
91
91
|
const res = await supertest(app)
|
|
@@ -410,7 +410,7 @@ describe("expressServer", () => {
|
|
|
410
410
|
});
|
|
411
411
|
});
|
|
412
412
|
|
|
413
|
-
describe("
|
|
413
|
+
describe("TerrenoApp", () => {
|
|
414
414
|
const originalEnv = process.env;
|
|
415
415
|
|
|
416
416
|
beforeEach(() => {
|
|
@@ -429,61 +429,60 @@ describe("expressServer", () => {
|
|
|
429
429
|
});
|
|
430
430
|
|
|
431
431
|
it("creates server with skipListen option", () => {
|
|
432
|
-
const
|
|
432
|
+
const configureApp = (): void => {};
|
|
433
433
|
|
|
434
|
-
const app =
|
|
435
|
-
|
|
434
|
+
const app = new TerrenoApp({
|
|
435
|
+
configureApp,
|
|
436
436
|
skipListen: true,
|
|
437
437
|
userModel: UserModel as any,
|
|
438
|
-
});
|
|
438
|
+
}).build();
|
|
439
439
|
|
|
440
440
|
expect(app).toBeDefined();
|
|
441
441
|
});
|
|
442
442
|
|
|
443
|
-
it("creates server with
|
|
443
|
+
it("creates server with beforeJsonSetup option", () => {
|
|
444
444
|
let middlewareCalled = false;
|
|
445
|
-
const
|
|
446
|
-
middlewareCalled = true;
|
|
447
|
-
app.use((_req: any, _res: any, next: any) => next());
|
|
448
|
-
};
|
|
449
|
-
const addRoutes = () => {};
|
|
445
|
+
const configureApp = (): void => {};
|
|
450
446
|
|
|
451
|
-
const app =
|
|
452
|
-
|
|
453
|
-
|
|
447
|
+
const app = new TerrenoApp({
|
|
448
|
+
beforeJsonSetup: (httpApp: any) => {
|
|
449
|
+
middlewareCalled = true;
|
|
450
|
+
httpApp.use((_req: any, _res: any, next: any) => next());
|
|
451
|
+
},
|
|
452
|
+
configureApp,
|
|
454
453
|
skipListen: true,
|
|
455
454
|
userModel: UserModel as any,
|
|
456
|
-
});
|
|
455
|
+
}).build();
|
|
457
456
|
|
|
458
457
|
expect(app).toBeDefined();
|
|
459
458
|
expect(middlewareCalled).toBe(true);
|
|
460
459
|
});
|
|
461
460
|
|
|
462
461
|
it("creates server with custom corsOrigin", () => {
|
|
463
|
-
const
|
|
462
|
+
const configureApp = (): void => {};
|
|
464
463
|
|
|
465
|
-
const app =
|
|
466
|
-
|
|
464
|
+
const app = new TerrenoApp({
|
|
465
|
+
configureApp,
|
|
467
466
|
corsOrigin: "https://example.com",
|
|
468
467
|
skipListen: true,
|
|
469
468
|
userModel: UserModel as any,
|
|
470
|
-
});
|
|
469
|
+
}).build();
|
|
471
470
|
|
|
472
471
|
expect(app).toBeDefined();
|
|
473
472
|
});
|
|
474
473
|
|
|
475
474
|
it("creates server with authOptions", () => {
|
|
476
|
-
const
|
|
475
|
+
const configureApp = (): void => {};
|
|
477
476
|
|
|
478
|
-
const app =
|
|
479
|
-
addRoutes,
|
|
477
|
+
const app = new TerrenoApp({
|
|
480
478
|
authOptions: {
|
|
481
479
|
generateJWTPayload: (user) => ({customField: "test", id: user._id}),
|
|
482
480
|
generateTokenExpiration: () => "2h",
|
|
483
481
|
},
|
|
482
|
+
configureApp,
|
|
484
483
|
skipListen: true,
|
|
485
484
|
userModel: UserModel as any,
|
|
486
|
-
});
|
|
485
|
+
}).build();
|
|
487
486
|
|
|
488
487
|
expect(app).toBeDefined();
|
|
489
488
|
});
|
|
@@ -514,7 +513,7 @@ describe("expressServer", () => {
|
|
|
514
513
|
});
|
|
515
514
|
});
|
|
516
515
|
|
|
517
|
-
describe("
|
|
516
|
+
describe("TerrenoApp with full integration", () => {
|
|
518
517
|
const originalEnv = process.env;
|
|
519
518
|
|
|
520
519
|
beforeEach(() => {
|
|
@@ -533,49 +532,49 @@ describe("expressServer", () => {
|
|
|
533
532
|
});
|
|
534
533
|
|
|
535
534
|
it("sets Sentry transaction ID tag from header", async () => {
|
|
536
|
-
const
|
|
537
|
-
|
|
535
|
+
const configureApp = (httpApp: any) => {
|
|
536
|
+
httpApp.get("/test", (_req: any, res: any) => {
|
|
538
537
|
res.json({ok: true});
|
|
539
538
|
});
|
|
540
539
|
};
|
|
541
540
|
|
|
542
|
-
const app =
|
|
543
|
-
|
|
541
|
+
const app = new TerrenoApp({
|
|
542
|
+
configureApp,
|
|
544
543
|
skipListen: true,
|
|
545
544
|
userModel: UserModel as any,
|
|
546
|
-
});
|
|
545
|
+
}).build();
|
|
547
546
|
|
|
548
547
|
await supertest(app).get("/test").set("X-Transaction-ID", "txn-123").expect(200);
|
|
549
548
|
});
|
|
550
549
|
|
|
551
550
|
it("sets Sentry session ID tag from header", async () => {
|
|
552
|
-
const
|
|
553
|
-
|
|
551
|
+
const configureApp = (httpApp: any) => {
|
|
552
|
+
httpApp.get("/test", (_req: any, res: any) => {
|
|
554
553
|
res.json({ok: true});
|
|
555
554
|
});
|
|
556
555
|
};
|
|
557
556
|
|
|
558
|
-
const app =
|
|
559
|
-
|
|
557
|
+
const app = new TerrenoApp({
|
|
558
|
+
configureApp,
|
|
560
559
|
skipListen: true,
|
|
561
560
|
userModel: UserModel as any,
|
|
562
|
-
});
|
|
561
|
+
}).build();
|
|
563
562
|
|
|
564
563
|
await supertest(app).get("/test").set("X-Session-ID", "session-456").expect(200);
|
|
565
564
|
});
|
|
566
565
|
|
|
567
566
|
it("sets both transaction and session ID tags", async () => {
|
|
568
|
-
const
|
|
569
|
-
|
|
567
|
+
const configureApp = (httpApp: any) => {
|
|
568
|
+
httpApp.get("/test", (_req: any, res: any) => {
|
|
570
569
|
res.json({ok: true});
|
|
571
570
|
});
|
|
572
571
|
};
|
|
573
572
|
|
|
574
|
-
const app =
|
|
575
|
-
|
|
573
|
+
const app = new TerrenoApp({
|
|
574
|
+
configureApp,
|
|
576
575
|
skipListen: true,
|
|
577
576
|
userModel: UserModel as any,
|
|
578
|
-
});
|
|
577
|
+
}).build();
|
|
579
578
|
|
|
580
579
|
await supertest(app)
|
|
581
580
|
.get("/test")
|
|
@@ -585,52 +584,52 @@ describe("expressServer", () => {
|
|
|
585
584
|
});
|
|
586
585
|
|
|
587
586
|
it("handles fallthrough error handler", async () => {
|
|
588
|
-
const
|
|
589
|
-
|
|
587
|
+
const configureApp = (httpApp: any) => {
|
|
588
|
+
httpApp.get("/error", (_req: any, _res: any) => {
|
|
590
589
|
throw new Error("Unexpected error");
|
|
591
590
|
});
|
|
592
591
|
};
|
|
593
592
|
|
|
594
|
-
const app =
|
|
595
|
-
|
|
593
|
+
const app = new TerrenoApp({
|
|
594
|
+
configureApp,
|
|
596
595
|
skipListen: true,
|
|
597
596
|
userModel: UserModel as any,
|
|
598
|
-
});
|
|
597
|
+
}).build();
|
|
599
598
|
|
|
600
599
|
await supertest(app).get("/error").expect(500);
|
|
601
600
|
});
|
|
602
601
|
|
|
603
|
-
it("handles loggingOptions passed to
|
|
604
|
-
const
|
|
605
|
-
|
|
602
|
+
it("handles loggingOptions passed to TerrenoApp", async () => {
|
|
603
|
+
const configureApp = (httpApp: any) => {
|
|
604
|
+
httpApp.get("/test", (_req: any, res: any) => {
|
|
606
605
|
res.json({ok: true});
|
|
607
606
|
});
|
|
608
607
|
};
|
|
609
608
|
|
|
610
|
-
const app =
|
|
611
|
-
|
|
609
|
+
const app = new TerrenoApp({
|
|
610
|
+
configureApp,
|
|
612
611
|
loggingOptions: {
|
|
613
612
|
logSlowRequests: true,
|
|
614
613
|
logSlowRequestsReadMs: 100,
|
|
615
614
|
},
|
|
616
615
|
skipListen: true,
|
|
617
616
|
userModel: UserModel as any,
|
|
618
|
-
});
|
|
617
|
+
}).build();
|
|
619
618
|
|
|
620
619
|
await supertest(app).get("/test").expect(200);
|
|
621
620
|
});
|
|
622
621
|
|
|
623
|
-
it("re-throws when
|
|
624
|
-
const
|
|
622
|
+
it("re-throws when configureApp throws during build", () => {
|
|
623
|
+
const configureApp = () => {
|
|
625
624
|
throw new Error("Route init boom");
|
|
626
625
|
};
|
|
627
626
|
|
|
628
627
|
expect(() =>
|
|
629
|
-
|
|
630
|
-
|
|
628
|
+
new TerrenoApp({
|
|
629
|
+
configureApp,
|
|
631
630
|
skipListen: true,
|
|
632
631
|
userModel: UserModel as any,
|
|
633
|
-
})
|
|
632
|
+
}).build()
|
|
634
633
|
).toThrow("Route init boom");
|
|
635
634
|
});
|
|
636
635
|
});
|
|
@@ -788,7 +787,7 @@ describe("expressServer", () => {
|
|
|
788
787
|
});
|
|
789
788
|
});
|
|
790
789
|
|
|
791
|
-
describe("
|
|
790
|
+
describe("TerrenoApp with listen (skipListen false)", () => {
|
|
792
791
|
const originalEnv = process.env;
|
|
793
792
|
|
|
794
793
|
beforeEach(() => {
|
|
@@ -808,11 +807,10 @@ describe("expressServer", () => {
|
|
|
808
807
|
});
|
|
809
808
|
|
|
810
809
|
it("starts the server when skipListen is false", async () => {
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
express.default.application.listen = mock(function (this: unknown, ...args: unknown[]) {
|
|
810
|
+
const configureApp = (): void => {};
|
|
811
|
+
const http = await import("node:http");
|
|
812
|
+
const originalListen = http.Server.prototype.listen;
|
|
813
|
+
http.Server.prototype.listen = mock(function (this: unknown, ...args: unknown[]) {
|
|
816
814
|
const cb = args.find((a: unknown) => typeof a === "function") as (() => void) | undefined;
|
|
817
815
|
if (cb) {
|
|
818
816
|
cb();
|
|
@@ -820,29 +818,28 @@ describe("expressServer", () => {
|
|
|
820
818
|
return this;
|
|
821
819
|
}) as unknown as typeof originalListen;
|
|
822
820
|
try {
|
|
823
|
-
const app =
|
|
824
|
-
|
|
821
|
+
const app = new TerrenoApp({
|
|
822
|
+
configureApp,
|
|
825
823
|
skipListen: false,
|
|
826
824
|
userModel: UserModel as any,
|
|
827
|
-
});
|
|
825
|
+
}).start();
|
|
828
826
|
expect(app).toBeDefined();
|
|
829
827
|
} finally {
|
|
830
|
-
|
|
828
|
+
http.Server.prototype.listen = originalListen;
|
|
831
829
|
}
|
|
832
830
|
});
|
|
833
831
|
|
|
834
832
|
it("handles listen error with invalid port", () => {
|
|
835
833
|
process.env.PORT = "-1";
|
|
836
|
-
const
|
|
837
|
-
// Using an invalid port should trigger the catch block and process.exit(1)
|
|
834
|
+
const configureApp = (): void => {};
|
|
838
835
|
const originalExit = process.exit;
|
|
839
836
|
process.exit = (() => {}) as unknown as typeof process.exit;
|
|
840
837
|
try {
|
|
841
|
-
|
|
842
|
-
|
|
838
|
+
new TerrenoApp({
|
|
839
|
+
configureApp,
|
|
843
840
|
skipListen: false,
|
|
844
841
|
userModel: UserModel as any,
|
|
845
|
-
});
|
|
842
|
+
}).start();
|
|
846
843
|
} catch {
|
|
847
844
|
// May throw
|
|
848
845
|
}
|
|
@@ -850,7 +847,7 @@ describe("expressServer", () => {
|
|
|
850
847
|
});
|
|
851
848
|
});
|
|
852
849
|
|
|
853
|
-
describe("
|
|
850
|
+
describe("TerrenoApp with listen", () => {
|
|
854
851
|
const originalEnv = process.env;
|
|
855
852
|
const http = require("node:http");
|
|
856
853
|
let activeServer: any = null;
|
|
@@ -884,13 +881,13 @@ describe("expressServer", () => {
|
|
|
884
881
|
});
|
|
885
882
|
|
|
886
883
|
it("starts listening on a port when skipListen is false", async () => {
|
|
887
|
-
const
|
|
884
|
+
const configureApp = (): void => {};
|
|
888
885
|
|
|
889
|
-
const app =
|
|
890
|
-
|
|
886
|
+
const app = new TerrenoApp({
|
|
887
|
+
configureApp,
|
|
891
888
|
skipListen: false,
|
|
892
889
|
userModel: UserModel as any,
|
|
893
|
-
});
|
|
890
|
+
}).start();
|
|
894
891
|
|
|
895
892
|
expect(app).toBeDefined();
|
|
896
893
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -947,7 +944,7 @@ describe("expressServer", () => {
|
|
|
947
944
|
});
|
|
948
945
|
});
|
|
949
946
|
|
|
950
|
-
describe("
|
|
947
|
+
describe("TerrenoApp error handling", () => {
|
|
951
948
|
const originalEnv = process.env;
|
|
952
949
|
|
|
953
950
|
beforeEach(() => {
|
|
@@ -965,17 +962,17 @@ describe("expressServer", () => {
|
|
|
965
962
|
process.env = originalEnv;
|
|
966
963
|
});
|
|
967
964
|
|
|
968
|
-
it("
|
|
969
|
-
const
|
|
965
|
+
it("throws when configureApp throws during build", () => {
|
|
966
|
+
const configureApp = () => {
|
|
970
967
|
throw new Error("route initialization failed");
|
|
971
968
|
};
|
|
972
969
|
|
|
973
970
|
expect(() =>
|
|
974
|
-
|
|
975
|
-
|
|
971
|
+
new TerrenoApp({
|
|
972
|
+
configureApp,
|
|
976
973
|
skipListen: true,
|
|
977
974
|
userModel: UserModel as any,
|
|
978
|
-
})
|
|
975
|
+
}).build()
|
|
979
976
|
).toThrow("route initialization failed");
|
|
980
977
|
});
|
|
981
978
|
});
|
package/src/expressServer.ts
CHANGED
|
@@ -1,30 +1,13 @@
|
|
|
1
1
|
import * as Sentry from "@sentry/bun";
|
|
2
|
-
import cors from "cors";
|
|
3
2
|
import cron from "cron";
|
|
4
3
|
import express, {type Router} from "express";
|
|
5
4
|
import type jwt from "jsonwebtoken";
|
|
6
5
|
import cloneDeep from "lodash/cloneDeep";
|
|
7
6
|
import onFinished from "on-finished";
|
|
8
7
|
import passport from "passport";
|
|
9
|
-
import qs from "qs";
|
|
10
8
|
import type {ModelRouterOptions} from "./api";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
apiErrorMiddleware,
|
|
14
|
-
apiFallthroughErrorMiddleware,
|
|
15
|
-
apiUnauthorizedMiddleware,
|
|
16
|
-
} from "./errors";
|
|
17
|
-
import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
|
|
18
|
-
import {type LoggingOptions, logger, setupLogging} from "./logger";
|
|
9
|
+
import {type LoggingOptions, logger} from "./logger";
|
|
19
10
|
import {sendToSlack} from "./notifiers";
|
|
20
|
-
import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
|
|
21
|
-
import {openApiEtagMiddleware} from "./openApiEtag";
|
|
22
|
-
import {
|
|
23
|
-
getCurrentRequestContext,
|
|
24
|
-
requestContextMiddleware,
|
|
25
|
-
updateRequestContextFromRequest,
|
|
26
|
-
} from "./requestContext";
|
|
27
|
-
import openapi from "./vendor/wesleytodd-openapi/index";
|
|
28
11
|
|
|
29
12
|
const SLOW_READ_MAX = 200;
|
|
30
13
|
const SLOW_WRITE_MAX = 500;
|
|
@@ -170,201 +153,6 @@ export interface AuthOptions {
|
|
|
170
153
|
generateRefreshTokenExpiration?: (user: any) => number | jwt.SignOptions["expiresIn"];
|
|
171
154
|
}
|
|
172
155
|
|
|
173
|
-
interface InitializeRoutesOptions {
|
|
174
|
-
corsOrigin?:
|
|
175
|
-
| string
|
|
176
|
-
| boolean
|
|
177
|
-
| RegExp
|
|
178
|
-
| Array<boolean | string | RegExp>
|
|
179
|
-
| ((
|
|
180
|
-
requestOrigin: string | undefined,
|
|
181
|
-
callback: (
|
|
182
|
-
err: Error | null,
|
|
183
|
-
origin?: boolean | string | RegExp | Array<boolean | string | RegExp>
|
|
184
|
-
) => void
|
|
185
|
-
) => void);
|
|
186
|
-
addMiddleware?: AddRoutes;
|
|
187
|
-
// The maximum number of array elements to parse in a query string. Defaults to 200.
|
|
188
|
-
arrayLimit?: number;
|
|
189
|
-
// Whether requests should be logged. In production, you may want to disable this if using another
|
|
190
|
-
// logger (e.g. Google Cloud).
|
|
191
|
-
logRequests?: boolean;
|
|
192
|
-
loggingOptions?: LoggingOptions;
|
|
193
|
-
authOptions?: AuthOptions;
|
|
194
|
-
/** GitHub OAuth configuration. When provided, enables GitHub authentication. */
|
|
195
|
-
githubAuth?: GitHubAuthOptions;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const initializeRoutes = (
|
|
199
|
-
UserModel: UserMongooseModel,
|
|
200
|
-
addRoutes: AddRoutes,
|
|
201
|
-
options: InitializeRoutesOptions = {}
|
|
202
|
-
): express.Application => {
|
|
203
|
-
const app = express();
|
|
204
|
-
|
|
205
|
-
// Record mount paths on layers for Express 5 → OpenAPI compat
|
|
206
|
-
patchAppUse(app);
|
|
207
|
-
|
|
208
|
-
app.set("query parser", (str: string) => qs.parse(str, {arrayLimit: options.arrayLimit ?? 200}));
|
|
209
|
-
|
|
210
|
-
app.use(requestContextMiddleware);
|
|
211
|
-
|
|
212
|
-
app.use(
|
|
213
|
-
cors({
|
|
214
|
-
origin: options.corsOrigin ?? "*",
|
|
215
|
-
})
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
if (options.addMiddleware) {
|
|
219
|
-
options.addMiddleware(app);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
app.use(express.json({limit: "50mb"}));
|
|
223
|
-
|
|
224
|
-
// Add login/signup/refresh_token before the JWT/auth middlewares
|
|
225
|
-
addAuthRoutes(app, UserModel, options?.authOptions);
|
|
226
|
-
setupAuth(app, UserModel);
|
|
227
|
-
app.use((req, res, next) => {
|
|
228
|
-
updateRequestContextFromRequest(req, res);
|
|
229
|
-
next();
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
if (options.logRequests !== false) {
|
|
233
|
-
app.use(logRequests);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Store the logging options on the request so we can access them later.
|
|
237
|
-
app.use((_req, res, next) => {
|
|
238
|
-
res.locals.loggingOptions = options.loggingOptions;
|
|
239
|
-
next();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// Add Sentry scopes for session, transaction, and userId if any are set
|
|
243
|
-
app.use((req: express.Request, _res: express.Response, next: express.NextFunction) => {
|
|
244
|
-
const context = getCurrentRequestContext();
|
|
245
|
-
const transactionId = req.header("X-Transaction-ID");
|
|
246
|
-
const sessionId = context?.sessionId ?? req.header("X-Session-ID");
|
|
247
|
-
if (context?.requestId) {
|
|
248
|
-
Sentry.getCurrentScope().setTag("request_id", context.requestId);
|
|
249
|
-
}
|
|
250
|
-
if (transactionId) {
|
|
251
|
-
Sentry.getCurrentScope().setTag("transaction_id", transactionId);
|
|
252
|
-
}
|
|
253
|
-
if (sessionId) {
|
|
254
|
-
Sentry.getCurrentScope().setTag("session_id", sessionId);
|
|
255
|
-
}
|
|
256
|
-
if (req.user?._id) {
|
|
257
|
-
Sentry.getCurrentScope().setTag("user", String(req.user._id));
|
|
258
|
-
}
|
|
259
|
-
next();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Add ETag middleware for OpenAPI JSON endpoint before the openapi middleware
|
|
263
|
-
app.use(openApiCompatMiddleware);
|
|
264
|
-
app.use(openApiEtagMiddleware);
|
|
265
|
-
|
|
266
|
-
const oapi = openapi({
|
|
267
|
-
info: {
|
|
268
|
-
description: "Generated docs from an Express api",
|
|
269
|
-
title: "Express Application",
|
|
270
|
-
version: "1.0.0",
|
|
271
|
-
},
|
|
272
|
-
openapi: "3.0.0",
|
|
273
|
-
});
|
|
274
|
-
app.use(oapi);
|
|
275
|
-
|
|
276
|
-
if (process.env.ENABLE_SWAGGER === "true") {
|
|
277
|
-
app.use("/swagger", oapi.swaggerui());
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
addMeRoutes(app, UserModel, options?.authOptions);
|
|
281
|
-
|
|
282
|
-
// Set up GitHub OAuth if configured (works with JWT auth)
|
|
283
|
-
if (options.githubAuth) {
|
|
284
|
-
setupGitHubAuth(app, UserModel, options.githubAuth);
|
|
285
|
-
addGitHubAuthRoutes(app, UserModel, options.githubAuth, options.authOptions);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
addRoutes(app, {openApi: oapi});
|
|
289
|
-
|
|
290
|
-
Sentry.setupExpressErrorHandler(app);
|
|
291
|
-
|
|
292
|
-
// Catch any thrown APIErrors and return them in an OpenAPI compatible format
|
|
293
|
-
app.use(apiUnauthorizedMiddleware);
|
|
294
|
-
app.use(apiErrorMiddleware);
|
|
295
|
-
|
|
296
|
-
app.use(apiFallthroughErrorMiddleware);
|
|
297
|
-
|
|
298
|
-
return app;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
export interface SetupServerOptions {
|
|
302
|
-
userModel: UserMongooseModel;
|
|
303
|
-
addRoutes: AddRoutes;
|
|
304
|
-
loggingOptions?: LoggingOptions;
|
|
305
|
-
// Whether requests should be logged. Defaults to true.
|
|
306
|
-
logRequests?: boolean;
|
|
307
|
-
authOptions?: AuthOptions;
|
|
308
|
-
/**
|
|
309
|
-
* GitHub OAuth configuration. When provided, enables GitHub authentication.
|
|
310
|
-
* Requires the user schema to have GitHub fields (use githubUserPlugin).
|
|
311
|
-
*/
|
|
312
|
-
githubAuth?: GitHubAuthOptions;
|
|
313
|
-
skipListen?: boolean;
|
|
314
|
-
corsOrigin?:
|
|
315
|
-
| string
|
|
316
|
-
| boolean
|
|
317
|
-
| RegExp
|
|
318
|
-
| Array<boolean | string | RegExp>
|
|
319
|
-
| ((
|
|
320
|
-
requestOrigin: string | undefined,
|
|
321
|
-
callback: (
|
|
322
|
-
err: Error | null,
|
|
323
|
-
origin?: boolean | string | RegExp | Array<boolean | string | RegExp>
|
|
324
|
-
) => void
|
|
325
|
-
) => void);
|
|
326
|
-
addMiddleware?: AddRoutes;
|
|
327
|
-
ignoreTraces?: string[];
|
|
328
|
-
sentryOptions?: Sentry.BunOptions;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export const setupServer = (options: SetupServerOptions): express.Application => {
|
|
332
|
-
const UserModel = options.userModel;
|
|
333
|
-
const addRoutes = options.addRoutes;
|
|
334
|
-
|
|
335
|
-
setupLogging(options.loggingOptions);
|
|
336
|
-
|
|
337
|
-
let app: express.Application;
|
|
338
|
-
try {
|
|
339
|
-
app = initializeRoutes(UserModel, addRoutes, {
|
|
340
|
-
addMiddleware: options.addMiddleware,
|
|
341
|
-
authOptions: options.authOptions,
|
|
342
|
-
corsOrigin: options.corsOrigin,
|
|
343
|
-
githubAuth: options.githubAuth,
|
|
344
|
-
loggingOptions: options.loggingOptions,
|
|
345
|
-
logRequests: options.logRequests,
|
|
346
|
-
});
|
|
347
|
-
} catch (error: unknown) {
|
|
348
|
-
const stack = error instanceof Error && error.stack ? error.stack : String(error);
|
|
349
|
-
logger.error(`Error initializing routes: ${stack}`);
|
|
350
|
-
throw error;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (!options.skipListen) {
|
|
354
|
-
const port = process.env.PORT || "9000";
|
|
355
|
-
try {
|
|
356
|
-
app.listen(port, () => {
|
|
357
|
-
logger.info(`Listening on port ${port}`);
|
|
358
|
-
});
|
|
359
|
-
} catch (error) {
|
|
360
|
-
const stack = error instanceof Error ? error.stack : String(error);
|
|
361
|
-
logger.error(`Error trying to start HTTP server: ${error}\n${stack}`);
|
|
362
|
-
process.exit(1);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return app;
|
|
366
|
-
};
|
|
367
|
-
|
|
368
156
|
export const cronjob = (
|
|
369
157
|
name: string,
|
|
370
158
|
schedule: "hourly" | "minutely" | string,
|