@fragno-dev/db 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.turbo/turbo-build.log +40 -37
  2. package/CHANGELOG.md +19 -0
  3. package/dist/adapters/drizzle/drizzle-query.d.ts +1 -0
  4. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
  5. package/dist/adapters/drizzle/drizzle-query.js +41 -38
  6. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  7. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +2 -0
  8. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  9. package/dist/adapters/drizzle/drizzle-uow-compiler.js +13 -1
  10. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  11. package/dist/adapters/drizzle/shared.d.ts +1 -0
  12. package/dist/adapters/kysely/kysely-adapter.d.ts +3 -2
  13. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  14. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  15. package/dist/adapters/kysely/kysely-query-builder.js +23 -12
  16. package/dist/adapters/kysely/kysely-query-builder.js.map +1 -1
  17. package/dist/adapters/kysely/kysely-query.d.ts +22 -0
  18. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
  19. package/dist/adapters/kysely/kysely-query.js +72 -50
  20. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  21. package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
  22. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  23. package/dist/migration-engine/generation-engine.d.ts +1 -1
  24. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  25. package/dist/migration-engine/generation-engine.js.map +1 -1
  26. package/dist/mod.d.ts +5 -5
  27. package/dist/mod.d.ts.map +1 -1
  28. package/dist/mod.js.map +1 -1
  29. package/dist/query/query.d.ts +24 -8
  30. package/dist/query/query.d.ts.map +1 -1
  31. package/dist/query/result-transform.js +17 -5
  32. package/dist/query/result-transform.js.map +1 -1
  33. package/dist/query/unit-of-work.d.ts +5 -4
  34. package/dist/query/unit-of-work.d.ts.map +1 -1
  35. package/dist/query/unit-of-work.js +2 -3
  36. package/dist/query/unit-of-work.js.map +1 -1
  37. package/dist/schema/serialize.js +2 -0
  38. package/dist/schema/serialize.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +170 -50
  41. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +89 -35
  42. package/src/adapters/drizzle/drizzle-query.test.ts +56 -6
  43. package/src/adapters/drizzle/drizzle-query.ts +68 -63
  44. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +63 -3
  45. package/src/adapters/drizzle/drizzle-uow-compiler.ts +27 -2
  46. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +88 -0
  47. package/src/adapters/kysely/kysely-adapter.ts +6 -3
  48. package/src/adapters/kysely/kysely-query-builder.ts +35 -11
  49. package/src/adapters/kysely/kysely-query.test.ts +498 -0
  50. package/src/adapters/kysely/kysely-query.ts +137 -82
  51. package/src/adapters/kysely/kysely-uow-compiler.test.ts +66 -0
  52. package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
  53. package/src/migration-engine/generation-engine.ts +2 -1
  54. package/src/mod.ts +6 -6
  55. package/src/query/query-type.test.ts +34 -14
  56. package/src/query/query.ts +77 -36
  57. package/src/query/result-transform.test.ts +5 -5
  58. package/src/query/result-transform.ts +29 -11
  59. package/src/query/unit-of-work.ts +8 -11
  60. package/src/schema/serialize.test.ts +223 -0
  61. package/src/schema/serialize.ts +16 -0
@@ -1,6 +1,11 @@
1
- import type { AbstractQuery } from "../../query/query";
2
- import type { AnySchema } from "../../schema/create";
3
- import type { CompiledMutation, UOWDecoder, UOWExecutor } from "../../query/unit-of-work";
1
+ import type { AbstractQuery, TableToUpdateValues } from "../../query/query";
2
+ import type { AnySchema, AnyTable } from "../../schema/create";
3
+ import type {
4
+ CompiledMutation,
5
+ UOWDecoder,
6
+ UOWExecutor,
7
+ ValidIndexName,
8
+ } from "../../query/unit-of-work";
4
9
  import { decodeResult } from "../../query/result-transform";
5
10
  import { createKyselyUOWCompiler } from "./kysely-uow-compiler";
6
11
  import { executeKyselyRetrievalPhase, executeKyselyMutationPhase } from "./kysely-uow-executor";
@@ -13,6 +18,53 @@ import type { SQLProvider } from "../../shared/providers";
13
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
19
  type KyselyAny = Kysely<any>;
15
20
 
21
+ /**
22
+ * Configuration options for creating a Kysely Unit of Work
23
+ */
24
+ export interface KyselyUOWConfig {
25
+ /**
26
+ * Optional callback to receive compiled SQL queries for logging/debugging
27
+ * This callback is invoked for each query as it's compiled
28
+ */
29
+ onQuery?: (query: CompiledQuery) => void;
30
+ /**
31
+ * If true, the query will not be executed and the query will be returned. Not respected for UOWs
32
+ * since those have to be manually executed.
33
+ */
34
+ dryRun?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Special builder for updateMany operations that captures configuration
39
+ */
40
+ class UpdateManySpecialBuilder<TTable extends AnyTable> {
41
+ #indexName?: string;
42
+ #condition?: unknown;
43
+ #setValues?: TableToUpdateValues<TTable>;
44
+
45
+ whereIndex<TIndexName extends ValidIndexName<TTable>>(
46
+ indexName: TIndexName,
47
+ condition?: unknown,
48
+ ): this {
49
+ this.#indexName = indexName as string;
50
+ this.#condition = condition;
51
+ return this;
52
+ }
53
+
54
+ set(values: TableToUpdateValues<TTable>): this {
55
+ this.#setValues = values;
56
+ return this;
57
+ }
58
+
59
+ getConfig() {
60
+ return {
61
+ indexName: this.#indexName,
62
+ condition: this.#condition,
63
+ setValues: this.#setValues,
64
+ };
65
+ }
66
+ }
67
+
16
68
  /**
17
69
  * Creates a Kysely-based query engine for the given schema.
18
70
  *
@@ -42,12 +94,18 @@ export function fromKysely<T extends AnySchema>(
42
94
  pool: ConnectionPool<KyselyAny>,
43
95
  provider: SQLProvider,
44
96
  mapper?: TableNameMapper,
45
- ): AbstractQuery<T> {
46
- function createUOW(name?: string): UnitOfWork<T, []> {
97
+ uowConfig?: KyselyUOWConfig,
98
+ ): AbstractQuery<T, KyselyUOWConfig> {
99
+ function createUOW(opts: { name?: string; config?: KyselyUOWConfig }) {
47
100
  const uowCompiler = createKyselyUOWCompiler(schema, pool, provider, mapper);
48
101
 
49
102
  const executor: UOWExecutor<CompiledQuery, unknown> = {
50
103
  async executeRetrievalPhase(retrievalBatch: CompiledQuery[]) {
104
+ // In dryRun mode, skip execution and return empty results
105
+ if (opts.config?.dryRun) {
106
+ return retrievalBatch.map(() => []);
107
+ }
108
+
51
109
  const conn = await pool.connect();
52
110
  try {
53
111
  return await executeKyselyRetrievalPhase(conn.db, retrievalBatch);
@@ -56,6 +114,14 @@ export function fromKysely<T extends AnySchema>(
56
114
  }
57
115
  },
58
116
  async executeMutationPhase(mutationBatch: CompiledMutation<CompiledQuery>[]) {
117
+ // In dryRun mode, skip execution and return success with mock internal IDs
118
+ if (opts.config?.dryRun) {
119
+ return {
120
+ success: true,
121
+ createdInternalIds: mutationBatch.map(() => null),
122
+ };
123
+ }
124
+
59
125
  const conn = await pool.connect();
60
126
  try {
61
127
  return await executeKyselyMutationPhase(conn.db, mutationBatch);
@@ -97,13 +163,30 @@ export function fromKysely<T extends AnySchema>(
97
163
  });
98
164
  };
99
165
 
100
- return new UnitOfWork(schema, uowCompiler, executor, decoder, name);
166
+ const { onQuery, ...restUowConfig } = opts.config ?? {};
167
+
168
+ return new UnitOfWork(schema, uowCompiler, executor, decoder, opts.name, {
169
+ ...restUowConfig,
170
+ onQuery: (query) => {
171
+ // CompiledMutation has { query: CompiledQuery, expectedAffectedRows: number | null }
172
+ // CompiledQuery has { query: QueryAST, sql: string, parameters: unknown[] }
173
+ // Check for expectedAffectedRows to distinguish CompiledMutation from CompiledQuery
174
+ const actualQuery =
175
+ query && typeof query === "object" && "expectedAffectedRows" in query
176
+ ? (query as CompiledMutation<CompiledQuery>).query
177
+ : (query as CompiledQuery);
178
+
179
+ opts.config?.onQuery?.(actualQuery);
180
+ },
181
+ });
101
182
  }
102
183
 
103
184
  return {
104
185
  async find(tableName, builderFn) {
105
- const uow = createUOW();
106
- uow.find(tableName, builderFn);
186
+ const uow = createUOW({ config: uowConfig });
187
+ // Safe: builderFn returns a FindBuilder (or void), which matches UnitOfWork signature
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ uow.find(tableName, builderFn as any);
107
190
  // executeRetrieve returns an array of results (one per find operation)
108
191
  // Since we only have one find, unwrap the first result
109
192
  const [result]: unknown[][] = await uow.executeRetrieve();
@@ -111,9 +194,12 @@ export function fromKysely<T extends AnySchema>(
111
194
  },
112
195
 
113
196
  async findFirst(tableName, builderFn) {
114
- const uow = createUOW();
197
+ const uow = createUOW({ config: uowConfig });
115
198
  if (builderFn) {
116
- uow.find(tableName, (b) => builderFn(b as never).pageSize(1));
199
+ uow.find(tableName, (b) => {
200
+ builderFn(b);
201
+ return b.pageSize(1);
202
+ });
117
203
  } else {
118
204
  uow.find(tableName, (b) => b.whereIndex("primary").pageSize(1));
119
205
  }
@@ -123,16 +209,15 @@ export function fromKysely<T extends AnySchema>(
123
209
  },
124
210
 
125
211
  async create(tableName, values) {
126
- const uow = createUOW();
212
+ const uow = createUOW({ config: uowConfig });
127
213
  uow.create(tableName, values);
128
214
  const { success } = await uow.executeMutations();
129
215
  if (!success) {
130
- // This should not happen because we don't `.check()` this call.
131
- // TODO: Verify what happens when there are unique constraints
132
216
  throw new Error("Failed to create record");
133
217
  }
134
218
 
135
- const [createdId] = uow.getCreatedIds();
219
+ const createdIds = uow.getCreatedIds();
220
+ const createdId = createdIds[0];
136
221
  if (!createdId) {
137
222
  throw new Error("Failed to get created ID");
138
223
  }
@@ -140,7 +225,7 @@ export function fromKysely<T extends AnySchema>(
140
225
  },
141
226
 
142
227
  async createMany(tableName, valuesArray) {
143
- const uow = createUOW();
228
+ const uow = createUOW({ config: uowConfig });
144
229
  for (const values of valuesArray) {
145
230
  uow.create(tableName, values);
146
231
  }
@@ -153,8 +238,8 @@ export function fromKysely<T extends AnySchema>(
153
238
  },
154
239
 
155
240
  async update(tableName, id, builderFn) {
156
- const uow = createUOW();
157
- uow.update(tableName, id, builderFn as never);
241
+ const uow = createUOW({ config: uowConfig });
242
+ uow.update(tableName, id, builderFn);
158
243
  const { success } = await uow.executeMutations();
159
244
  if (!success) {
160
245
  throw new Error("Failed to update record (version conflict or record not found)");
@@ -162,51 +247,41 @@ export function fromKysely<T extends AnySchema>(
162
247
  },
163
248
 
164
249
  async updateMany(tableName, builderFn) {
165
- // Create a special builder that captures both where and set operations
166
- let whereConfig: { indexName?: string; condition?: unknown } = {};
167
- let setValues: unknown;
168
-
169
- const specialBuilder = {
170
- whereIndex(indexName: string, condition?: unknown) {
171
- whereConfig = { indexName, condition };
172
- return this;
173
- },
174
- set(values: unknown) {
175
- setValues = values;
176
- return this;
177
- },
178
- };
250
+ const table = schema.tables[tableName];
251
+ if (!table) {
252
+ throw new Error(`Table ${tableName} not found in schema`);
253
+ }
179
254
 
255
+ const specialBuilder = new UpdateManySpecialBuilder<typeof table>();
180
256
  builderFn(specialBuilder);
181
257
 
182
- if (!whereConfig.indexName) {
258
+ const { indexName, condition, setValues } = specialBuilder.getConfig();
259
+
260
+ if (!indexName) {
183
261
  throw new Error("whereIndex() must be called in updateMany");
184
262
  }
185
263
  if (!setValues) {
186
264
  throw new Error("set() must be called in updateMany");
187
265
  }
188
266
 
189
- // First, find all matching records
190
- const findUow = createUOW();
267
+ const findUow = createUOW({ config: uowConfig });
191
268
  findUow.find(tableName, (b) => {
192
- if (whereConfig.condition) {
193
- return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
269
+ if (condition) {
270
+ // Safe: condition is captured from whereIndex call with proper typing
271
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
+ return b.whereIndex(indexName as ValidIndexName<typeof table>, condition as any);
194
273
  }
195
- return b.whereIndex(whereConfig.indexName as never);
274
+ return b.whereIndex(indexName as ValidIndexName<typeof table>);
196
275
  });
197
- const findResults: unknown[][] = await findUow.executeRetrieve();
198
- const records = findResults[0];
276
+ const [records]: unknown[][] = await findUow.executeRetrieve();
199
277
 
200
278
  if (!records || records.length === 0) {
201
279
  return;
202
280
  }
203
281
 
204
- // Now update all found records
205
- const updateUow = createUOW();
206
- for (const record of records as never as Array<{ id: unknown }>) {
207
- updateUow.update(tableName as string, record.id as string, (b) =>
208
- b.set(setValues as never),
209
- );
282
+ const updateUow = createUOW({ config: uowConfig });
283
+ for (const record of records as Array<{ id: unknown }>) {
284
+ updateUow.update(tableName, record.id as string, (b) => b.set(setValues));
210
285
  }
211
286
  const { success } = await updateUow.executeMutations();
212
287
  if (!success) {
@@ -215,8 +290,8 @@ export function fromKysely<T extends AnySchema>(
215
290
  },
216
291
 
217
292
  async delete(tableName, id, builderFn) {
218
- const uow = createUOW();
219
- uow.delete(tableName, id, builderFn as never);
293
+ const uow = createUOW({ config: uowConfig });
294
+ uow.delete(tableName, id, builderFn);
220
295
  const { success } = await uow.executeMutations();
221
296
  if (!success) {
222
297
  throw new Error("Failed to delete record (version conflict or record not found)");
@@ -224,43 +299,17 @@ export function fromKysely<T extends AnySchema>(
224
299
  },
225
300
 
226
301
  async deleteMany(tableName, builderFn) {
227
- // Create a special builder that captures where configuration
228
- let whereConfig: { indexName?: string; condition?: unknown } = {};
229
-
230
- const specialBuilder = {
231
- whereIndex(indexName: string, condition?: unknown) {
232
- whereConfig = { indexName, condition };
233
- return this;
234
- },
235
- };
236
-
237
- // Safe: Call builderFn to capture the configuration
238
- builderFn(specialBuilder as never);
302
+ const findUow = createUOW({ config: uowConfig });
303
+ findUow.find(tableName, builderFn);
304
+ const [records]: unknown[][] = await findUow.executeRetrieve();
239
305
 
240
- if (!whereConfig.indexName) {
241
- throw new Error("whereIndex() must be called in deleteMany");
242
- }
243
-
244
- // First, find all matching records
245
- const findUow = createUOW();
246
- findUow.find(tableName as string, (b) => {
247
- if (whereConfig.condition) {
248
- return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
249
- }
250
- return b.whereIndex(whereConfig.indexName as never);
251
- });
252
- const findResults2 = await findUow.executeRetrieve();
253
- const records = (findResults2 as unknown as [unknown])[0];
254
-
255
- // @ts-expect-error - Type narrowing doesn't work through unknown cast
256
306
  if (!records || records.length === 0) {
257
307
  return;
258
308
  }
259
309
 
260
- // Now delete all found records
261
- const deleteUow = createUOW();
262
- for (const record of records as never as Array<{ id: unknown }>) {
263
- deleteUow.delete(tableName as string, record.id as string);
310
+ const deleteUow = createUOW({ config: uowConfig });
311
+ for (const record of records as Array<{ id: unknown }>) {
312
+ deleteUow.delete(tableName, record.id as string);
264
313
  }
265
314
  const { success } = await deleteUow.executeMutations();
266
315
  if (!success) {
@@ -268,8 +317,14 @@ export function fromKysely<T extends AnySchema>(
268
317
  }
269
318
  },
270
319
 
271
- createUnitOfWork(name) {
272
- return createUOW(name);
320
+ createUnitOfWork(name, nestedUowConfig) {
321
+ return createUOW({
322
+ name,
323
+ config: {
324
+ ...uowConfig,
325
+ ...nestedUowConfig,
326
+ },
327
+ });
273
328
  },
274
- } as AbstractQuery<T>;
329
+ } as AbstractQuery<T, KyselyUOWConfig>;
275
330
  }
@@ -913,4 +913,70 @@ describe("kysely-uow-compiler", () => {
913
913
  expect(compiled.mutationBatch).toHaveLength(1);
914
914
  });
915
915
  });
916
+
917
+ describe("create and use ID in same UOW", () => {
918
+ it("should support creating and using ID in same UOW", () => {
919
+ const uow = createTestUOW("create-user-and-post");
920
+
921
+ // Create user and capture the returned ID
922
+ const userId = uow.create("users", {
923
+ name: "John Doe",
924
+ email: "john@example.com",
925
+ age: 30,
926
+ });
927
+
928
+ // Use the returned FragnoId directly to create a post
929
+ // The compiler should extract externalId and generate a subquery
930
+ uow.create("posts", {
931
+ userId: userId,
932
+ title: "My First Post",
933
+ content: "This is my first post",
934
+ });
935
+
936
+ const compiler = createKyselyUOWCompiler(testSchema, pool, "postgresql");
937
+ const compiled = uow.compile(compiler);
938
+
939
+ // Should have no retrieval operations
940
+ expect(compiled.retrievalBatch).toHaveLength(0);
941
+
942
+ // Should have 2 mutation operations (create user, create post)
943
+ expect(compiled.mutationBatch).toHaveLength(2);
944
+
945
+ const [userCreate, postCreate] = compiled.mutationBatch;
946
+ assert(userCreate);
947
+ assert(postCreate);
948
+
949
+ // Verify user create SQL
950
+ expect(userCreate.query.sql).toMatchInlineSnapshot(
951
+ `"insert into "users" ("id", "name", "email", "age") values ($1, $2, $3, $4) returning "users"."id" as "id", "users"."name" as "name", "users"."email" as "email", "users"."age" as "age", "users"."invitedBy" as "invitedBy", "users"."_internalId" as "_internalId", "users"."_version" as "_version""`,
952
+ );
953
+ expect(userCreate.query.parameters).toMatchObject([
954
+ userId.externalId, // The generated ID
955
+ "John Doe",
956
+ "john@example.com",
957
+ 30,
958
+ ]);
959
+ expect(userCreate.expectedAffectedRows).toBeNull();
960
+
961
+ // Verify post create SQL - FragnoId generates subquery to lookup internal ID
962
+ expect(postCreate.query.sql).toMatchInlineSnapshot(
963
+ `"insert into "posts" ("id", "title", "content", "userId") values ($1, $2, $3, (select "_internalId" from "users" where "id" = $4 limit $5)) returning "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."userId" as "userId", "posts"."viewCount" as "viewCount", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version""`,
964
+ );
965
+ expect(postCreate.query.parameters).toMatchObject([
966
+ expect.any(String), // generated post ID
967
+ "My First Post",
968
+ "This is my first post",
969
+ userId.externalId, // FragnoId's externalId is used in the subquery
970
+ 1, // limit parameter
971
+ ]);
972
+ expect(postCreate.expectedAffectedRows).toBeNull();
973
+
974
+ // Verify the returned FragnoId has the expected structure
975
+ expect(userId).toMatchObject({
976
+ externalId: expect.any(String),
977
+ version: 0,
978
+ internalId: undefined,
979
+ });
980
+ });
981
+ });
916
982
  });
@@ -1,4 +1,4 @@
1
- import type { Kysely, QueryResult } from "kysely";
1
+ import type { CompiledQuery, Kysely, QueryResult } from "kysely";
2
2
  import type { CompiledMutation, MutationResult } from "../../query/unit-of-work";
3
3
 
4
4
  function getAffectedRows(result: QueryResult<unknown>): number {
@@ -43,9 +43,7 @@ function getAffectedRows(result: QueryResult<unknown>): number {
43
43
  export async function executeKyselyRetrievalPhase(
44
44
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
45
  kysely: Kysely<any>,
46
- retrievalBatch: (Kysely<unknown>["executeQuery"] extends (query: infer Q) => unknown
47
- ? Q
48
- : never)[],
46
+ retrievalBatch: CompiledQuery[],
49
47
  ): Promise<unknown[]> {
50
48
  // If no retrieval operations, return empty array immediately
51
49
  if (retrievalBatch.length === 0) {
@@ -56,8 +54,8 @@ export async function executeKyselyRetrievalPhase(
56
54
 
57
55
  // Execute all retrieval queries inside a transaction for snapshot isolation
58
56
  await kysely.transaction().execute(async (tx) => {
59
- for (const query of retrievalBatch) {
60
- const result = await tx.executeQuery(query);
57
+ for (const compiledQuery of retrievalBatch) {
58
+ const result = await tx.executeQuery(compiledQuery);
61
59
  retrievalResults.push(result.rows);
62
60
  }
63
61
  });
@@ -87,9 +85,7 @@ export async function executeKyselyRetrievalPhase(
87
85
  export async function executeKyselyMutationPhase(
88
86
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
89
87
  kysely: Kysely<any>,
90
- mutationBatch: CompiledMutation<
91
- Kysely<unknown>["executeQuery"] extends (query: infer Q) => unknown ? Q : never
92
- >[],
88
+ mutationBatch: CompiledMutation<CompiledQuery>[],
93
89
  ): Promise<MutationResult> {
94
90
  // If there are no mutations, return success immediately
95
91
  if (mutationBatch.length === 0) {
@@ -34,7 +34,8 @@ export interface ExecuteMigrationResult {
34
34
  }
35
35
 
36
36
  export async function generateMigrationsOrSchema<
37
- const TDatabases extends FragnoDatabase<AnySchema>[],
37
+ // oxlint-disable-next-line no-explicit-any
38
+ const TDatabases extends FragnoDatabase<AnySchema, any>[],
38
39
  >(
39
40
  databases: TDatabases,
40
41
  options?: {
package/src/mod.ts CHANGED
@@ -52,7 +52,7 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
52
52
  /**
53
53
  * Creates a FragnoDatabase instance by binding an adapter to this definition.
54
54
  */
55
- create(adapter: DatabaseAdapter): FragnoDatabase<T> {
55
+ create<TUOWConfig = void>(adapter: DatabaseAdapter<TUOWConfig>): FragnoDatabase<T, TUOWConfig> {
56
56
  return new FragnoDatabase({
57
57
  namespace: this.#namespace,
58
58
  schema: this.#schema,
@@ -65,12 +65,12 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
65
65
  * A Fragno database instance with a bound adapter.
66
66
  * Created from a FragnoDatabaseDefinition by calling .create(adapter).
67
67
  */
68
- export class FragnoDatabase<const T extends AnySchema> {
68
+ export class FragnoDatabase<const T extends AnySchema, TUOWConfig = void> {
69
69
  #namespace: string;
70
70
  #schema: T;
71
- #adapter: DatabaseAdapter;
71
+ #adapter: DatabaseAdapter<TUOWConfig>;
72
72
 
73
- constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter }) {
73
+ constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter<TUOWConfig> }) {
74
74
  this.#namespace = options.namespace;
75
75
  this.#schema = options.schema;
76
76
  this.#adapter = options.adapter;
@@ -80,7 +80,7 @@ export class FragnoDatabase<const T extends AnySchema> {
80
80
  return fragnoDatabaseFakeSymbol;
81
81
  }
82
82
 
83
- async createClient(): Promise<AbstractQuery<T>> {
83
+ async createClient(): Promise<AbstractQuery<T, TUOWConfig>> {
84
84
  const dbVersion = await this.#adapter.getSchemaVersion(this.#namespace);
85
85
  if (dbVersion !== this.#schema.version.toString()) {
86
86
  throw new Error(
@@ -112,7 +112,7 @@ export class FragnoDatabase<const T extends AnySchema> {
112
112
  return this.#schema;
113
113
  }
114
114
 
115
- get adapter(): DatabaseAdapter {
115
+ get adapter(): DatabaseAdapter<TUOWConfig> {
116
116
  return this.#adapter;
117
117
  }
118
118
  }
@@ -111,7 +111,11 @@ describe("query type tests", () => {
111
111
  it("should return selected columns only", () => {
112
112
  const _query = {} as Query;
113
113
 
114
- type Result = Awaited<ReturnType<typeof _query.findFirst<"users", ["name", "email"]>>>;
114
+ // Test type inference through builder pattern
115
+ function selectNameAndEmailFirst(q: Query) {
116
+ return q.findFirst("users", (b) => b.select(["name", "email"]));
117
+ }
118
+ type Result = Awaited<ReturnType<typeof selectNameAndEmailFirst>>;
115
119
 
116
120
  expectTypeOf<Result>().toExtend<{
117
121
  name: string;
@@ -122,10 +126,14 @@ describe("query type tests", () => {
122
126
  it("should handle nullable columns correctly", () => {
123
127
  const _query = {} as Query;
124
128
 
125
- type Result = Awaited<ReturnType<typeof _query.findFirst<"users", ["age"]>>>;
129
+ // Test type inference through builder pattern
130
+ function selectAge(q: Query) {
131
+ return q.findFirst("users", (b) => b.select(["age"]));
132
+ }
133
+ type Result = Awaited<ReturnType<typeof selectAge>>;
126
134
  type NonNullResult = Exclude<Result, null>;
127
135
 
128
- expectTypeOf<NonNullResult>().toMatchTypeOf<{ age: number | null }>();
136
+ expectTypeOf<NonNullResult>().toMatchObjectType<{ age: number | null }>();
129
137
  });
130
138
  });
131
139
 
@@ -148,14 +156,25 @@ describe("query type tests", () => {
148
156
  it("should return array of selected columns only", () => {
149
157
  const _query = {} as Query;
150
158
 
151
- type Result = Awaited<ReturnType<typeof _query.find<"users", ["name", "email"], object>>>;
159
+ // Test type inference through builder pattern (mimics actual usage)
160
+ function selectNameAndEmail(q: Query) {
161
+ return q.find("users", (b) => b.select(["name", "email"]));
162
+ }
152
163
 
153
- expectTypeOf<Result>().toExtend<
154
- {
155
- name: string;
156
- email: string;
157
- }[]
158
- >();
164
+ type Result = Awaited<ReturnType<typeof selectNameAndEmail>>;
165
+ type ResultElement = Result[number];
166
+
167
+ // Verify the result array contains the selected columns
168
+ expectTypeOf<ResultElement>().toMatchObjectType<{
169
+ name: string;
170
+ email: string;
171
+ }>();
172
+
173
+ // Verify that only selected columns exist (not age or isActive)
174
+ // @ts-expect-error - age should not exist on the result type
175
+ type _AgeType = ResultElement["age"];
176
+ // @ts-expect-error - isActive should not exist on the result type
177
+ type _IsActiveType = ResultElement["isActive"];
159
178
  });
160
179
  });
161
180
 
@@ -302,10 +321,11 @@ describe("query type tests", () => {
302
321
  type PostResult = Awaited<ReturnType<typeof _query.create<"posts">>>;
303
322
  expectTypeOf<PostResult>().toEqualTypeOf<FragnoId>();
304
323
 
305
- // Find posts by user return type
306
- type UserPostsResult = Awaited<
307
- ReturnType<typeof _query.find<"posts", ["title", "viewCount"], object>>
308
- >;
324
+ // Find posts by user return type - type-only test
325
+ type UserPostsResult = {
326
+ title: string;
327
+ viewCount: number;
328
+ }[];
309
329
 
310
330
  expectTypeOf<UserPostsResult>().toExtend<
311
331
  {