@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.
Files changed (107) hide show
  1. package/.ai/guidelines/core.md +71 -0
  2. package/.ai/skills/mongoose-schema-safety/SKILL.md +143 -0
  3. package/README.md +54 -1
  4. package/bunfig.toml +1 -1
  5. package/dist/__tests__/versionCheckPlugin.test.js +29 -7
  6. package/dist/actions.openApi.test.js +13 -11
  7. package/dist/api.js +98 -11
  8. package/dist/api.query.test.js +31 -1
  9. package/dist/api.test.js +211 -0
  10. package/dist/auth.test.js +418 -43
  11. package/dist/betterAuth.d.ts +1 -1
  12. package/dist/consentApp.test.js +1 -0
  13. package/dist/example.js +4 -4
  14. package/dist/expressServer.d.ts +0 -22
  15. package/dist/expressServer.js +1 -125
  16. package/dist/expressServer.test.js +90 -91
  17. package/dist/githubAuth.test.js +22 -22
  18. package/dist/logger.d.ts +154 -0
  19. package/dist/logger.js +445 -26
  20. package/dist/logger.test.js +435 -0
  21. package/dist/middleware.d.ts +7 -0
  22. package/dist/middleware.js +58 -1
  23. package/dist/middleware.test.js +159 -0
  24. package/dist/models/consentForm.js +2 -1
  25. package/dist/models/consentResponse.js +2 -1
  26. package/dist/models/versionConfig.js +2 -1
  27. package/dist/openApi.test.js +10 -17
  28. package/dist/openApiBuilder.d.ts +18 -0
  29. package/dist/openApiBuilder.js +21 -0
  30. package/dist/openApiBuilder.test.js +34 -10
  31. package/dist/permissions.test.js +10 -43
  32. package/dist/populate.test.js +10 -42
  33. package/dist/realtime/changeStreamWatcher.d.ts +4 -4
  34. package/dist/realtime/changeStreamWatcher.js +2 -4
  35. package/dist/realtime/queryMatcher.d.ts +1 -1
  36. package/dist/realtime/queryMatcher.js +39 -14
  37. package/dist/realtime/types.d.ts +3 -3
  38. package/dist/requestContext.d.ts +61 -0
  39. package/dist/requestContext.js +74 -0
  40. package/dist/secretProviders.test.js +335 -0
  41. package/dist/syncConsents.test.js +2 -2
  42. package/dist/terrenoApp.d.ts +27 -15
  43. package/dist/terrenoApp.js +24 -14
  44. package/dist/terrenoApp.test.js +52 -0
  45. package/dist/tests/bunSetup.js +66 -262
  46. package/dist/tests/createTestData.d.ts +9 -0
  47. package/dist/tests/createTestData.js +272 -0
  48. package/dist/tests/models.d.ts +71 -0
  49. package/dist/tests/models.js +134 -0
  50. package/dist/tests/mongoTestSetup.d.ts +7 -0
  51. package/dist/tests/mongoTestSetup.js +150 -0
  52. package/dist/tests/testEnv.d.ts +0 -0
  53. package/dist/tests/testEnv.js +6 -0
  54. package/dist/tests/testHelper.d.ts +22 -0
  55. package/dist/tests/testHelper.js +115 -0
  56. package/dist/tests/types.d.ts +29 -0
  57. package/dist/tests/types.js +2 -0
  58. package/dist/tests.d.ts +10 -78
  59. package/dist/tests.js +24 -241
  60. package/dist/transformers.test.js +14 -50
  61. package/package.json +18 -4
  62. package/src/__snapshots__/openApiBuilder.test.ts.snap +1 -0
  63. package/src/__tests__/versionCheckPlugin.test.ts +43 -15
  64. package/src/actions.openApi.test.ts +12 -10
  65. package/src/api.query.test.ts +24 -1
  66. package/src/api.test.ts +169 -0
  67. package/src/api.ts +71 -0
  68. package/src/auth.test.ts +287 -39
  69. package/src/betterAuth.ts +1 -1
  70. package/src/consentApp.test.ts +1 -0
  71. package/src/example.ts +4 -4
  72. package/src/expressServer.test.ts +82 -85
  73. package/src/expressServer.ts +1 -213
  74. package/src/githubAuth.test.ts +22 -22
  75. package/src/logger.test.ts +466 -1
  76. package/src/logger.ts +477 -14
  77. package/src/middleware.test.ts +74 -2
  78. package/src/middleware.ts +57 -0
  79. package/src/models/consentForm.ts +3 -4
  80. package/src/models/consentResponse.ts +6 -4
  81. package/src/models/versionConfig.ts +3 -4
  82. package/src/openApi.test.ts +10 -17
  83. package/src/openApiBuilder.test.ts +27 -10
  84. package/src/openApiBuilder.ts +24 -0
  85. package/src/permissions.test.ts +8 -23
  86. package/src/populate.test.ts +7 -22
  87. package/src/realtime/changeStreamWatcher.ts +15 -10
  88. package/src/realtime/queryMatcher.ts +54 -27
  89. package/src/realtime/types.ts +4 -4
  90. package/src/requestContext.ts +86 -0
  91. package/src/secretProviders.test.ts +219 -1
  92. package/src/syncConsents.test.ts +1 -1
  93. package/src/terrenoApp.test.ts +38 -0
  94. package/src/terrenoApp.ts +37 -15
  95. package/src/tests/bunSetup.ts +22 -236
  96. package/src/tests/createTestData.ts +176 -0
  97. package/src/tests/models.ts +164 -0
  98. package/src/tests/mongoTestSetup.ts +69 -0
  99. package/src/tests/testEnv.ts +4 -0
  100. package/src/tests/testHelper.ts +57 -0
  101. package/src/tests/types.ts +35 -0
  102. package/src/tests.ts +40 -231
  103. package/src/transformers.test.ts +11 -30
  104. package/tsconfig.typedoc.json +4 -0
  105. package/dist/tests/index.d.ts +0 -1
  106. package/dist/tests/index.js +0 -17
  107. package/src/tests/index.ts +0 -1
@@ -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 = setupServer({
79
- addRoutes: (router) => {
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("setupServer", () => {
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 addRoutes = () => {};
432
+ const configureApp = (): void => {};
433
433
 
434
- const app = setupServer({
435
- addRoutes,
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 addMiddleware option", () => {
443
+ it("creates server with beforeJsonSetup option", () => {
444
444
  let middlewareCalled = false;
445
- const addMiddleware = (app: any) => {
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 = setupServer({
452
- addMiddleware,
453
- addRoutes,
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 addRoutes = () => {};
462
+ const configureApp = (): void => {};
464
463
 
465
- const app = setupServer({
466
- addRoutes,
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 addRoutes = () => {};
475
+ const configureApp = (): void => {};
477
476
 
478
- const app = setupServer({
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("setupServer with full integration", () => {
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 addRoutes = (app: any) => {
537
- app.get("/test", (_req: any, res: any) => {
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 = setupServer({
543
- addRoutes,
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 addRoutes = (app: any) => {
553
- app.get("/test", (_req: any, res: any) => {
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 = setupServer({
559
- addRoutes,
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 addRoutes = (app: any) => {
569
- app.get("/test", (_req: any, res: any) => {
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 = setupServer({
575
- addRoutes,
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 addRoutes = (app: any) => {
589
- app.get("/error", (_req: any, _res: any) => {
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 = setupServer({
595
- addRoutes,
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 setupServer", async () => {
604
- const addRoutes = (app: any) => {
605
- app.get("/test", (_req: any, res: any) => {
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 = setupServer({
611
- addRoutes,
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 addRoutes throws during route initialization", () => {
624
- const addRoutes = () => {
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
- setupServer({
630
- addRoutes,
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("setupServer with listen (skipListen false)", () => {
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 addRoutes = () => {};
812
- // Mock app.listen on the Express prototype to avoid opening a real port
813
- const express = await import("express");
814
- const originalListen = express.default.application.listen;
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 = setupServer({
824
- addRoutes,
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
- express.default.application.listen = originalListen;
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 addRoutes = () => {};
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
- setupServer({
842
- addRoutes,
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("setupServer with listen", () => {
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 addRoutes = () => {};
884
+ const configureApp = (): void => {};
888
885
 
889
- const app = setupServer({
890
- addRoutes,
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("setupServer error handling", () => {
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("catches and rethrows errors from initializeRoutes", () => {
969
- const addRoutes = () => {
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
- setupServer({
975
- addRoutes,
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
  });
@@ -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 {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
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,