@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
@@ -8,6 +8,7 @@ import type {
8
8
  UpdateManyBuilder,
9
9
  } from "./unit-of-work";
10
10
  import type { Prettify } from "../util/types";
11
+ import type { CursorResult } from "./cursor";
11
12
 
12
13
  export type AnySelectClause = SelectClause<AnyTable>;
13
14
 
@@ -17,7 +18,7 @@ export type RawColumnValues<T extends AnyTable> = {
17
18
  [K in keyof T["columns"] as string extends K ? never : K]: T["columns"][K]["$out"];
18
19
  };
19
20
 
20
- export type TableToColumnValues<T extends AnyTable> = RawColumnValues<T>;
21
+ export type TableToColumnValues<T extends AnyTable> = Prettify<RawColumnValues<T>>;
21
22
 
22
23
  type PickNullable<T> = {
23
24
  [P in keyof T as null extends T[P] ? P : never]: T[P];
@@ -44,18 +45,16 @@ export type TableToUpdateValues<T extends AnyTable> = {
44
45
  type MainSelectResult<S extends SelectClause<T>, T extends AnyTable> = S extends true
45
46
  ? TableToColumnValues<T>
46
47
  : S extends (keyof T["columns"])[]
47
- ? {
48
+ ? Prettify<{
48
49
  [K in S[number] as string extends K ? never : K]: K extends keyof T["columns"]
49
50
  ? T["columns"][K]["$out"]
50
51
  : never;
51
- }
52
+ }>
52
53
  : never;
53
54
 
54
- export type SelectResult<
55
- T extends AnyTable,
56
- JoinOut,
57
- Select extends SelectClause<T>,
58
- > = MainSelectResult<Select, T> & JoinOut;
55
+ export type SelectResult<T extends AnyTable, JoinOut, Select extends SelectClause<T>> = Prettify<
56
+ MainSelectResult<Select, T> & JoinOut
57
+ >;
59
58
 
60
59
  interface MapRelationType<Type> {
61
60
  one: Type | null;
@@ -68,15 +67,43 @@ export type JoinBuilder<T extends AnyTable, Out = {}> = {
68
67
  options?: FindManyOptions<Target, Select, JoinOut, false>,
69
68
  ) => JoinBuilder<
70
69
  T,
71
- Out & {
72
- [$K in K]: MapRelationType<SelectResult<Target, JoinOut, Select>>[Type];
73
- }
70
+ Prettify<
71
+ Out & {
72
+ [$K in K]: MapRelationType<SelectResult<Target, JoinOut, Select>>[Type];
73
+ }
74
+ >
74
75
  >
75
76
  : never;
76
77
  };
77
78
 
78
79
  export type OrderBy<Column = string> = [columnName: Column, "asc" | "desc"];
79
80
 
81
+ /**
82
+ * Extract Select type parameter from a FindBuilder type (handles Omit wrapper)
83
+ * @internal
84
+ */
85
+ type ExtractSelect<T> =
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ T extends FindBuilder<any, infer TSelect, any>
88
+ ? TSelect
89
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
+ T extends Omit<FindBuilder<any, infer TSelect, any>, any>
91
+ ? TSelect
92
+ : true;
93
+
94
+ /**
95
+ * Extract JoinOut type parameter from a FindBuilder type (handles Omit wrapper)
96
+ * @internal
97
+ */
98
+ type ExtractJoinOut<T> =
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ T extends FindBuilder<any, any, infer TJoinOut>
101
+ ? TJoinOut
102
+ : // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ T extends Omit<FindBuilder<any, any, infer TJoinOut>, any>
104
+ ? TJoinOut
105
+ : {};
106
+
80
107
  export type FindFirstOptions<
81
108
  T extends AnyTable = AnyTable,
82
109
  Select extends SelectClause<T> = SelectClause<T>,
@@ -109,31 +136,66 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
109
136
  /**
110
137
  * Find multiple records using a builder pattern
111
138
  */
112
- find: <
113
- TableName extends keyof TSchema["tables"] & string,
114
- Select extends SelectClause<TSchema["tables"][TableName]> = true,
115
- JoinOut = {},
116
- >(
139
+ find: {
140
+ // Overload when builder function is provided - infer Select and JoinOut from builder
141
+ <TableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
142
+ table: TableName,
143
+ builderFn: (
144
+ builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
145
+ ) => TBuilderResult,
146
+ ): Promise<
147
+ SelectResult<
148
+ TSchema["tables"][TableName],
149
+ ExtractJoinOut<TBuilderResult>,
150
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TableName]>>
151
+ >[]
152
+ >;
153
+ // Overload when no builder function - return all columns
154
+ <TableName extends keyof TSchema["tables"] & string>(
155
+ table: TableName,
156
+ ): Promise<SelectResult<TSchema["tables"][TableName], {}, true>[]>;
157
+ };
158
+
159
+ /**
160
+ * Find multiple records with cursor pagination metadata
161
+ */
162
+ findWithCursor: <TableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
117
163
  table: TableName,
118
- builderFn?: (
164
+ builderFn: (
119
165
  builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
120
- ) => Omit<FindBuilder<TSchema["tables"][TableName], Select, JoinOut>, "build">,
121
- ) => Promise<SelectResult<TSchema["tables"][TableName], JoinOut, Select>[]>;
166
+ ) => TBuilderResult,
167
+ ) => Promise<
168
+ CursorResult<
169
+ SelectResult<
170
+ TSchema["tables"][TableName],
171
+ ExtractJoinOut<TBuilderResult>,
172
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TableName]>>
173
+ >
174
+ >
175
+ >;
122
176
 
123
177
  /**
124
178
  * Find the first record matching the criteria
125
179
  * Implemented as a wrapper around find() with pageSize(1)
126
180
  */
127
- findFirst: <
128
- TableName extends keyof TSchema["tables"] & string,
129
- Select extends SelectClause<TSchema["tables"][TableName]> = true,
130
- JoinOut = {},
131
- >(
132
- table: TableName,
133
- builderFn?: (
134
- builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
135
- ) => Omit<FindBuilder<TSchema["tables"][TableName], Select, JoinOut>, "build">,
136
- ) => Promise<SelectResult<TSchema["tables"][TableName], JoinOut, Select> | null>;
181
+ findFirst: {
182
+ // Overload when builder function is provided - infer Select and JoinOut from builder
183
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
184
+ <TableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
185
+ table: TableName,
186
+ builderFn: (
187
+ builder: Omit<FindBuilder<TSchema["tables"][TableName]>, "build">,
188
+ ) => TBuilderResult,
189
+ ): Promise<SelectResult<
190
+ TSchema["tables"][TableName],
191
+ ExtractJoinOut<TBuilderResult>,
192
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TableName]>>
193
+ > | null>;
194
+ // Overload when no builder function - return all columns
195
+ <TableName extends keyof TSchema["tables"] & string>(
196
+ table: TableName,
197
+ ): Promise<SelectResult<TSchema["tables"][TableName], {}, true> | null>;
198
+ };
137
199
 
138
200
  /**
139
201
  * Create a single record
@@ -161,8 +223,8 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
161
223
  table: TableName,
162
224
  id: FragnoId | string,
163
225
  builderFn: (
164
- builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build" | "check">,
165
- ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build" | "check">,
226
+ builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
227
+ ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
166
228
  ) => Promise<void>;
167
229
 
168
230
  /**
@@ -180,9 +242,7 @@ export interface AbstractQuery<TSchema extends AnySchema, TUOWConfig = void> {
180
242
  delete: <TableName extends keyof TSchema["tables"] & string>(
181
243
  table: TableName,
182
244
  id: FragnoId | string,
183
- builderFn?: (
184
- builder: Omit<DeleteBuilder, "build" | "check">,
185
- ) => Omit<DeleteBuilder, "build" | "check">,
245
+ builderFn?: (builder: Omit<DeleteBuilder, "build">) => Omit<DeleteBuilder, "build">,
186
246
  ) => Promise<void>;
187
247
 
188
248
  /**
@@ -226,7 +226,7 @@ describe("encodeValues", () => {
226
226
  });
227
227
  });
228
228
 
229
- it("should fallback to external ID for reference when internal ID unavailable", () => {
229
+ it("should convert FragnoId without internalId to ReferenceSubquery for reference columns", () => {
230
230
  const fragnoId = new FragnoId({
231
231
  externalId: "user123",
232
232
  version: 1,
@@ -238,10 +238,10 @@ describe("encodeValues", () => {
238
238
  "postgresql",
239
239
  );
240
240
 
241
- expect(result).toEqual({
242
- title: "Test Post",
243
- userId: "user123",
244
- });
241
+ // FragnoId without internalId should be converted to ReferenceSubquery for database lookup
242
+ expect(result["title"]).toBe("Test Post");
243
+ expect(result["userId"]).toBeInstanceOf(ReferenceSubquery);
244
+ expect((result["userId"] as ReferenceSubquery).externalIdValue).toBe("user123");
245
245
  });
246
246
 
247
247
  it("should handle FragnoId across different providers", () => {
@@ -123,18 +123,36 @@ export function encodeValues(
123
123
  }
124
124
 
125
125
  if (value !== undefined) {
126
- // Handle string references - convert external ID to internal ID via subquery
127
- if (col.role === "reference" && typeof value === "string") {
128
- // Find relation that uses this column
129
- const relation = Object.values(table.relations).find((rel) =>
130
- rel.on.some(([localCol]) => localCol === k),
131
- );
132
- if (relation) {
133
- result[col.name] = new ReferenceSubquery(relation.table, value);
134
- continue;
126
+ // Handle string references and FragnoId objects
127
+ if (col.role === "reference") {
128
+ if (typeof value === "string") {
129
+ // String external ID - generate subquery
130
+ const relation = Object.values(table.relations).find((rel) =>
131
+ rel.on.some(([localCol]) => localCol === k),
132
+ );
133
+ if (relation) {
134
+ result[col.name] = new ReferenceSubquery(relation.table, value);
135
+ continue;
136
+ }
137
+ throw new Error(`Reference column ${k} not found in table ${table.name}`);
138
+ } else if (value instanceof FragnoId) {
139
+ // FragnoId object
140
+ if (value.internalId !== undefined) {
141
+ // If internal ID is populated, use it directly (no subquery needed)
142
+ result[col.name] = value.internalId;
143
+ continue;
144
+ } else {
145
+ // If internal ID is not populated, use external ID via subquery
146
+ const relation = Object.values(table.relations).find((rel) =>
147
+ rel.on.some(([localCol]) => localCol === k),
148
+ );
149
+ if (relation) {
150
+ result[col.name] = new ReferenceSubquery(relation.table, value.externalId);
151
+ continue;
152
+ }
153
+ throw new Error(`Reference column ${k} not found in table ${table.name}`);
154
+ }
135
155
  }
136
-
137
- throw new Error(`Reference column ${k} not found in table ${table.name}`);
138
156
  }
139
157
 
140
158
  result[col.name] = serialize(value, col, provider);
@@ -4,6 +4,8 @@ import type { Condition, ConditionBuilder } from "./condition-builder";
4
4
  import type { SelectClause, TableToInsertValues, TableToUpdateValues, SelectResult } from "./query";
5
5
  import { buildCondition } from "./condition-builder";
6
6
  import type { CompiledJoin } from "./orm/orm";
7
+ import type { CursorResult } from "./cursor";
8
+ import { Cursor } from "./cursor";
7
9
 
8
10
  /**
9
11
  * Builder for updateMany operations that supports both whereIndex and set chaining
@@ -112,11 +114,11 @@ type FindOptions<
112
114
  /**
113
115
  * Cursor for pagination - continue after this cursor
114
116
  */
115
- after?: string;
117
+ after?: Cursor | string;
116
118
  /**
117
119
  * Cursor for pagination - continue before this cursor
118
120
  */
119
- before?: string;
121
+ before?: Cursor | string;
120
122
  /**
121
123
  * Number of results per page
122
124
  */
@@ -144,6 +146,7 @@ export type RetrievalOperation<
144
146
  table: TTable;
145
147
  indexName: string;
146
148
  options: FindOptions<TTable, SelectClause<TTable>>;
149
+ withCursor?: boolean;
147
150
  }
148
151
  | {
149
152
  type: "count";
@@ -262,12 +265,13 @@ export class FindBuilder<
262
265
  indexName: string;
263
266
  direction: "asc" | "desc";
264
267
  };
265
- #afterCursor?: string;
266
- #beforeCursor?: string;
268
+ #afterCursor?: Cursor | string;
269
+ #beforeCursor?: Cursor | string;
267
270
  #pageSizeValue?: number;
268
271
  #selectClause?: TSelect;
269
272
  #joinClause?: (jb: IndexedJoinBuilder<TTable, {}>) => IndexedJoinBuilder<TTable, TJoinOut>;
270
273
  #countMode = false;
274
+ #cursorMetadata?: Cursor;
271
275
 
272
276
  constructor(tableName: string, table: TTable) {
273
277
  this.#tableName = tableName;
@@ -357,17 +361,27 @@ export class FindBuilder<
357
361
 
358
362
  /**
359
363
  * Set cursor to continue pagination after this point (forward pagination)
364
+ * If a Cursor object is provided, its metadata will be used to set defaults for
365
+ * index, orderByIndex, and pageSize (if not explicitly set)
360
366
  */
361
- after(cursor: string): this {
367
+ after(cursor: Cursor | string): this {
362
368
  this.#afterCursor = cursor;
369
+ if (cursor instanceof Cursor) {
370
+ this.#cursorMetadata = cursor;
371
+ }
363
372
  return this;
364
373
  }
365
374
 
366
375
  /**
367
376
  * Set cursor to continue pagination before this point (backward pagination)
377
+ * If a Cursor object is provided, its metadata will be used to set defaults for
378
+ * index, orderByIndex, and pageSize (if not explicitly set)
368
379
  */
369
- before(cursor: string): this {
380
+ before(cursor: Cursor | string): this {
370
381
  this.#beforeCursor = cursor;
382
+ if (cursor instanceof Cursor) {
383
+ this.#cursorMetadata = cursor;
384
+ }
371
385
  return this;
372
386
  }
373
387
 
@@ -400,7 +414,47 @@ export class FindBuilder<
400
414
  indexName: string;
401
415
  options: Pick<FindOptions<TTable>, "where" | "useIndex">;
402
416
  } {
403
- if (!this.#indexName) {
417
+ // Apply cursor metadata as defaults if available and not explicitly set
418
+ let indexName = this.#indexName;
419
+ let orderByIndex = this.#orderByIndexClause;
420
+ let pageSize = this.#pageSizeValue;
421
+
422
+ if (this.#cursorMetadata) {
423
+ // Use cursor metadata as defaults
424
+ if (!indexName) {
425
+ indexName = this.#cursorMetadata.indexName;
426
+ }
427
+ if (!orderByIndex) {
428
+ orderByIndex = {
429
+ indexName: this.#cursorMetadata.indexName,
430
+ direction: this.#cursorMetadata.orderDirection,
431
+ };
432
+ }
433
+ if (pageSize === undefined) {
434
+ pageSize = this.#cursorMetadata.pageSize;
435
+ }
436
+
437
+ // Validate that explicit params match cursor params
438
+ if (indexName && indexName !== this.#cursorMetadata.indexName) {
439
+ throw new Error(
440
+ `Index mismatch: builder specifies "${indexName}" but cursor specifies "${this.#cursorMetadata.indexName}"`,
441
+ );
442
+ }
443
+ if (
444
+ orderByIndex &&
445
+ (orderByIndex.indexName !== this.#cursorMetadata.indexName ||
446
+ orderByIndex.direction !== this.#cursorMetadata.orderDirection)
447
+ ) {
448
+ throw new Error(`Order mismatch: builder and cursor specify different ordering`);
449
+ }
450
+ if (pageSize !== undefined && pageSize !== this.#cursorMetadata.pageSize) {
451
+ throw new Error(
452
+ `Page size mismatch: builder specifies ${pageSize} but cursor specifies ${this.#cursorMetadata.pageSize}`,
453
+ );
454
+ }
455
+ }
456
+
457
+ if (!indexName) {
404
458
  throw new Error(
405
459
  `Must specify an index using .whereIndex() before finalizing find operation on table "${this.#tableName}"`,
406
460
  );
@@ -410,9 +464,9 @@ export class FindBuilder<
410
464
  if (this.#countMode) {
411
465
  return {
412
466
  type: "count",
413
- indexName: this.#indexName,
467
+ indexName,
414
468
  options: {
415
- useIndex: this.#indexName,
469
+ useIndex: indexName,
416
470
  where: this.#whereClause,
417
471
  },
418
472
  };
@@ -424,18 +478,24 @@ export class FindBuilder<
424
478
  compiledJoins = buildJoinIndexed(this.#table, this.#joinClause);
425
479
  }
426
480
 
481
+ // Convert Cursor objects to strings for after/before
482
+ const afterCursor =
483
+ this.#afterCursor instanceof Cursor ? this.#afterCursor.encode() : this.#afterCursor;
484
+ const beforeCursor =
485
+ this.#beforeCursor instanceof Cursor ? this.#beforeCursor.encode() : this.#beforeCursor;
486
+
427
487
  const options: FindOptions<TTable, TSelect> = {
428
- useIndex: this.#indexName,
488
+ useIndex: indexName,
429
489
  select: this.#selectClause,
430
490
  where: this.#whereClause,
431
- orderByIndex: this.#orderByIndexClause,
432
- after: this.#afterCursor,
433
- before: this.#beforeCursor,
434
- pageSize: this.#pageSizeValue,
491
+ orderByIndex,
492
+ after: afterCursor,
493
+ before: beforeCursor,
494
+ pageSize,
435
495
  joins: compiledJoins,
436
496
  };
437
497
 
438
- return { type: "find", indexName: this.#indexName, options };
498
+ return { type: "find", indexName, options };
439
499
  }
440
500
  }
441
501
 
@@ -939,7 +999,7 @@ export class UnitOfWork<
939
999
  builderFn?: (
940
1000
  // We omit "build" because we don't want to expose it to the user
941
1001
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
942
- ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build">,
1002
+ ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
943
1003
  ): UnitOfWork<
944
1004
  TSchema,
945
1005
  [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>[]],
@@ -983,13 +1043,72 @@ export class UnitOfWork<
983
1043
  >;
984
1044
  }
985
1045
 
1046
+ /**
1047
+ * Add a find operation with cursor metadata (retrieval phase only)
1048
+ */
1049
+ findWithCursor<
1050
+ TTableName extends keyof TSchema["tables"] & string,
1051
+ TSelect extends SelectClause<TSchema["tables"][TTableName]> = true,
1052
+ TJoinOut = {},
1053
+ >(
1054
+ tableName: TTableName,
1055
+ builderFn: (
1056
+ // We omit "build" because we don't want to expose it to the user
1057
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1058
+ ) => Omit<FindBuilder<TSchema["tables"][TTableName], TSelect, TJoinOut>, "build"> | void,
1059
+ ): UnitOfWork<
1060
+ TSchema,
1061
+ [
1062
+ ...TRetrievalResults,
1063
+ CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1064
+ ],
1065
+ TRawInput
1066
+ > {
1067
+ if (this.#state !== "building-retrieval") {
1068
+ throw new Error(
1069
+ `findWithCursor() can only be called during retrieval phase. Current state: ${this.#state}`,
1070
+ );
1071
+ }
1072
+
1073
+ const table = this.#schema.tables[tableName];
1074
+ if (!table) {
1075
+ throw new Error(`Table ${tableName} not found in schema`);
1076
+ }
1077
+
1078
+ // Create builder and pass to callback
1079
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1080
+ builderFn(builder);
1081
+ const { indexName, options, type } = builder.build();
1082
+
1083
+ this.#retrievalOps.push({
1084
+ type,
1085
+ // Safe: we know the table is part of the schema from the findWithCursor() method
1086
+ table: table as TSchema["tables"][TTableName],
1087
+ indexName,
1088
+ // Safe: we're storing the options for later compilation
1089
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1090
+ options: options as any,
1091
+ withCursor: true,
1092
+ });
1093
+
1094
+ return this as unknown as UnitOfWork<
1095
+ TSchema,
1096
+ [
1097
+ ...TRetrievalResults,
1098
+ CursorResult<SelectResult<TSchema["tables"][TTableName], TJoinOut, TSelect>>,
1099
+ ],
1100
+ TRawInput
1101
+ >;
1102
+ }
1103
+
986
1104
  /**
987
1105
  * Add a create operation (mutation phase only)
1106
+ * Returns a FragnoId with the external ID that can be used immediately in subsequent operations
988
1107
  */
989
1108
  create<TableName extends keyof TSchema["tables"] & string>(
990
1109
  table: TableName,
991
1110
  values: TableToInsertValues<TSchema["tables"][TableName]>,
992
- ): this {
1111
+ ): FragnoId {
993
1112
  if (this.#state === "executed") {
994
1113
  throw new Error(`create() can only be called during mutation phase.`);
995
1114
  }
@@ -1041,7 +1160,7 @@ export class UnitOfWork<
1041
1160
  generatedExternalId: externalId,
1042
1161
  });
1043
1162
 
1044
- return this;
1163
+ return FragnoId.fromExternal(externalId, 0);
1045
1164
  }
1046
1165
 
1047
1166
  /**
@@ -1053,8 +1172,8 @@ export class UnitOfWork<
1053
1172
  builderFn: (
1054
1173
  // We omit "build" because we don't want to expose it to the user
1055
1174
  builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1056
- ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1057
- ): this {
1175
+ ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
1176
+ ): void {
1058
1177
  if (this.#state === "executed") {
1059
1178
  throw new Error(`update() can only be called during mutation phase.`);
1060
1179
  }
@@ -1071,8 +1190,6 @@ export class UnitOfWork<
1071
1190
  checkVersion,
1072
1191
  set,
1073
1192
  });
1074
-
1075
- return this;
1076
1193
  }
1077
1194
 
1078
1195
  /**
@@ -1084,8 +1201,8 @@ export class UnitOfWork<
1084
1201
  builderFn?: (
1085
1202
  // We omit "build" because we don't want to expose it to the user
1086
1203
  builder: Omit<DeleteBuilder, "build">,
1087
- ) => Omit<DeleteBuilder, "build">,
1088
- ): this {
1204
+ ) => Omit<DeleteBuilder, "build"> | void,
1205
+ ): void {
1089
1206
  if (this.#state === "executed") {
1090
1207
  throw new Error(`delete() can only be called during mutation phase.`);
1091
1208
  }
@@ -1101,8 +1218,6 @@ export class UnitOfWork<
1101
1218
  id: opId,
1102
1219
  checkVersion,
1103
1220
  });
1104
-
1105
- return this;
1106
1221
  }
1107
1222
 
1108
1223
  /**