@fragno-dev/test 0.1.1 → 0.1.2
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 +7 -7
- package/CHANGELOG.md +8 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +141 -0
- package/src/index.ts +88 -28
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @fragno-dev/test@0.1.
|
|
2
|
+
> @fragno-dev/test@0.1.2 build /home/runner/work/fragno/fragno/packages/fragno-test
|
|
3
3
|
> tsdown
|
|
4
4
|
|
|
5
5
|
[34mℹ[39m tsdown [2mv0.15.10[22m powered by rolldown [2mv1.0.0-beta.44[22m
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
8
8
|
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
9
|
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.js[22m [
|
|
11
|
-
[34mℹ[39m [2mdist/[22mindex.js.map [
|
|
12
|
-
[34mℹ[39m [2mdist/[22mindex.d.ts.map [2m1.
|
|
13
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.ts[22m[39m [2m2.
|
|
14
|
-
[34mℹ[39m 4 files, total:
|
|
15
|
-
[32m✔[39m Build complete in [
|
|
10
|
+
[34mℹ[39m [2mdist/[22m[1mindex.js[22m [2m2.63 kB[22m [2m│ gzip: 0.89 kB[22m
|
|
11
|
+
[34mℹ[39m [2mdist/[22mindex.js.map [2m8.17 kB[22m [2m│ gzip: 2.51 kB[22m
|
|
12
|
+
[34mℹ[39m [2mdist/[22mindex.d.ts.map [2m1.28 kB[22m [2m│ gzip: 0.59 kB[22m
|
|
13
|
+
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.ts[22m[39m [2m2.55 kB[22m [2m│ gzip: 0.80 kB[22m
|
|
14
|
+
[34mℹ[39m 4 files, total: 14.63 kB
|
|
15
|
+
[32m✔[39m Build complete in [32m4126ms[39m
|
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,11 @@ interface CreateDatabaseFragmentForTestOptions<TConfig, TDeps, TServices, TAddit
|
|
|
21
21
|
interface DatabaseFragmentForTest<TConfig, TDeps, TServices, TAdditionalContext extends Record<string, unknown>, TOptions extends FragnoPublicConfig> extends FragmentForTest$1<TConfig, TDeps, TServices, TAdditionalContext, TOptions> {
|
|
22
22
|
kysely: Kysely<any>;
|
|
23
23
|
adapter: DatabaseAdapter<any>;
|
|
24
|
+
/**
|
|
25
|
+
* Resets the database by creating a fresh in-memory database instance and re-running migrations.
|
|
26
|
+
* After calling this, you should re-initialize any routes to ensure they use the new database instance.
|
|
27
|
+
*/
|
|
28
|
+
resetDatabase: () => Promise<void>;
|
|
24
29
|
}
|
|
25
30
|
declare function createDatabaseFragmentForTest<const TConfig, const TDeps, const TServices extends Record<string, unknown>, const TAdditionalContext extends Record<string, unknown>, const TOptions extends FragnoPublicConfig, const TSchema extends AnySchema>(fragmentBuilder: {
|
|
26
31
|
definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA0BA;;AAKmB,UALF,oCAKE,CAAA,OAAA,EAAA,KAAA,EAAA,SAAA,EAAA,2BADU,MACV,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,iBAAA,kBAAA,CAAA,SACT,IADS,CAEf,8BAFe,CAEc,OAFd,EAEuB,KAFvB,EAE8B,SAF9B,EAEyC,kBAFzC,EAE6D,QAF7D,CAAA,EAAA,QAAA,CAAA,CAAA;EAEc,YAAA,CAAA,EAAA,MAAA;EAAS,gBAAA,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAKtC,OALsC;;;;;AADvC,UAYO,uBAZP,CAAA,OAAA,EAAA,KAAA,EAAA,SAAA,EAAA,2BAgBmB,MAhBnB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,iBAiBS,kBAjBT,CAAA,SAkBA,iBAlBA,CAkBgB,OAlBhB,EAkByB,KAlBzB,EAkBgC,SAlBhC,EAkB2C,kBAlB3C,EAkB+D,QAlB/D,CAAA,CAAA;EAAI,MAAA,EAoBJ,MApBI,CAAA,GAAA,CAAA;EAYG,OAAA,EAUN,eAVM,CAAA,GAAuB,CAAA;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;AA0BA;;AAKmB,UALF,oCAKE,CAAA,OAAA,EAAA,KAAA,EAAA,SAAA,EAAA,2BADU,MACV,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,iBAAA,kBAAA,CAAA,SACT,IADS,CAEf,8BAFe,CAEc,OAFd,EAEuB,KAFvB,EAE8B,SAF9B,EAEyC,kBAFzC,EAE6D,QAF7D,CAAA,EAAA,QAAA,CAAA,CAAA;EAEc,YAAA,CAAA,EAAA,MAAA;EAAS,gBAAA,CAAA,EAAA,MAAA;EAAO,MAAA,CAAA,EAKtC,OALsC;;;;;AADvC,UAYO,uBAZP,CAAA,OAAA,EAAA,KAAA,EAAA,SAAA,EAAA,2BAgBmB,MAhBnB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,iBAiBS,kBAjBT,CAAA,SAkBA,iBAlBA,CAkBgB,OAlBhB,EAkByB,KAlBzB,EAkBgC,SAlBhC,EAkB2C,kBAlB3C,EAkB+D,QAlB/D,CAAA,CAAA;EAAI,MAAA,EAoBJ,MApBI,CAAA,GAAA,CAAA;EAYG,OAAA,EAUN,eAVM,CAAA,GAAuB,CAAA;EAIX;;;;EAEa,aAAA,EAAA,GAAA,GASnB,OATmB,CAAA,IAAA,CAAA;;AAA+B,iBAYnD,6BAZmD,CAAA,aAAA,EAAA,WAAA,EAAA,wBAe/C,MAf+C,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,iCAgBtC,MAhBsC,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA,uBAiBhD,kBAjBgD,EAAA,sBAkBjD,SAlBiD,CAAA,CAAA,eAAA,EAAA;EAE/D,UAAA,EAmBM,kBAnBN,CAmByB,OAnBzB,EAmBkC,KAnBlC,EAmByC,SAnBzC,EAmBoD,kBAnBpD,CAAA;EAEC,gBAAA,EAkBW,QAlBX;CAKY,EAAA,OAAA,CAAA,EAeX,oCAfW,CAgBnB,OAhBmB,EAiBnB,KAjBmB,EAkBnB,SAlBmB,EAmBnB,kBAnBmB,EAoBnB,QApBmB,CAAA,CAAA,EAsBpB,OAtBoB,CAsBZ,uBAtBY,CAsBY,OAtBZ,EAsBqB,KAtBrB,EAsB4B,SAtB5B,EAsBuC,kBAtBvC,EAsB2D,QAtB3D,CAAA,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -6,31 +6,79 @@ import { createFragmentForTest, createFragmentForTest as createFragmentForTest$1
|
|
|
6
6
|
//#region src/index.ts
|
|
7
7
|
async function createDatabaseFragmentForTest(fragmentBuilder, options) {
|
|
8
8
|
const { databasePath = ":memory:", migrateToVersion, config, options: fragmentOptions, deps, services, additionalContext } = options ?? {};
|
|
9
|
-
const { dialect } = new SQLocalKysely(databasePath);
|
|
10
|
-
const kysely = new Kysely({ dialect });
|
|
11
|
-
const adapter = new KyselyAdapter({
|
|
12
|
-
db: kysely,
|
|
13
|
-
provider: "sqlite"
|
|
14
|
-
});
|
|
15
9
|
const fragmentAdditionalContext = fragmentBuilder.definition.additionalContext;
|
|
16
10
|
const schema = fragmentAdditionalContext?.databaseSchema;
|
|
17
11
|
const namespace = fragmentAdditionalContext?.databaseNamespace ?? "";
|
|
18
12
|
if (!schema) throw new Error(`Fragment '${fragmentBuilder.definition.name}' does not have a database schema. Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
const createDatabase = async () => {
|
|
14
|
+
const { dialect } = new SQLocalKysely(databasePath);
|
|
15
|
+
const kysely$1 = new Kysely({ dialect });
|
|
16
|
+
const adapter$1 = new KyselyAdapter({
|
|
17
|
+
db: kysely$1,
|
|
18
|
+
provider: "sqlite"
|
|
19
|
+
});
|
|
20
|
+
const migrator = adapter$1.createMigrationEngine(schema, namespace);
|
|
21
|
+
await (migrateToVersion ? await migrator.prepareMigrationTo(migrateToVersion, { updateSettings: false }) : await migrator.prepareMigration({ updateSettings: false })).execute();
|
|
22
|
+
return {
|
|
23
|
+
kysely: kysely$1,
|
|
24
|
+
adapter: adapter$1
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
let { kysely, adapter } = await createDatabase();
|
|
28
|
+
let mergedOptions = {
|
|
29
|
+
...fragmentOptions,
|
|
30
|
+
databaseAdapter: adapter
|
|
31
|
+
};
|
|
32
|
+
let fragment = createFragmentForTest$1(fragmentBuilder, {
|
|
33
|
+
config,
|
|
34
|
+
options: mergedOptions,
|
|
35
|
+
deps,
|
|
36
|
+
services,
|
|
37
|
+
additionalContext
|
|
38
|
+
});
|
|
39
|
+
const resetDatabase = async () => {
|
|
40
|
+
await kysely.destroy();
|
|
41
|
+
const newDb = await createDatabase();
|
|
42
|
+
kysely = newDb.kysely;
|
|
43
|
+
adapter = newDb.adapter;
|
|
44
|
+
mergedOptions = {
|
|
45
|
+
...fragmentOptions,
|
|
46
|
+
databaseAdapter: adapter
|
|
47
|
+
};
|
|
48
|
+
fragment = createFragmentForTest$1(fragmentBuilder, {
|
|
23
49
|
config,
|
|
24
|
-
options:
|
|
25
|
-
...fragmentOptions,
|
|
26
|
-
databaseAdapter: adapter
|
|
27
|
-
},
|
|
50
|
+
options: mergedOptions,
|
|
28
51
|
deps,
|
|
29
52
|
services,
|
|
30
53
|
additionalContext
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
get services() {
|
|
58
|
+
return fragment.services;
|
|
59
|
+
},
|
|
60
|
+
get initRoutes() {
|
|
61
|
+
return fragment.initRoutes;
|
|
62
|
+
},
|
|
63
|
+
get handler() {
|
|
64
|
+
return fragment.handler;
|
|
65
|
+
},
|
|
66
|
+
get config() {
|
|
67
|
+
return fragment.config;
|
|
68
|
+
},
|
|
69
|
+
get deps() {
|
|
70
|
+
return fragment.deps;
|
|
71
|
+
},
|
|
72
|
+
get additionalContext() {
|
|
73
|
+
return fragment.additionalContext;
|
|
74
|
+
},
|
|
75
|
+
get kysely() {
|
|
76
|
+
return kysely;
|
|
77
|
+
},
|
|
78
|
+
get adapter() {
|
|
79
|
+
return adapter;
|
|
80
|
+
},
|
|
81
|
+
resetDatabase
|
|
34
82
|
};
|
|
35
83
|
}
|
|
36
84
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["createFragmentForTest"],"sources":["../src/index.ts"],"sourcesContent":["import { Kysely } from \"kysely\";\nimport { SQLocalKysely } from \"sqlocal/kysely\";\nimport { KyselyAdapter } from \"@fragno-dev/db/adapters/kysely\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\nimport type { DatabaseAdapter } from \"@fragno-dev/db/adapters\";\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\";\n\n// Re-export utilities from @fragno-dev/core/test\nexport {\n createFragmentForTest,\n type TestResponse,\n type CreateFragmentForTestOptions,\n type RouteHandlerInputOptions,\n type FragmentForTest,\n type InitRoutesOverrides,\n} from \"@fragno-dev/core/test\";\n\n/**\n * Options for creating a database fragment for testing\n */\nexport interface CreateDatabaseFragmentForTestOptions<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> extends Omit<\n CreateFragmentForTestOptions<TConfig, TDeps, TServices, TAdditionalContext, TOptions>,\n \"config\"\n > {\n databasePath?: string;\n migrateToVersion?: number;\n config?: TConfig;\n}\n\n/**\n * Extended fragment test instance with database adapter and Kysely instance\n */\nexport interface DatabaseFragmentForTest<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> extends FragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n kysely: Kysely<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n adapter: DatabaseAdapter<any>;\n}\n\nexport async function createDatabaseFragmentForTest<\n const TConfig,\n const TDeps,\n const TServices extends Record<string, unknown>,\n const TAdditionalContext extends Record<string, unknown>,\n const TOptions extends FragnoPublicConfig,\n const TSchema extends AnySchema,\n>(\n fragmentBuilder: {\n definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;\n $requiredOptions: TOptions;\n },\n options?: CreateDatabaseFragmentForTestOptions<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext,\n TOptions\n >,\n): Promise<DatabaseFragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions>> {\n const {\n databasePath = \":memory:\",\n migrateToVersion,\n config,\n options: fragmentOptions,\n deps,\n services,\n additionalContext,\n } = options ?? {};\n\n //
|
|
1
|
+
{"version":3,"file":"index.js","names":["kysely","adapter","createFragmentForTest"],"sources":["../src/index.ts"],"sourcesContent":["import { Kysely } from \"kysely\";\nimport { SQLocalKysely } from \"sqlocal/kysely\";\nimport { KyselyAdapter } from \"@fragno-dev/db/adapters/kysely\";\nimport type { AnySchema } from \"@fragno-dev/db/schema\";\nimport type { DatabaseAdapter } from \"@fragno-dev/db/adapters\";\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\";\n\n// Re-export utilities from @fragno-dev/core/test\nexport {\n createFragmentForTest,\n type TestResponse,\n type CreateFragmentForTestOptions,\n type RouteHandlerInputOptions,\n type FragmentForTest,\n type InitRoutesOverrides,\n} from \"@fragno-dev/core/test\";\n\n/**\n * Options for creating a database fragment for testing\n */\nexport interface CreateDatabaseFragmentForTestOptions<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> extends Omit<\n CreateFragmentForTestOptions<TConfig, TDeps, TServices, TAdditionalContext, TOptions>,\n \"config\"\n > {\n databasePath?: string;\n migrateToVersion?: number;\n config?: TConfig;\n}\n\n/**\n * Extended fragment test instance with database adapter and Kysely instance\n */\nexport interface DatabaseFragmentForTest<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> extends FragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n kysely: Kysely<any>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n adapter: DatabaseAdapter<any>;\n /**\n * Resets the database by creating a fresh in-memory database instance and re-running migrations.\n * After calling this, you should re-initialize any routes to ensure they use the new database instance.\n */\n resetDatabase: () => Promise<void>;\n}\n\nexport async function createDatabaseFragmentForTest<\n const TConfig,\n const TDeps,\n const TServices extends Record<string, unknown>,\n const TAdditionalContext extends Record<string, unknown>,\n const TOptions extends FragnoPublicConfig,\n const TSchema extends AnySchema,\n>(\n fragmentBuilder: {\n definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;\n $requiredOptions: TOptions;\n },\n options?: CreateDatabaseFragmentForTestOptions<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext,\n TOptions\n >,\n): Promise<DatabaseFragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions>> {\n const {\n databasePath = \":memory:\",\n migrateToVersion,\n config,\n options: fragmentOptions,\n deps,\n services,\n additionalContext,\n } = options ?? {};\n\n // Get schema and namespace from fragment definition's additionalContext\n // Safe cast: DatabaseFragmentBuilder adds databaseSchema and databaseNamespace to additionalContext\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const fragmentAdditionalContext = fragmentBuilder.definition.additionalContext as any;\n const schema = fragmentAdditionalContext?.databaseSchema as TSchema | undefined;\n const namespace = (fragmentAdditionalContext?.databaseNamespace as string | undefined) ?? \"\";\n\n if (!schema) {\n throw new Error(\n `Fragment '${fragmentBuilder.definition.name}' does not have a database schema. ` +\n `Make sure you're using defineFragmentWithDatabase().withDatabase(schema).`,\n );\n }\n\n // Helper to create a new database instance and run migrations\n const createDatabase = async () => {\n // Create SQLocalKysely instance\n const { dialect } = new SQLocalKysely(databasePath);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const kysely = new Kysely<any>({\n dialect,\n });\n\n // Create KyselyAdapter\n const adapter = new KyselyAdapter({\n db: kysely,\n provider: \"sqlite\",\n });\n\n // Run migrations\n const migrator = adapter.createMigrationEngine(schema, namespace);\n const preparedMigration = migrateToVersion\n ? await migrator.prepareMigrationTo(migrateToVersion, {\n updateSettings: false,\n })\n : await migrator.prepareMigration({\n updateSettings: false,\n });\n await preparedMigration.execute();\n\n return { kysely, adapter };\n };\n\n // Create initial database\n let { kysely, adapter } = await createDatabase();\n\n // Create fragment with database adapter in options\n // Safe cast: We're merging the user's options with the databaseAdapter, which is required by TOptions\n // The user's TOptions is constrained to FragnoPublicConfig (or a subtype), which we extend with databaseAdapter\n let mergedOptions = {\n ...fragmentOptions,\n databaseAdapter: adapter,\n } as unknown as TOptions;\n\n // Safe cast: If config is not provided, we pass undefined as TConfig.\n // The base createFragmentForTest expects config: TConfig, but if TConfig allows undefined\n // or if the fragment doesn't use config in its dependencies function, this will work correctly.\n let fragment = createFragmentForTest(fragmentBuilder, {\n config: config as TConfig,\n options: mergedOptions,\n deps,\n services,\n additionalContext,\n });\n\n // Reset database function - creates a fresh in-memory database and re-runs migrations\n const resetDatabase = async () => {\n // Destroy the old Kysely instance\n await kysely.destroy();\n\n // Create a new database instance\n const newDb = await createDatabase();\n kysely = newDb.kysely;\n adapter = newDb.adapter;\n\n // Recreate the fragment with the new adapter\n mergedOptions = {\n ...fragmentOptions,\n databaseAdapter: adapter,\n } as unknown as TOptions;\n\n fragment = createFragmentForTest(fragmentBuilder, {\n config: config as TConfig,\n options: mergedOptions,\n deps,\n services,\n additionalContext,\n });\n };\n\n return {\n get services() {\n return fragment.services;\n },\n get initRoutes() {\n return fragment.initRoutes;\n },\n get handler() {\n return fragment.handler;\n },\n get config() {\n return fragment.config;\n },\n get deps() {\n return fragment.deps;\n },\n get additionalContext() {\n return fragment.additionalContext;\n },\n get kysely() {\n return kysely;\n },\n get adapter() {\n return adapter;\n },\n resetDatabase,\n };\n}\n"],"mappings":";;;;;;AA8DA,eAAsB,8BAQpB,iBAIA,SAO2F;CAC3F,MAAM,EACJ,eAAe,YACf,kBACA,QACA,SAAS,iBACT,MACA,UACA,sBACE,WAAW,EAAE;CAKjB,MAAM,4BAA4B,gBAAgB,WAAW;CAC7D,MAAM,SAAS,2BAA2B;CAC1C,MAAM,YAAa,2BAA2B,qBAA4C;AAE1F,KAAI,CAAC,OACH,OAAM,IAAI,MACR,aAAa,gBAAgB,WAAW,KAAK,8GAE9C;CAIH,MAAM,iBAAiB,YAAY;EAEjC,MAAM,EAAE,YAAY,IAAI,cAAc,aAAa;EAEnD,MAAMA,WAAS,IAAI,OAAY,EAC7B,SACD,CAAC;EAGF,MAAMC,YAAU,IAAI,cAAc;GAChC,IAAID;GACJ,UAAU;GACX,CAAC;EAGF,MAAM,WAAWC,UAAQ,sBAAsB,QAAQ,UAAU;AAQjE,SAP0B,mBACtB,MAAM,SAAS,mBAAmB,kBAAkB,EAClD,gBAAgB,OACjB,CAAC,GACF,MAAM,SAAS,iBAAiB,EAC9B,gBAAgB,OACjB,CAAC,EACkB,SAAS;AAEjC,SAAO;GAAE;GAAQ;GAAS;;CAI5B,IAAI,EAAE,QAAQ,YAAY,MAAM,gBAAgB;CAKhD,IAAI,gBAAgB;EAClB,GAAG;EACH,iBAAiB;EAClB;CAKD,IAAI,WAAWC,wBAAsB,iBAAiB;EAC5C;EACR,SAAS;EACT;EACA;EACA;EACD,CAAC;CAGF,MAAM,gBAAgB,YAAY;AAEhC,QAAM,OAAO,SAAS;EAGtB,MAAM,QAAQ,MAAM,gBAAgB;AACpC,WAAS,MAAM;AACf,YAAU,MAAM;AAGhB,kBAAgB;GACd,GAAG;GACH,iBAAiB;GAClB;AAED,aAAWA,wBAAsB,iBAAiB;GACxC;GACR,SAAS;GACT;GACA;GACA;GACD,CAAC;;AAGJ,QAAO;EACL,IAAI,WAAW;AACb,UAAO,SAAS;;EAElB,IAAI,aAAa;AACf,UAAO,SAAS;;EAElB,IAAI,UAAU;AACZ,UAAO,SAAS;;EAElB,IAAI,SAAS;AACX,UAAO,SAAS;;EAElB,IAAI,OAAO;AACT,UAAO,SAAS;;EAElB,IAAI,oBAAoB;AACtB,UAAO,SAAS;;EAElB,IAAI,SAAS;AACX,UAAO;;EAET,IAAI,UAAU;AACZ,UAAO;;EAET;EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragno-dev/test",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"kysely": "^0.28.7",
|
|
17
17
|
"sqlocal": "^0.15.2",
|
|
18
18
|
"@fragno-dev/core": "0.1.3",
|
|
19
|
-
"@fragno-dev/db": "0.1.
|
|
19
|
+
"@fragno-dev/db": "0.1.6"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22",
|
package/src/index.test.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
|
3
3
|
import { defineFragmentWithDatabase } from "@fragno-dev/db/fragment";
|
|
4
4
|
import { createDatabaseFragmentForTest } from "./index";
|
|
5
5
|
import { unlinkSync, existsSync } from "node:fs";
|
|
6
|
+
import { defineRoute, defineRoutes } from "@fragno-dev/core";
|
|
7
|
+
import { z } from "zod";
|
|
6
8
|
|
|
7
9
|
// Test schema with multiple versions
|
|
8
10
|
const testSchema = schema((s) => {
|
|
@@ -260,4 +262,143 @@ describe("createDatabaseFragmentForTest", () => {
|
|
|
260
262
|
).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
|
|
261
263
|
});
|
|
262
264
|
});
|
|
265
|
+
|
|
266
|
+
describe("route handling with defineRoutes", () => {
|
|
267
|
+
it("should handle route factory with multiple routes", async () => {
|
|
268
|
+
const fragment = await createDatabaseFragmentForTest(testFragmentDef);
|
|
269
|
+
|
|
270
|
+
type Config = {};
|
|
271
|
+
type Deps = {};
|
|
272
|
+
type Services = {
|
|
273
|
+
createUser: (data: { name: string; email: string; age?: number | null }) => Promise<{
|
|
274
|
+
name: string;
|
|
275
|
+
email: string;
|
|
276
|
+
age?: number | null;
|
|
277
|
+
id: string;
|
|
278
|
+
}>;
|
|
279
|
+
getUsers: () => Promise<{ name: string; email: string; age: number | null; id: string }[]>;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
|
|
283
|
+
defineRoute({
|
|
284
|
+
method: "POST",
|
|
285
|
+
path: "/users",
|
|
286
|
+
inputSchema: z.object({
|
|
287
|
+
name: z.string(),
|
|
288
|
+
email: z.string(),
|
|
289
|
+
age: z.number().nullable().optional(),
|
|
290
|
+
}),
|
|
291
|
+
outputSchema: z.object({
|
|
292
|
+
id: z.string(),
|
|
293
|
+
name: z.string(),
|
|
294
|
+
email: z.string(),
|
|
295
|
+
age: z.number().nullable().optional(),
|
|
296
|
+
}),
|
|
297
|
+
handler: async ({ input }, { json }) => {
|
|
298
|
+
if (input) {
|
|
299
|
+
const data = await input.valid();
|
|
300
|
+
const user = await services.createUser(data);
|
|
301
|
+
return json(user);
|
|
302
|
+
}
|
|
303
|
+
return json({ id: "", name: "", email: "", age: null });
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
defineRoute({
|
|
307
|
+
method: "GET",
|
|
308
|
+
path: "/users",
|
|
309
|
+
outputSchema: z.array(
|
|
310
|
+
z.object({
|
|
311
|
+
id: z.string(),
|
|
312
|
+
name: z.string(),
|
|
313
|
+
email: z.string(),
|
|
314
|
+
age: z.number().nullable(),
|
|
315
|
+
}),
|
|
316
|
+
),
|
|
317
|
+
handler: async (_ctx, { json }) => {
|
|
318
|
+
const users = await services.getUsers();
|
|
319
|
+
return json(users);
|
|
320
|
+
},
|
|
321
|
+
}),
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
const routes = [routeFactory] as const;
|
|
325
|
+
const [createUserRoute, getUsersRoute] = fragment.initRoutes(routes);
|
|
326
|
+
|
|
327
|
+
// Test creating a user
|
|
328
|
+
const createResponse = await fragment.handler(createUserRoute, {
|
|
329
|
+
body: { name: "John Doe", email: "john@example.com", age: 30 },
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(createResponse.type).toBe("json");
|
|
333
|
+
if (createResponse.type === "json") {
|
|
334
|
+
expect(createResponse.data).toMatchObject({
|
|
335
|
+
id: expect.any(String),
|
|
336
|
+
name: "John Doe",
|
|
337
|
+
email: "john@example.com",
|
|
338
|
+
age: 30,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Test getting users
|
|
343
|
+
const getUsersResponse = await fragment.handler(getUsersRoute);
|
|
344
|
+
|
|
345
|
+
expect(getUsersResponse.type).toBe("json");
|
|
346
|
+
if (getUsersResponse.type === "json") {
|
|
347
|
+
expect(getUsersResponse.data).toHaveLength(1);
|
|
348
|
+
expect(getUsersResponse.data[0]).toMatchObject({
|
|
349
|
+
id: expect.any(String),
|
|
350
|
+
name: "John Doe",
|
|
351
|
+
email: "john@example.com",
|
|
352
|
+
age: 30,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("resetDatabase", () => {
|
|
359
|
+
it("should clear all data and recreate a fresh database", async () => {
|
|
360
|
+
const fragment = await createDatabaseFragmentForTest(testFragmentDef);
|
|
361
|
+
|
|
362
|
+
// Create some users
|
|
363
|
+
await fragment.services.createUser({
|
|
364
|
+
name: "User 1",
|
|
365
|
+
email: "user1@example.com",
|
|
366
|
+
age: 25,
|
|
367
|
+
});
|
|
368
|
+
await fragment.services.createUser({
|
|
369
|
+
name: "User 2",
|
|
370
|
+
email: "user2@example.com",
|
|
371
|
+
age: 30,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Verify users exist
|
|
375
|
+
let users = await fragment.services.getUsers();
|
|
376
|
+
expect(users).toHaveLength(2);
|
|
377
|
+
|
|
378
|
+
// Reset the database
|
|
379
|
+
await fragment.resetDatabase();
|
|
380
|
+
|
|
381
|
+
// Verify database is empty
|
|
382
|
+
users = await fragment.services.getUsers();
|
|
383
|
+
expect(users).toHaveLength(0);
|
|
384
|
+
|
|
385
|
+
// Verify we can still create new users after reset
|
|
386
|
+
const newUser = await fragment.services.createUser({
|
|
387
|
+
name: "User After Reset",
|
|
388
|
+
email: "after@example.com",
|
|
389
|
+
age: 35,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(newUser).toMatchObject({
|
|
393
|
+
id: expect.any(String),
|
|
394
|
+
name: "User After Reset",
|
|
395
|
+
email: "after@example.com",
|
|
396
|
+
age: 35,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
users = await fragment.services.getUsers();
|
|
400
|
+
expect(users).toHaveLength(1);
|
|
401
|
+
expect(users[0]).toMatchObject(newUser);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
263
404
|
});
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface DatabaseFragmentForTest<
|
|
|
53
53
|
kysely: Kysely<any>;
|
|
54
54
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
55
|
adapter: DatabaseAdapter<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Resets the database by creating a fresh in-memory database instance and re-running migrations.
|
|
58
|
+
* After calling this, you should re-initialize any routes to ensure they use the new database instance.
|
|
59
|
+
*/
|
|
60
|
+
resetDatabase: () => Promise<void>;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export async function createDatabaseFragmentForTest<
|
|
@@ -85,19 +90,6 @@ export async function createDatabaseFragmentForTest<
|
|
|
85
90
|
additionalContext,
|
|
86
91
|
} = options ?? {};
|
|
87
92
|
|
|
88
|
-
// Create SQLocalKysely instance
|
|
89
|
-
const { dialect } = new SQLocalKysely(databasePath);
|
|
90
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
-
const kysely = new Kysely<any>({
|
|
92
|
-
dialect,
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// Create KyselyAdapter
|
|
96
|
-
const adapter = new KyselyAdapter({
|
|
97
|
-
db: kysely,
|
|
98
|
-
provider: "sqlite",
|
|
99
|
-
});
|
|
100
|
-
|
|
101
93
|
// Get schema and namespace from fragment definition's additionalContext
|
|
102
94
|
// Safe cast: DatabaseFragmentBuilder adds databaseSchema and databaseNamespace to additionalContext
|
|
103
95
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -112,21 +104,42 @@ export async function createDatabaseFragmentForTest<
|
|
|
112
104
|
);
|
|
113
105
|
}
|
|
114
106
|
|
|
115
|
-
//
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
// Helper to create a new database instance and run migrations
|
|
108
|
+
const createDatabase = async () => {
|
|
109
|
+
// Create SQLocalKysely instance
|
|
110
|
+
const { dialect } = new SQLocalKysely(databasePath);
|
|
111
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
|
+
const kysely = new Kysely<any>({
|
|
113
|
+
dialect,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Create KyselyAdapter
|
|
117
|
+
const adapter = new KyselyAdapter({
|
|
118
|
+
db: kysely,
|
|
119
|
+
provider: "sqlite",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Run migrations
|
|
123
|
+
const migrator = adapter.createMigrationEngine(schema, namespace);
|
|
124
|
+
const preparedMigration = migrateToVersion
|
|
125
|
+
? await migrator.prepareMigrationTo(migrateToVersion, {
|
|
126
|
+
updateSettings: false,
|
|
127
|
+
})
|
|
128
|
+
: await migrator.prepareMigration({
|
|
129
|
+
updateSettings: false,
|
|
130
|
+
});
|
|
131
|
+
await preparedMigration.execute();
|
|
132
|
+
|
|
133
|
+
return { kysely, adapter };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Create initial database
|
|
137
|
+
let { kysely, adapter } = await createDatabase();
|
|
125
138
|
|
|
126
139
|
// Create fragment with database adapter in options
|
|
127
140
|
// Safe cast: We're merging the user's options with the databaseAdapter, which is required by TOptions
|
|
128
141
|
// The user's TOptions is constrained to FragnoPublicConfig (or a subtype), which we extend with databaseAdapter
|
|
129
|
-
|
|
142
|
+
let mergedOptions = {
|
|
130
143
|
...fragmentOptions,
|
|
131
144
|
databaseAdapter: adapter,
|
|
132
145
|
} as unknown as TOptions;
|
|
@@ -134,7 +147,7 @@ export async function createDatabaseFragmentForTest<
|
|
|
134
147
|
// Safe cast: If config is not provided, we pass undefined as TConfig.
|
|
135
148
|
// The base createFragmentForTest expects config: TConfig, but if TConfig allows undefined
|
|
136
149
|
// or if the fragment doesn't use config in its dependencies function, this will work correctly.
|
|
137
|
-
|
|
150
|
+
let fragment = createFragmentForTest(fragmentBuilder, {
|
|
138
151
|
config: config as TConfig,
|
|
139
152
|
options: mergedOptions,
|
|
140
153
|
deps,
|
|
@@ -142,9 +155,56 @@ export async function createDatabaseFragmentForTest<
|
|
|
142
155
|
additionalContext,
|
|
143
156
|
});
|
|
144
157
|
|
|
158
|
+
// Reset database function - creates a fresh in-memory database and re-runs migrations
|
|
159
|
+
const resetDatabase = async () => {
|
|
160
|
+
// Destroy the old Kysely instance
|
|
161
|
+
await kysely.destroy();
|
|
162
|
+
|
|
163
|
+
// Create a new database instance
|
|
164
|
+
const newDb = await createDatabase();
|
|
165
|
+
kysely = newDb.kysely;
|
|
166
|
+
adapter = newDb.adapter;
|
|
167
|
+
|
|
168
|
+
// Recreate the fragment with the new adapter
|
|
169
|
+
mergedOptions = {
|
|
170
|
+
...fragmentOptions,
|
|
171
|
+
databaseAdapter: adapter,
|
|
172
|
+
} as unknown as TOptions;
|
|
173
|
+
|
|
174
|
+
fragment = createFragmentForTest(fragmentBuilder, {
|
|
175
|
+
config: config as TConfig,
|
|
176
|
+
options: mergedOptions,
|
|
177
|
+
deps,
|
|
178
|
+
services,
|
|
179
|
+
additionalContext,
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
145
183
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
184
|
+
get services() {
|
|
185
|
+
return fragment.services;
|
|
186
|
+
},
|
|
187
|
+
get initRoutes() {
|
|
188
|
+
return fragment.initRoutes;
|
|
189
|
+
},
|
|
190
|
+
get handler() {
|
|
191
|
+
return fragment.handler;
|
|
192
|
+
},
|
|
193
|
+
get config() {
|
|
194
|
+
return fragment.config;
|
|
195
|
+
},
|
|
196
|
+
get deps() {
|
|
197
|
+
return fragment.deps;
|
|
198
|
+
},
|
|
199
|
+
get additionalContext() {
|
|
200
|
+
return fragment.additionalContext;
|
|
201
|
+
},
|
|
202
|
+
get kysely() {
|
|
203
|
+
return kysely;
|
|
204
|
+
},
|
|
205
|
+
get adapter() {
|
|
206
|
+
return adapter;
|
|
207
|
+
},
|
|
208
|
+
resetDatabase,
|
|
149
209
|
};
|
|
150
210
|
}
|