@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/.turbo/turbo-build.log +11 -7
- package/CHANGELOG.md +27 -0
- package/dist/db-test.d.ts +129 -0
- package/dist/db-test.d.ts.map +1 -0
- package/dist/db-test.js +214 -0
- package/dist/db-test.js.map +1 -0
- package/dist/index.d.ts +5 -82
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -143
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
- package/src/db-test.test.ts +352 -0
- package/src/db-test.ts +574 -0
- package/src/index.test.ts +94 -94
- package/src/index.ts +10 -398
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createFragmentForTest
|
|
3
|
-
import { withUnitOfWork } from "@fragno-dev/db";
|
|
1
|
+
import { DatabaseFragmentsTestBuilder, buildDatabaseFragmentsTest } from "./db-test.js";
|
|
2
|
+
import { createFragmentForTest } from "@fragno-dev/core/test";
|
|
4
3
|
|
|
5
4
|
//#region src/index.ts
|
|
6
5
|
/**
|
|
@@ -8,147 +7,13 @@ import { withUnitOfWork } from "@fragno-dev/db";
|
|
|
8
7
|
* This is used internally by adapter implementations to avoid code duplication
|
|
9
8
|
*/
|
|
10
9
|
function createCommonTestContextMethods(ormMap) {
|
|
11
|
-
return {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
createUnitOfWork: (name) => {
|
|
18
|
-
const firstOrm = ormMap.values().next().value;
|
|
19
|
-
if (!firstOrm) throw new Error("No ORMs available to create UnitOfWork");
|
|
20
|
-
return firstOrm.createUnitOfWork(name);
|
|
21
|
-
},
|
|
22
|
-
withUnitOfWork: async (fn) => {
|
|
23
|
-
const firstOrm = ormMap.values().next().value;
|
|
24
|
-
if (!firstOrm) throw new Error("No ORMs available to create UnitOfWork");
|
|
25
|
-
const uow = firstOrm.createUnitOfWork();
|
|
26
|
-
return withUnitOfWork(uow, async () => {
|
|
27
|
-
return await fn(uow);
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
callService: async (fn) => {
|
|
31
|
-
const firstOrm = ormMap.values().next().value;
|
|
32
|
-
if (!firstOrm) throw new Error("No ORMs available to create UnitOfWork");
|
|
33
|
-
const uow = firstOrm.createUnitOfWork();
|
|
34
|
-
return withUnitOfWork(uow, async () => {
|
|
35
|
-
const resultPromise = fn();
|
|
36
|
-
await uow.executeRetrieve();
|
|
37
|
-
await uow.executeMutations();
|
|
38
|
-
return await resultPromise;
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Implementation of createDatabaseFragmentsForTest
|
|
45
|
-
*/
|
|
46
|
-
async function createDatabaseFragmentsForTest(fragments, options) {
|
|
47
|
-
const { adapter: adapterConfig } = options;
|
|
48
|
-
const isArray = Array.isArray(fragments);
|
|
49
|
-
const fragmentsArray = isArray ? fragments : Object.values(fragments);
|
|
50
|
-
const schemaConfigs = fragmentsArray.map((fragmentConfig) => {
|
|
51
|
-
const fragmentAdditionalContext = fragmentConfig.definition.definition.additionalContext;
|
|
52
|
-
const schema = fragmentAdditionalContext?.databaseSchema;
|
|
53
|
-
const namespace = fragmentAdditionalContext?.databaseNamespace ?? fragmentConfig.definition.definition.name + "-db";
|
|
54
|
-
if (!schema) throw new Error(`Fragment '${fragmentConfig.definition.definition.name}' does not have a database schema. Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`);
|
|
55
|
-
return {
|
|
56
|
-
schema,
|
|
57
|
-
namespace,
|
|
58
|
-
migrateToVersion: fragmentConfig.migrateToVersion
|
|
59
|
-
};
|
|
60
|
-
});
|
|
61
|
-
const { testContext, adapter } = await createAdapter(adapterConfig, schemaConfigs);
|
|
62
|
-
const createFragments = () => {
|
|
63
|
-
const providedServicesByName = {};
|
|
64
|
-
for (let i = 0; i < fragmentsArray.length; i++) {
|
|
65
|
-
const fragmentConfig = fragmentsArray[i];
|
|
66
|
-
const namespace = schemaConfigs[i].namespace;
|
|
67
|
-
const orm = testContext.getOrm(namespace);
|
|
68
|
-
const mergedOptions = { databaseAdapter: adapter };
|
|
69
|
-
const tempFragment = createFragmentForTest$1(fragmentConfig.definition, [], {
|
|
70
|
-
config: fragmentConfig.config ?? {},
|
|
71
|
-
options: mergedOptions,
|
|
72
|
-
interfaceImplementations: {}
|
|
73
|
-
});
|
|
74
|
-
const providedServicesMetadata = fragmentConfig.definition.definition.providedServices;
|
|
75
|
-
if (providedServicesMetadata) {
|
|
76
|
-
const serviceNames = typeof providedServicesMetadata === "function" ? Object.keys(tempFragment.services) : Object.keys(providedServicesMetadata);
|
|
77
|
-
for (const serviceName of serviceNames) if (tempFragment.services[serviceName]) providedServicesByName[serviceName] = {
|
|
78
|
-
service: tempFragment.services[serviceName],
|
|
79
|
-
orm
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return fragmentsArray.map((fragmentConfig, index) => {
|
|
84
|
-
const namespace = schemaConfigs[index].namespace;
|
|
85
|
-
const schema = schemaConfigs[index].schema;
|
|
86
|
-
const orm = testContext.getOrm(namespace);
|
|
87
|
-
const mergedOptions = { databaseAdapter: adapter };
|
|
88
|
-
const interfaceImplementations = {};
|
|
89
|
-
const usedServices = fragmentConfig.definition.definition.usedServices;
|
|
90
|
-
if (usedServices) {
|
|
91
|
-
for (const serviceName of Object.keys(usedServices)) if (providedServicesByName[serviceName]) interfaceImplementations[serviceName] = providedServicesByName[serviceName].service;
|
|
92
|
-
}
|
|
93
|
-
const fragment = createFragmentForTest$1(fragmentConfig.definition, fragmentConfig.routes, {
|
|
94
|
-
config: fragmentConfig.config ?? {},
|
|
95
|
-
options: mergedOptions,
|
|
96
|
-
interfaceImplementations
|
|
97
|
-
});
|
|
98
|
-
return {
|
|
99
|
-
fragment,
|
|
100
|
-
services: fragment.services,
|
|
101
|
-
callRoute: fragment.callRoute,
|
|
102
|
-
config: fragment.config,
|
|
103
|
-
deps: fragment.deps,
|
|
104
|
-
additionalContext: fragment.additionalContext,
|
|
105
|
-
get db() {
|
|
106
|
-
return orm;
|
|
107
|
-
},
|
|
108
|
-
_orm: orm,
|
|
109
|
-
_schema: schema
|
|
110
|
-
};
|
|
111
|
-
});
|
|
112
|
-
};
|
|
113
|
-
const fragmentResults = createFragments();
|
|
114
|
-
const originalResetDatabase = testContext.resetDatabase;
|
|
115
|
-
const resetDatabase = async () => {
|
|
116
|
-
await originalResetDatabase();
|
|
117
|
-
createFragments().forEach((newResult, index) => {
|
|
118
|
-
const result = fragmentResults[index];
|
|
119
|
-
result.fragment = newResult.fragment;
|
|
120
|
-
result.services = newResult.services;
|
|
121
|
-
result.callRoute = newResult.callRoute;
|
|
122
|
-
result.config = newResult.config;
|
|
123
|
-
result.deps = newResult.deps;
|
|
124
|
-
result.additionalContext = newResult.additionalContext;
|
|
125
|
-
result._orm = newResult._orm;
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
const finalTestContext = {
|
|
129
|
-
...testContext,
|
|
130
|
-
resetDatabase
|
|
131
|
-
};
|
|
132
|
-
if (isArray) return {
|
|
133
|
-
fragments: fragmentResults,
|
|
134
|
-
test: finalTestContext
|
|
135
|
-
};
|
|
136
|
-
else {
|
|
137
|
-
const keys = Object.keys(fragments);
|
|
138
|
-
return {
|
|
139
|
-
fragments: Object.fromEntries(keys.map((key, index) => [key, fragmentResults[index]])),
|
|
140
|
-
test: finalTestContext
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
async function createDatabaseFragmentForTest(fragment, options) {
|
|
145
|
-
const result = await createDatabaseFragmentsForTest([fragment], options);
|
|
146
|
-
return {
|
|
147
|
-
fragment: result.fragments[0],
|
|
148
|
-
test: result.test
|
|
149
|
-
};
|
|
10
|
+
return { getOrm: (namespace) => {
|
|
11
|
+
const orm = ormMap.get(namespace);
|
|
12
|
+
if (!orm) throw new Error(`No ORM found for namespace: ${namespace}`);
|
|
13
|
+
return orm;
|
|
14
|
+
} };
|
|
150
15
|
}
|
|
151
16
|
|
|
152
17
|
//#endregion
|
|
153
|
-
export {
|
|
18
|
+
export { DatabaseFragmentsTestBuilder, buildDatabaseFragmentsTest, createCommonTestContextMethods, createFragmentForTest };
|
|
154
19
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["fragmentsArray: FragmentConfig[]","schemaConfigs: SchemaConfig[]","providedServicesByName: Record<string, { service: any; orm: any }>","createFragmentForTest","interfaceImplementations: Record<string, any>"],"sources":["../src/index.ts"],"sourcesContent":["import type { AnySchema } from \"@fragno-dev/db/schema\";\nimport {\n createFragmentForTest,\n type FragmentForTest,\n type CreateFragmentForTestOptions,\n} from \"@fragno-dev/core/test\";\nimport type { FragnoPublicConfig } from \"@fragno-dev/core/api/fragment-instantiation\";\nimport type { FragmentDefinition } from \"@fragno-dev/core/api/fragment-builder\";\nimport type { AnyRouteOrFactory, FlattenRouteFactories } from \"@fragno-dev/core/api/route\";\nimport {\n createAdapter,\n type SupportedAdapter,\n type AdapterContext,\n type KyselySqliteAdapter,\n type KyselyPgliteAdapter,\n type DrizzlePgliteAdapter,\n type SchemaConfig,\n} from \"./adapters\";\nimport { withUnitOfWork, type IUnitOfWorkBase, type DatabaseAdapter } from \"@fragno-dev/db\";\nimport type { AbstractQuery } from \"@fragno-dev/db/query\";\n\n// Re-export utilities from @fragno-dev/core/test\nexport {\n createFragmentForTest,\n type CreateFragmentForTestOptions,\n type RouteHandlerInputOptions,\n type FragmentForTest,\n} from \"@fragno-dev/core/test\";\n\n// Re-export adapter types\nexport type {\n SupportedAdapter,\n KyselySqliteAdapter,\n KyselyPgliteAdapter,\n DrizzlePgliteAdapter,\n AdapterContext,\n} from \"./adapters\";\n\n/**\n * Base test context with common functionality across all adapters\n */\nexport interface BaseTestContext {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly adapter: DatabaseAdapter<any>;\n createUnitOfWork: (name?: string) => IUnitOfWorkBase;\n withUnitOfWork: <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => Promise<T>;\n callService: <T>(fn: () => T | Promise<T>) => Promise<T>;\n resetDatabase: () => Promise<void>;\n cleanup: () => Promise<void>;\n}\n\n/**\n * Internal interface with getOrm for adapter implementations\n */\nexport interface InternalTestContextMethods {\n getOrm: <TSchema extends AnySchema>(namespace: string) => AbstractQuery<TSchema>;\n createUnitOfWork: (name?: string) => IUnitOfWorkBase;\n withUnitOfWork: <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => Promise<T>;\n callService: <T>(fn: () => T | Promise<T>) => Promise<T>;\n}\n\n/**\n * Helper to create common test context methods from an ORM map\n * This is used internally by adapter implementations to avoid code duplication\n */\nexport function createCommonTestContextMethods(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ormMap: Map<string, AbstractQuery<any>>,\n): InternalTestContextMethods {\n return {\n getOrm: <TSchema extends AnySchema>(namespace: string) => {\n const orm = ormMap.get(namespace);\n if (!orm) {\n throw new Error(`No ORM found for namespace: ${namespace}`);\n }\n return orm as AbstractQuery<TSchema>;\n },\n createUnitOfWork: (name?: string) => {\n // Use the first schema's ORM to create a base UOW\n const firstOrm = ormMap.values().next().value;\n if (!firstOrm) {\n throw new Error(\"No ORMs available to create UnitOfWork\");\n }\n return firstOrm.createUnitOfWork(name);\n },\n withUnitOfWork: async <T>(fn: (uow: IUnitOfWorkBase) => Promise<T>) => {\n const firstOrm = ormMap.values().next().value;\n if (!firstOrm) {\n throw new Error(\"No ORMs available to create UnitOfWork\");\n }\n const uow = firstOrm.createUnitOfWork();\n return withUnitOfWork(uow, async () => {\n return await fn(uow);\n });\n },\n callService: async <T>(fn: () => T | Promise<T>) => {\n const firstOrm = ormMap.values().next().value;\n if (!firstOrm) {\n throw new Error(\"No ORMs available to create UnitOfWork\");\n }\n const uow = firstOrm.createUnitOfWork();\n return withUnitOfWork(uow, async () => {\n // Call the function to schedule operations (don't await yet)\n const resultPromise = fn();\n\n // Execute UOW phases\n await uow.executeRetrieve();\n await uow.executeMutations();\n\n // Now await the result\n return await resultPromise;\n });\n },\n };\n}\n\n/**\n * Complete test context combining base and adapter-specific functionality\n */\nexport type TestContext<T extends SupportedAdapter> = BaseTestContext & AdapterContext<T>;\n\n/**\n * Helper type to extract the schema from a fragment definition's additional context\n */\ntype ExtractSchemaFromAdditionalContext<TAdditionalContext> = TAdditionalContext extends {\n databaseSchema?: infer TSchema extends AnySchema;\n}\n ? TSchema\n : AnySchema;\n\n/**\n * Fragment configuration for multi-fragment setup\n */\nexport interface FragmentConfig<\n TDef extends {\n definition: FragmentDefinition<any, any, any, any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\n $requiredOptions: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n } = {\n definition: FragmentDefinition<any, any, any, any, any, any>; // eslint-disable-line @typescript-eslint/no-explicit-any\n $requiredOptions: any; // eslint-disable-line @typescript-eslint/no-explicit-any\n },\n TRoutes extends readonly AnyRouteOrFactory[] = readonly AnyRouteOrFactory[],\n> {\n definition: TDef;\n routes: TRoutes;\n config?: TDef[\"definition\"] extends FragmentDefinition<infer TConfig, any, any, any, any, any> // eslint-disable-line @typescript-eslint/no-explicit-any\n ? TConfig\n : never;\n migrateToVersion?: number;\n}\n\n/**\n * Options for creating multiple database fragments for testing\n */\nexport interface MultiFragmentTestOptions<TAdapter extends SupportedAdapter> {\n adapter: TAdapter;\n}\n\n/**\n * Result type for a single fragment in a multi-fragment setup\n */\ntype FragmentResultFromConfig<TConfig extends FragmentConfig> = TConfig[\"definition\"] extends {\n definition: FragmentDefinition<\n infer TConf,\n infer TDeps,\n infer TServices,\n infer TAdditionalCtx,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n any,\n infer TProvidedServices\n >;\n $requiredOptions: infer TOptions extends FragnoPublicConfig;\n}\n ? {\n fragment: FragmentForTest<\n TConf,\n TDeps,\n TServices & TProvidedServices,\n TAdditionalCtx,\n TOptions,\n FlattenRouteFactories<TConfig[\"routes\"]>\n >;\n services: TServices & TProvidedServices;\n callRoute: FragmentForTest<\n TConf,\n TDeps,\n TServices & TProvidedServices,\n TAdditionalCtx,\n TOptions,\n FlattenRouteFactories<TConfig[\"routes\"]>\n >[\"callRoute\"];\n config: TConf;\n deps: TDeps;\n additionalContext: TAdditionalCtx;\n db: AbstractQuery<ExtractSchemaFromAdditionalContext<TAdditionalCtx>>;\n }\n : never;\n\nexport interface SingleFragmentTestResult<\n TFragment extends FragmentConfig,\n TAdapter extends SupportedAdapter,\n> {\n fragment: FragmentResultFromConfig<TFragment>;\n test: TestContext<TAdapter>;\n}\n\n/**\n * Result of creating multiple database fragments for testing (array input)\n */\nexport interface MultiFragmentTestResult<\n TFragments extends readonly FragmentConfig[],\n TAdapter extends SupportedAdapter,\n> {\n fragments: {\n [K in keyof TFragments]: FragmentResultFromConfig<TFragments[K]>;\n };\n test: TestContext<TAdapter>;\n}\n\n/**\n * Result of creating multiple database fragments for testing (object input)\n */\nexport interface NamedMultiFragmentTestResult<\n TFragments extends Record<string, FragmentConfig>,\n TAdapter extends SupportedAdapter,\n> {\n fragments: {\n [K in keyof TFragments]: FragmentResultFromConfig<TFragments[K]>;\n };\n test: TestContext<TAdapter>;\n}\n\n/**\n * Create multiple database fragments for testing with a shared adapter (array input)\n */\nexport async function createDatabaseFragmentsForTest<\n const TFragments extends readonly FragmentConfig[],\n const TAdapter extends SupportedAdapter,\n>(\n fragments: TFragments,\n options: MultiFragmentTestOptions<TAdapter>,\n): Promise<MultiFragmentTestResult<TFragments, TAdapter>>;\n\n/**\n * Create multiple database fragments for testing with a shared adapter (object input)\n */\nexport async function createDatabaseFragmentsForTest<\n const TFragments extends Record<string, FragmentConfig>,\n const TAdapter extends SupportedAdapter,\n>(\n fragments: TFragments,\n options: MultiFragmentTestOptions<TAdapter>,\n): Promise<NamedMultiFragmentTestResult<TFragments, TAdapter>>;\n\n/**\n * Implementation of createDatabaseFragmentsForTest\n */\nexport async function createDatabaseFragmentsForTest<\n const TFragments extends readonly FragmentConfig[] | Record<string, FragmentConfig>,\n const TAdapter extends SupportedAdapter,\n>(\n fragments: TFragments,\n options: MultiFragmentTestOptions<TAdapter>,\n): Promise<\n | MultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any\n | NamedMultiFragmentTestResult<any, TAdapter> // eslint-disable-line @typescript-eslint/no-explicit-any\n> {\n const { adapter: adapterConfig } = options;\n\n // Convert to array for processing\n const isArray = Array.isArray(fragments);\n const fragmentsArray: FragmentConfig[] = isArray\n ? fragments\n : Object.values(fragments as Record<string, FragmentConfig>);\n\n // Extract schemas from all fragments\n const schemaConfigs: SchemaConfig[] = fragmentsArray.map((fragmentConfig) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const fragmentAdditionalContext = fragmentConfig.definition.definition.additionalContext as any;\n const schema = fragmentAdditionalContext?.databaseSchema as AnySchema | undefined;\n const namespace =\n (fragmentAdditionalContext?.databaseNamespace as string | undefined) ??\n fragmentConfig.definition.definition.name + \"-db\";\n\n if (!schema) {\n throw new Error(\n `Fragment '${fragmentConfig.definition.definition.name}' does not have a database schema. ` +\n `Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`,\n );\n }\n\n return {\n schema,\n namespace,\n migrateToVersion: fragmentConfig.migrateToVersion,\n };\n });\n\n // Create adapter with all schemas\n const { testContext, adapter } = await createAdapter(adapterConfig, schemaConfigs);\n\n // Helper to create fragments with service wiring\n const createFragments = () => {\n // First pass: create fragments without dependencies to extract their provided services\n // Map from service name to { service: wrapped service, orm: ORM for that service's schema }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const providedServicesByName: Record<string, { service: any; orm: any }> = {};\n\n for (let i = 0; i < fragmentsArray.length; i++) {\n const fragmentConfig = fragmentsArray[i]!;\n const namespace = schemaConfigs[i]!.namespace;\n const orm = testContext.getOrm(namespace);\n\n // Create fragment without interface implementations to extract its services\n const mergedOptions = {\n databaseAdapter: adapter,\n };\n\n const tempFragment = createFragmentForTest(fragmentConfig.definition, [], {\n config: fragmentConfig.config ?? {},\n options: mergedOptions,\n interfaceImplementations: {},\n });\n\n // Extract provided services from the created fragment\n // Check which services this fragment provides\n const providedServicesMetadata = fragmentConfig.definition.definition.providedServices;\n if (providedServicesMetadata) {\n // If providedServices is a function, it returns all services\n // If it's an object, the keys are the service names\n const serviceNames =\n typeof providedServicesMetadata === \"function\"\n ? Object.keys(tempFragment.services)\n : Object.keys(providedServicesMetadata);\n\n for (const serviceName of serviceNames) {\n if (tempFragment.services[serviceName]) {\n providedServicesByName[serviceName] = {\n service: tempFragment.services[serviceName],\n orm,\n };\n }\n }\n }\n }\n\n // Second pass: create fragments with service dependencies wired up\n return fragmentsArray.map((fragmentConfig, index) => {\n const namespace = schemaConfigs[index]!.namespace;\n\n // Get ORM for this fragment's namespace\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const schema = schemaConfigs[index]!.schema as any;\n const orm = testContext.getOrm(namespace);\n\n // Create fragment with database adapter in options\n const mergedOptions = {\n databaseAdapter: adapter,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } as any;\n\n // Build interface implementations for services this fragment uses\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const interfaceImplementations: Record<string, any> = {};\n const usedServices = fragmentConfig.definition.definition.usedServices;\n\n if (usedServices) {\n for (const serviceName of Object.keys(usedServices)) {\n if (providedServicesByName[serviceName]) {\n // Use the wrapped service\n interfaceImplementations[serviceName] = providedServicesByName[serviceName]!.service;\n }\n }\n }\n\n const fragment = createFragmentForTest(fragmentConfig.definition, fragmentConfig.routes, {\n config: (fragmentConfig.config ?? {}) as any, // eslint-disable-line @typescript-eslint/no-explicit-any\n options: mergedOptions,\n interfaceImplementations,\n });\n\n // Return fragment services without wrapping - users manage UOW lifecycle explicitly\n return {\n fragment,\n services: fragment.services,\n callRoute: fragment.callRoute,\n config: fragment.config,\n deps: fragment.deps,\n additionalContext: fragment.additionalContext,\n get db() {\n return orm;\n },\n _orm: orm,\n _schema: schema,\n };\n });\n };\n\n const fragmentResults = createFragments();\n\n // Wrap resetDatabase to also recreate all fragments\n const originalResetDatabase = testContext.resetDatabase;\n const resetDatabase = async () => {\n await originalResetDatabase();\n\n // Recreate all fragments with service wiring\n const newFragmentResults = createFragments();\n\n // Update the result objects\n newFragmentResults.forEach((newResult, index) => {\n const result = fragmentResults[index]!;\n result.fragment = newResult.fragment;\n result.services = newResult.services;\n result.callRoute = newResult.callRoute;\n result.config = newResult.config;\n result.deps = newResult.deps;\n result.additionalContext = newResult.additionalContext;\n result._orm = newResult._orm;\n });\n };\n\n const finalTestContext = {\n ...testContext,\n resetDatabase,\n };\n\n // Return in the same structure as input\n if (isArray) {\n return {\n fragments: fragmentResults as any, // eslint-disable-line @typescript-eslint/no-explicit-any\n test: finalTestContext,\n };\n } else {\n const keys = Object.keys(fragments as Record<string, FragmentConfig>);\n const fragmentsObject = Object.fromEntries(\n keys.map((key, index) => [key, fragmentResults[index]]),\n );\n return {\n fragments: fragmentsObject as any, // eslint-disable-line @typescript-eslint/no-explicit-any\n test: finalTestContext,\n };\n }\n}\n\nexport async function createDatabaseFragmentForTest<\n const TFragment extends FragmentConfig,\n const TAdapter extends SupportedAdapter,\n>(\n fragment: TFragment,\n options: MultiFragmentTestOptions<TAdapter>,\n): Promise<SingleFragmentTestResult<TFragment, TAdapter>> {\n const result = await createDatabaseFragmentsForTest([fragment], options);\n\n return {\n fragment: result.fragments[0]!,\n test: result.test,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { AnySchema } from \"@fragno-dev/db/schema\";\nimport type {\n SupportedAdapter,\n AdapterContext,\n KyselySqliteAdapter,\n KyselyPgliteAdapter,\n DrizzlePgliteAdapter,\n} from \"./adapters\";\nimport type { DatabaseAdapter } from \"@fragno-dev/db\";\nimport type { AbstractQuery } from \"@fragno-dev/db/query\";\n\n// Re-export utilities from @fragno-dev/core/test\nexport {\n createFragmentForTest,\n type CreateFragmentForTestOptions,\n type RouteHandlerInputOptions,\n} from \"@fragno-dev/core/test\";\n\n// Re-export adapter types\nexport type {\n SupportedAdapter,\n KyselySqliteAdapter,\n KyselyPgliteAdapter,\n DrizzlePgliteAdapter,\n AdapterContext,\n} from \"./adapters\";\n\n// Re-export new builder-based database test utilities\nexport { buildDatabaseFragmentsTest, DatabaseFragmentsTestBuilder } from \"./db-test\";\n\n/**\n * Base test context with common functionality across all adapters\n */\nexport interface BaseTestContext {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly adapter: DatabaseAdapter<any>;\n resetDatabase: () => Promise<void>;\n cleanup: () => Promise<void>;\n}\n\n/**\n * Internal interface with getOrm for adapter implementations\n */\nexport interface InternalTestContextMethods {\n getOrm: <TSchema extends AnySchema>(namespace: string) => AbstractQuery<TSchema>;\n}\n\n/**\n * Helper to create common test context methods from an ORM map\n * This is used internally by adapter implementations to avoid code duplication\n */\nexport function createCommonTestContextMethods(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n ormMap: Map<string, AbstractQuery<any>>,\n): InternalTestContextMethods {\n return {\n getOrm: <TSchema extends AnySchema>(namespace: string) => {\n const orm = ormMap.get(namespace);\n if (!orm) {\n throw new Error(`No ORM found for namespace: ${namespace}`);\n }\n return orm as AbstractQuery<TSchema>;\n },\n };\n}\n\n/**\n * Complete test context combining base and adapter-specific functionality\n */\nexport type TestContext<T extends SupportedAdapter> = BaseTestContext & AdapterContext<T>;\n"],"mappings":";;;;;;;;AAmDA,SAAgB,+BAEd,QAC4B;AAC5B,QAAO,EACL,SAAoC,cAAsB;EACxD,MAAM,MAAM,OAAO,IAAI,UAAU;AACjC,MAAI,CAAC,IACH,OAAM,IAAI,MAAM,+BAA+B,YAAY;AAE7D,SAAO;IAEV"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragno-dev/test",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@standard-schema/spec": "^1.0.0",
|
|
17
17
|
"kysely": "^0.28.7",
|
|
18
|
-
"sqlocal": "^0.15.2"
|
|
19
|
-
"@fragno-dev/core": "0.1.8",
|
|
20
|
-
"@fragno-dev/db": "0.1.14"
|
|
18
|
+
"sqlocal": "^0.15.2"
|
|
21
19
|
},
|
|
22
20
|
"peerDependencies": {
|
|
23
21
|
"@electric-sql/pglite": "^0.3.11",
|
|
24
22
|
"drizzle-kit": "^0.30.3",
|
|
25
23
|
"drizzle-orm": "^0.44.7",
|
|
26
|
-
"kysely-pglite": "^0.6.1"
|
|
24
|
+
"kysely-pglite": "^0.6.1",
|
|
25
|
+
"@fragno-dev/core": "0.1.9",
|
|
26
|
+
"@fragno-dev/db": "0.1.15"
|
|
27
27
|
},
|
|
28
28
|
"peerDependenciesMeta": {
|
|
29
29
|
"@electric-sql/pglite": {
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"kysely-pglite": "^0.6.1",
|
|
50
50
|
"vitest": "^3.2.4",
|
|
51
51
|
"zod": "^4.1.12",
|
|
52
|
+
"@fragno-dev/core": "0.1.9",
|
|
53
|
+
"@fragno-dev/db": "0.1.15",
|
|
52
54
|
"@fragno-private/typescript-config": "0.0.1",
|
|
53
55
|
"@fragno-private/vitest-config": "0.0.0"
|
|
54
56
|
},
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { assert, describe, expect, it } from "vitest";
|
|
2
|
+
import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
3
|
+
import { withDatabase } from "@fragno-dev/db";
|
|
4
|
+
import { defineFragment, instantiate } from "@fragno-dev/core";
|
|
5
|
+
import { defineRoute } from "@fragno-dev/core/route";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { buildDatabaseFragmentsTest } from "./db-test";
|
|
8
|
+
|
|
9
|
+
// Test schema with users table
|
|
10
|
+
const userSchema = schema((s) => {
|
|
11
|
+
return s.addTable("users", (t) => {
|
|
12
|
+
return t
|
|
13
|
+
.addColumn("id", idColumn())
|
|
14
|
+
.addColumn("name", column("string"))
|
|
15
|
+
.addColumn("email", column("string"))
|
|
16
|
+
.createIndex("idx_users_all", ["id"]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Test schema with posts table
|
|
21
|
+
const postSchema = schema((s) => {
|
|
22
|
+
return s.addTable("posts", (t) => {
|
|
23
|
+
return t
|
|
24
|
+
.addColumn("id", idColumn())
|
|
25
|
+
.addColumn("title", column("string"))
|
|
26
|
+
.addColumn("userId", column("string"))
|
|
27
|
+
.createIndex("idx_posts_all", ["id"]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("buildDatabaseFragmentsTest", () => {
|
|
32
|
+
it("should create multiple fragments with shared adapter", async () => {
|
|
33
|
+
// Define fragments using new API
|
|
34
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
35
|
+
.extend(withDatabase(userSchema))
|
|
36
|
+
.providesBaseService(({ deps }) => ({
|
|
37
|
+
createUser: async (data: { name: string; email: string }) => {
|
|
38
|
+
const id = await deps.db.create("users", data);
|
|
39
|
+
return { ...data, id: id.valueOf() };
|
|
40
|
+
},
|
|
41
|
+
getUsers: async () => {
|
|
42
|
+
const users = await deps.db.find("users", (b) =>
|
|
43
|
+
b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
|
|
44
|
+
);
|
|
45
|
+
return users.map((u) => ({ ...u, id: u.id.valueOf() }));
|
|
46
|
+
},
|
|
47
|
+
}))
|
|
48
|
+
.build();
|
|
49
|
+
|
|
50
|
+
const postFragmentDef = defineFragment<{}>("post-fragment")
|
|
51
|
+
.extend(withDatabase(postSchema))
|
|
52
|
+
.providesBaseService(({ deps }) => ({
|
|
53
|
+
createPost: async (data: { title: string; userId: string }) => {
|
|
54
|
+
const id = await deps.db.create("posts", data);
|
|
55
|
+
return { ...data, id: id.valueOf() };
|
|
56
|
+
},
|
|
57
|
+
getPosts: async () => {
|
|
58
|
+
const posts = await deps.db.find("posts", (b) =>
|
|
59
|
+
b.whereIndex("idx_posts_all", (eb) => eb("id", "!=", "")),
|
|
60
|
+
);
|
|
61
|
+
return posts.map((p) => ({ ...p, id: p.id.valueOf() }));
|
|
62
|
+
},
|
|
63
|
+
}))
|
|
64
|
+
.build();
|
|
65
|
+
|
|
66
|
+
// Build test setup with new builder API
|
|
67
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
68
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
69
|
+
.withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
|
|
70
|
+
.withFragment("post", instantiate(postFragmentDef).withConfig({}).withRoutes([]))
|
|
71
|
+
.build();
|
|
72
|
+
|
|
73
|
+
// Test user fragment
|
|
74
|
+
const user = await fragments.user.services.createUser({
|
|
75
|
+
name: "Test User",
|
|
76
|
+
email: "test@example.com",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(user).toMatchObject({
|
|
80
|
+
id: expect.any(String),
|
|
81
|
+
name: "Test User",
|
|
82
|
+
email: "test@example.com",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Test post fragment
|
|
86
|
+
const post = await fragments.post.services.createPost({
|
|
87
|
+
title: "Test Post",
|
|
88
|
+
userId: user.id,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(post).toMatchObject({
|
|
92
|
+
id: expect.any(String),
|
|
93
|
+
title: "Test Post",
|
|
94
|
+
userId: user.id,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Verify data exists
|
|
98
|
+
const users = await fragments.user.services.getUsers();
|
|
99
|
+
expect(users).toHaveLength(1);
|
|
100
|
+
|
|
101
|
+
const posts = await fragments.post.services.getPosts();
|
|
102
|
+
expect(posts).toHaveLength(1);
|
|
103
|
+
expect(posts[0]!.userId).toBe(user.id);
|
|
104
|
+
|
|
105
|
+
// Cleanup
|
|
106
|
+
await test.cleanup();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should reset database and recreate fragments", async () => {
|
|
110
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
111
|
+
.extend(withDatabase(userSchema))
|
|
112
|
+
.providesBaseService(({ deps }) => ({
|
|
113
|
+
createUser: async (data: { name: string; email: string }) => {
|
|
114
|
+
const id = await deps.db.create("users", data);
|
|
115
|
+
return { ...data, id: id.valueOf() };
|
|
116
|
+
},
|
|
117
|
+
getUsers: async () => {
|
|
118
|
+
const users = await deps.db.find("users", (b) =>
|
|
119
|
+
b.whereIndex("idx_users_all", (eb) => eb("id", "!=", "")),
|
|
120
|
+
);
|
|
121
|
+
return users.map((u) => ({ ...u, id: u.id.valueOf() }));
|
|
122
|
+
},
|
|
123
|
+
}))
|
|
124
|
+
.build();
|
|
125
|
+
|
|
126
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
127
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
128
|
+
.withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
|
|
129
|
+
.build();
|
|
130
|
+
|
|
131
|
+
// Create a user
|
|
132
|
+
await fragments.user.services.createUser({
|
|
133
|
+
name: "User 1",
|
|
134
|
+
email: "user1@example.com",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Verify user exists
|
|
138
|
+
let users = await fragments.user.services.getUsers();
|
|
139
|
+
expect(users).toHaveLength(1);
|
|
140
|
+
|
|
141
|
+
// Reset database
|
|
142
|
+
await test.resetDatabase();
|
|
143
|
+
|
|
144
|
+
// Verify database is empty
|
|
145
|
+
users = await fragments.user.services.getUsers();
|
|
146
|
+
expect(users).toHaveLength(0);
|
|
147
|
+
|
|
148
|
+
// Cleanup
|
|
149
|
+
await test.cleanup();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should expose db for direct queries", async () => {
|
|
153
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
154
|
+
.extend(withDatabase(userSchema))
|
|
155
|
+
.providesBaseService(() => ({}))
|
|
156
|
+
.build();
|
|
157
|
+
|
|
158
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
159
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
160
|
+
.withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
|
|
161
|
+
.build();
|
|
162
|
+
|
|
163
|
+
// Use db directly
|
|
164
|
+
const userId = await fragments.user.db.create("users", {
|
|
165
|
+
name: "Direct DB User",
|
|
166
|
+
email: "direct@example.com",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(userId).toBeDefined();
|
|
170
|
+
expect(typeof userId.valueOf()).toBe("string");
|
|
171
|
+
|
|
172
|
+
// Find using db
|
|
173
|
+
const users = await fragments.user.db.find("users", (b) =>
|
|
174
|
+
b.whereIndex("idx_users_all", (eb) => eb("id", "=", userId)),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(users).toHaveLength(1);
|
|
178
|
+
expect(users[0]).toMatchObject({
|
|
179
|
+
id: userId,
|
|
180
|
+
name: "Direct DB User",
|
|
181
|
+
email: "direct@example.com",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await test.cleanup();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("should expose deps and adapter", async () => {
|
|
188
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
189
|
+
.extend(withDatabase(userSchema))
|
|
190
|
+
.withDependencies(() => ({
|
|
191
|
+
testValue: "test-dependency",
|
|
192
|
+
}))
|
|
193
|
+
.providesBaseService(({ deps }) => ({
|
|
194
|
+
getTestValue: () => deps.testValue,
|
|
195
|
+
}))
|
|
196
|
+
.build();
|
|
197
|
+
|
|
198
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
199
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
200
|
+
.withFragment("user", instantiate(userFragmentDef).withConfig({}).withRoutes([]))
|
|
201
|
+
.build();
|
|
202
|
+
|
|
203
|
+
// Test that deps are accessible
|
|
204
|
+
expect(fragments.user.deps).toBeDefined();
|
|
205
|
+
expect(fragments.user.deps.testValue).toBe("test-dependency");
|
|
206
|
+
expect(fragments.user.deps.db).toBeDefined();
|
|
207
|
+
expect(fragments.user.deps.schema).toBeDefined();
|
|
208
|
+
|
|
209
|
+
// Test that adapter is accessible
|
|
210
|
+
expect(test.adapter).toBeDefined();
|
|
211
|
+
expect(test.adapter.createQueryEngine).toBeDefined();
|
|
212
|
+
|
|
213
|
+
await test.cleanup();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("should support callRoute with database operations", async () => {
|
|
217
|
+
// This is a simpler test that verifies callRoute exists and can be called.
|
|
218
|
+
// For now, we just verify that the method exists and is callable.
|
|
219
|
+
const userFragmentDef = defineFragment<{}>("user-fragment")
|
|
220
|
+
.extend(withDatabase(userSchema))
|
|
221
|
+
.providesBaseService(() => ({}))
|
|
222
|
+
.build();
|
|
223
|
+
|
|
224
|
+
const createUserRoute = defineRoute({
|
|
225
|
+
method: "POST",
|
|
226
|
+
path: "/users",
|
|
227
|
+
inputSchema: z.object({
|
|
228
|
+
name: z.string(),
|
|
229
|
+
email: z.string(),
|
|
230
|
+
}),
|
|
231
|
+
outputSchema: z.object({
|
|
232
|
+
id: z.string(),
|
|
233
|
+
name: z.string(),
|
|
234
|
+
email: z.string(),
|
|
235
|
+
}),
|
|
236
|
+
handler: async ({ input }, { json }) => {
|
|
237
|
+
const body = await input.valid();
|
|
238
|
+
return json({ ...body, id: "123" });
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
243
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
244
|
+
.withFragment(
|
|
245
|
+
"user",
|
|
246
|
+
instantiate(userFragmentDef).withConfig({}).withRoutes([createUserRoute]),
|
|
247
|
+
)
|
|
248
|
+
.build();
|
|
249
|
+
|
|
250
|
+
const response = await fragments.user.callRoute("POST", "/users", {
|
|
251
|
+
body: { name: "Test User", email: "test@example.com" },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
assert(response.type === "json");
|
|
255
|
+
expect(response.data).toMatchObject({
|
|
256
|
+
id: "123",
|
|
257
|
+
name: "Test User",
|
|
258
|
+
email: "test@example.com",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await test.cleanup();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should use actual config during schema extraction", async () => {
|
|
265
|
+
// Test that the builder uses the actual config provided via .withConfig()
|
|
266
|
+
// This is important for fragments like Stripe that need API keys to initialize dependencies
|
|
267
|
+
interface RequiredConfigFragmentConfig {
|
|
268
|
+
apiKey: string;
|
|
269
|
+
apiSecret: string;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const requiredConfigFragmentDef = defineFragment<RequiredConfigFragmentConfig>(
|
|
273
|
+
"required-config-fragment",
|
|
274
|
+
)
|
|
275
|
+
.extend(withDatabase(userSchema))
|
|
276
|
+
.withDependencies(({ config }) => {
|
|
277
|
+
// This should receive the actual config, not an empty mock
|
|
278
|
+
return {
|
|
279
|
+
client: { key: config.apiKey, secret: config.apiSecret },
|
|
280
|
+
apiKey: config.apiKey,
|
|
281
|
+
};
|
|
282
|
+
})
|
|
283
|
+
.providesBaseService(({ deps }) => ({
|
|
284
|
+
getApiKey: () => deps.apiKey,
|
|
285
|
+
createUser: async (data: { name: string; email: string }) => {
|
|
286
|
+
const id = await deps.db.create("users", data);
|
|
287
|
+
return { ...data, id: id.valueOf() };
|
|
288
|
+
},
|
|
289
|
+
}))
|
|
290
|
+
.build();
|
|
291
|
+
|
|
292
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
293
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
294
|
+
.withFragment(
|
|
295
|
+
"requiredConfig",
|
|
296
|
+
instantiate(requiredConfigFragmentDef)
|
|
297
|
+
.withConfig({
|
|
298
|
+
apiKey: "test-key",
|
|
299
|
+
apiSecret: "test-secret",
|
|
300
|
+
})
|
|
301
|
+
.withRoutes([]),
|
|
302
|
+
)
|
|
303
|
+
.build();
|
|
304
|
+
|
|
305
|
+
// Verify the fragment was created with actual config
|
|
306
|
+
expect(fragments.requiredConfig.deps.apiKey).toBe("test-key");
|
|
307
|
+
expect(fragments.requiredConfig.deps.client).toEqual({
|
|
308
|
+
key: "test-key",
|
|
309
|
+
secret: "test-secret",
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Verify database operations work
|
|
313
|
+
const user = await fragments.requiredConfig.services.createUser({
|
|
314
|
+
name: "Config Test User",
|
|
315
|
+
email: "config@example.com",
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
expect(user).toMatchObject({
|
|
319
|
+
id: expect.any(String),
|
|
320
|
+
name: "Config Test User",
|
|
321
|
+
email: "config@example.com",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await test.cleanup();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should provide helpful error when config is missing", async () => {
|
|
328
|
+
// Test that we get a helpful error when required config is not provided
|
|
329
|
+
const badFragmentDef = defineFragment<{ apiKey: string }>("bad-fragment")
|
|
330
|
+
.extend(withDatabase(userSchema))
|
|
331
|
+
.withDependencies(({ config }) => {
|
|
332
|
+
// This will throw if apiKey is undefined
|
|
333
|
+
if (!config.apiKey) {
|
|
334
|
+
throw new Error("API key is required");
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
apiKey: config.apiKey,
|
|
338
|
+
};
|
|
339
|
+
})
|
|
340
|
+
.providesBaseService(() => ({}))
|
|
341
|
+
.build();
|
|
342
|
+
|
|
343
|
+
// Intentionally omit the required config to test error handling
|
|
344
|
+
const buildPromise = buildDatabaseFragmentsTest()
|
|
345
|
+
.withTestAdapter({ type: "kysely-sqlite" })
|
|
346
|
+
.withFragment("bad", instantiate(badFragmentDef).withRoutes([]))
|
|
347
|
+
.build();
|
|
348
|
+
|
|
349
|
+
await expect(buildPromise).rejects.toThrow(/Failed to extract schema from fragment/);
|
|
350
|
+
await expect(buildPromise).rejects.toThrow(/API key is required/);
|
|
351
|
+
});
|
|
352
|
+
});
|