@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.
- package/.turbo/turbo-build.log +41 -39
- package/CHANGELOG.md +19 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +1 -1
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +42 -34
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +2 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +25 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/generate.js +1 -1
- package/dist/adapters/kysely/kysely-adapter.d.ts +4 -3
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.d.ts +22 -0
- package/dist/adapters/kysely/kysely-query.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-query.js +101 -51
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js +2 -1
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/adapters/kysely/kysely-uow-executor.js +2 -2
- package/dist/adapters/kysely/kysely-uow-executor.js.map +1 -1
- package/dist/adapters/kysely/migration/execute-base.js +1 -1
- package/dist/migration-engine/generation-engine.d.ts +1 -1
- package/dist/migration-engine/generation-engine.d.ts.map +1 -1
- package/dist/migration-engine/generation-engine.js.map +1 -1
- package/dist/mod.d.ts +7 -6
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -1
- package/dist/mod.js.map +1 -1
- package/dist/query/cursor.d.ts +67 -32
- package/dist/query/cursor.d.ts.map +1 -1
- package/dist/query/cursor.js +84 -31
- package/dist/query/cursor.js.map +1 -1
- package/dist/query/query.d.ts +29 -8
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/result-transform.js +17 -5
- package/dist/query/result-transform.js.map +1 -1
- package/dist/query/unit-of-work.d.ts +19 -8
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +54 -12
- package/dist/query/unit-of-work.js.map +1 -1
- package/dist/schema/serialize.js +2 -0
- package/dist/schema/serialize.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +242 -55
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +95 -39
- package/src/adapters/drizzle/drizzle-query.test.ts +54 -4
- package/src/adapters/drizzle/drizzle-query.ts +74 -60
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +82 -6
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +3 -2
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +40 -1
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +190 -4
- package/src/adapters/kysely/kysely-adapter.ts +6 -3
- package/src/adapters/kysely/kysely-query.test.ts +498 -0
- package/src/adapters/kysely/kysely-query.ts +187 -83
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +85 -3
- package/src/adapters/kysely/kysely-uow-compiler.ts +3 -2
- package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
- package/src/migration-engine/generation-engine.ts +2 -1
- package/src/mod.ts +12 -7
- package/src/query/cursor.test.ts +113 -68
- package/src/query/cursor.ts +127 -36
- package/src/query/query-type.test.ts +34 -14
- package/src/query/query.ts +94 -34
- package/src/query/result-transform.test.ts +5 -5
- package/src/query/result-transform.ts +29 -11
- package/src/query/unit-of-work.ts +141 -26
- package/src/schema/serialize.test.ts +223 -0
- package/src/schema/serialize.ts +16 -0
package/src/query/query.ts
CHANGED
|
@@ -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
|
|
56
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
) =>
|
|
121
|
-
) => Promise<
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
)
|
|
136
|
-
|
|
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"
|
|
165
|
-
) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
127
|
-
if (col.role === "reference"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
|
467
|
+
indexName,
|
|
414
468
|
options: {
|
|
415
|
-
useIndex:
|
|
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:
|
|
488
|
+
useIndex: indexName,
|
|
429
489
|
select: this.#selectClause,
|
|
430
490
|
where: this.#whereClause,
|
|
431
|
-
orderByIndex
|
|
432
|
-
after:
|
|
433
|
-
before:
|
|
434
|
-
pageSize
|
|
491
|
+
orderByIndex,
|
|
492
|
+
after: afterCursor,
|
|
493
|
+
before: beforeCursor,
|
|
494
|
+
pageSize,
|
|
435
495
|
joins: compiledJoins,
|
|
436
496
|
};
|
|
437
497
|
|
|
438
|
-
return { type: "find", indexName
|
|
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
|
-
):
|
|
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
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
/**
|