@fragno-dev/test 0.1.11 → 0.1.12
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +8 -0
- package/dist/adapters.d.ts +3 -11
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +65 -50
- package/dist/adapters.js.map +1 -1
- package/dist/index.d.ts +95 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +140 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters.ts +137 -118
- package/src/index.test.ts +276 -535
- package/src/index.ts +400 -214
package/src/index.ts
CHANGED
|
@@ -10,14 +10,14 @@ import type { AnyRouteOrFactory, FlattenRouteFactories } from "@fragno-dev/core/
|
|
|
10
10
|
import {
|
|
11
11
|
createAdapter,
|
|
12
12
|
type SupportedAdapter,
|
|
13
|
-
type
|
|
13
|
+
type AdapterContext,
|
|
14
14
|
type KyselySqliteAdapter,
|
|
15
15
|
type KyselyPgliteAdapter,
|
|
16
16
|
type DrizzlePgliteAdapter,
|
|
17
|
+
type SchemaConfig,
|
|
17
18
|
} from "./adapters";
|
|
18
|
-
import type
|
|
19
|
-
import type {
|
|
20
|
-
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
19
|
+
import { withUnitOfWork, type IUnitOfWorkBase, type DatabaseAdapter } from "@fragno-dev/db";
|
|
20
|
+
import type { AbstractQuery } from "@fragno-dev/db/query";
|
|
21
21
|
|
|
22
22
|
// Re-export utilities from @fragno-dev/core/test
|
|
23
23
|
export {
|
|
@@ -33,240 +33,426 @@ export type {
|
|
|
33
33
|
KyselySqliteAdapter,
|
|
34
34
|
KyselyPgliteAdapter,
|
|
35
35
|
DrizzlePgliteAdapter,
|
|
36
|
-
|
|
36
|
+
AdapterContext,
|
|
37
37
|
} from "./adapters";
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Base test context with common functionality across all adapters
|
|
41
41
|
*/
|
|
42
|
-
export interface
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
export interface BaseTestContext {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
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
|
+
resetDatabase: () => Promise<void>;
|
|
49
|
+
cleanup: () => Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Internal interface with getOrm for adapter implementations
|
|
54
|
+
*/
|
|
55
|
+
export interface InternalTestContextMethods {
|
|
56
|
+
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
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Helper to create common test context methods from an ORM map
|
|
64
|
+
* This is used internally by adapter implementations to avoid code duplication
|
|
65
|
+
*/
|
|
66
|
+
export function createCommonTestContextMethods(
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
ormMap: Map<string, AbstractQuery<any>>,
|
|
69
|
+
): InternalTestContextMethods {
|
|
70
|
+
return {
|
|
71
|
+
getOrm: <TSchema extends AnySchema>(namespace: string) => {
|
|
72
|
+
const orm = ormMap.get(namespace);
|
|
73
|
+
if (!orm) {
|
|
74
|
+
throw new Error(`No ORM found for namespace: ${namespace}`);
|
|
75
|
+
}
|
|
76
|
+
return orm as AbstractQuery<TSchema>;
|
|
77
|
+
},
|
|
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
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Complete test context combining base and adapter-specific functionality
|
|
119
|
+
*/
|
|
120
|
+
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;
|
|
54
149
|
migrateToVersion?: number;
|
|
55
|
-
config?: TConfig;
|
|
56
150
|
}
|
|
57
151
|
|
|
58
152
|
/**
|
|
59
|
-
*
|
|
60
|
-
|
|
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
|
|
61
161
|
*/
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,
|
|
68
201
|
TAdapter extends SupportedAdapter,
|
|
69
|
-
TRoutes extends readonly FragnoRouteConfig<
|
|
70
|
-
HTTPMethod,
|
|
71
|
-
string,
|
|
72
|
-
StandardSchemaV1 | undefined,
|
|
73
|
-
StandardSchemaV1 | undefined,
|
|
74
|
-
string,
|
|
75
|
-
string
|
|
76
|
-
>[],
|
|
77
202
|
> {
|
|
78
|
-
|
|
79
|
-
TConfig,
|
|
80
|
-
TDeps,
|
|
81
|
-
TServices,
|
|
82
|
-
TAdditionalContext,
|
|
83
|
-
TOptions,
|
|
84
|
-
TRoutes
|
|
85
|
-
>;
|
|
86
|
-
readonly services: TServices;
|
|
87
|
-
readonly callRoute: FragmentForTest<
|
|
88
|
-
TConfig,
|
|
89
|
-
TDeps,
|
|
90
|
-
TServices,
|
|
91
|
-
TAdditionalContext,
|
|
92
|
-
TOptions,
|
|
93
|
-
TRoutes
|
|
94
|
-
>["callRoute"];
|
|
95
|
-
readonly config: TConfig;
|
|
96
|
-
readonly deps: TDeps;
|
|
97
|
-
readonly additionalContext: TAdditionalContext;
|
|
203
|
+
fragment: FragmentResultFromConfig<TFragment>;
|
|
98
204
|
test: TestContext<TAdapter>;
|
|
99
205
|
}
|
|
100
206
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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[],
|
|
108
238
|
const TAdapter extends SupportedAdapter,
|
|
109
|
-
const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
|
|
110
239
|
>(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
>,
|
|
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>,
|
|
124
264
|
): Promise<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
TDeps,
|
|
128
|
-
TServices,
|
|
129
|
-
TAdditionalContext,
|
|
130
|
-
TOptions,
|
|
131
|
-
TAdapter,
|
|
132
|
-
FlattenRouteFactories<TRoutesOrFactories>
|
|
133
|
-
>
|
|
265
|
+
| MultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
266
|
+
| NamedMultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
134
267
|
> {
|
|
135
|
-
const {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!schema) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`Fragment '${fragmentBuilder.definition.name}' does not have a database schema. ` +
|
|
155
|
-
`Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`,
|
|
156
|
-
);
|
|
157
|
-
}
|
|
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";
|
|
158
284
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
...fragmentOptions,
|
|
172
|
-
databaseAdapter: adapter,
|
|
173
|
-
} as unknown as TOptions;
|
|
174
|
-
|
|
175
|
-
let fragment = createFragmentForTest(fragmentBuilder, routesOrFactories, {
|
|
176
|
-
// Safe cast: If config is not provided, we pass undefined as TConfig.
|
|
177
|
-
// The base createFragmentForTest expects config: TConfig, but if TConfig allows undefined
|
|
178
|
-
// or if the fragment doesn't use config in its dependencies function, this will work correctly.
|
|
179
|
-
config: config as TConfig,
|
|
180
|
-
options: mergedOptions,
|
|
181
|
-
deps,
|
|
182
|
-
services,
|
|
183
|
-
additionalContext,
|
|
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
|
+
};
|
|
184
297
|
});
|
|
185
298
|
|
|
186
|
-
//
|
|
187
|
-
const
|
|
299
|
+
// Create adapter with all schemas
|
|
300
|
+
const { testContext, adapter } = await createAdapter(adapterConfig, schemaConfigs);
|
|
188
301
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
210
359
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
}
|
|
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);
|
|
249
453
|
|
|
250
|
-
// Return an object with getters for fragment properties so they always reference the current fragment
|
|
251
454
|
return {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
get services() {
|
|
256
|
-
return fragment.services;
|
|
257
|
-
},
|
|
258
|
-
get callRoute() {
|
|
259
|
-
return fragment.callRoute;
|
|
260
|
-
},
|
|
261
|
-
get config() {
|
|
262
|
-
return fragment.config;
|
|
263
|
-
},
|
|
264
|
-
get deps() {
|
|
265
|
-
return fragment.deps;
|
|
266
|
-
},
|
|
267
|
-
get additionalContext() {
|
|
268
|
-
return fragment.additionalContext;
|
|
269
|
-
},
|
|
270
|
-
test: testContext,
|
|
455
|
+
fragment: result.fragments[0]!,
|
|
456
|
+
test: result.test,
|
|
271
457
|
};
|
|
272
458
|
}
|