@fragno-dev/db 0.1.11 → 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.
Files changed (71) hide show
  1. package/.turbo/turbo-build.log +41 -39
  2. package/CHANGELOG.md +19 -0
  3. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  4. package/dist/adapters/drizzle/drizzle-adapter.js +1 -1
  5. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-query.js +42 -34
  7. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-uow-compiler.js +2 -1
  9. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-uow-decoder.js +25 -1
  11. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  12. package/dist/adapters/drizzle/generate.js +1 -1
  13. package/dist/adapters/kysely/kysely-adapter.d.ts +4 -3
  14. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  15. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  16. package/dist/adapters/kysely/kysely-query.d.ts +22 -0
  17. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
  18. package/dist/adapters/kysely/kysely-query.js +101 -51
  19. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  20. package/dist/adapters/kysely/kysely-uow-compiler.js +2 -1
  21. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  22. package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
  23. package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
  24. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  25. package/dist/migration-engine/generation-engine.d.ts +1 -1
  26. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  27. package/dist/migration-engine/generation-engine.js.map +1 -1
  28. package/dist/mod.d.ts +7 -6
  29. package/dist/mod.d.ts.map +1 -1
  30. package/dist/mod.js +2 -1
  31. package/dist/mod.js.map +1 -1
  32. package/dist/query/cursor.d.ts +67 -32
  33. package/dist/query/cursor.d.ts.map +1 -1
  34. package/dist/query/cursor.js +84 -31
  35. package/dist/query/cursor.js.map +1 -1
  36. package/dist/query/query.d.ts +29 -8
  37. package/dist/query/query.d.ts.map +1 -1
  38. package/dist/query/result-transform.js +17 -5
  39. package/dist/query/result-transform.js.map +1 -1
  40. package/dist/query/unit-of-work.d.ts +19 -8
  41. package/dist/query/unit-of-work.d.ts.map +1 -1
  42. package/dist/query/unit-of-work.js +54 -12
  43. package/dist/query/unit-of-work.js.map +1 -1
  44. package/dist/schema/serialize.js +2 -0
  45. package/dist/schema/serialize.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +242 -55
  48. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +95 -39
  49. package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
  50. package/src/adapters/drizzle/drizzle-query.ts +74 -60
  51. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +82 -6
  52. package/src/adapters/drizzle/drizzle-uow-compiler.ts +3 -2
  53. package/src/adapters/drizzle/drizzle-uow-decoder.ts +40 -1
  54. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +190 -4
  55. package/src/adapters/kysely/kysely-adapter.ts +6 -3
  56. package/src/adapters/kysely/kysely-query.test.ts +498 -0
  57. package/src/adapters/kysely/kysely-query.ts +187 -83
  58. package/src/adapters/kysely/kysely-uow-compiler.test.ts +85 -3
  59. package/src/adapters/kysely/kysely-uow-compiler.ts +3 -2
  60. package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
  61. package/src/migration-engine/generation-engine.ts +2 -1
  62. package/src/mod.ts +12 -7
  63. package/src/query/cursor.test.ts +113 -68
  64. package/src/query/cursor.ts +127 -36
  65. package/src/query/query-type.test.ts +34 -14
  66. package/src/query/query.ts +94 -34
  67. package/src/query/result-transform.test.ts +5 -5
  68. package/src/query/result-transform.ts +29 -11
  69. package/src/query/unit-of-work.ts +141 -26
  70. package/src/schema/serialize.test.ts +223 -0
  71. package/src/schema/serialize.ts +16 -0
@@ -1,6 +1,6 @@
1
1
  import { drizzle } from "drizzle-orm/pglite";
2
- import { beforeAll, beforeEach, describe, expect, it } from "vitest";
3
- import { column, idColumn, referenceColumn, schema } from "../../schema/create";
2
+ import { beforeAll, beforeEach, describe, expect, expectTypeOf, it } from "vitest";
3
+ import { column, FragnoId, idColumn, referenceColumn, schema } from "../../schema/create";
4
4
  import type { DBType } from "./shared";
5
5
  import { writeAndLoadSchema } from "./test-utils";
6
6
  import { fromDrizzle } from "./drizzle-query";
@@ -134,10 +134,14 @@ describe("drizzle-query", () => {
134
134
  it("should find with select subset of columns", async () => {
135
135
  const someExternalId = "some-external-id";
136
136
 
137
- await orm.findFirst("user", (b) =>
137
+ const res = await orm.findFirst("user", (b) =>
138
138
  b.whereIndex("primary", (eb) => eb("id", "=", someExternalId)).select(["id", "email"]),
139
139
  );
140
140
 
141
+ if (res) {
142
+ expectTypeOf(res.email).toEqualTypeOf<string>();
143
+ }
144
+
141
145
  const [query] = queries;
142
146
  expect(query.sql).toMatchInlineSnapshot(
143
147
  `"select "id", "email", "_internalId", "_version" from "user" "user" where "user"."id" = $1 limit $2"`,
@@ -194,7 +198,7 @@ describe("drizzle-query", () => {
194
198
  });
195
199
 
196
200
  it("should find with select subset", async () => {
197
- await orm.find("user", (b) => b.whereIndex("primary").select(["id", "email"]));
201
+ const _res = await orm.find("user", (b) => b.whereIndex("primary").select(["id", "email"]));
198
202
 
199
203
  const [query] = queries;
200
204
  expect(query.sql).toMatchInlineSnapshot(
@@ -331,6 +335,33 @@ describe("drizzle-query", () => {
331
335
  );
332
336
  expect(query.params).toEqual([newExpiresAt.toISOString(), sessionId]);
333
337
  });
338
+
339
+ it("should update with version check using FragnoId", async () => {
340
+ const userId = FragnoId.fromExternal("user-123", 5);
341
+
342
+ await orm.update("user", userId, (b) =>
343
+ b
344
+ .set({
345
+ email: "checked@example.com",
346
+ })
347
+ .check(),
348
+ );
349
+
350
+ // Verify the SQL query includes version check in WHERE clause
351
+ const [query] = queries;
352
+ expect(query.sql).toMatchInlineSnapshot(
353
+ `"update "user" set "email" = $1, "_version" = COALESCE(_version, 0) + 1 where ("user"."id" = $2 and "user"."_version" = $3)"`,
354
+ );
355
+ expect(query.params).toEqual(["checked@example.com", "user-123", 5]);
356
+ });
357
+
358
+ it("should throw when trying to check() with string ID", async () => {
359
+ await expect(
360
+ orm.update("user", "user-123", (b) => b.set({ email: "test@example.com" }).check()),
361
+ ).rejects.toThrow(
362
+ 'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
363
+ );
364
+ });
334
365
  });
335
366
 
336
367
  describe("updateMany", () => {
@@ -378,6 +409,25 @@ describe("drizzle-query", () => {
378
409
  expect(query.sql).toMatchInlineSnapshot(`"delete from "session" where "session"."id" = $1"`);
379
410
  expect(query.params).toEqual([sessionId]);
380
411
  });
412
+
413
+ it("should delete with version check using FragnoId", async () => {
414
+ const userId = FragnoId.fromExternal("user-789", 3);
415
+
416
+ await orm.delete("user", userId, (b) => b.check());
417
+
418
+ // Verify the SQL query includes version check in WHERE clause
419
+ const [query] = queries;
420
+ expect(query.sql).toMatchInlineSnapshot(
421
+ `"delete from "user" where ("user"."id" = $1 and "user"."_version" = $2)"`,
422
+ );
423
+ expect(query.params).toEqual(["user-789", 3]);
424
+ });
425
+
426
+ it("should throw when trying to check() with string ID on delete", async () => {
427
+ await expect(orm.delete("user", "user-123", (b) => b.check())).rejects.toThrow(
428
+ 'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
429
+ );
430
+ });
381
431
  });
382
432
 
383
433
  describe("deleteMany", () => {
@@ -1,12 +1,14 @@
1
1
  import type { AbstractQuery } from "../../query/query";
2
- import type { AnySchema } from "../../schema/create";
3
- import type { CompiledMutation, UOWExecutor } from "../../query/unit-of-work";
2
+ import type { AnySchema, AnyTable } from "../../schema/create";
3
+ import type { CompiledMutation, UOWExecutor, ValidIndexName } from "../../query/unit-of-work";
4
4
  import { createDrizzleUOWCompiler, type DrizzleCompiledQuery } from "./drizzle-uow-compiler";
5
5
  import { executeDrizzleRetrievalPhase, executeDrizzleMutationPhase } from "./drizzle-uow-executor";
6
6
  import { UnitOfWork } from "../../query/unit-of-work";
7
7
  import { parseDrizzle, type DrizzleResult, type TableNameMapper, type DBType } from "./shared";
8
8
  import { createDrizzleUOWDecoder } from "./drizzle-uow-decoder";
9
9
  import type { ConnectionPool } from "../../shared/connection-pool";
10
+ import type { TableToUpdateValues } from "../../query/query";
11
+ import type { CursorResult } from "../../query/cursor";
10
12
 
11
13
  /**
12
14
  * Configuration options for creating a Drizzle Unit of Work
@@ -24,6 +26,37 @@ export interface DrizzleUOWConfig {
24
26
  dryRun?: boolean;
25
27
  }
26
28
 
29
+ /**
30
+ * Special builder for updateMany operations that captures configuration
31
+ */
32
+ class UpdateManySpecialBuilder<TTable extends AnyTable> {
33
+ #indexName?: string;
34
+ #condition?: unknown;
35
+ #setValues?: TableToUpdateValues<TTable>;
36
+
37
+ whereIndex<TIndexName extends ValidIndexName<TTable>>(
38
+ indexName: TIndexName,
39
+ condition?: unknown,
40
+ ): this {
41
+ this.#indexName = indexName as string;
42
+ this.#condition = condition;
43
+ return this;
44
+ }
45
+
46
+ set(values: TableToUpdateValues<TTable>): this {
47
+ this.#setValues = values;
48
+ return this;
49
+ }
50
+
51
+ getConfig() {
52
+ return {
53
+ indexName: this.#indexName,
54
+ condition: this.#condition,
55
+ setValues: this.#setValues,
56
+ };
57
+ }
58
+ }
59
+
27
60
  /**
28
61
  * Creates a Drizzle-based query engine for the given schema.
29
62
  *
@@ -114,15 +147,28 @@ export function fromDrizzle<T extends AnySchema>(
114
147
 
115
148
  return {
116
149
  async find(tableName, builderFn) {
117
- const uow = createUOW({ config: uowConfig }).find(tableName, builderFn);
150
+ // Safe: builderFn returns a FindBuilder (or void), which matches UnitOfWork signature
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ const uow = createUOW({ config: uowConfig }).find(tableName, builderFn as any);
118
153
  const [result] = await uow.executeRetrieve();
119
154
  return result;
120
155
  },
121
156
 
157
+ async findWithCursor(tableName, builderFn) {
158
+ // Safe: builderFn returns a FindBuilder, which matches UnitOfWork signature
159
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
160
+ const uow = createUOW({ config: uowConfig }).findWithCursor(tableName, builderFn as any);
161
+ const [result] = await uow.executeRetrieve();
162
+ return result as CursorResult<unknown>;
163
+ },
164
+
122
165
  async findFirst(tableName, builderFn) {
123
166
  const uow = createUOW({ config: uowConfig });
124
167
  if (builderFn) {
125
- uow.find(tableName, (b) => builderFn(b as never).pageSize(1));
168
+ uow.find(tableName, (b) => {
169
+ builderFn(b);
170
+ return b.pageSize(1);
171
+ });
126
172
  } else {
127
173
  uow.find(tableName, (b) => b.whereIndex("primary").pageSize(1));
128
174
  }
@@ -133,7 +179,7 @@ export function fromDrizzle<T extends AnySchema>(
133
179
 
134
180
  async create(tableName, values) {
135
181
  const uow = createUOW({ config: uowConfig });
136
- uow.create(tableName as string, values as never);
182
+ uow.create(tableName, values);
137
183
  const { success } = await uow.executeMutations();
138
184
  if (!success) {
139
185
  throw new Error("Failed to create record");
@@ -150,7 +196,7 @@ export function fromDrizzle<T extends AnySchema>(
150
196
  async createMany(tableName, valuesArray) {
151
197
  const uow = createUOW({ config: uowConfig });
152
198
  for (const values of valuesArray) {
153
- uow.create(tableName as string, values as never);
199
+ uow.create(tableName, values);
154
200
  }
155
201
  const { success } = await uow.executeMutations();
156
202
  if (!success) {
@@ -162,7 +208,7 @@ export function fromDrizzle<T extends AnySchema>(
162
208
 
163
209
  async update(tableName, id, builderFn) {
164
210
  const uow = createUOW({ config: uowConfig });
165
- uow.update(tableName as string, id, builderFn as never);
211
+ uow.update(tableName, id, builderFn);
166
212
  const { success } = await uow.executeMutations();
167
213
  if (!success) {
168
214
  throw new Error("Failed to update record (version conflict or record not found)");
@@ -170,25 +216,17 @@ export function fromDrizzle<T extends AnySchema>(
170
216
  },
171
217
 
172
218
  async updateMany(tableName, builderFn) {
173
- // FIXME: This is not correct
174
-
175
- let whereConfig: { indexName?: string; condition?: unknown } = {};
176
- let setValues: unknown;
177
-
178
- const specialBuilder = {
179
- whereIndex(indexName: string, condition?: unknown) {
180
- whereConfig = { indexName, condition };
181
- return this;
182
- },
183
- set(values: unknown) {
184
- setValues = values;
185
- return this;
186
- },
187
- };
219
+ const table = schema.tables[tableName];
220
+ if (!table) {
221
+ throw new Error(`Table ${tableName} not found in schema`);
222
+ }
188
223
 
224
+ const specialBuilder = new UpdateManySpecialBuilder<typeof table>();
189
225
  builderFn(specialBuilder);
190
226
 
191
- if (!whereConfig.indexName) {
227
+ const { indexName, condition, setValues } = specialBuilder.getConfig();
228
+
229
+ if (!indexName) {
192
230
  throw new Error("whereIndex() must be called in updateMany");
193
231
  }
194
232
  if (!setValues) {
@@ -197,24 +235,22 @@ export function fromDrizzle<T extends AnySchema>(
197
235
 
198
236
  const findUow = createUOW({ config: uowConfig });
199
237
  findUow.find(tableName, (b) => {
200
- if (whereConfig.condition) {
201
- return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
238
+ if (condition) {
239
+ // Safe: condition is captured from whereIndex call with proper typing
240
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
+ return b.whereIndex(indexName as ValidIndexName<typeof table>, condition as any);
202
242
  }
203
- return b.whereIndex(whereConfig.indexName as never);
243
+ return b.whereIndex(indexName as ValidIndexName<typeof table>);
204
244
  });
205
- const findResults = await findUow.executeRetrieve();
206
- const records = (findResults as unknown as [unknown])[0];
245
+ const [records]: unknown[][] = await findUow.executeRetrieve();
207
246
 
208
- // @ts-expect-error - Type narrowing doesn't work through unknown cast
209
247
  if (!records || records.length === 0) {
210
248
  return;
211
249
  }
212
250
 
213
251
  const updateUow = createUOW({ config: uowConfig });
214
- for (const record of records as never as Array<{ id: unknown }>) {
215
- updateUow.update(tableName as string, record.id as string, (b) =>
216
- b.set(setValues as never),
217
- );
252
+ for (const record of records as Array<{ id: unknown }>) {
253
+ updateUow.update(tableName, record.id as string, (b) => b.set(setValues));
218
254
  }
219
255
  const { success } = await updateUow.executeMutations();
220
256
  if (!success) {
@@ -224,7 +260,7 @@ export function fromDrizzle<T extends AnySchema>(
224
260
 
225
261
  async delete(tableName, id, builderFn) {
226
262
  const uow = createUOW({ config: uowConfig });
227
- uow.delete(tableName as string, id, builderFn as never);
263
+ uow.delete(tableName, id, builderFn);
228
264
  const { success } = await uow.executeMutations();
229
265
  if (!success) {
230
266
  throw new Error("Failed to delete record (version conflict or record not found)");
@@ -232,39 +268,17 @@ export function fromDrizzle<T extends AnySchema>(
232
268
  },
233
269
 
234
270
  async deleteMany(tableName, builderFn) {
235
- let whereConfig: { indexName?: string; condition?: unknown } = {};
236
-
237
- const specialBuilder = {
238
- whereIndex(indexName: string, condition?: unknown) {
239
- whereConfig = { indexName, condition };
240
- return this;
241
- },
242
- };
243
-
244
- builderFn(specialBuilder as never);
245
-
246
- if (!whereConfig.indexName) {
247
- throw new Error("whereIndex() must be called in deleteMany");
248
- }
249
-
250
271
  const findUow = createUOW({ config: uowConfig });
251
- findUow.find(tableName as string, (b) => {
252
- if (whereConfig.condition) {
253
- return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
254
- }
255
- return b.whereIndex(whereConfig.indexName as never);
256
- });
257
- const findResults2 = await findUow.executeRetrieve();
258
- const records = (findResults2 as unknown as [unknown])[0];
272
+ findUow.find(tableName, builderFn);
273
+ const [records]: unknown[][] = await findUow.executeRetrieve();
259
274
 
260
- // @ts-expect-error - Type narrowing doesn't work through unknown cast
261
275
  if (!records || records.length === 0) {
262
276
  return;
263
277
  }
264
278
 
265
279
  const deleteUow = createUOW({ config: uowConfig });
266
- for (const record of records as never as Array<{ id: unknown }>) {
267
- deleteUow.delete(tableName as string, record.id as string);
280
+ for (const record of records as Array<{ id: unknown }>) {
281
+ deleteUow.delete(tableName, record.id as string);
268
282
  }
269
283
  const { success } = await deleteUow.executeMutations();
270
284
  if (!success) {
@@ -14,6 +14,7 @@ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
14
14
  import { writeAndLoadSchema } from "./test-utils";
15
15
  import type { ConnectionPool } from "../../shared/connection-pool";
16
16
  import { createDrizzleConnectionPool } from "./drizzle-connection-pool";
17
+ import { Cursor } from "../../query/cursor";
17
18
 
18
19
  /**
19
20
  * Integration tests for Drizzle UOW compiler and executor.
@@ -233,7 +234,12 @@ describe("drizzle-uow-compiler", () => {
233
234
 
234
235
  it("should compile find operation with cursor pagination using after", () => {
235
236
  const uow = createTestUOW();
236
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9"; // {"indexValues":{"name":"Alice"},"direction":"forward"}
237
+ const cursor = new Cursor({
238
+ indexName: "idx_name",
239
+ orderDirection: "asc",
240
+ pageSize: 10,
241
+ indexValues: { name: "Alice" },
242
+ });
237
243
  uow.find("users", (b) =>
238
244
  b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
239
245
  );
@@ -250,7 +256,12 @@ describe("drizzle-uow-compiler", () => {
250
256
 
251
257
  it("should compile find operation with cursor pagination using before", () => {
252
258
  const uow = createTestUOW();
253
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQm9iIn0sImRpcmVjdGlvbiI6ImJhY2t3YXJkIn0="; // {"indexValues":{"name":"Bob"},"direction":"backward"}
259
+ const cursor = new Cursor({
260
+ indexName: "idx_name",
261
+ orderDirection: "desc",
262
+ pageSize: 10,
263
+ indexValues: { name: "Bob" },
264
+ });
254
265
  uow.find("users", (b) =>
255
266
  b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
256
267
  );
@@ -267,7 +278,12 @@ describe("drizzle-uow-compiler", () => {
267
278
 
268
279
  it("should compile find operation with cursor pagination and additional where conditions", () => {
269
280
  const uow = createTestUOW();
270
- const cursor = "eyJpbmRleFZhbHVlcyI6eyJuYW1lIjoiQWxpY2UifSwiZGlyZWN0aW9uIjoiZm9yd2FyZCJ9";
281
+ const cursor = new Cursor({
282
+ indexName: "idx_name",
283
+ orderDirection: "asc",
284
+ pageSize: 5,
285
+ indexValues: { name: "Alice" },
286
+ });
271
287
  uow.find("users", (b) =>
272
288
  b
273
289
  .whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
@@ -457,10 +473,9 @@ describe("drizzle-uow-compiler", () => {
457
473
  const [batch] = compiled.mutationBatch;
458
474
  assert(batch);
459
475
  expect(batch.expectedAffectedRows).toBeNull();
460
- // FragnoId should use internal ID directly (no subquery needed if available)
461
- // But since we don't have the internal ID populated in the test, it should serialize
476
+ // FragnoId should generate a subquery to lookup the internal ID from external ID
462
477
  expect(batch.query.sql).toMatchInlineSnapshot(
463
- `"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values ($1, $2, $3, $4, default, default, default)"`,
478
+ `"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values ($1, $2, $3, (select "_internalId" from "users" where "id" = $4 limit 1), default, default, default)"`,
464
479
  );
465
480
  });
466
481
 
@@ -1303,5 +1318,66 @@ describe("drizzle-uow-compiler", () => {
1303
1318
  );
1304
1319
  expect(compiled.retrievalBatch[0].params).toEqual([1, sessionId]);
1305
1320
  });
1321
+
1322
+ it("should support creating and using ID in same UOW", () => {
1323
+ const uow = createAuthUOW("create-user-and-session");
1324
+
1325
+ // Create user and capture the returned ID
1326
+ const userId = uow.create("user", {
1327
+ email: "test@example.com",
1328
+ passwordHash: "hashed_password",
1329
+ });
1330
+
1331
+ // Use the returned FragnoId directly to create a session
1332
+ // The compiler should extract externalId and generate a subquery
1333
+ uow.create("session", {
1334
+ userId: userId,
1335
+ expiresAt: new Date("2025-12-31"),
1336
+ });
1337
+
1338
+ const compiler = createDrizzleUOWCompiler(authSchema, authPool, "postgresql");
1339
+ const compiled = uow.compile(compiler);
1340
+
1341
+ // Should have no retrieval operations
1342
+ expect(compiled.retrievalBatch).toHaveLength(0);
1343
+
1344
+ // Should have 2 mutation operations (create user, create session)
1345
+ expect(compiled.mutationBatch).toHaveLength(2);
1346
+
1347
+ const [userCreate, sessionCreate] = compiled.mutationBatch;
1348
+ assert(userCreate);
1349
+ assert(sessionCreate);
1350
+
1351
+ // Verify user create SQL
1352
+ expect(userCreate.query.sql).toMatchInlineSnapshot(
1353
+ `"insert into "user" ("id", "email", "passwordHash", "createdAt", "_internalId", "_version") values ($1, $2, $3, $4, default, default)"`,
1354
+ );
1355
+ expect(userCreate.query.params).toMatchObject([
1356
+ userId.externalId, // The generated ID
1357
+ "test@example.com",
1358
+ "hashed_password",
1359
+ expect.any(String), // timestamp
1360
+ ]);
1361
+ expect(userCreate.expectedAffectedRows).toBeNull();
1362
+
1363
+ // Verify session create SQL - FragnoId generates subquery to lookup internal ID
1364
+ expect(sessionCreate.query.sql).toMatchInlineSnapshot(
1365
+ `"insert into "session" ("id", "userId", "expiresAt", "createdAt", "_internalId", "_version") values ($1, (select "_internalId" from "user" where "id" = $2 limit 1), $3, $4, default, default)"`,
1366
+ );
1367
+ expect(sessionCreate.query.params).toMatchObject([
1368
+ expect.any(String), // generated session ID
1369
+ userId.externalId, // FragnoId's externalId is used in the subquery
1370
+ expect.any(String), // expiresAt timestamp
1371
+ expect.any(String), // createdAt timestamp
1372
+ ]);
1373
+ expect(sessionCreate.expectedAffectedRows).toBeNull();
1374
+
1375
+ // Verify the returned FragnoId has the expected structure
1376
+ expect(userId).toMatchObject({
1377
+ externalId: expect.any(String),
1378
+ version: 0,
1379
+ internalId: undefined,
1380
+ });
1381
+ });
1306
1382
  });
1307
1383
  });
@@ -423,8 +423,9 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
423
423
  // Add cursor-based pagination conditions
424
424
  if ((after || before) && indexColumns.length > 0) {
425
425
  const cursor = after || before;
426
- const cursorData = decodeCursor(cursor!);
427
- const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
426
+ // Decode cursor if it's a string, otherwise use it as-is
427
+ const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
428
+ const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
428
429
 
429
430
  // Build tuple comparison for cursor pagination
430
431
  // For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
@@ -4,6 +4,7 @@ import type { RetrievalOperation, UOWDecoder } from "../../query/unit-of-work";
4
4
  import { decodeResult } from "../../query/result-transform";
5
5
  import { getOrderedJoinColumns } from "./join-column-utils";
6
6
  import type { DrizzleResult } from "./shared";
7
+ import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
7
8
 
8
9
  /**
9
10
  * Join information with nested join support
@@ -173,10 +174,48 @@ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
173
174
  }
174
175
 
175
176
  // Handle find operations - decode each row
176
- return result.rows.map((row) => {
177
+ const decodedRows = result.rows.map((row) => {
177
178
  const transformedRow = transformJoinArraysToObjects(row, op, provider);
178
179
  return decodeResult(transformedRow, op.table, provider);
179
180
  });
181
+
182
+ // If cursor generation is requested, wrap in CursorResult
183
+ if (op.withCursor) {
184
+ let cursor: Cursor | undefined;
185
+
186
+ // Generate cursor from last item if results exist
187
+ if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
188
+ const lastItem = decodedRows[decodedRows.length - 1];
189
+ const indexName = op.options.orderByIndex.indexName;
190
+
191
+ // Get index columns
192
+ let indexColumns;
193
+ if (indexName === "_primary") {
194
+ indexColumns = [op.table.getIdColumn()];
195
+ } else {
196
+ const index = op.table.indexes[indexName];
197
+ if (index) {
198
+ indexColumns = index.columns;
199
+ }
200
+ }
201
+
202
+ if (indexColumns && lastItem) {
203
+ cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
204
+ indexName: op.options.orderByIndex.indexName,
205
+ orderDirection: op.options.orderByIndex.direction,
206
+ pageSize: op.options.pageSize,
207
+ });
208
+ }
209
+ }
210
+
211
+ const cursorResult: CursorResult<unknown> = {
212
+ items: decodedRows,
213
+ cursor,
214
+ };
215
+ return cursorResult;
216
+ }
217
+
218
+ return decodedRows;
180
219
  });
181
220
  };
182
221
  }