@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
package/src/terrenoApp.ts CHANGED
@@ -11,9 +11,10 @@ import {
11
11
  apiFallthroughErrorMiddleware,
12
12
  apiUnauthorizedMiddleware,
13
13
  } from "./errors";
14
- import {type AuthOptions, logRequests} from "./expressServer";
14
+ import {type AddRoutes, type AuthOptions, logRequests} from "./expressServer";
15
15
  import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
16
16
  import {type LoggingOptions, logger, setupLogging} from "./logger";
17
+ import {jsonResponseRequestIdMiddleware} from "./middleware";
17
18
  import {openApiCompatMiddleware, patchAppUse} from "./openApiCompat";
18
19
  import {openApiEtagMiddleware} from "./openApiEtag";
19
20
  import {RealtimeApp} from "./realtime/realtimeApp";
@@ -68,28 +69,41 @@ export interface TerrenoAppOptions {
68
69
  * Set to `true` for defaults, or pass a RealtimeAppOptions object for full control.
69
70
  */
70
71
  realtime?: boolean | RealtimeAppOptions;
72
+ /**
73
+ * Runs after CORS and before the `addMiddleware` chain and JSON body parsing.
74
+ * Use to attach early middleware via `app.use(...)` before JSON parsing.
75
+ */
76
+ beforeJsonSetup?: (app: express.Application) => void;
77
+ /**
78
+ * Invoked after registered plugins/model routers and before `/auth/me`.
79
+ * Receives the Express app and OpenAPI bundle for `modelRouter` / `createOpenApiBuilder` wiring.
80
+ */
81
+ configureApp?: AddRoutes;
71
82
  }
72
83
 
73
84
  /**
74
85
  * Fluent API for building Express applications with Terreno framework.
75
86
  *
76
- * TerrenoApp provides an alternative to `setupServer` using a registration
77
- * pattern instead of callbacks. Build applications by registering model
78
- * routers and plugins, then calling `start()` to begin listening.
87
+ * TerrenoApp is the supported way to assemble the Terreno Express stack.
88
+ * Build applications by registering model routers and plugins (and/or
89
+ * `configureApp`), then calling `start()` to listen.
79
90
  *
80
91
  * The middleware stack is configured in this order:
81
92
  * 1. CORS
82
- * 2. Custom middleware (via addMiddleware)
83
- * 3. JSON body parser
84
- * 4. Auth routes (/auth/login, /auth/signup, etc.)
85
- * 5. JWT authentication setup
86
- * 6. Request logging
87
- * 7. Sentry scopes
88
- * 8. OpenAPI middleware
89
- * 9. /auth/me routes
93
+ * 2. Optional `beforeJsonSetup` (configure the app before JSON parsing)
94
+ * 3. Custom middleware (via addMiddleware)
95
+ * 4. JSON body parser
96
+ * 5. Auth routes (/auth/login, /auth/signup, etc.)
97
+ * 6. JWT authentication setup
98
+ * 7. Request logging
99
+ * 8. Sentry scopes
100
+ * 9. OpenAPI middleware (including JSON `requestId` on object responses)
90
101
  * 10. GitHub OAuth routes (if enabled)
91
- * 11. Registered model routers and plugins
92
- * 12. Error handling middleware
102
+ * 11. Configuration app (if any)
103
+ * 12. Registered model routers and plugins
104
+ * 13. Optional `configureApp` callback
105
+ * 14. /auth/me routes
106
+ * 15. Error handling middleware
93
107
  *
94
108
  * @example
95
109
  * ```typescript
@@ -126,7 +140,6 @@ export interface TerrenoAppOptions {
126
140
  * .start();
127
141
  * ```
128
142
  *
129
- * @see setupServer for the callback-based alternative
130
143
  * @see TerrenoPlugin for creating reusable plugins
131
144
  * @see modelRouter for creating CRUD route registrations
132
145
  */
@@ -263,6 +276,10 @@ export class TerrenoApp {
263
276
 
264
277
  app.use(cors({credentials: true, origin: options.corsOrigin ?? "*"}));
265
278
 
279
+ if (options.beforeJsonSetup) {
280
+ options.beforeJsonSetup(app);
281
+ }
282
+
266
283
  // Apply custom middleware before JSON parsing
267
284
  for (const fn of this.middlewareFns) {
268
285
  if (fn.length <= 3) {
@@ -317,6 +334,7 @@ export class TerrenoApp {
317
334
  // OpenAPI
318
335
  app.use(openApiCompatMiddleware);
319
336
  app.use(openApiEtagMiddleware);
337
+ app.use(jsonResponseRequestIdMiddleware);
320
338
  const oapi = openapi({
321
339
  info: {
322
340
  description: "Generated docs from an Express api",
@@ -352,6 +370,10 @@ export class TerrenoApp {
352
370
  }
353
371
  }
354
372
 
373
+ if (options.configureApp) {
374
+ options.configureApp(app, {openApi: oapi});
375
+ }
376
+
355
377
  // /auth/me must be registered after plugins so that session middleware
356
378
  // (e.g. Better Auth) has a chance to populate req.user first.
357
379
  addMeRoutes(app, options.userModel, options.authOptions);
@@ -1,241 +1,27 @@
1
- // biome-ignore-all lint/suspicious/noExplicitAny: test mock typing
2
- import {afterAll, afterEach, beforeAll, beforeEach, mock} from "bun:test";
3
- import {Writable} from "node:stream";
4
- import mongoose from "mongoose";
5
- import winston from "winston";
1
+ import {registerBackendPreload, registerSimpleMongoPreload} from "@terreno/test";
6
2
 
7
- import {setupEnvironment} from "../expressServer";
8
- import {logger, winstonLogger} from "../logger";
3
+ const useFixtureCache = process.env.TERRENO_TEST_USE_FIXTURE_CACHE === "true";
9
4
 
10
- const shouldConnectToTestDb = process.env.BUN_TEST_DISABLE_DB !== "true";
11
-
12
- // Connect to MongoDB once for all tests
13
- if (shouldConnectToTestDb) {
14
- beforeAll(async () => {
15
- await mongoose
16
- .connect("mongodb://127.0.0.1/terreno?&connectTimeoutMS=360000")
17
- .catch(logger.catch);
5
+ if (useFixtureCache) {
6
+ registerBackendPreload({
7
+ connectMongoInBeforeAll: true,
8
+ loadTestDataFromCache: async () => {
9
+ const {loadTestDataFromCache} = await import("./mongoTestSetup");
10
+ await loadTestDataFromCache();
11
+ },
12
+ mongo: {
13
+ baseDatabaseName: "terrenoTest_base",
14
+ useReplSet: true,
15
+ },
16
+ testEnv: {
17
+ tokenIssuer: "terreno-api.test",
18
+ },
19
+ useTransactions: true,
18
20
  });
19
- }
20
-
21
- // Close MongoDB connection after all tests
22
- if (shouldConnectToTestDb) {
23
- afterAll(async () => {
24
- await mongoose.connection.close();
21
+ } else {
22
+ registerSimpleMongoPreload({
23
+ testEnv: {
24
+ tokenIssuer: "terreno-api.test",
25
+ },
25
26
  });
26
27
  }
27
-
28
- let logs: string[] = [];
29
-
30
- const SHOW_ALL_LOGS = process.env.SHOW_ALL_TEST_LOGS === "true";
31
-
32
- // Create a custom stream that captures logs
33
- const logStream = new Writable({
34
- write(chunk: any, _encoding: any, callback: any) {
35
- logs.push(chunk.toString());
36
- if (SHOW_ALL_LOGS) {
37
- process.stdout.write(chunk);
38
- }
39
- callback();
40
- },
41
- });
42
-
43
- // Silence both winston loggers by replacing all transports with our capturing stream
44
- const silentTransport = new winston.transports.Stream({
45
- format: winston.format.simple(),
46
- stream: logStream,
47
- });
48
-
49
- // Clear and silence the default winston logger
50
- winston.clear();
51
- winston.add(silentTransport);
52
-
53
- // Clear and silence the custom winstonLogger
54
- winstonLogger.clear();
55
- winstonLogger.add(silentTransport);
56
-
57
- // Capture and silence console methods
58
- const originalConsole = {
59
- debug: console.debug,
60
- error: console.error,
61
- info: console.info,
62
- // biome-ignore lint/suspicious/noConsole: We keep the original reference.
63
- log: console.log,
64
- warn: console.warn,
65
- };
66
-
67
- const captureConsoleMethod = (method: keyof typeof originalConsole): void => {
68
- (console as any)[method] = (...args: any[]) => {
69
- const logMessage = `[console.${method}] ${args.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : String(arg))).join(" ")}`;
70
- logs.push(logMessage);
71
- if (SHOW_ALL_LOGS) {
72
- originalConsole[method](...args);
73
- }
74
- };
75
- };
76
-
77
- captureConsoleMethod("log");
78
- captureConsoleMethod("info");
79
- captureConsoleMethod("warn");
80
- captureConsoleMethod("error");
81
- captureConsoleMethod("debug");
82
-
83
- // Setup before each test
84
- beforeEach(() => {
85
- process.env.TOKEN_SECRET = "secret";
86
- process.env.TOKEN_ISSUER = "terreno-api.test";
87
- process.env.SESSION_SECRET = "sessionSecret";
88
- process.env.REFRESH_TOKEN_SECRET = "refreshTokenSecret";
89
- setupEnvironment();
90
- // Re-silence loggers after setupEnvironment which may reconfigure them
91
- winston.clear();
92
- winston.add(silentTransport);
93
- winstonLogger.clear();
94
- winstonLogger.add(silentTransport);
95
- logs = [];
96
- });
97
-
98
- // Clear logs after each test
99
- afterEach(() => {
100
- logs = [];
101
- });
102
-
103
- // Mock @sentry/bun module
104
- mock.module("@sentry/bun", () => {
105
- const mockFn = (): ReturnType<typeof mock> => mock(() => {});
106
-
107
- // Mock Scope
108
- const mockScope = {
109
- addBreadcrumb: mockFn(),
110
- clear: mockFn(),
111
- getSpan: mockFn(),
112
- setContext: mockFn(),
113
- setFingerprint: mockFn(),
114
- setLevel: mockFn(),
115
- setSpan: mockFn(),
116
- setTag: mockFn(),
117
- setTags: mockFn(),
118
- setTransactionName: mockFn(),
119
- setUser: mockFn(),
120
- };
121
-
122
- // Mock Hub
123
- const mockClient = {
124
- captureException: mockFn(),
125
- captureMessage: mockFn(),
126
- close: mock(() => Promise.resolve(true)),
127
- flush: mock(() => Promise.resolve(true)),
128
- getOptions: mock(() => ({})),
129
- };
130
-
131
- const mockHub = {
132
- addBreadcrumb: mockFn(),
133
- captureException: mockFn(),
134
- captureMessage: mockFn(),
135
- configureScope: mockFn(),
136
- getClient: mock(() => mockClient),
137
- getScope: mock(() => mockScope),
138
- popScope: mockFn(),
139
- pushScope: mockFn(),
140
- setContext: mockFn(),
141
- setTag: mockFn(),
142
- setTags: mockFn(),
143
- setUser: mockFn(),
144
- withScope: mockFn(),
145
- };
146
-
147
- const mockSpan: any = {
148
- finish: mockFn(),
149
- setData: mockFn(),
150
- setStatus: mockFn(),
151
- setTag: mockFn(),
152
- startChild: mockFn(),
153
- toTraceparent: mock(() => "mock-trace-parent"),
154
- };
155
- mockSpan.startChild = mock(() => mockSpan);
156
-
157
- const mockTransaction = {
158
- finish: mockFn(),
159
- setData: mockFn(),
160
- setName: mockFn(),
161
- setStatus: mockFn(),
162
- setTag: mockFn(),
163
- startChild: mock(() => mockSpan),
164
- toTraceparent: mock(() => "mock-trace-parent"),
165
- };
166
-
167
- return {
168
- addBreadcrumb: mockFn(),
169
- captureException: mockFn(),
170
- captureMessage: mockFn(),
171
- clearScope: mockFn(),
172
- close: mock(() => Promise.resolve(true)),
173
- configureScope: mockFn(),
174
- default: {
175
- addBreadcrumb: mockFn(),
176
- captureException: mockFn(),
177
- captureMessage: mockFn(),
178
- clearScope: mockFn(),
179
- close: mock(() => Promise.resolve(true)),
180
- configureScope: mockFn(),
181
- flush: mock(() => Promise.resolve(true)),
182
- getClient: mock(() => mockClient),
183
- getCurrentHub: mock(() => mockHub),
184
- getCurrentScope: mock(() => mockScope),
185
- Handlers: {
186
- errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
187
- requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
188
- tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
189
- },
190
- init: mockFn(),
191
- isInitialized: mock(() => true),
192
- popScope: mockFn(),
193
- pushScope: mockFn(),
194
- Severity: {
195
- Debug: "debug",
196
- Error: "error",
197
- Fatal: "fatal",
198
- Info: "info",
199
- Warning: "warning",
200
- } as const,
201
- setContext: mockFn(),
202
- setFingerprint: mockFn(),
203
- setLevel: mockFn(),
204
- setTag: mockFn(),
205
- setTags: mockFn(),
206
- setUser: mockFn(),
207
- setupExpressErrorHandler: mockFn(),
208
- startTransaction: mock(() => mockTransaction),
209
- withScope: mock((callback: any) => callback(mockScope)),
210
- },
211
- flush: mock(() => Promise.resolve(true)),
212
- getClient: mock(() => mockClient),
213
- getCurrentHub: mock(() => mockHub),
214
- getCurrentScope: mock(() => mockScope),
215
- Handlers: {
216
- errorHandler: mock(() => (err: any, _req: any, _res: any, next: any) => next(err)),
217
- requestHandler: mock(() => (_req: any, _res: any, next: any) => next()),
218
- tracingHandler: mock(() => (_req: any, _res: any, next: any) => next()),
219
- },
220
- init: mockFn(),
221
- isInitialized: mock(() => true),
222
- popScope: mockFn(),
223
- pushScope: mockFn(),
224
- Severity: {
225
- Debug: "debug",
226
- Error: "error",
227
- Fatal: "fatal",
228
- Info: "info",
229
- Warning: "warning",
230
- } as const,
231
- setContext: mockFn(),
232
- setFingerprint: mockFn(),
233
- setLevel: mockFn(),
234
- setTag: mockFn(),
235
- setTags: mockFn(),
236
- setUser: mockFn(),
237
- setupExpressErrorHandler: mockFn(),
238
- startTransaction: mock(() => mockTransaction),
239
- withScope: mock((callback: any) => callback(mockScope)),
240
- };
241
- });
@@ -0,0 +1,176 @@
1
+ import type {HydratedDocument} from "mongoose";
2
+ import type {PassportLocalMongooseDocument} from "passport-local-mongoose";
3
+
4
+ import {logger} from "../logger";
5
+ import {FoodModel, RequiredModel, type User, UserModel} from "./models";
6
+ import type {CachedTestData, TestData, TestFoods, TestRequired, TestUsers} from "./types";
7
+
8
+ const setPassword = async (user: HydratedDocument<User>, password: string): Promise<void> => {
9
+ await (user as unknown as PassportLocalMongooseDocument).setPassword(password);
10
+ await user.save();
11
+ };
12
+
13
+ export const clearTestCollections = async (): Promise<void> => {
14
+ await Promise.all([
15
+ UserModel.deleteMany({}),
16
+ FoodModel.deleteMany({}),
17
+ RequiredModel.deleteMany({}),
18
+ ]).catch(logger.catch);
19
+ };
20
+
21
+ export const createTestUsers = async (): Promise<TestUsers> => {
22
+ const [notAdmin, admin, adminOther] = await Promise.all([
23
+ UserModel.create({email: "notAdmin@example.com", name: "Not Admin"}),
24
+ UserModel.create({admin: true, email: "admin@example.com", name: "Admin"}),
25
+ UserModel.create({admin: true, email: "admin+other@example.com", name: "Admin Other"}),
26
+ ]);
27
+
28
+ await Promise.all([
29
+ setPassword(notAdmin, "password"),
30
+ setPassword(admin, "securePassword"),
31
+ setPassword(adminOther, "otherPassword"),
32
+ ]);
33
+
34
+ return {admin, adminOther, notAdmin};
35
+ };
36
+
37
+ export const createStandardFoods = async (users: TestUsers): Promise<TestFoods> => {
38
+ const {admin, adminOther, notAdmin} = users;
39
+
40
+ const [spinach, apple, carrots, pizza] = await Promise.all([
41
+ FoodModel.create({
42
+ calories: 1,
43
+ categories: [{name: "Vegetables", show: true}],
44
+ created: new Date("2021-12-03T00:00:20.000Z"),
45
+ eatenBy: [admin._id],
46
+ expiration: "2026-12-31",
47
+ hidden: false,
48
+ lastEatenWith: {
49
+ dressing: new Date("2021-12-03T19:00:30.000Z"),
50
+ },
51
+ likesIds: [
52
+ {likes: true, userId: admin._id},
53
+ {likes: false, userId: notAdmin._id},
54
+ ],
55
+ name: "Spinach",
56
+ ownerId: notAdmin._id,
57
+ source: {
58
+ dateAdded: "2023-12-13T12:30:00.000Z",
59
+ href: "https://www.example.com/spinach",
60
+ name: "Brand",
61
+ },
62
+ tags: ["healthy"],
63
+ }),
64
+ FoodModel.create({
65
+ calories: 100,
66
+ created: new Date("2021-12-03T00:00:30.000Z"),
67
+ expiration: "2026-12-31",
68
+ hidden: true,
69
+ likesIds: [{likes: true, userId: admin._id}],
70
+ name: "Apple",
71
+ ownerId: admin._id,
72
+ source: {name: "Orchard"},
73
+ tags: ["healthy"],
74
+ }),
75
+ FoodModel.create({
76
+ calories: 100,
77
+ created: new Date("2021-12-03T00:00:00.000Z"),
78
+ eatenBy: [admin._id, notAdmin._id],
79
+ expiration: "2026-12-31",
80
+ hidden: false,
81
+ likesIds: [{likes: false, userId: notAdmin._id}],
82
+ name: "Carrots",
83
+ ownerId: admin._id,
84
+ source: {name: "Farm"},
85
+ tags: ["vegetable"],
86
+ }),
87
+ FoodModel.create({
88
+ calories: 800,
89
+ created: new Date("2022-01-01T00:00:00.000Z"),
90
+ expiration: "2026-12-31",
91
+ hidden: false,
92
+ likesIds: [{likes: true, userId: adminOther._id}],
93
+ name: "Pizza",
94
+ ownerId: adminOther._id,
95
+ source: {name: "Pizzeria"},
96
+ tags: ["comfort"],
97
+ }),
98
+ ]);
99
+
100
+ return {apple, carrots, pizza, spinach};
101
+ };
102
+
103
+ export const createRequiredFixtures = async (): Promise<TestRequired> => {
104
+ const [sample, withAbout] = await Promise.all([
105
+ RequiredModel.create({name: "Sample Required"}),
106
+ RequiredModel.create({about: "Optional about text", name: "Required With About"}),
107
+ ]);
108
+
109
+ return {sample, withAbout};
110
+ };
111
+
112
+ /** Builds the standard Terreno API test database (users, foods, required docs). */
113
+ export const createTestData = async (): Promise<TestData> => {
114
+ await clearTestCollections();
115
+
116
+ const users = await createTestUsers();
117
+ const [foods, required] = await Promise.all([
118
+ createStandardFoods(users),
119
+ createRequiredFixtures(),
120
+ ]);
121
+
122
+ return {foods, required, users};
123
+ };
124
+
125
+ export const toCachedTestData = (testData: TestData): CachedTestData => ({
126
+ foods: {
127
+ apple: testData.foods.apple.id,
128
+ carrots: testData.foods.carrots.id,
129
+ pizza: testData.foods.pizza.id,
130
+ spinach: testData.foods.spinach.id,
131
+ },
132
+ required: {
133
+ sample: testData.required.sample.id,
134
+ withAbout: testData.required.withAbout.id,
135
+ },
136
+ users: {
137
+ admin: testData.users.admin.id,
138
+ adminOther: testData.users.adminOther.id,
139
+ notAdmin: testData.users.notAdmin.id,
140
+ },
141
+ });
142
+
143
+ export const loadTestDataFromDocuments = async (cached: CachedTestData): Promise<TestData> => {
144
+ const [admin, notAdmin, adminOther, spinach, apple, carrots, pizza, sample, withAbout] =
145
+ await Promise.all([
146
+ UserModel.findById(cached.users.admin),
147
+ UserModel.findById(cached.users.notAdmin),
148
+ UserModel.findById(cached.users.adminOther),
149
+ FoodModel.findById(cached.foods.spinach),
150
+ FoodModel.findById(cached.foods.apple),
151
+ FoodModel.findById(cached.foods.carrots),
152
+ FoodModel.findById(cached.foods.pizza),
153
+ RequiredModel.findById(cached.required.sample),
154
+ RequiredModel.findById(cached.required.withAbout),
155
+ ]);
156
+
157
+ if (
158
+ !admin ||
159
+ !notAdmin ||
160
+ !adminOther ||
161
+ !spinach ||
162
+ !apple ||
163
+ !carrots ||
164
+ !pizza ||
165
+ !sample ||
166
+ !withAbout
167
+ ) {
168
+ throw new Error("[createTestData] Cached test data references missing documents");
169
+ }
170
+
171
+ return {
172
+ foods: {apple, carrots, pizza, spinach},
173
+ required: {sample, withAbout},
174
+ users: {admin, adminOther, notAdmin},
175
+ };
176
+ };