@fragno-dev/db 0.1.12 → 0.1.14

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 (85) hide show
  1. package/.turbo/turbo-build.log +51 -44
  2. package/CHANGELOG.md +12 -0
  3. package/dist/adapters/adapters.d.ts +13 -1
  4. package/dist/adapters/adapters.d.ts.map +1 -1
  5. package/dist/adapters/adapters.js.map +1 -1
  6. package/dist/adapters/drizzle/drizzle-adapter.d.ts +2 -0
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  8. package/dist/adapters/drizzle/drizzle-adapter.js +7 -2
  9. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  10. package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
  11. package/dist/adapters/drizzle/drizzle-query.js +10 -4
  12. package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
  13. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
  14. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
  15. package/dist/adapters/drizzle/drizzle-uow-compiler.js +51 -37
  16. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
  17. package/dist/adapters/drizzle/drizzle-uow-decoder.js +26 -2
  18. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
  19. package/dist/adapters/drizzle/generate.js +1 -1
  20. package/dist/adapters/drizzle/shared.d.ts +14 -1
  21. package/dist/adapters/drizzle/shared.d.ts.map +1 -0
  22. package/dist/adapters/kysely/kysely-adapter.d.ts +3 -1
  23. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  24. package/dist/adapters/kysely/kysely-adapter.js +7 -2
  25. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  26. package/dist/adapters/kysely/kysely-query.d.ts.map +1 -1
  27. package/dist/adapters/kysely/kysely-query.js +34 -4
  28. package/dist/adapters/kysely/kysely-query.js.map +1 -1
  29. package/dist/adapters/kysely/kysely-shared.d.ts +11 -0
  30. package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
  31. package/dist/adapters/kysely/kysely-uow-compiler.js +40 -10
  32. package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
  33. package/dist/adapters/kysely/migration/execute-base.js +1 -1
  34. package/dist/bind-services.d.ts +7 -0
  35. package/dist/bind-services.d.ts.map +1 -0
  36. package/dist/bind-services.js +14 -0
  37. package/dist/bind-services.js.map +1 -0
  38. package/dist/fragment.d.ts +131 -12
  39. package/dist/fragment.d.ts.map +1 -1
  40. package/dist/fragment.js +107 -8
  41. package/dist/fragment.js.map +1 -1
  42. package/dist/mod.d.ts +5 -2
  43. package/dist/mod.d.ts.map +1 -1
  44. package/dist/mod.js +4 -2
  45. package/dist/mod.js.map +1 -1
  46. package/dist/query/cursor.d.ts +67 -32
  47. package/dist/query/cursor.d.ts.map +1 -1
  48. package/dist/query/cursor.js +84 -31
  49. package/dist/query/cursor.js.map +1 -1
  50. package/dist/query/query.d.ts +7 -2
  51. package/dist/query/query.d.ts.map +1 -1
  52. package/dist/query/unit-of-work.d.ts +113 -18
  53. package/dist/query/unit-of-work.d.ts.map +1 -1
  54. package/dist/query/unit-of-work.js +266 -16
  55. package/dist/query/unit-of-work.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/adapters/adapters.ts +14 -0
  58. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +78 -6
  59. package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +139 -9
  60. package/src/adapters/drizzle/drizzle-adapter.ts +16 -1
  61. package/src/adapters/drizzle/drizzle-query.ts +35 -15
  62. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +76 -60
  63. package/src/adapters/drizzle/drizzle-uow-compiler.ts +82 -41
  64. package/src/adapters/drizzle/drizzle-uow-decoder.ts +42 -6
  65. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +104 -6
  66. package/src/adapters/kysely/kysely-adapter.ts +16 -1
  67. package/src/adapters/kysely/kysely-query.ts +76 -16
  68. package/src/adapters/kysely/kysely-uow-compiler.test.ts +62 -46
  69. package/src/adapters/kysely/kysely-uow-compiler.ts +53 -16
  70. package/src/adapters/kysely/kysely-uow-joins.test.ts +30 -30
  71. package/src/bind-services.test.ts +214 -0
  72. package/src/bind-services.ts +37 -0
  73. package/src/db-fragment.test.ts +800 -0
  74. package/src/fragment.ts +557 -28
  75. package/src/mod.ts +25 -1
  76. package/src/query/cursor.test.ts +113 -68
  77. package/src/query/cursor.ts +127 -36
  78. package/src/query/query.ts +21 -2
  79. package/src/query/unit-of-work-multi-schema.test.ts +64 -0
  80. package/src/query/unit-of-work-types.test.ts +13 -0
  81. package/src/query/unit-of-work.test.ts +5 -9
  82. package/src/query/unit-of-work.ts +629 -62
  83. package/src/uow-context-integration.test.ts +102 -0
  84. package/src/uow-context.test.ts +182 -0
  85. package/src/fragment.test.ts +0 -341
@@ -14,6 +14,7 @@ import {
14
14
  type TableNameMapper,
15
15
  parseDrizzle,
16
16
  type DBType,
17
+ createTableNameMapper,
17
18
  } from "./shared";
18
19
  import { encodeValues, ReferenceSubquery } from "../../query/result-transform";
19
20
  import { serialize } from "../../schema/serialize";
@@ -33,30 +34,42 @@ export type DrizzleCompiledQuery = {
33
34
  * This compiler translates UOW operations into Drizzle query functions
34
35
  * that can be executed as a batch/transaction.
35
36
  *
36
- * @param schema - The database schema
37
37
  * @param pool - Connection pool for acquiring database connections
38
38
  * @param provider - SQL provider (sqlite, mysql, postgresql)
39
- * @param mapper - Optional table name mapper for namespace prefixing
39
+ * @param mapper - Optional table name mapper for namespace prefixing (fallback for operations without explicit namespace)
40
40
  * @returns A UOWCompiler instance for Drizzle
41
41
  */
42
- export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
43
- schema: TSchema,
42
+ export function createDrizzleUOWCompiler(
44
43
  pool: ConnectionPool<DBType>,
45
44
  provider: "sqlite" | "mysql" | "postgresql",
46
45
  mapper?: TableNameMapper,
47
- ): UOWCompiler<TSchema, DrizzleCompiledQuery> {
46
+ ): UOWCompiler<DrizzleCompiledQuery> {
48
47
  // Get db synchronously for compilation (doesn't execute, just builds SQL)
49
48
  // TODO: We don't even need a Drizzle instance with a db client attached here. `drizzle({ schema })` is enough.
50
49
  const dbRaw = pool.getDatabaseSync();
51
50
  const [db, drizzleTables] = parseDrizzle(dbRaw);
52
51
 
52
+ /**
53
+ * Get the mapper for a specific operation
54
+ * Uses operation's namespace if provided, otherwise falls back to the default mapper
55
+ */
56
+ function getMapperForOperation(namespace: string | undefined): TableNameMapper | undefined {
57
+ if (namespace) {
58
+ return createTableNameMapper(namespace);
59
+ }
60
+ return mapper;
61
+ }
62
+
53
63
  /**
54
64
  * Convert a Fragno table to a Drizzle table
55
65
  * @throws Error if table is not found in Drizzle schema
56
66
  */
57
- function toDrizzleTable(table: AnyTable): TableType {
58
- // Map logical table name to physical table name using the mapper
59
- const physicalTableName = mapper ? mapper.toPhysical(table.ormName) : table.ormName;
67
+ function toDrizzleTable(table: AnyTable, namespace: string | undefined): TableType {
68
+ // Get the mapper for this operation's namespace
69
+ const opMapper = getMapperForOperation(namespace);
70
+
71
+ // Map logical table name to physical table name using the operation-specific mapper
72
+ const physicalTableName = opMapper ? opMapper.toPhysical(table.ormName) : table.ormName;
60
73
  const out = drizzleTables[physicalTableName];
61
74
  if (out) {
62
75
  return out;
@@ -71,13 +84,17 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
71
84
  * Convert a Fragno column to a Drizzle column
72
85
  * @throws Error if column is not found in Drizzle table
73
86
  */
74
- function toDrizzleColumn(col: AnyColumn): ColumnType {
87
+ function toDrizzleColumn(
88
+ schema: AnySchema,
89
+ namespace: string | undefined,
90
+ col: AnyColumn,
91
+ ): ColumnType {
75
92
  const fragnoTable = schema.tables[col.tableName];
76
93
  if (!fragnoTable) {
77
94
  throw new Error(`[Drizzle] Unknown table ${col.tableName} for column ${col.ormName}.`);
78
95
  }
79
96
 
80
- const table = toDrizzleTable(fragnoTable);
97
+ const table = toDrizzleTable(fragnoTable, namespace);
81
98
  const out = table[col.ormName];
82
99
  if (out) {
83
100
  return out;
@@ -89,13 +106,17 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
89
106
  /**
90
107
  * Build a WHERE clause from a condition using Drizzle's query builder
91
108
  */
92
- function buildWhere(condition: Condition): Drizzle.SQL | undefined {
109
+ function buildWhere(
110
+ schema: AnySchema,
111
+ namespace: string | undefined,
112
+ condition: Condition,
113
+ ): Drizzle.SQL | undefined {
93
114
  if (condition.type === "compare") {
94
- const left = toDrizzleColumn(condition.a);
115
+ const left = toDrizzleColumn(schema, namespace, condition.a);
95
116
  const op = condition.operator;
96
117
  let right = condition.b;
97
118
  if (right instanceof Column) {
98
- right = toDrizzleColumn(right);
119
+ right = toDrizzleColumn(schema, namespace, right);
99
120
  } else {
100
121
  // Handle string references - convert external ID to internal ID via subquery
101
122
  if (condition.a.role === "reference" && typeof right === "string") {
@@ -181,11 +202,11 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
181
202
  }
182
203
 
183
204
  if (condition.type === "and") {
184
- return Drizzle.and(...condition.items.map((item) => buildWhere(item)));
205
+ return Drizzle.and(...condition.items.map((item) => buildWhere(schema, namespace, item)));
185
206
  }
186
207
 
187
208
  if (condition.type === "not") {
188
- const result = buildWhere(condition.item);
209
+ const result = buildWhere(schema, namespace, condition.item);
189
210
  if (!result) {
190
211
  return;
191
212
  }
@@ -193,7 +214,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
193
214
  return Drizzle.not(result);
194
215
  }
195
216
 
196
- return Drizzle.or(...condition.items.map((item) => buildWhere(item)));
217
+ return Drizzle.or(...condition.items.map((item) => buildWhere(schema, namespace, item)));
197
218
  }
198
219
 
199
220
  /**
@@ -229,7 +250,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
229
250
  * Get table from schema by name
230
251
  * @throws Error if table is not found in schema
231
252
  */
232
- function getTable(name: unknown): AnyTable {
253
+ function getTable(schema: AnySchema, name: unknown): AnyTable {
233
254
  const table = schema.tables[name as string];
234
255
  if (!table) {
235
256
  throw new Error(`Invalid table name ${name}.`);
@@ -260,6 +281,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
260
281
  * Process joins recursively to support nested joins with orderBy and limit
261
282
  */
262
283
  function processJoins(
284
+ schema: AnySchema,
285
+ namespace: string | undefined,
263
286
  joins: CompiledJoin[],
264
287
  ): Record<string, Drizzle.DBQueryConfig<"many", boolean>> {
265
288
  const result: Record<string, Drizzle.DBQueryConfig<"many", boolean>> = {};
@@ -286,7 +309,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
286
309
  let joinOrderBy: Drizzle.SQL[] | undefined;
287
310
  if (options.orderBy && options.orderBy.length > 0) {
288
311
  joinOrderBy = options.orderBy.map(([col, direction]) => {
289
- const drizzleCol = toDrizzleColumn(col);
312
+ const drizzleCol = toDrizzleColumn(schema, namespace, col);
290
313
  return direction === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
291
314
  });
292
315
  }
@@ -294,7 +317,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
294
317
  // Build WHERE clause for this join if provided
295
318
  let joinWhere: Drizzle.SQL | undefined;
296
319
  if (options.where) {
297
- joinWhere = buildWhere(options.where);
320
+ joinWhere = buildWhere(schema, namespace, options.where);
298
321
  }
299
322
 
300
323
  // Build the join config
@@ -307,7 +330,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
307
330
 
308
331
  // Recursively process nested joins
309
332
  if (options.join && options.join.length > 0) {
310
- joinConfig.with = processJoins(options.join);
333
+ joinConfig.with = processJoins(schema, namespace, options.join);
311
334
  }
312
335
 
313
336
  result[joinName] = joinConfig;
@@ -317,7 +340,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
317
340
  }
318
341
 
319
342
  return {
320
- compileRetrievalOperation(op: RetrievalOperation<TSchema>): DrizzleCompiledQuery | null {
343
+ compileRetrievalOperation(op: RetrievalOperation<AnySchema>): DrizzleCompiledQuery | null {
344
+ const schema = op.schema;
321
345
  switch (op.type) {
322
346
  case "count": {
323
347
  // Build WHERE clause
@@ -329,11 +353,11 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
329
353
  return null;
330
354
  }
331
355
  if (condition !== true) {
332
- whereClause = buildWhere(condition);
356
+ whereClause = buildWhere(schema, op.namespace, condition);
333
357
  }
334
358
  }
335
359
 
336
- const drizzleTable = toDrizzleTable(op.table);
360
+ const drizzleTable = toDrizzleTable(op.table, op.namespace);
337
361
  const query = db.select({ count: Drizzle.count() }).from(drizzleTable);
338
362
 
339
363
  const compiledQuery = whereClause ? query.where(whereClause).toSQL() : query.toSQL();
@@ -377,7 +401,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
377
401
  let orderBy: Drizzle.SQL[] | undefined;
378
402
  if (indexColumns.length > 0) {
379
403
  orderBy = indexColumns.map((col) => {
380
- const drizzleCol = toDrizzleColumn(col);
404
+ const drizzleCol = toDrizzleColumn(schema, op.namespace, col);
381
405
  return orderDirection === "asc" ? Drizzle.asc(drizzleCol) : Drizzle.desc(drizzleCol);
382
406
  });
383
407
  }
@@ -413,7 +437,7 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
413
437
  return null;
414
438
  }
415
439
  if (condition !== true) {
416
- const clause = buildWhere(condition);
440
+ const clause = buildWhere(schema, op.namespace, condition);
417
441
  if (clause) {
418
442
  whereClauses.push(clause);
419
443
  }
@@ -423,8 +447,9 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
423
447
  // Add cursor-based pagination conditions
424
448
  if ((after || before) && indexColumns.length > 0) {
425
449
  const cursor = after || before;
426
- const cursorData = decodeCursor(cursor!);
427
- const serializedValues = serializeCursorValues(cursorData, indexColumns, provider);
450
+ // Decode cursor if it's a string, otherwise use it as-is
451
+ const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
452
+ const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
428
453
 
429
454
  // Build tuple comparison for cursor pagination
430
455
  // For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
@@ -435,12 +460,12 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
435
460
 
436
461
  if (indexColumns.length === 1) {
437
462
  // Simple single-column case
438
- const col = toDrizzleColumn(indexColumns[0]!);
463
+ const col = toDrizzleColumn(schema, op.namespace, indexColumns[0]!);
439
464
  const val = serializedValues[indexColumns[0]!.ormName];
440
465
  whereClauses.push(useGreaterThan ? Drizzle.gt(col, val) : Drizzle.lt(col, val));
441
466
  } else {
442
467
  // Multi-column tuple comparison using SQL
443
- const drizzleCols = indexColumns.map((c) => toDrizzleColumn(c));
468
+ const drizzleCols = indexColumns.map((c) => toDrizzleColumn(schema, op.namespace, c));
444
469
  const vals = indexColumns.map((c) => serializedValues[c.ormName]);
445
470
  const operator = useGreaterThan ? ">" : "<";
446
471
  // Safe cast: building a SQL comparison expression for cursor pagination
@@ -468,23 +493,37 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
468
493
 
469
494
  // Process joins recursively to support nested joins
470
495
  if (joins) {
471
- queryConfig.with = processJoins(joins);
496
+ queryConfig.with = processJoins(schema, op.namespace, joins);
497
+ }
498
+
499
+ // For multi-schema support: get the mapper for the operation's namespace
500
+ const opMapper = getMapperForOperation(op.namespace);
501
+ const physicalTableName = opMapper
502
+ ? opMapper.toPhysical(op.table.ormName)
503
+ : op.table.ormName;
504
+ const tableQuery = db.query[physicalTableName];
505
+
506
+ if (!tableQuery) {
507
+ throw new Error(
508
+ `[Drizzle] Table ${op.table.ormName} (physical: ${physicalTableName}) not found in db.query. ` +
509
+ `Available tables: ${Object.keys(db.query).join(", ")}`,
510
+ );
472
511
  }
473
512
 
474
- const physicalTableName = mapper ? mapper.toPhysical(op.table.ormName) : op.table.ormName;
475
- const compiledQuery = db.query[physicalTableName].findMany(queryConfig).toSQL();
513
+ const compiledQuery = tableQuery.findMany(queryConfig).toSQL();
476
514
  return compiledQuery;
477
515
  }
478
516
  }
479
517
  },
480
518
 
481
519
  compileMutationOperation(
482
- op: MutationOperation<TSchema>,
520
+ op: MutationOperation<AnySchema>,
483
521
  ): CompiledMutation<DrizzleCompiledQuery> | null {
522
+ const schema = op.schema;
484
523
  switch (op.type) {
485
524
  case "create": {
486
- const table = getTable(op.table);
487
- const drizzleTable = toDrizzleTable(table);
525
+ const table = getTable(schema, op.table);
526
+ const drizzleTable = toDrizzleTable(table, op.namespace);
488
527
  // encodeValues now handles runtime defaults automatically
489
528
  const encodedValues = encodeValues(op.values, table, true, provider);
490
529
  const values = processReferenceSubqueries(encodedValues);
@@ -497,10 +536,10 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
497
536
  }
498
537
 
499
538
  case "update": {
500
- const table = getTable(op.table);
539
+ const table = getTable(schema, op.table);
501
540
  const idColumn = table.getIdColumn();
502
541
  const versionColumn = table.getVersionColumn();
503
- const drizzleTable = toDrizzleTable(table);
542
+ const drizzleTable = toDrizzleTable(table, op.namespace);
504
543
 
505
544
  const externalId = typeof op.id === "string" ? op.id : op.id.externalId;
506
545
  const versionToCheck = getVersionToCheck(op.id, op.checkVersion);
@@ -522,7 +561,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
522
561
  return null;
523
562
  }
524
563
 
525
- const whereClause = condition === true ? undefined : buildWhere(condition);
564
+ const whereClause =
565
+ condition === true ? undefined : buildWhere(schema, op.namespace, condition);
526
566
  const encodedSetValues = encodeValues(op.set, table, false, provider);
527
567
  const setValues = processReferenceSubqueries(encodedSetValues);
528
568
 
@@ -540,10 +580,10 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
540
580
  }
541
581
 
542
582
  case "delete": {
543
- const table = getTable(op.table);
583
+ const table = getTable(schema, op.table);
544
584
  const idColumn = table.getIdColumn();
545
585
  const versionColumn = table.getVersionColumn();
546
- const drizzleTable = toDrizzleTable(table);
586
+ const drizzleTable = toDrizzleTable(table, op.namespace);
547
587
 
548
588
  if (!op.id) {
549
589
  throw new Error(
@@ -581,7 +621,8 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
581
621
  return null;
582
622
  }
583
623
 
584
- const whereClause = condition === true ? undefined : buildWhere(condition);
624
+ const whereClause =
625
+ condition === true ? undefined : buildWhere(schema, op.namespace, condition);
585
626
 
586
627
  const compiledQuery = db.delete(drizzleTable).where(whereClause).toSQL();
587
628
  return {
@@ -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
@@ -142,17 +143,14 @@ function transformJoinArraysToObjects(
142
143
  return transformedRow;
143
144
  }
144
145
 
145
- export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
146
- _schema: TSchema,
147
- provider: SQLProvider,
148
- ): UOWDecoder<TSchema, DrizzleResult> {
146
+ export function createDrizzleUOWDecoder(provider: SQLProvider): UOWDecoder<DrizzleResult> {
149
147
  return (rawResults, ops) => {
150
148
  if (rawResults.length !== ops.length) {
151
149
  throw new Error("rawResults and ops must have the same length");
152
150
  }
153
151
 
154
152
  return rawResults.map((result, index) => {
155
- const op = ops[index] as RetrievalOperation<TSchema>;
153
+ const op = ops[index] as RetrievalOperation<AnySchema>;
156
154
  if (!op) {
157
155
  throw new Error("op must be defined");
158
156
  }
@@ -173,10 +171,48 @@ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
173
171
  }
174
172
 
175
173
  // Handle find operations - decode each row
176
- return result.rows.map((row) => {
174
+ const decodedRows = result.rows.map((row) => {
177
175
  const transformedRow = transformJoinArraysToObjects(row, op, provider);
178
176
  return decodeResult(transformedRow, op.table, provider);
179
177
  });
178
+
179
+ // If cursor generation is requested, wrap in CursorResult
180
+ if (op.withCursor) {
181
+ let cursor: Cursor | undefined;
182
+
183
+ // Generate cursor from last item if results exist
184
+ if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
185
+ const lastItem = decodedRows[decodedRows.length - 1];
186
+ const indexName = op.options.orderByIndex.indexName;
187
+
188
+ // Get index columns
189
+ let indexColumns;
190
+ if (indexName === "_primary") {
191
+ indexColumns = [op.table.getIdColumn()];
192
+ } else {
193
+ const index = op.table.indexes[indexName];
194
+ if (index) {
195
+ indexColumns = index.columns;
196
+ }
197
+ }
198
+
199
+ if (indexColumns && lastItem) {
200
+ cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
201
+ indexName: op.options.orderByIndex.indexName,
202
+ orderDirection: op.options.orderByIndex.direction,
203
+ pageSize: op.options.pageSize,
204
+ });
205
+ }
206
+ }
207
+
208
+ const cursorResult: CursorResult<unknown> = {
209
+ items: decodedRows,
210
+ cursor,
211
+ };
212
+ return cursorResult;
213
+ }
214
+
215
+ return decodedRows;
180
216
  });
181
217
  };
182
218
  }
@@ -10,7 +10,7 @@ import {
10
10
  type FragnoId,
11
11
  type FragnoReference,
12
12
  } from "../../schema/create";
13
- import { encodeCursor } from "../../query/cursor";
13
+ import { Cursor } from "../../query/cursor";
14
14
 
15
15
  describe("KyselyAdapter PGLite", () => {
16
16
  const testSchema = schema((s) => {
@@ -396,10 +396,12 @@ describe("KyselyAdapter PGLite", () => {
396
396
 
397
397
  // Get cursor for pagination (using the last item from page 1)
398
398
  const lastItem = page1Results[page1Results.length - 1]!;
399
- const cursor = encodeCursor({
399
+ const cursor = new Cursor({
400
+ indexName: "name_idx",
401
+ orderDirection: "asc",
402
+ pageSize: 2,
400
403
  indexValues: { name: lastItem.name },
401
- direction: "forward",
402
- });
404
+ }).encode();
403
405
 
404
406
  // Get page 2 using the cursor
405
407
  const page2 = queryEngine
@@ -785,7 +787,7 @@ describe("KyselyAdapter PGLite", () => {
785
787
  });
786
788
  });
787
789
 
788
- it("should handle timestamps and timezones correctly", async () => {
790
+ it("should handle timestamps and time zones correctly", async () => {
789
791
  const queryEngine = adapter.createQueryEngine(testSchema, "test");
790
792
 
791
793
  // Create a user
@@ -824,7 +826,7 @@ describe("KyselyAdapter PGLite", () => {
824
826
  const specificDate = new Date("2024-06-15T14:30:00Z");
825
827
  expect(specificDate.toISOString()).toBe("2024-06-15T14:30:00.000Z");
826
828
 
827
- // Verify that dates from different timezones are handled correctly
829
+ // Verify that dates from different time zones are handled correctly
828
830
  const localDate = new Date("2024-06-15T14:30:00");
829
831
  expect(localDate).toBeInstanceOf(Date);
830
832
  expect(typeof localDate.getTimezoneOffset()).toBe("number");
@@ -872,4 +874,100 @@ describe("KyselyAdapter PGLite", () => {
872
874
  // Verify the foreign key relationship is correct
873
875
  expect(post?.user_id.internalId).toBe(user?.id.internalId);
874
876
  });
877
+
878
+ it("should support cursor-based pagination with findWithCursor()", async () => {
879
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
880
+
881
+ // Create multiple users for pagination testing with unique prefix
882
+ const prefix = "CursorPagTest";
883
+ const userIds: FragnoId[] = [];
884
+ for (let i = 1; i <= 25; i++) {
885
+ const userId = await queryEngine.create("users", {
886
+ name: `${prefix} ${i.toString().padStart(2, "0")}`,
887
+ age: 20 + i,
888
+ });
889
+ userIds.push(userId);
890
+ }
891
+
892
+ // Fetch first page with cursor (filter by prefix to avoid other test data)
893
+ const firstPage = await queryEngine.findWithCursor("users", (b) =>
894
+ b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(10),
895
+ );
896
+
897
+ // Check structure
898
+ expect(firstPage).toHaveProperty("items");
899
+ expect(firstPage).toHaveProperty("cursor");
900
+ expect(Array.isArray(firstPage.items)).toBe(true);
901
+ expect(firstPage.items.length).toBeGreaterThan(0);
902
+
903
+ assert(firstPage.cursor instanceof Cursor);
904
+
905
+ // Fetch second page using cursor
906
+ const secondPage = await queryEngine.findWithCursor("users", (b) =>
907
+ b
908
+ .whereIndex("name_idx")
909
+ .after(firstPage.cursor!)
910
+ .orderByIndex("name_idx", "asc")
911
+ .pageSize(10),
912
+ );
913
+
914
+ expect(secondPage.items.length).toBeGreaterThan(0);
915
+
916
+ // Verify no overlap - all names in second page should be different from first page
917
+ const firstPageNames = new Set(firstPage.items.map((u) => u.name));
918
+ const secondPageNames = secondPage.items.map((u) => u.name);
919
+
920
+ for (const name of secondPageNames) {
921
+ expect(firstPageNames.has(name)).toBe(false);
922
+ }
923
+
924
+ // Verify ordering - last item of first page should come before first item of second page
925
+ const firstPageLast = firstPage.items[firstPage.items.length - 1].name;
926
+ const secondPageFirst = secondPage.items[0].name;
927
+ expect(firstPageLast < secondPageFirst).toBe(true);
928
+
929
+ // Verify our test data is present
930
+ const testUsers = await queryEngine.find("users", (b) =>
931
+ b.whereIndex("name_idx").pageSize(100),
932
+ );
933
+ const testUserNames = testUsers.filter((u) => u.name.startsWith(prefix)).map((u) => u.name);
934
+ expect(testUserNames).toHaveLength(25);
935
+ });
936
+
937
+ it("should support findWithCursor() in Unit of Work", async () => {
938
+ const queryEngine = adapter.createQueryEngine(testSchema, "test");
939
+
940
+ // Create test users if not already present
941
+ const existingUsers = await queryEngine.find("users", (b) =>
942
+ b.whereIndex("name_idx").pageSize(1),
943
+ );
944
+
945
+ if (existingUsers.length === 0) {
946
+ for (let i = 1; i <= 5; i++) {
947
+ await queryEngine.create("users", {
948
+ name: `UOW Cursor User ${i}`,
949
+ age: 30 + i,
950
+ });
951
+ }
952
+ }
953
+
954
+ // Use findWithCursor in UOW
955
+ const uow = queryEngine
956
+ .createUnitOfWork("cursor-test")
957
+ .findWithCursor("users", (b) =>
958
+ b.whereIndex("name_idx").orderByIndex("name_idx", "asc").pageSize(3),
959
+ );
960
+
961
+ const [result] = await uow.executeRetrieve();
962
+
963
+ // Verify result structure
964
+ expect(result).toHaveProperty("items");
965
+ expect(result).toHaveProperty("cursor");
966
+ expect(Array.isArray(result.items)).toBe(true);
967
+ expect(result.items.length).toBeGreaterThan(0);
968
+
969
+ if (result.items.length === 3) {
970
+ expect(result.cursor).toBeInstanceOf(Cursor);
971
+ }
972
+ });
875
973
  });
@@ -28,6 +28,7 @@ export interface KyselyConfig {
28
28
  export class KyselyAdapter implements DatabaseAdapter<KyselyUOWConfig> {
29
29
  #connectionPool: ConnectionPool<KyselyAny>;
30
30
  #provider: SQLProvider;
31
+ #schemaNamespaceMap = new WeakMap<AnySchema, string>();
31
32
 
32
33
  constructor(config: KyselyConfig) {
33
34
  this.#connectionPool = createKyselyConnectionPool(config.db);
@@ -46,13 +47,27 @@ export class KyselyAdapter implements DatabaseAdapter<KyselyUOWConfig> {
46
47
  await this.#connectionPool.close();
47
48
  }
48
49
 
50
+ createTableNameMapper(namespace: string) {
51
+ return createTableNameMapper(namespace);
52
+ }
53
+
49
54
  createQueryEngine<T extends AnySchema>(
50
55
  schema: T,
51
56
  namespace: string,
52
57
  ): AbstractQuery<T, KyselyUOWConfig> {
58
+ // Register schema-namespace mapping
59
+ this.#schemaNamespaceMap.set(schema, namespace);
60
+
53
61
  // Only create mapper if namespace is non-empty
54
62
  const mapper = namespace ? createTableNameMapper(namespace) : undefined;
55
- return fromKysely(schema, this.#connectionPool, this.#provider, mapper);
63
+ return fromKysely(
64
+ schema,
65
+ this.#connectionPool,
66
+ this.#provider,
67
+ mapper,
68
+ undefined,
69
+ this.#schemaNamespaceMap,
70
+ );
56
71
  }
57
72
 
58
73
  async isConnectionHealthy(): Promise<boolean> {