@fragno-dev/test 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +31 -15
- package/CHANGELOG.md +54 -0
- package/dist/adapters.d.ts +21 -3
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +125 -31
- package/dist/adapters.js.map +1 -1
- package/dist/db-test.d.ts.map +1 -1
- package/dist/db-test.js +33 -2
- package/dist/db-test.js.map +1 -1
- package/dist/durable-hooks.d.ts +7 -0
- package/dist/durable-hooks.d.ts.map +1 -0
- package/dist/durable-hooks.js +12 -0
- package/dist/durable-hooks.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/model-checker-actors.d.ts +41 -0
- package/dist/model-checker-actors.d.ts.map +1 -0
- package/dist/model-checker-actors.js +406 -0
- package/dist/model-checker-actors.js.map +1 -0
- package/dist/model-checker-adapter.d.ts +32 -0
- package/dist/model-checker-adapter.d.ts.map +1 -0
- package/dist/model-checker-adapter.js +109 -0
- package/dist/model-checker-adapter.js.map +1 -0
- package/dist/model-checker.d.ts +128 -0
- package/dist/model-checker.d.ts.map +1 -0
- package/dist/model-checker.js +443 -0
- package/dist/model-checker.js.map +1 -0
- package/package.json +13 -12
- package/src/adapter-conformance.test.ts +322 -0
- package/src/adapters.ts +199 -36
- package/src/db-test.test.ts +2 -2
- package/src/db-test.ts +53 -3
- package/src/durable-hooks.ts +13 -0
- package/src/index.test.ts +84 -7
- package/src/index.ts +39 -4
- package/src/model-checker-actors.test.ts +78 -0
- package/src/model-checker-actors.ts +642 -0
- package/src/model-checker-adapter.ts +200 -0
- package/src/model-checker.test.ts +399 -0
- package/src/model-checker.ts +799 -0
package/src/adapters.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
// Test database adapter helpers and reset logic for fragment suites.
|
|
1
2
|
import { Kysely } from "kysely";
|
|
2
3
|
import { SQLocalKysely } from "sqlocal/kysely";
|
|
3
4
|
import { KyselyPGlite } from "kysely-pglite";
|
|
4
5
|
import { drizzle } from "drizzle-orm/pglite";
|
|
5
6
|
import { PGlite } from "@electric-sql/pglite";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { InMemoryAdapter, type InMemoryAdapterOptions } from "@fragno-dev/db/adapters/in-memory";
|
|
8
|
+
import { SqlAdapter } from "@fragno-dev/db/adapters/sql";
|
|
8
9
|
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
9
10
|
import type { DatabaseAdapter } from "@fragno-dev/db/adapters";
|
|
11
|
+
import type { UnitOfWorkConfig } from "@fragno-dev/db/adapters/sql";
|
|
12
|
+
import type { OutboxConfig } from "@fragno-dev/db/adapters/sql";
|
|
10
13
|
import { rm } from "node:fs/promises";
|
|
11
14
|
import { existsSync } from "node:fs";
|
|
12
15
|
import type { BaseTestContext } from ".";
|
|
16
|
+
import { ModelCheckerAdapter } from "./model-checker-adapter";
|
|
13
17
|
import { createCommonTestContextMethods } from ".";
|
|
14
18
|
import { PGLiteDriverConfig, SQLocalDriverConfig } from "@fragno-dev/db/drivers";
|
|
15
19
|
import { internalFragmentDef } from "@fragno-dev/db";
|
|
@@ -18,30 +22,52 @@ import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
|
18
22
|
// Adapter configuration types
|
|
19
23
|
export interface KyselySqliteAdapter {
|
|
20
24
|
type: "kysely-sqlite";
|
|
25
|
+
uowConfig?: UnitOfWorkConfig;
|
|
26
|
+
outbox?: OutboxConfig;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface KyselyPgliteAdapter {
|
|
24
30
|
type: "kysely-pglite";
|
|
25
31
|
databasePath?: string;
|
|
32
|
+
uowConfig?: UnitOfWorkConfig;
|
|
33
|
+
outbox?: OutboxConfig;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export interface DrizzlePgliteAdapter {
|
|
29
37
|
type: "drizzle-pglite";
|
|
30
38
|
databasePath?: string;
|
|
39
|
+
uowConfig?: UnitOfWorkConfig;
|
|
40
|
+
outbox?: OutboxConfig;
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
export
|
|
43
|
+
export interface InMemoryAdapterConfig {
|
|
44
|
+
type: "in-memory";
|
|
45
|
+
options?: InMemoryAdapterOptions;
|
|
46
|
+
uowConfig?: UnitOfWorkConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ModelCheckerAdapterConfig {
|
|
50
|
+
type: "model-checker";
|
|
51
|
+
options?: InMemoryAdapterOptions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type SupportedAdapter =
|
|
55
|
+
| KyselySqliteAdapter
|
|
56
|
+
| KyselyPgliteAdapter
|
|
57
|
+
| DrizzlePgliteAdapter
|
|
58
|
+
| InMemoryAdapterConfig
|
|
59
|
+
| ModelCheckerAdapterConfig;
|
|
34
60
|
|
|
35
61
|
// Schema configuration for multi-schema adapters
|
|
36
62
|
export interface SchemaConfig {
|
|
37
63
|
schema: AnySchema;
|
|
38
|
-
namespace: string;
|
|
64
|
+
namespace: string | null;
|
|
39
65
|
migrateToVersion?: number;
|
|
40
66
|
}
|
|
41
67
|
|
|
42
68
|
// Internal test context extends BaseTestContext with getOrm (not exposed publicly)
|
|
43
69
|
interface InternalTestContext extends BaseTestContext {
|
|
44
|
-
getOrm: <TSchema extends AnySchema>(namespace: string) => SimpleQueryInterface<TSchema>;
|
|
70
|
+
getOrm: <TSchema extends AnySchema>(namespace: string | null) => SimpleQueryInterface<TSchema>;
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
// Conditional return types based on adapter (adapter-specific properties only)
|
|
@@ -55,7 +81,9 @@ export type AdapterContext<T extends SupportedAdapter> = T extends
|
|
|
55
81
|
? {
|
|
56
82
|
readonly drizzle: ReturnType<typeof drizzle<any>>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
57
83
|
}
|
|
58
|
-
:
|
|
84
|
+
: T extends InMemoryAdapterConfig | ModelCheckerAdapterConfig
|
|
85
|
+
? {}
|
|
86
|
+
: never;
|
|
59
87
|
|
|
60
88
|
// Factory function return type
|
|
61
89
|
interface AdapterFactoryResult<T extends SupportedAdapter> {
|
|
@@ -64,14 +92,49 @@ interface AdapterFactoryResult<T extends SupportedAdapter> {
|
|
|
64
92
|
adapter: DatabaseAdapter<any>;
|
|
65
93
|
}
|
|
66
94
|
|
|
95
|
+
const runInternalFragmentMigrations = async (
|
|
96
|
+
adapter: SqlAdapter,
|
|
97
|
+
): Promise<SchemaConfig | undefined> => {
|
|
98
|
+
const dependencies = internalFragmentDef.dependencies;
|
|
99
|
+
if (!dependencies) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const databaseDeps = dependencies({
|
|
104
|
+
config: {},
|
|
105
|
+
options: { databaseAdapter: adapter, databaseNamespace: null },
|
|
106
|
+
});
|
|
107
|
+
if (databaseDeps?.schema) {
|
|
108
|
+
const migrations = adapter.prepareMigrations(databaseDeps.schema, databaseDeps.namespace);
|
|
109
|
+
await migrations.executeWithDriver(adapter.driver, 0);
|
|
110
|
+
return { schema: databaseDeps.schema, namespace: databaseDeps.namespace };
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const resolveSchemaName = (
|
|
116
|
+
adapter: DatabaseAdapter<any>, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
117
|
+
namespace: string | null,
|
|
118
|
+
): string | null => {
|
|
119
|
+
if (adapter.namingStrategy.namespaceScope !== "schema") {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
if (!namespace || namespace.length === 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return adapter.namingStrategy.namespaceToSchema(namespace);
|
|
126
|
+
};
|
|
127
|
+
|
|
67
128
|
/**
|
|
68
129
|
* Create Kysely + SQLite adapter using SQLocalKysely (always in-memory)
|
|
69
130
|
* Supports multiple schemas with separate namespaces
|
|
70
131
|
*/
|
|
71
132
|
export async function createKyselySqliteAdapter(
|
|
72
|
-
|
|
133
|
+
config: KyselySqliteAdapter,
|
|
73
134
|
schemas: SchemaConfig[],
|
|
74
135
|
): Promise<AdapterFactoryResult<KyselySqliteAdapter>> {
|
|
136
|
+
let internalSchemaConfig: SchemaConfig | undefined;
|
|
137
|
+
|
|
75
138
|
// Helper to create a new database instance and run migrations for all schemas
|
|
76
139
|
const createDatabase = async () => {
|
|
77
140
|
// Create SQLocalKysely instance (always in-memory for tests)
|
|
@@ -81,15 +144,18 @@ export async function createKyselySqliteAdapter(
|
|
|
81
144
|
dialect,
|
|
82
145
|
});
|
|
83
146
|
|
|
84
|
-
// Create
|
|
85
|
-
const adapter = new
|
|
147
|
+
// Create SqlAdapter
|
|
148
|
+
const adapter = new SqlAdapter({
|
|
86
149
|
dialect,
|
|
87
150
|
driverConfig: new SQLocalDriverConfig(),
|
|
151
|
+
uowConfig: config.uowConfig,
|
|
152
|
+
outbox: config.outbox,
|
|
88
153
|
});
|
|
154
|
+
internalSchemaConfig = await runInternalFragmentMigrations(adapter);
|
|
89
155
|
|
|
90
156
|
// Run migrations for all schemas in order
|
|
91
157
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
-
const ormMap = new Map<string, SimpleQueryInterface<any, any>>();
|
|
158
|
+
const ormMap = new Map<string | null, SimpleQueryInterface<any, any>>();
|
|
93
159
|
|
|
94
160
|
for (const { schema, namespace, migrateToVersion } of schemas) {
|
|
95
161
|
// Run migrations
|
|
@@ -113,11 +179,12 @@ export async function createKyselySqliteAdapter(
|
|
|
113
179
|
|
|
114
180
|
// Reset database function - truncates all tables (only supported for in-memory databases)
|
|
115
181
|
const resetDatabase = async () => {
|
|
182
|
+
const schemasToTruncate = internalSchemaConfig ? [internalSchemaConfig, ...schemas] : schemas;
|
|
183
|
+
|
|
116
184
|
// For SQLite, truncate all tables by deleting rows
|
|
117
|
-
for (const { schema, namespace } of
|
|
118
|
-
const mapper = adapter.createTableNameMapper(namespace);
|
|
185
|
+
for (const { schema, namespace } of schemasToTruncate) {
|
|
119
186
|
for (const tableName of Object.keys(schema.tables)) {
|
|
120
|
-
const physicalTableName =
|
|
187
|
+
const physicalTableName = adapter.namingStrategy.tableName(tableName, namespace);
|
|
121
188
|
await kysely.deleteFrom(physicalTableName).execute();
|
|
122
189
|
}
|
|
123
190
|
}
|
|
@@ -157,6 +224,7 @@ export async function createKyselyPgliteAdapter(
|
|
|
157
224
|
schemas: SchemaConfig[],
|
|
158
225
|
): Promise<AdapterFactoryResult<KyselyPgliteAdapter>> {
|
|
159
226
|
const databasePath = config.databasePath;
|
|
227
|
+
let internalSchemaConfig: SchemaConfig | undefined;
|
|
160
228
|
|
|
161
229
|
// Helper to create a new database instance and run migrations for all schemas
|
|
162
230
|
const createDatabase = async () => {
|
|
@@ -169,15 +237,18 @@ export async function createKyselyPgliteAdapter(
|
|
|
169
237
|
dialect: kyselyPglite.dialect,
|
|
170
238
|
});
|
|
171
239
|
|
|
172
|
-
// Create
|
|
173
|
-
const adapter = new
|
|
240
|
+
// Create SqlAdapter
|
|
241
|
+
const adapter = new SqlAdapter({
|
|
174
242
|
dialect: kyselyPglite.dialect,
|
|
175
243
|
driverConfig: new PGLiteDriverConfig(),
|
|
244
|
+
uowConfig: config.uowConfig,
|
|
245
|
+
outbox: config.outbox,
|
|
176
246
|
});
|
|
247
|
+
internalSchemaConfig = await runInternalFragmentMigrations(adapter);
|
|
177
248
|
|
|
178
249
|
// Run migrations for all schemas in order
|
|
179
250
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
180
|
-
const ormMap = new Map<string, SimpleQueryInterface<any, any>>();
|
|
251
|
+
const ormMap = new Map<string | null, SimpleQueryInterface<any, any>>();
|
|
181
252
|
|
|
182
253
|
for (const { schema, namespace, migrateToVersion } of schemas) {
|
|
183
254
|
// Run migrations
|
|
@@ -205,12 +276,15 @@ export async function createKyselyPgliteAdapter(
|
|
|
205
276
|
throw new Error("resetDatabase is only supported for in-memory databases");
|
|
206
277
|
}
|
|
207
278
|
|
|
279
|
+
const schemasToTruncate = internalSchemaConfig ? [internalSchemaConfig, ...schemas] : schemas;
|
|
280
|
+
|
|
208
281
|
// Truncate all tables
|
|
209
|
-
for (const { schema, namespace } of
|
|
210
|
-
const mapper = adapter.createTableNameMapper(namespace);
|
|
282
|
+
for (const { schema, namespace } of schemasToTruncate) {
|
|
211
283
|
for (const tableName of Object.keys(schema.tables)) {
|
|
212
|
-
const physicalTableName =
|
|
213
|
-
|
|
284
|
+
const physicalTableName = adapter.namingStrategy.tableName(tableName, namespace);
|
|
285
|
+
const schemaName = resolveSchemaName(adapter, namespace);
|
|
286
|
+
const scopedKysely = schemaName ? kysely.withSchema(schemaName) : kysely;
|
|
287
|
+
await scopedKysely.deleteFrom(physicalTableName).execute();
|
|
214
288
|
}
|
|
215
289
|
}
|
|
216
290
|
};
|
|
@@ -260,6 +334,7 @@ export async function createDrizzlePgliteAdapter(
|
|
|
260
334
|
schemas: SchemaConfig[],
|
|
261
335
|
): Promise<AdapterFactoryResult<DrizzlePgliteAdapter>> {
|
|
262
336
|
const databasePath = config.databasePath;
|
|
337
|
+
let internalSchemaConfig: SchemaConfig | undefined;
|
|
263
338
|
|
|
264
339
|
// Helper to create a new database instance and run migrations for all schemas
|
|
265
340
|
const createDatabase = async () => {
|
|
@@ -267,23 +342,18 @@ export async function createDrizzlePgliteAdapter(
|
|
|
267
342
|
|
|
268
343
|
const { dialect } = new KyselyPGlite(pglite);
|
|
269
344
|
|
|
270
|
-
const adapter = new
|
|
345
|
+
const adapter = new SqlAdapter({
|
|
271
346
|
dialect,
|
|
272
347
|
driverConfig: new PGLiteDriverConfig(),
|
|
348
|
+
uowConfig: config.uowConfig,
|
|
349
|
+
outbox: config.outbox,
|
|
273
350
|
});
|
|
274
351
|
|
|
352
|
+
internalSchemaConfig = await runInternalFragmentMigrations(adapter);
|
|
353
|
+
|
|
275
354
|
// Run migrations for all schemas
|
|
276
355
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
277
|
-
const ormMap = new Map<string, SimpleQueryInterface<any, any>>();
|
|
278
|
-
|
|
279
|
-
const databaseDeps = internalFragmentDef.dependencies?.({
|
|
280
|
-
config: {},
|
|
281
|
-
options: { databaseAdapter: adapter },
|
|
282
|
-
});
|
|
283
|
-
if (databaseDeps?.schema) {
|
|
284
|
-
const migrations = adapter.prepareMigrations(databaseDeps.schema, databaseDeps.namespace);
|
|
285
|
-
await migrations.executeWithDriver(adapter.driver, 0);
|
|
286
|
-
}
|
|
356
|
+
const ormMap = new Map<string | null, SimpleQueryInterface<any, any>>();
|
|
287
357
|
|
|
288
358
|
for (const { schema, namespace, migrateToVersion } of schemas) {
|
|
289
359
|
const preparedMigrations = adapter.prepareMigrations(schema, namespace);
|
|
@@ -313,12 +383,18 @@ export async function createDrizzlePgliteAdapter(
|
|
|
313
383
|
throw new Error("resetDatabase is only supported for in-memory databases");
|
|
314
384
|
}
|
|
315
385
|
|
|
386
|
+
const schemasToTruncate = internalSchemaConfig ? [internalSchemaConfig, ...schemas] : schemas;
|
|
387
|
+
|
|
316
388
|
// Truncate all tables by deleting rows
|
|
317
|
-
for (const { schema, namespace } of
|
|
318
|
-
const
|
|
319
|
-
for (const tableName of
|
|
320
|
-
const physicalTableName =
|
|
321
|
-
|
|
389
|
+
for (const { schema, namespace } of schemasToTruncate) {
|
|
390
|
+
const tableNames = Object.keys(schema.tables).slice().reverse();
|
|
391
|
+
for (const tableName of tableNames) {
|
|
392
|
+
const physicalTableName = adapter.namingStrategy.tableName(tableName, namespace);
|
|
393
|
+
const schemaName = resolveSchemaName(adapter, namespace);
|
|
394
|
+
const qualifiedTable = schemaName
|
|
395
|
+
? `"${schemaName}"."${physicalTableName}"`
|
|
396
|
+
: `"${physicalTableName}"`;
|
|
397
|
+
await drizzleDb.execute(`DELETE FROM ${qualifiedTable}`);
|
|
322
398
|
}
|
|
323
399
|
}
|
|
324
400
|
};
|
|
@@ -354,6 +430,89 @@ export async function createDrizzlePgliteAdapter(
|
|
|
354
430
|
};
|
|
355
431
|
}
|
|
356
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Create InMemory adapter (no migrations required).
|
|
435
|
+
*/
|
|
436
|
+
export async function createInMemoryAdapter(
|
|
437
|
+
config: InMemoryAdapterConfig,
|
|
438
|
+
schemas: SchemaConfig[],
|
|
439
|
+
): Promise<AdapterFactoryResult<InMemoryAdapterConfig>> {
|
|
440
|
+
const adapter = new InMemoryAdapter(config.options);
|
|
441
|
+
|
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
443
|
+
const ormMap = new Map<string | null, SimpleQueryInterface<any, any>>();
|
|
444
|
+
for (const { schema, namespace } of schemas) {
|
|
445
|
+
const orm = adapter.createQueryEngine(schema, namespace);
|
|
446
|
+
ormMap.set(namespace, orm);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const resetDatabase = async () => {
|
|
450
|
+
await adapter.reset();
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const cleanup = async () => {
|
|
454
|
+
await adapter.close();
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const commonMethods = createCommonTestContextMethods(ormMap);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
testContext: {
|
|
461
|
+
get adapter() {
|
|
462
|
+
return adapter;
|
|
463
|
+
},
|
|
464
|
+
...commonMethods,
|
|
465
|
+
resetDatabase,
|
|
466
|
+
cleanup,
|
|
467
|
+
},
|
|
468
|
+
get adapter() {
|
|
469
|
+
return adapter;
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Create ModelChecker adapter (wraps the in-memory adapter).
|
|
476
|
+
*/
|
|
477
|
+
export async function createModelCheckerAdapter(
|
|
478
|
+
config: ModelCheckerAdapterConfig,
|
|
479
|
+
schemas: SchemaConfig[],
|
|
480
|
+
): Promise<AdapterFactoryResult<ModelCheckerAdapterConfig>> {
|
|
481
|
+
const baseAdapter = new InMemoryAdapter(config.options);
|
|
482
|
+
const adapter = new ModelCheckerAdapter(baseAdapter);
|
|
483
|
+
|
|
484
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
485
|
+
const ormMap = new Map<string | null, SimpleQueryInterface<any, any>>();
|
|
486
|
+
for (const { schema, namespace } of schemas) {
|
|
487
|
+
const orm = adapter.createQueryEngine(schema, namespace);
|
|
488
|
+
ormMap.set(namespace, orm);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const resetDatabase = async () => {
|
|
492
|
+
await baseAdapter.reset();
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const cleanup = async () => {
|
|
496
|
+
await adapter.close();
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const commonMethods = createCommonTestContextMethods(ormMap);
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
testContext: {
|
|
503
|
+
get adapter() {
|
|
504
|
+
return adapter;
|
|
505
|
+
},
|
|
506
|
+
...commonMethods,
|
|
507
|
+
resetDatabase,
|
|
508
|
+
cleanup,
|
|
509
|
+
},
|
|
510
|
+
get adapter() {
|
|
511
|
+
return adapter;
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
357
516
|
/**
|
|
358
517
|
* Create adapter based on configuration
|
|
359
518
|
* Supports multiple schemas with separate namespaces
|
|
@@ -368,6 +527,10 @@ export async function createAdapter<T extends SupportedAdapter>(
|
|
|
368
527
|
return createKyselyPgliteAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
|
|
369
528
|
} else if (adapterConfig.type === "drizzle-pglite") {
|
|
370
529
|
return createDrizzlePgliteAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
|
|
530
|
+
} else if (adapterConfig.type === "in-memory") {
|
|
531
|
+
return createInMemoryAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
|
|
532
|
+
} else if (adapterConfig.type === "model-checker") {
|
|
533
|
+
return createModelCheckerAdapter(adapterConfig, schemas) as Promise<AdapterFactoryResult<T>>;
|
|
371
534
|
}
|
|
372
535
|
|
|
373
536
|
throw new Error(`Unsupported adapter type: ${(adapterConfig as SupportedAdapter).type}`);
|
package/src/db-test.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { buildDatabaseFragmentsTest } from "./db-test";
|
|
8
8
|
|
|
9
9
|
// Test schema with users table
|
|
10
|
-
const userSchema = schema((s) => {
|
|
10
|
+
const userSchema = schema("user", (s) => {
|
|
11
11
|
return s.addTable("users", (t) => {
|
|
12
12
|
return t
|
|
13
13
|
.addColumn("id", idColumn())
|
|
@@ -18,7 +18,7 @@ const userSchema = schema((s) => {
|
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
// Test schema with posts table
|
|
21
|
-
const postSchema = schema((s) => {
|
|
21
|
+
const postSchema = schema("post", (s) => {
|
|
22
22
|
return s.addTable("posts", (t) => {
|
|
23
23
|
return t
|
|
24
24
|
.addColumn("id", idColumn())
|
package/src/db-test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import type { DatabaseAdapter } from "@fragno-dev/db";
|
|
17
17
|
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
18
18
|
import type { BaseTestContext } from ".";
|
|
19
|
+
import { drainDurableHooks } from "./durable-hooks";
|
|
19
20
|
|
|
20
21
|
// BoundServices is an internal type that strips 'this' parameters from service methods
|
|
21
22
|
// It's used to represent services after they've been bound to a context
|
|
@@ -303,14 +304,26 @@ export class DatabaseFragmentsTestBuilder<
|
|
|
303
304
|
|
|
304
305
|
// Extract schema and namespace from definition by calling dependencies with a mock adapter
|
|
305
306
|
let schema: AnySchema | undefined;
|
|
306
|
-
let namespace: string | undefined;
|
|
307
|
+
let namespace: string | null | undefined;
|
|
307
308
|
|
|
308
309
|
if (definition.dependencies) {
|
|
309
310
|
try {
|
|
310
311
|
// Create a mock adapter to extract the schema
|
|
311
312
|
const mockAdapter = {
|
|
312
313
|
createQueryEngine: () => ({ schema: null }),
|
|
314
|
+
getSchemaVersion: async () => undefined,
|
|
315
|
+
namingStrategy: {
|
|
316
|
+
namespaceScope: "suffix",
|
|
317
|
+
namespaceToSchema: (value: string) => value,
|
|
318
|
+
tableName: (logicalTable: string, ns: string | null) =>
|
|
319
|
+
ns ? `${logicalTable}_${ns}` : logicalTable,
|
|
320
|
+
columnName: (logicalColumn: string) => logicalColumn,
|
|
321
|
+
indexName: (logicalIndex: string) => logicalIndex,
|
|
322
|
+
uniqueIndexName: (logicalIndex: string) => logicalIndex,
|
|
323
|
+
foreignKeyName: ({ referenceName }: { referenceName: string }) => referenceName,
|
|
324
|
+
},
|
|
313
325
|
contextStorage: { run: (_data: unknown, fn: () => unknown) => fn() },
|
|
326
|
+
close: async () => {},
|
|
314
327
|
};
|
|
315
328
|
|
|
316
329
|
// Use the actual config from the builder instead of an empty mock
|
|
@@ -353,7 +366,7 @@ export class DatabaseFragmentsTestBuilder<
|
|
|
353
366
|
);
|
|
354
367
|
}
|
|
355
368
|
|
|
356
|
-
if (
|
|
369
|
+
if (namespace === undefined) {
|
|
357
370
|
throw new Error(
|
|
358
371
|
`Fragment '${definition.name}' does not have a namespace in dependencies. ` +
|
|
359
372
|
`This should be automatically provided by withDatabase().`,
|
|
@@ -374,7 +387,6 @@ export class DatabaseFragmentsTestBuilder<
|
|
|
374
387
|
});
|
|
375
388
|
}
|
|
376
389
|
|
|
377
|
-
// Create adapter with all schemas
|
|
378
390
|
const { testContext, adapter } = await createAdapter(adapterConfig, schemaConfigs);
|
|
379
391
|
|
|
380
392
|
// Helper to create fragments with service wiring
|
|
@@ -510,9 +522,47 @@ export class DatabaseFragmentsTestBuilder<
|
|
|
510
522
|
throw new Error("At least one fragment must be added");
|
|
511
523
|
}
|
|
512
524
|
|
|
525
|
+
const originalCleanup = testContext.cleanup;
|
|
526
|
+
const cleanup = async () => {
|
|
527
|
+
let drainError: unknown;
|
|
528
|
+
let cleanupError: unknown;
|
|
529
|
+
|
|
530
|
+
for (const result of fragmentResults) {
|
|
531
|
+
try {
|
|
532
|
+
await drainDurableHooks(result.fragment);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (!drainError) {
|
|
535
|
+
drainError = error;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
await originalCleanup();
|
|
542
|
+
} catch (error) {
|
|
543
|
+
cleanupError = error;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (drainError && cleanupError) {
|
|
547
|
+
throw new AggregateError(
|
|
548
|
+
[drainError, cleanupError],
|
|
549
|
+
"Failed to drain durable hooks and clean up test context",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (drainError) {
|
|
554
|
+
throw drainError;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (cleanupError) {
|
|
558
|
+
throw cleanupError;
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
|
|
513
562
|
const finalTestContext = {
|
|
514
563
|
...testContext,
|
|
515
564
|
resetDatabase,
|
|
565
|
+
cleanup,
|
|
516
566
|
adapter,
|
|
517
567
|
inContext: firstFragment.inContext.bind(firstFragment),
|
|
518
568
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDurableHooksProcessor,
|
|
3
|
+
type AnyFragnoInstantiatedDatabaseFragment,
|
|
4
|
+
} from "@fragno-dev/db";
|
|
5
|
+
import type { AnyFragnoInstantiatedFragment } from "@fragno-dev/core";
|
|
6
|
+
|
|
7
|
+
export async function drainDurableHooks(fragment: AnyFragnoInstantiatedFragment): Promise<void> {
|
|
8
|
+
const processor = createDurableHooksProcessor(fragment as AnyFragnoInstantiatedDatabaseFragment);
|
|
9
|
+
if (!processor) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
await processor.drain();
|
|
13
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
2
|
import { column, idColumn, schema } from "@fragno-dev/db/schema";
|
|
3
|
-
import { withDatabase } from "@fragno-dev/db";
|
|
3
|
+
import { Cursor, withDatabase } from "@fragno-dev/db";
|
|
4
4
|
import { defineFragment } from "@fragno-dev/core";
|
|
5
5
|
import { instantiate } from "@fragno-dev/core";
|
|
6
6
|
import { buildDatabaseFragmentsTest } from "./db-test";
|
|
7
7
|
import type { ExtractFragmentServices } from "@fragno-dev/core/route";
|
|
8
8
|
|
|
9
9
|
// Test schema with multiple versions
|
|
10
|
-
const testSchema = schema((s) => {
|
|
10
|
+
const testSchema = schema("test", (s) => {
|
|
11
11
|
return s
|
|
12
12
|
.addTable("users", (t) => {
|
|
13
13
|
return t
|
|
14
14
|
.addColumn("id", idColumn())
|
|
15
15
|
.addColumn("name", column("string"))
|
|
16
16
|
.addColumn("email", column("string"))
|
|
17
|
+
.createIndex("idx_users_name", ["name"])
|
|
17
18
|
.createIndex("idx_users_all", ["id"]); // Index for querying
|
|
18
19
|
})
|
|
19
20
|
.alterTable("users", (t) => {
|
|
@@ -36,6 +37,18 @@ const testFragmentDef = defineFragment<{}>("test-fragment")
|
|
|
36
37
|
);
|
|
37
38
|
return users.map((u) => ({ ...u, id: u.id.valueOf() }));
|
|
38
39
|
},
|
|
40
|
+
getUsersWithCursor: async (cursor?: Cursor | string) => {
|
|
41
|
+
return deps.db.findWithCursor("users", (b) => {
|
|
42
|
+
let builder = b
|
|
43
|
+
.whereIndex("idx_users_name")
|
|
44
|
+
.orderByIndex("idx_users_name", "asc")
|
|
45
|
+
.pageSize(2);
|
|
46
|
+
if (cursor) {
|
|
47
|
+
builder = builder.after(cursor);
|
|
48
|
+
}
|
|
49
|
+
return builder;
|
|
50
|
+
});
|
|
51
|
+
},
|
|
39
52
|
};
|
|
40
53
|
})
|
|
41
54
|
.build();
|
|
@@ -83,6 +96,70 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
83
96
|
).rejects.toThrow("Fragment 'non-db-fragment' does not have a database schema");
|
|
84
97
|
});
|
|
85
98
|
|
|
99
|
+
it("should support the in-memory adapter", async () => {
|
|
100
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
101
|
+
.withTestAdapter({ type: "in-memory" })
|
|
102
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
103
|
+
.build();
|
|
104
|
+
|
|
105
|
+
const user = await fragments.test.services.createUser({
|
|
106
|
+
name: "Memory User",
|
|
107
|
+
email: "memory@example.com",
|
|
108
|
+
age: 31,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(user).toMatchObject({
|
|
112
|
+
id: expect.any(String),
|
|
113
|
+
name: "Memory User",
|
|
114
|
+
email: "memory@example.com",
|
|
115
|
+
age: 31,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const users = await fragments.test.services.getUsers();
|
|
119
|
+
expect(users).toHaveLength(1);
|
|
120
|
+
expect(users[0]).toMatchObject(user);
|
|
121
|
+
|
|
122
|
+
await test.cleanup();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should support cursor pagination with in-memory adapter", async () => {
|
|
126
|
+
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
127
|
+
.withTestAdapter({ type: "in-memory" })
|
|
128
|
+
.withFragment("test", instantiate(testFragmentDef).withConfig({}).withRoutes([]))
|
|
129
|
+
.build();
|
|
130
|
+
|
|
131
|
+
const fragment = fragments.test;
|
|
132
|
+
|
|
133
|
+
const users = [
|
|
134
|
+
{ name: "Alice", email: "alice@example.com" },
|
|
135
|
+
{ name: "Brett", email: "brett@example.com" },
|
|
136
|
+
{ name: "Cora", email: "cora@example.com" },
|
|
137
|
+
{ name: "Dylan", email: "dylan@example.com" },
|
|
138
|
+
{ name: "Emma", email: "emma@example.com" },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const user of users) {
|
|
142
|
+
await fragment.services.createUser(user);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const firstPage = await fragment.services.getUsersWithCursor();
|
|
146
|
+
expect(firstPage.items.map((item) => item.name)).toEqual(["Alice", "Brett"]);
|
|
147
|
+
expect(firstPage.hasNextPage).toBe(true);
|
|
148
|
+
expect(firstPage.cursor).toBeDefined();
|
|
149
|
+
|
|
150
|
+
const secondPage = await fragment.services.getUsersWithCursor(firstPage.cursor);
|
|
151
|
+
expect(secondPage.items.map((item) => item.name)).toEqual(["Cora", "Dylan"]);
|
|
152
|
+
expect(secondPage.hasNextPage).toBe(true);
|
|
153
|
+
expect(secondPage.cursor).toBeDefined();
|
|
154
|
+
|
|
155
|
+
const thirdPage = await fragment.services.getUsersWithCursor(secondPage.cursor);
|
|
156
|
+
expect(thirdPage.items.map((item) => item.name)).toEqual(["Emma"]);
|
|
157
|
+
expect(thirdPage.hasNextPage).toBe(false);
|
|
158
|
+
expect(thirdPage.cursor).toBeUndefined();
|
|
159
|
+
|
|
160
|
+
await test.cleanup();
|
|
161
|
+
});
|
|
162
|
+
|
|
86
163
|
it("should reset database by truncating tables", async () => {
|
|
87
164
|
const { fragments, test } = await buildDatabaseFragmentsTest()
|
|
88
165
|
.withTestAdapter({ type: "kysely-sqlite" })
|
|
@@ -149,7 +226,7 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
149
226
|
|
|
150
227
|
it("should work with multi-table schema", async () => {
|
|
151
228
|
// Simplified auth schema for testing
|
|
152
|
-
const authSchema = schema((s) => {
|
|
229
|
+
const authSchema = schema("auth", (s) => {
|
|
153
230
|
return s
|
|
154
231
|
.addTable("user", (t) => {
|
|
155
232
|
return t
|
|
@@ -214,7 +291,7 @@ describe("buildDatabaseFragmentsTest", () => {
|
|
|
214
291
|
|
|
215
292
|
describe("multi-fragment tests", () => {
|
|
216
293
|
// Create two different schemas
|
|
217
|
-
const userSchema = schema((s) => {
|
|
294
|
+
const userSchema = schema("user", (s) => {
|
|
218
295
|
return s.addTable("user", (t) => {
|
|
219
296
|
return t
|
|
220
297
|
.addColumn("id", idColumn())
|
|
@@ -224,7 +301,7 @@ describe("multi-fragment tests", () => {
|
|
|
224
301
|
});
|
|
225
302
|
});
|
|
226
303
|
|
|
227
|
-
const postSchema = schema((s) => {
|
|
304
|
+
const postSchema = schema("post", (s) => {
|
|
228
305
|
return s.addTable("post", (t) => {
|
|
229
306
|
return t
|
|
230
307
|
.addColumn("id", idColumn())
|
|
@@ -325,7 +402,7 @@ describe("multi-fragment tests", () => {
|
|
|
325
402
|
|
|
326
403
|
describe("ExtractFragmentServices", () => {
|
|
327
404
|
it("extracts provided services from database fragment with new API", () => {
|
|
328
|
-
const testSchema = schema((s) => s);
|
|
405
|
+
const testSchema = schema("test", (s) => s);
|
|
329
406
|
|
|
330
407
|
interface ITestService {
|
|
331
408
|
doSomething: (input: string) => Promise<string>;
|
|
@@ -352,7 +429,7 @@ describe("ExtractFragmentServices", () => {
|
|
|
352
429
|
});
|
|
353
430
|
|
|
354
431
|
it("merges base services and provided services in database fragment", () => {
|
|
355
|
-
const testSchema = schema((s) => s);
|
|
432
|
+
const testSchema = schema("test", (s) => s);
|
|
356
433
|
|
|
357
434
|
const fragment = defineFragment<{}>("test-db-fragment")
|
|
358
435
|
.extend(withDatabase(testSchema))
|