@fragno-dev/test 0.1.12 → 0.1.13

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/src/index.ts CHANGED
@@ -1,22 +1,12 @@
1
1
  import type { AnySchema } from "@fragno-dev/db/schema";
2
- import {
3
- createFragmentForTest,
4
- type FragmentForTest,
5
- type CreateFragmentForTestOptions,
6
- } from "@fragno-dev/core/test";
7
- import type { FragnoPublicConfig } from "@fragno-dev/core/api/fragment-instantiation";
8
- import type { FragmentDefinition } from "@fragno-dev/core/api/fragment-builder";
9
- import type { AnyRouteOrFactory, FlattenRouteFactories } from "@fragno-dev/core/api/route";
10
- import {
11
- createAdapter,
12
- type SupportedAdapter,
13
- type AdapterContext,
14
- type KyselySqliteAdapter,
15
- type KyselyPgliteAdapter,
16
- type DrizzlePgliteAdapter,
17
- type SchemaConfig,
2
+ import type {
3
+ SupportedAdapter,
4
+ AdapterContext,
5
+ KyselySqliteAdapter,
6
+ KyselyPgliteAdapter,
7
+ DrizzlePgliteAdapter,
18
8
  } from "./adapters";
19
- import { withUnitOfWork, type IUnitOfWorkBase, type DatabaseAdapter } from "@fragno-dev/db";
9
+ import type { DatabaseAdapter } from "@fragno-dev/db";
20
10
  import type { AbstractQuery } from "@fragno-dev/db/query";
21
11
 
22
12
  // Re-export utilities from @fragno-dev/core/test
@@ -24,7 +14,6 @@ export {
24
14
  createFragmentForTest,
25
15
  type CreateFragmentForTestOptions,
26
16
  type RouteHandlerInputOptions,
27
- type FragmentForTest,
28
17
  } from "@fragno-dev/core/test";
29
18
 
30
19
  // Re-export adapter types
@@ -36,15 +25,15 @@ export type {
36
25
  AdapterContext,
37
26
  } from "./adapters";
38
27
 
28
+ // Re-export new builder-based database test utilities
29
+ export { buildDatabaseFragmentsTest, DatabaseFragmentsTestBuilder } from "./db-test";
30
+
39
31
  /**
40
32
  * Base test context with common functionality across all adapters
41
33
  */
42
34
  export interface BaseTestContext {
43
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
36
  readonly adapter: DatabaseAdapter<any>;
45
- createUnitOfWork: (name?: string) => IUnitOfWorkBase;
46
- withUnitOfWork: <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => Promise<T>;
47
- callService: <T>(fn: () => T | Promise<T>) => Promise<T>;
48
37
  resetDatabase: () => Promise<void>;
49
38
  cleanup: () => Promise<void>;
50
39
  }
@@ -54,9 +43,6 @@ export interface BaseTestContext {
54
43
  */
55
44
  export interface InternalTestContextMethods {
56
45
  getOrm: <TSchema extends AnySchema>(namespace: string) => AbstractQuery<TSchema>;
57
- createUnitOfWork: (name?: string) => IUnitOfWorkBase;
58
- withUnitOfWork: <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => Promise<T>;
59
- callService: <T>(fn: () => T | Promise<T>) => Promise<T>;
60
46
  }
61
47
 
62
48
  /**
@@ -75,42 +61,6 @@ export function createCommonTestContextMethods(
75
61
  }
76
62
  return orm as AbstractQuery<TSchema>;
77
63
  },
78
- createUnitOfWork: (name?: string) => {
79
- // Use the first schema's ORM to create a base UOW
80
- const firstOrm = ormMap.values().next().value;
81
- if (!firstOrm) {
82
- throw new Error("No ORMs available to create UnitOfWork");
83
- }
84
- return firstOrm.createUnitOfWork(name);
85
- },
86
- withUnitOfWork: async <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => {
87
- const firstOrm = ormMap.values().next().value;
88
- if (!firstOrm) {
89
- throw new Error("No ORMs available to create UnitOfWork");
90
- }
91
- const uow = firstOrm.createUnitOfWork();
92
- return withUnitOfWork(uow, async () => {
93
- return await fn(uow);
94
- });
95
- },
96
- callService: async <T>(fn: () => T | Promise<T>) => {
97
- const firstOrm = ormMap.values().next().value;
98
- if (!firstOrm) {
99
- throw new Error("No ORMs available to create UnitOfWork");
100
- }
101
- const uow = firstOrm.createUnitOfWork();
102
- return withUnitOfWork(uow, async () => {
103
- // Call the function to schedule operations (don't await yet)
104
- const resultPromise = fn();
105
-
106
- // Execute UOW phases
107
- await uow.executeRetrieve();
108
- await uow.executeMutations();
109
-
110
- // Now await the result
111
- return await resultPromise;
112
- });
113
- },
114
64
  };
115
65
  }
116
66
 
@@ -118,341 +68,3 @@ export function createCommonTestContextMethods(
118
68
  * Complete test context combining base and adapter-specific functionality
119
69
  */
120
70
  export type TestContext<T extends SupportedAdapter> = BaseTestContext & AdapterContext<T>;
121
-
122
- /**
123
- * Helper type to extract the schema from a fragment definition's additional context
124
- */
125
- type ExtractSchemaFromAdditionalContext<TAdditionalContext> = TAdditionalContext extends {
126
- databaseSchema?: infer TSchema extends AnySchema;
127
- }
128
- ? TSchema
129
- : AnySchema;
130
-
131
- /**
132
- * Fragment configuration for multi-fragment setup
133
- */
134
- export interface FragmentConfig<
135
- TDef extends {
136
- definition: FragmentDefinition<any, any, any, any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
137
- $requiredOptions: any; // eslint-disable-line @typescript-eslint/no-explicit-any
138
- } = {
139
- definition: FragmentDefinition<any, any, any, any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
140
- $requiredOptions: any; // eslint-disable-line @typescript-eslint/no-explicit-any
141
- },
142
- TRoutes extends readonly AnyRouteOrFactory[] = readonly AnyRouteOrFactory[],
143
- > {
144
- definition: TDef;
145
- routes: TRoutes;
146
- config?: TDef["definition"] extends FragmentDefinition<infer TConfig, any, any, any, any, any> // eslint-disable-line @typescript-eslint/no-explicit-any
147
- ? TConfig
148
- : never;
149
- migrateToVersion?: number;
150
- }
151
-
152
- /**
153
- * Options for creating multiple database fragments for testing
154
- */
155
- export interface MultiFragmentTestOptions<TAdapter extends SupportedAdapter> {
156
- adapter: TAdapter;
157
- }
158
-
159
- /**
160
- * Result type for a single fragment in a multi-fragment setup
161
- */
162
- type FragmentResultFromConfig<TConfig extends FragmentConfig> = TConfig["definition"] extends {
163
- definition: FragmentDefinition<
164
- infer TConf,
165
- infer TDeps,
166
- infer TServices,
167
- infer TAdditionalCtx,
168
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
169
- any,
170
- infer TProvidedServices
171
- >;
172
- $requiredOptions: infer TOptions extends FragnoPublicConfig;
173
- }
174
- ? {
175
- fragment: FragmentForTest<
176
- TConf,
177
- TDeps,
178
- TServices & TProvidedServices,
179
- TAdditionalCtx,
180
- TOptions,
181
- FlattenRouteFactories<TConfig["routes"]>
182
- >;
183
- services: TServices & TProvidedServices;
184
- callRoute: FragmentForTest<
185
- TConf,
186
- TDeps,
187
- TServices & TProvidedServices,
188
- TAdditionalCtx,
189
- TOptions,
190
- FlattenRouteFactories<TConfig["routes"]>
191
- >["callRoute"];
192
- config: TConf;
193
- deps: TDeps;
194
- additionalContext: TAdditionalCtx;
195
- db: AbstractQuery<ExtractSchemaFromAdditionalContext<TAdditionalCtx>>;
196
- }
197
- : never;
198
-
199
- export interface SingleFragmentTestResult<
200
- TFragment extends FragmentConfig,
201
- TAdapter extends SupportedAdapter,
202
- > {
203
- fragment: FragmentResultFromConfig<TFragment>;
204
- test: TestContext<TAdapter>;
205
- }
206
-
207
- /**
208
- * Result of creating multiple database fragments for testing (array input)
209
- */
210
- export interface MultiFragmentTestResult<
211
- TFragments extends readonly FragmentConfig[],
212
- TAdapter extends SupportedAdapter,
213
- > {
214
- fragments: {
215
- [K in keyof TFragments]: FragmentResultFromConfig<TFragments[K]>;
216
- };
217
- test: TestContext<TAdapter>;
218
- }
219
-
220
- /**
221
- * Result of creating multiple database fragments for testing (object input)
222
- */
223
- export interface NamedMultiFragmentTestResult<
224
- TFragments extends Record<string, FragmentConfig>,
225
- TAdapter extends SupportedAdapter,
226
- > {
227
- fragments: {
228
- [K in keyof TFragments]: FragmentResultFromConfig<TFragments[K]>;
229
- };
230
- test: TestContext<TAdapter>;
231
- }
232
-
233
- /**
234
- * Create multiple database fragments for testing with a shared adapter (array input)
235
- */
236
- export async function createDatabaseFragmentsForTest<
237
- const TFragments extends readonly FragmentConfig[],
238
- const TAdapter extends SupportedAdapter,
239
- >(
240
- fragments: TFragments,
241
- options: MultiFragmentTestOptions<TAdapter>,
242
- ): Promise<MultiFragmentTestResult<TFragments, TAdapter>>;
243
-
244
- /**
245
- * Create multiple database fragments for testing with a shared adapter (object input)
246
- */
247
- export async function createDatabaseFragmentsForTest<
248
- const TFragments extends Record<string, FragmentConfig>,
249
- const TAdapter extends SupportedAdapter,
250
- >(
251
- fragments: TFragments,
252
- options: MultiFragmentTestOptions<TAdapter>,
253
- ): Promise<NamedMultiFragmentTestResult<TFragments, TAdapter>>;
254
-
255
- /**
256
- * Implementation of createDatabaseFragmentsForTest
257
- */
258
- export async function createDatabaseFragmentsForTest<
259
- const TFragments extends readonly FragmentConfig[] | Record<string, FragmentConfig>,
260
- const TAdapter extends SupportedAdapter,
261
- >(
262
- fragments: TFragments,
263
- options: MultiFragmentTestOptions<TAdapter>,
264
- ): Promise<
265
- | MultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any
266
- | NamedMultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any
267
- > {
268
- const { adapter: adapterConfig } = options;
269
-
270
- // Convert to array for processing
271
- const isArray = Array.isArray(fragments);
272
- const fragmentsArray: FragmentConfig[] = isArray
273
- ? fragments
274
- : Object.values(fragments as Record<string, FragmentConfig>);
275
-
276
- // Extract schemas from all fragments
277
- const schemaConfigs: SchemaConfig[] = fragmentsArray.map((fragmentConfig) => {
278
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
- const fragmentAdditionalContext = fragmentConfig.definition.definition.additionalContext as any;
280
- const schema = fragmentAdditionalContext?.databaseSchema as AnySchema | undefined;
281
- const namespace =
282
- (fragmentAdditionalContext?.databaseNamespace as string | undefined) ??
283
- fragmentConfig.definition.definition.name + "-db";
284
-
285
- if (!schema) {
286
- throw new Error(
287
- `Fragment '${fragmentConfig.definition.definition.name}' does not have a database schema. ` +
288
- `Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`,
289
- );
290
- }
291
-
292
- return {
293
- schema,
294
- namespace,
295
- migrateToVersion: fragmentConfig.migrateToVersion,
296
- };
297
- });
298
-
299
- // Create adapter with all schemas
300
- const { testContext, adapter } = await createAdapter(adapterConfig, schemaConfigs);
301
-
302
- // Helper to create fragments with service wiring
303
- const createFragments = () => {
304
- // First pass: create fragments without dependencies to extract their provided services
305
- // Map from service name to { service: wrapped service, orm: ORM for that service's schema }
306
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
307
- const providedServicesByName: Record<string, { service: any; orm: any }> = {};
308
-
309
- for (let i = 0; i < fragmentsArray.length; i++) {
310
- const fragmentConfig = fragmentsArray[i]!;
311
- const namespace = schemaConfigs[i]!.namespace;
312
- const orm = testContext.getOrm(namespace);
313
-
314
- // Create fragment without interface implementations to extract its services
315
- const mergedOptions = {
316
- databaseAdapter: adapter,
317
- };
318
-
319
- const tempFragment = createFragmentForTest(fragmentConfig.definition, [], {
320
- config: fragmentConfig.config ?? {},
321
- options: mergedOptions,
322
- interfaceImplementations: {},
323
- });
324
-
325
- // Extract provided services from the created fragment
326
- // Check which services this fragment provides
327
- const providedServicesMetadata = fragmentConfig.definition.definition.providedServices;
328
- if (providedServicesMetadata) {
329
- // If providedServices is a function, it returns all services
330
- // If it's an object, the keys are the service names
331
- const serviceNames =
332
- typeof providedServicesMetadata === "function"
333
- ? Object.keys(tempFragment.services)
334
- : Object.keys(providedServicesMetadata);
335
-
336
- for (const serviceName of serviceNames) {
337
- if (tempFragment.services[serviceName]) {
338
- providedServicesByName[serviceName] = {
339
- service: tempFragment.services[serviceName],
340
- orm,
341
- };
342
- }
343
- }
344
- }
345
- }
346
-
347
- // Second pass: create fragments with service dependencies wired up
348
- return fragmentsArray.map((fragmentConfig, index) => {
349
- const namespace = schemaConfigs[index]!.namespace;
350
-
351
- // Get ORM for this fragment's namespace
352
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
- const schema = schemaConfigs[index]!.schema as any;
354
- const orm = testContext.getOrm(namespace);
355
-
356
- // Create fragment with database adapter in options
357
- const mergedOptions = {
358
- databaseAdapter: adapter,
359
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
- } as any;
361
-
362
- // Build interface implementations for services this fragment uses
363
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
364
- const interfaceImplementations: Record<string, any> = {};
365
- const usedServices = fragmentConfig.definition.definition.usedServices;
366
-
367
- if (usedServices) {
368
- for (const serviceName of Object.keys(usedServices)) {
369
- if (providedServicesByName[serviceName]) {
370
- // Use the wrapped service
371
- interfaceImplementations[serviceName] = providedServicesByName[serviceName]!.service;
372
- }
373
- }
374
- }
375
-
376
- const fragment = createFragmentForTest(fragmentConfig.definition, fragmentConfig.routes, {
377
- config: (fragmentConfig.config ?? {}) as any, // eslint-disable-line @typescript-eslint/no-explicit-any
378
- options: mergedOptions,
379
- interfaceImplementations,
380
- });
381
-
382
- // Return fragment services without wrapping - users manage UOW lifecycle explicitly
383
- return {
384
- fragment,
385
- services: fragment.services,
386
- callRoute: fragment.callRoute,
387
- config: fragment.config,
388
- deps: fragment.deps,
389
- additionalContext: fragment.additionalContext,
390
- get db() {
391
- return orm;
392
- },
393
- _orm: orm,
394
- _schema: schema,
395
- };
396
- });
397
- };
398
-
399
- const fragmentResults = createFragments();
400
-
401
- // Wrap resetDatabase to also recreate all fragments
402
- const originalResetDatabase = testContext.resetDatabase;
403
- const resetDatabase = async () => {
404
- await originalResetDatabase();
405
-
406
- // Recreate all fragments with service wiring
407
- const newFragmentResults = createFragments();
408
-
409
- // Update the result objects
410
- newFragmentResults.forEach((newResult, index) => {
411
- const result = fragmentResults[index]!;
412
- result.fragment = newResult.fragment;
413
- result.services = newResult.services;
414
- result.callRoute = newResult.callRoute;
415
- result.config = newResult.config;
416
- result.deps = newResult.deps;
417
- result.additionalContext = newResult.additionalContext;
418
- result._orm = newResult._orm;
419
- });
420
- };
421
-
422
- const finalTestContext = {
423
- ...testContext,
424
- resetDatabase,
425
- };
426
-
427
- // Return in the same structure as input
428
- if (isArray) {
429
- return {
430
- fragments: fragmentResults as any, // eslint-disable-line @typescript-eslint/no-explicit-any
431
- test: finalTestContext,
432
- };
433
- } else {
434
- const keys = Object.keys(fragments as Record<string, FragmentConfig>);
435
- const fragmentsObject = Object.fromEntries(
436
- keys.map((key, index) => [key, fragmentResults[index]]),
437
- );
438
- return {
439
- fragments: fragmentsObject as any, // eslint-disable-line @typescript-eslint/no-explicit-any
440
- test: finalTestContext,
441
- };
442
- }
443
- }
444
-
445
- export async function createDatabaseFragmentForTest<
446
- const TFragment extends FragmentConfig,
447
- const TAdapter extends SupportedAdapter,
448
- >(
449
- fragment: TFragment,
450
- options: MultiFragmentTestOptions<TAdapter>,
451
- ): Promise<SingleFragmentTestResult<TFragment, TAdapter>> {
452
- const result = await createDatabaseFragmentsForTest([fragment], options);
453
-
454
- return {
455
- fragment: result.fragments[0]!,
456
- test: result.test,
457
- };
458
- }