@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
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { AbstractQuery } from "../../query/query";
|
|
2
|
-
import type { AnySchema } from "../../schema/create";
|
|
3
|
-
import type {
|
|
1
|
+
import type { AbstractQuery, TableToUpdateValues } from "../../query/query";
|
|
2
|
+
import type { AnySchema, AnyTable } from "../../schema/create";
|
|
3
|
+
import type {
|
|
4
|
+
CompiledMutation,
|
|
5
|
+
UOWDecoder,
|
|
6
|
+
UOWExecutor,
|
|
7
|
+
ValidIndexName,
|
|
8
|
+
} from "../../query/unit-of-work";
|
|
4
9
|
import { decodeResult } from "../../query/result-transform";
|
|
5
10
|
import { createKyselyUOWCompiler } from "./kysely-uow-compiler";
|
|
6
11
|
import { executeKyselyRetrievalPhase, executeKyselyMutationPhase } from "./kysely-uow-executor";
|
|
@@ -9,10 +14,58 @@ import type { CompiledQuery, Kysely } from "kysely";
|
|
|
9
14
|
import type { TableNameMapper } from "./kysely-shared";
|
|
10
15
|
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
11
16
|
import type { SQLProvider } from "../../shared/providers";
|
|
17
|
+
import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
|
|
12
18
|
|
|
13
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
20
|
type KyselyAny = Kysely<any>;
|
|
15
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Configuration options for creating a Kysely Unit of Work
|
|
24
|
+
*/
|
|
25
|
+
export interface KyselyUOWConfig {
|
|
26
|
+
/**
|
|
27
|
+
* Optional callback to receive compiled SQL queries for logging/debugging
|
|
28
|
+
* This callback is invoked for each query as it's compiled
|
|
29
|
+
*/
|
|
30
|
+
onQuery?: (query: CompiledQuery) => void;
|
|
31
|
+
/**
|
|
32
|
+
* If true, the query will not be executed and the query will be returned. Not respected for UOWs
|
|
33
|
+
* since those have to be manually executed.
|
|
34
|
+
*/
|
|
35
|
+
dryRun?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Special builder for updateMany operations that captures configuration
|
|
40
|
+
*/
|
|
41
|
+
class UpdateManySpecialBuilder<TTable extends AnyTable> {
|
|
42
|
+
#indexName?: string;
|
|
43
|
+
#condition?: unknown;
|
|
44
|
+
#setValues?: TableToUpdateValues<TTable>;
|
|
45
|
+
|
|
46
|
+
whereIndex<TIndexName extends ValidIndexName<TTable>>(
|
|
47
|
+
indexName: TIndexName,
|
|
48
|
+
condition?: unknown,
|
|
49
|
+
): this {
|
|
50
|
+
this.#indexName = indexName as string;
|
|
51
|
+
this.#condition = condition;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
set(values: TableToUpdateValues<TTable>): this {
|
|
56
|
+
this.#setValues = values;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getConfig() {
|
|
61
|
+
return {
|
|
62
|
+
indexName: this.#indexName,
|
|
63
|
+
condition: this.#condition,
|
|
64
|
+
setValues: this.#setValues,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
16
69
|
/**
|
|
17
70
|
* Creates a Kysely-based query engine for the given schema.
|
|
18
71
|
*
|
|
@@ -42,12 +95,18 @@ export function fromKysely<T extends AnySchema>(
|
|
|
42
95
|
pool: ConnectionPool<KyselyAny>,
|
|
43
96
|
provider: SQLProvider,
|
|
44
97
|
mapper?: TableNameMapper,
|
|
45
|
-
|
|
46
|
-
|
|
98
|
+
uowConfig?: KyselyUOWConfig,
|
|
99
|
+
): AbstractQuery<T, KyselyUOWConfig> {
|
|
100
|
+
function createUOW(opts: { name?: string; config?: KyselyUOWConfig }) {
|
|
47
101
|
const uowCompiler = createKyselyUOWCompiler(schema, pool, provider, mapper);
|
|
48
102
|
|
|
49
103
|
const executor: UOWExecutor<CompiledQuery, unknown> = {
|
|
50
104
|
async executeRetrievalPhase(retrievalBatch: CompiledQuery[]) {
|
|
105
|
+
// In dryRun mode, skip execution and return empty results
|
|
106
|
+
if (opts.config?.dryRun) {
|
|
107
|
+
return retrievalBatch.map(() => []);
|
|
108
|
+
}
|
|
109
|
+
|
|
51
110
|
const conn = await pool.connect();
|
|
52
111
|
try {
|
|
53
112
|
return await executeKyselyRetrievalPhase(conn.db, retrievalBatch);
|
|
@@ -56,6 +115,14 @@ export function fromKysely<T extends AnySchema>(
|
|
|
56
115
|
}
|
|
57
116
|
},
|
|
58
117
|
async executeMutationPhase(mutationBatch: CompiledMutation<CompiledQuery>[]) {
|
|
118
|
+
// In dryRun mode, skip execution and return success with mock internal IDs
|
|
119
|
+
if (opts.config?.dryRun) {
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
createdInternalIds: mutationBatch.map(() => null),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
59
126
|
const conn = await pool.connect();
|
|
60
127
|
try {
|
|
61
128
|
return await executeKyselyMutationPhase(conn.db, mutationBatch);
|
|
@@ -93,27 +160,95 @@ export function fromKysely<T extends AnySchema>(
|
|
|
93
160
|
|
|
94
161
|
// Each result is an array of rows - decode each row
|
|
95
162
|
const rowArray = rows as Record<string, unknown>[];
|
|
96
|
-
|
|
163
|
+
const decodedRows = rowArray.map((row) => decodeResult(row, op.table, provider));
|
|
164
|
+
|
|
165
|
+
// If cursor generation is requested, wrap in CursorResult
|
|
166
|
+
if (op.withCursor) {
|
|
167
|
+
let cursor: Cursor | undefined;
|
|
168
|
+
|
|
169
|
+
// Generate cursor from last item if results exist
|
|
170
|
+
if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
|
|
171
|
+
const lastItem = decodedRows[decodedRows.length - 1];
|
|
172
|
+
const indexName = op.options.orderByIndex.indexName;
|
|
173
|
+
|
|
174
|
+
// Get index columns
|
|
175
|
+
let indexColumns;
|
|
176
|
+
if (indexName === "_primary") {
|
|
177
|
+
indexColumns = [op.table.getIdColumn()];
|
|
178
|
+
} else {
|
|
179
|
+
const index = op.table.indexes[indexName];
|
|
180
|
+
if (index) {
|
|
181
|
+
indexColumns = index.columns;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (indexColumns && lastItem) {
|
|
186
|
+
cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
|
|
187
|
+
indexName: op.options.orderByIndex.indexName,
|
|
188
|
+
orderDirection: op.options.orderByIndex.direction,
|
|
189
|
+
pageSize: op.options.pageSize,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result: CursorResult<unknown> = {
|
|
195
|
+
items: decodedRows,
|
|
196
|
+
cursor,
|
|
197
|
+
};
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return decodedRows;
|
|
97
202
|
});
|
|
98
203
|
};
|
|
99
204
|
|
|
100
|
-
|
|
205
|
+
const { onQuery, ...restUowConfig } = opts.config ?? {};
|
|
206
|
+
|
|
207
|
+
return new UnitOfWork(schema, uowCompiler, executor, decoder, opts.name, {
|
|
208
|
+
...restUowConfig,
|
|
209
|
+
onQuery: (query) => {
|
|
210
|
+
// CompiledMutation has { query: CompiledQuery, expectedAffectedRows: number | null }
|
|
211
|
+
// CompiledQuery has { query: QueryAST, sql: string, parameters: unknown[] }
|
|
212
|
+
// Check for expectedAffectedRows to distinguish CompiledMutation from CompiledQuery
|
|
213
|
+
const actualQuery =
|
|
214
|
+
query && typeof query === "object" && "expectedAffectedRows" in query
|
|
215
|
+
? (query as CompiledMutation<CompiledQuery>).query
|
|
216
|
+
: (query as CompiledQuery);
|
|
217
|
+
|
|
218
|
+
opts.config?.onQuery?.(actualQuery);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
101
221
|
}
|
|
102
222
|
|
|
103
223
|
return {
|
|
104
224
|
async find(tableName, builderFn) {
|
|
105
|
-
const uow = createUOW();
|
|
106
|
-
|
|
225
|
+
const uow = createUOW({ config: uowConfig });
|
|
226
|
+
// Safe: builderFn returns a FindBuilder (or void), which matches UnitOfWork signature
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
uow.find(tableName, builderFn as any);
|
|
107
229
|
// executeRetrieve returns an array of results (one per find operation)
|
|
108
230
|
// Since we only have one find, unwrap the first result
|
|
109
231
|
const [result]: unknown[][] = await uow.executeRetrieve();
|
|
110
232
|
return result ?? [];
|
|
111
233
|
},
|
|
112
234
|
|
|
235
|
+
async findWithCursor(tableName, builderFn) {
|
|
236
|
+
// Safe: builderFn returns a FindBuilder, which matches UnitOfWork signature
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
+
const uow = createUOW({ config: uowConfig }).findWithCursor(tableName, builderFn as any);
|
|
239
|
+
// executeRetrieve returns an array of results (one per find operation)
|
|
240
|
+
// Since we only have one findWithCursor, unwrap the first result
|
|
241
|
+
const [result] = await uow.executeRetrieve();
|
|
242
|
+
return result as CursorResult<unknown>;
|
|
243
|
+
},
|
|
244
|
+
|
|
113
245
|
async findFirst(tableName, builderFn) {
|
|
114
|
-
const uow = createUOW();
|
|
246
|
+
const uow = createUOW({ config: uowConfig });
|
|
115
247
|
if (builderFn) {
|
|
116
|
-
uow.find(tableName, (b) =>
|
|
248
|
+
uow.find(tableName, (b) => {
|
|
249
|
+
builderFn(b);
|
|
250
|
+
return b.pageSize(1);
|
|
251
|
+
});
|
|
117
252
|
} else {
|
|
118
253
|
uow.find(tableName, (b) => b.whereIndex("primary").pageSize(1));
|
|
119
254
|
}
|
|
@@ -123,16 +258,15 @@ export function fromKysely<T extends AnySchema>(
|
|
|
123
258
|
},
|
|
124
259
|
|
|
125
260
|
async create(tableName, values) {
|
|
126
|
-
const uow = createUOW();
|
|
261
|
+
const uow = createUOW({ config: uowConfig });
|
|
127
262
|
uow.create(tableName, values);
|
|
128
263
|
const { success } = await uow.executeMutations();
|
|
129
264
|
if (!success) {
|
|
130
|
-
// This should not happen because we don't `.check()` this call.
|
|
131
|
-
// TODO: Verify what happens when there are unique constraints
|
|
132
265
|
throw new Error("Failed to create record");
|
|
133
266
|
}
|
|
134
267
|
|
|
135
|
-
const
|
|
268
|
+
const createdIds = uow.getCreatedIds();
|
|
269
|
+
const createdId = createdIds[0];
|
|
136
270
|
if (!createdId) {
|
|
137
271
|
throw new Error("Failed to get created ID");
|
|
138
272
|
}
|
|
@@ -140,7 +274,7 @@ export function fromKysely<T extends AnySchema>(
|
|
|
140
274
|
},
|
|
141
275
|
|
|
142
276
|
async createMany(tableName, valuesArray) {
|
|
143
|
-
const uow = createUOW();
|
|
277
|
+
const uow = createUOW({ config: uowConfig });
|
|
144
278
|
for (const values of valuesArray) {
|
|
145
279
|
uow.create(tableName, values);
|
|
146
280
|
}
|
|
@@ -153,8 +287,8 @@ export function fromKysely<T extends AnySchema>(
|
|
|
153
287
|
},
|
|
154
288
|
|
|
155
289
|
async update(tableName, id, builderFn) {
|
|
156
|
-
const uow = createUOW();
|
|
157
|
-
uow.update(tableName, id, builderFn
|
|
290
|
+
const uow = createUOW({ config: uowConfig });
|
|
291
|
+
uow.update(tableName, id, builderFn);
|
|
158
292
|
const { success } = await uow.executeMutations();
|
|
159
293
|
if (!success) {
|
|
160
294
|
throw new Error("Failed to update record (version conflict or record not found)");
|
|
@@ -162,51 +296,41 @@ export function fromKysely<T extends AnySchema>(
|
|
|
162
296
|
},
|
|
163
297
|
|
|
164
298
|
async updateMany(tableName, builderFn) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const specialBuilder = {
|
|
170
|
-
whereIndex(indexName: string, condition?: unknown) {
|
|
171
|
-
whereConfig = { indexName, condition };
|
|
172
|
-
return this;
|
|
173
|
-
},
|
|
174
|
-
set(values: unknown) {
|
|
175
|
-
setValues = values;
|
|
176
|
-
return this;
|
|
177
|
-
},
|
|
178
|
-
};
|
|
299
|
+
const table = schema.tables[tableName];
|
|
300
|
+
if (!table) {
|
|
301
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
302
|
+
}
|
|
179
303
|
|
|
304
|
+
const specialBuilder = new UpdateManySpecialBuilder<typeof table>();
|
|
180
305
|
builderFn(specialBuilder);
|
|
181
306
|
|
|
182
|
-
|
|
307
|
+
const { indexName, condition, setValues } = specialBuilder.getConfig();
|
|
308
|
+
|
|
309
|
+
if (!indexName) {
|
|
183
310
|
throw new Error("whereIndex() must be called in updateMany");
|
|
184
311
|
}
|
|
185
312
|
if (!setValues) {
|
|
186
313
|
throw new Error("set() must be called in updateMany");
|
|
187
314
|
}
|
|
188
315
|
|
|
189
|
-
|
|
190
|
-
const findUow = createUOW();
|
|
316
|
+
const findUow = createUOW({ config: uowConfig });
|
|
191
317
|
findUow.find(tableName, (b) => {
|
|
192
|
-
if (
|
|
193
|
-
|
|
318
|
+
if (condition) {
|
|
319
|
+
// Safe: condition is captured from whereIndex call with proper typing
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
321
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>, condition as any);
|
|
194
322
|
}
|
|
195
|
-
return b.whereIndex(
|
|
323
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>);
|
|
196
324
|
});
|
|
197
|
-
const
|
|
198
|
-
const records = findResults[0];
|
|
325
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
199
326
|
|
|
200
327
|
if (!records || records.length === 0) {
|
|
201
328
|
return;
|
|
202
329
|
}
|
|
203
330
|
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
updateUow.update(tableName as string, record.id as string, (b) =>
|
|
208
|
-
b.set(setValues as never),
|
|
209
|
-
);
|
|
331
|
+
const updateUow = createUOW({ config: uowConfig });
|
|
332
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
333
|
+
updateUow.update(tableName, record.id as string, (b) => b.set(setValues));
|
|
210
334
|
}
|
|
211
335
|
const { success } = await updateUow.executeMutations();
|
|
212
336
|
if (!success) {
|
|
@@ -215,8 +339,8 @@ export function fromKysely<T extends AnySchema>(
|
|
|
215
339
|
},
|
|
216
340
|
|
|
217
341
|
async delete(tableName, id, builderFn) {
|
|
218
|
-
const uow = createUOW();
|
|
219
|
-
uow.delete(tableName, id, builderFn
|
|
342
|
+
const uow = createUOW({ config: uowConfig });
|
|
343
|
+
uow.delete(tableName, id, builderFn);
|
|
220
344
|
const { success } = await uow.executeMutations();
|
|
221
345
|
if (!success) {
|
|
222
346
|
throw new Error("Failed to delete record (version conflict or record not found)");
|
|
@@ -224,43 +348,17 @@ export function fromKysely<T extends AnySchema>(
|
|
|
224
348
|
},
|
|
225
349
|
|
|
226
350
|
async deleteMany(tableName, builderFn) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const specialBuilder = {
|
|
231
|
-
whereIndex(indexName: string, condition?: unknown) {
|
|
232
|
-
whereConfig = { indexName, condition };
|
|
233
|
-
return this;
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// Safe: Call builderFn to capture the configuration
|
|
238
|
-
builderFn(specialBuilder as never);
|
|
239
|
-
|
|
240
|
-
if (!whereConfig.indexName) {
|
|
241
|
-
throw new Error("whereIndex() must be called in deleteMany");
|
|
242
|
-
}
|
|
351
|
+
const findUow = createUOW({ config: uowConfig });
|
|
352
|
+
findUow.find(tableName, builderFn);
|
|
353
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
243
354
|
|
|
244
|
-
// First, find all matching records
|
|
245
|
-
const findUow = createUOW();
|
|
246
|
-
findUow.find(tableName as string, (b) => {
|
|
247
|
-
if (whereConfig.condition) {
|
|
248
|
-
return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
|
|
249
|
-
}
|
|
250
|
-
return b.whereIndex(whereConfig.indexName as never);
|
|
251
|
-
});
|
|
252
|
-
const findResults2 = await findUow.executeRetrieve();
|
|
253
|
-
const records = (findResults2 as unknown as [unknown])[0];
|
|
254
|
-
|
|
255
|
-
// @ts-expect-error - Type narrowing doesn't work through unknown cast
|
|
256
355
|
if (!records || records.length === 0) {
|
|
257
356
|
return;
|
|
258
357
|
}
|
|
259
358
|
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
deleteUow.delete(tableName as string, record.id as string);
|
|
359
|
+
const deleteUow = createUOW({ config: uowConfig });
|
|
360
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
361
|
+
deleteUow.delete(tableName, record.id as string);
|
|
264
362
|
}
|
|
265
363
|
const { success } = await deleteUow.executeMutations();
|
|
266
364
|
if (!success) {
|
|
@@ -268,8 +366,14 @@ export function fromKysely<T extends AnySchema>(
|
|
|
268
366
|
}
|
|
269
367
|
},
|
|
270
368
|
|
|
271
|
-
createUnitOfWork(name) {
|
|
272
|
-
return createUOW(
|
|
369
|
+
createUnitOfWork(name, nestedUowConfig) {
|
|
370
|
+
return createUOW({
|
|
371
|
+
name,
|
|
372
|
+
config: {
|
|
373
|
+
...uowConfig,
|
|
374
|
+
...nestedUowConfig,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
273
377
|
},
|
|
274
|
-
} as AbstractQuery<T>;
|
|
378
|
+
} as AbstractQuery<T, KyselyUOWConfig>;
|
|
275
379
|
}
|
|
@@ -5,6 +5,7 @@ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
|
|
|
5
5
|
import { createKyselyUOWCompiler } from "./kysely-uow-compiler";
|
|
6
6
|
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
7
7
|
import { createKyselyConnectionPool } from "./kysely-connection-pool";
|
|
8
|
+
import { Cursor } from "../../query/cursor";
|
|
8
9
|
|
|
9
10
|
describe("kysely-uow-compiler", () => {
|
|
10
11
|
const testSchema = schema((s) => {
|
|
@@ -252,7 +253,12 @@ describe("kysely-uow-compiler", () => {
|
|
|
252
253
|
|
|
253
254
|
it("should compile find operation with cursor pagination using after", () => {
|
|
254
255
|
const uow = createTestUOW();
|
|
255
|
-
const cursor =
|
|
256
|
+
const cursor = new Cursor({
|
|
257
|
+
indexName: "idx_name",
|
|
258
|
+
orderDirection: "asc",
|
|
259
|
+
pageSize: 10,
|
|
260
|
+
indexValues: { name: "Alice" },
|
|
261
|
+
});
|
|
256
262
|
uow.find("users", (b) =>
|
|
257
263
|
b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
|
|
258
264
|
);
|
|
@@ -269,7 +275,12 @@ describe("kysely-uow-compiler", () => {
|
|
|
269
275
|
|
|
270
276
|
it("should compile find operation with cursor pagination using before", () => {
|
|
271
277
|
const uow = createTestUOW();
|
|
272
|
-
const cursor =
|
|
278
|
+
const cursor = new Cursor({
|
|
279
|
+
indexName: "idx_name",
|
|
280
|
+
orderDirection: "desc",
|
|
281
|
+
pageSize: 10,
|
|
282
|
+
indexValues: { name: "Bob" },
|
|
283
|
+
});
|
|
273
284
|
uow.find("users", (b) =>
|
|
274
285
|
b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
|
|
275
286
|
);
|
|
@@ -286,7 +297,12 @@ describe("kysely-uow-compiler", () => {
|
|
|
286
297
|
|
|
287
298
|
it("should compile find operation with cursor pagination and additional where conditions", () => {
|
|
288
299
|
const uow = createTestUOW();
|
|
289
|
-
const cursor =
|
|
300
|
+
const cursor = new Cursor({
|
|
301
|
+
indexName: "idx_name",
|
|
302
|
+
orderDirection: "asc",
|
|
303
|
+
pageSize: 5,
|
|
304
|
+
indexValues: { name: "Alice" },
|
|
305
|
+
});
|
|
290
306
|
uow.find("users", (b) =>
|
|
291
307
|
b
|
|
292
308
|
.whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
|
|
@@ -913,4 +929,70 @@ describe("kysely-uow-compiler", () => {
|
|
|
913
929
|
expect(compiled.mutationBatch).toHaveLength(1);
|
|
914
930
|
});
|
|
915
931
|
});
|
|
932
|
+
|
|
933
|
+
describe("create and use ID in same UOW", () => {
|
|
934
|
+
it("should support creating and using ID in same UOW", () => {
|
|
935
|
+
const uow = createTestUOW("create-user-and-post");
|
|
936
|
+
|
|
937
|
+
// Create user and capture the returned ID
|
|
938
|
+
const userId = uow.create("users", {
|
|
939
|
+
name: "John Doe",
|
|
940
|
+
email: "john@example.com",
|
|
941
|
+
age: 30,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// Use the returned FragnoId directly to create a post
|
|
945
|
+
// The compiler should extract externalId and generate a subquery
|
|
946
|
+
uow.create("posts", {
|
|
947
|
+
userId: userId,
|
|
948
|
+
title: "My First Post",
|
|
949
|
+
content: "This is my first post",
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const compiler = createKyselyUOWCompiler(testSchema, pool, "postgresql");
|
|
953
|
+
const compiled = uow.compile(compiler);
|
|
954
|
+
|
|
955
|
+
// Should have no retrieval operations
|
|
956
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
957
|
+
|
|
958
|
+
// Should have 2 mutation operations (create user, create post)
|
|
959
|
+
expect(compiled.mutationBatch).toHaveLength(2);
|
|
960
|
+
|
|
961
|
+
const [userCreate, postCreate] = compiled.mutationBatch;
|
|
962
|
+
assert(userCreate);
|
|
963
|
+
assert(postCreate);
|
|
964
|
+
|
|
965
|
+
// Verify user create SQL
|
|
966
|
+
expect(userCreate.query.sql).toMatchInlineSnapshot(
|
|
967
|
+
`"insert into "users" ("id", "name", "email", "age") values ($1, $2, $3, $4) returning "users"."id" as "id", "users"."name" as "name", "users"."email" as "email", "users"."age" as "age", "users"."invitedBy" as "invitedBy", "users"."_internalId" as "_internalId", "users"."_version" as "_version""`,
|
|
968
|
+
);
|
|
969
|
+
expect(userCreate.query.parameters).toMatchObject([
|
|
970
|
+
userId.externalId, // The generated ID
|
|
971
|
+
"John Doe",
|
|
972
|
+
"john@example.com",
|
|
973
|
+
30,
|
|
974
|
+
]);
|
|
975
|
+
expect(userCreate.expectedAffectedRows).toBeNull();
|
|
976
|
+
|
|
977
|
+
// Verify post create SQL - FragnoId generates subquery to lookup internal ID
|
|
978
|
+
expect(postCreate.query.sql).toMatchInlineSnapshot(
|
|
979
|
+
`"insert into "posts" ("id", "title", "content", "userId") values ($1, $2, $3, (select "_internalId" from "users" where "id" = $4 limit $5)) returning "posts"."id" as "id", "posts"."title" as "title", "posts"."content" as "content", "posts"."userId" as "userId", "posts"."viewCount" as "viewCount", "posts"."_internalId" as "_internalId", "posts"."_version" as "_version""`,
|
|
980
|
+
);
|
|
981
|
+
expect(postCreate.query.parameters).toMatchObject([
|
|
982
|
+
expect.any(String), // generated post ID
|
|
983
|
+
"My First Post",
|
|
984
|
+
"This is my first post",
|
|
985
|
+
userId.externalId, // FragnoId's externalId is used in the subquery
|
|
986
|
+
1, // limit parameter
|
|
987
|
+
]);
|
|
988
|
+
expect(postCreate.expectedAffectedRows).toBeNull();
|
|
989
|
+
|
|
990
|
+
// Verify the returned FragnoId has the expected structure
|
|
991
|
+
expect(userId).toMatchObject({
|
|
992
|
+
externalId: expect.any(String),
|
|
993
|
+
version: 0,
|
|
994
|
+
internalId: undefined,
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
});
|
|
916
998
|
});
|
|
@@ -105,8 +105,9 @@ export function createKyselyUOWCompiler<TSchema extends AnySchema>(
|
|
|
105
105
|
|
|
106
106
|
if ((after || before) && indexColumns.length > 0) {
|
|
107
107
|
const cursor = after || before;
|
|
108
|
-
|
|
109
|
-
const
|
|
108
|
+
// Decode cursor if it's a string, otherwise use it as-is
|
|
109
|
+
const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
|
|
110
|
+
const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
|
|
110
111
|
|
|
111
112
|
// Build tuple comparison for cursor pagination
|
|
112
113
|
// For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Kysely, QueryResult } from "kysely";
|
|
1
|
+
import type { CompiledQuery, Kysely, QueryResult } from "kysely";
|
|
2
2
|
import type { CompiledMutation, MutationResult } from "../../query/unit-of-work";
|
|
3
3
|
|
|
4
4
|
function getAffectedRows(result: QueryResult<unknown>): number {
|
|
@@ -43,9 +43,7 @@ function getAffectedRows(result: QueryResult<unknown>): number {
|
|
|
43
43
|
export async function executeKyselyRetrievalPhase(
|
|
44
44
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
45
|
kysely: Kysely<any>,
|
|
46
|
-
retrievalBatch:
|
|
47
|
-
? Q
|
|
48
|
-
: never)[],
|
|
46
|
+
retrievalBatch: CompiledQuery[],
|
|
49
47
|
): Promise<unknown[]> {
|
|
50
48
|
// If no retrieval operations, return empty array immediately
|
|
51
49
|
if (retrievalBatch.length === 0) {
|
|
@@ -56,8 +54,8 @@ export async function executeKyselyRetrievalPhase(
|
|
|
56
54
|
|
|
57
55
|
// Execute all retrieval queries inside a transaction for snapshot isolation
|
|
58
56
|
await kysely.transaction().execute(async (tx) => {
|
|
59
|
-
for (const
|
|
60
|
-
const result = await tx.executeQuery(
|
|
57
|
+
for (const compiledQuery of retrievalBatch) {
|
|
58
|
+
const result = await tx.executeQuery(compiledQuery);
|
|
61
59
|
retrievalResults.push(result.rows);
|
|
62
60
|
}
|
|
63
61
|
});
|
|
@@ -87,9 +85,7 @@ export async function executeKyselyRetrievalPhase(
|
|
|
87
85
|
export async function executeKyselyMutationPhase(
|
|
88
86
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
87
|
kysely: Kysely<any>,
|
|
90
|
-
mutationBatch: CompiledMutation<
|
|
91
|
-
Kysely<unknown>["executeQuery"] extends (query: infer Q) => unknown ? Q : never
|
|
92
|
-
>[],
|
|
88
|
+
mutationBatch: CompiledMutation<CompiledQuery>[],
|
|
93
89
|
): Promise<MutationResult> {
|
|
94
90
|
// If there are no mutations, return success immediately
|
|
95
91
|
if (mutationBatch.length === 0) {
|
|
@@ -34,7 +34,8 @@ export interface ExecuteMigrationResult {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export async function generateMigrationsOrSchema<
|
|
37
|
-
|
|
37
|
+
// oxlint-disable-next-line no-explicit-any
|
|
38
|
+
const TDatabases extends FragnoDatabase<AnySchema, any>[],
|
|
38
39
|
>(
|
|
39
40
|
databases: TDatabases,
|
|
40
41
|
options?: {
|
package/src/mod.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { DatabaseAdapter } from "./adapters/adapters";
|
|
2
2
|
import type { AnySchema } from "./schema/create";
|
|
3
3
|
import type { AbstractQuery } from "./query/query";
|
|
4
|
+
import type { CursorResult } from "./query/cursor";
|
|
5
|
+
import { Cursor } from "./query/cursor";
|
|
4
6
|
|
|
5
|
-
export type { DatabaseAdapter };
|
|
7
|
+
export type { DatabaseAdapter, CursorResult };
|
|
8
|
+
export { Cursor };
|
|
6
9
|
|
|
7
10
|
export const fragnoDatabaseFakeSymbol = "$fragno-database" as const;
|
|
8
11
|
export const fragnoDatabaseLibraryVersion = "0.1" as const;
|
|
@@ -52,7 +55,7 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
|
|
|
52
55
|
/**
|
|
53
56
|
* Creates a FragnoDatabase instance by binding an adapter to this definition.
|
|
54
57
|
*/
|
|
55
|
-
create(adapter: DatabaseAdapter): FragnoDatabase<T> {
|
|
58
|
+
create<TUOWConfig = void>(adapter: DatabaseAdapter<TUOWConfig>): FragnoDatabase<T, TUOWConfig> {
|
|
56
59
|
return new FragnoDatabase({
|
|
57
60
|
namespace: this.#namespace,
|
|
58
61
|
schema: this.#schema,
|
|
@@ -65,12 +68,12 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
|
|
|
65
68
|
* A Fragno database instance with a bound adapter.
|
|
66
69
|
* Created from a FragnoDatabaseDefinition by calling .create(adapter).
|
|
67
70
|
*/
|
|
68
|
-
export class FragnoDatabase<const T extends AnySchema> {
|
|
71
|
+
export class FragnoDatabase<const T extends AnySchema, TUOWConfig = void> {
|
|
69
72
|
#namespace: string;
|
|
70
73
|
#schema: T;
|
|
71
|
-
#adapter: DatabaseAdapter
|
|
74
|
+
#adapter: DatabaseAdapter<TUOWConfig>;
|
|
72
75
|
|
|
73
|
-
constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter }) {
|
|
76
|
+
constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter<TUOWConfig> }) {
|
|
74
77
|
this.#namespace = options.namespace;
|
|
75
78
|
this.#schema = options.schema;
|
|
76
79
|
this.#adapter = options.adapter;
|
|
@@ -80,7 +83,7 @@ export class FragnoDatabase<const T extends AnySchema> {
|
|
|
80
83
|
return fragnoDatabaseFakeSymbol;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
async createClient(): Promise<AbstractQuery<T>> {
|
|
86
|
+
async createClient(): Promise<AbstractQuery<T, TUOWConfig>> {
|
|
84
87
|
const dbVersion = await this.#adapter.getSchemaVersion(this.#namespace);
|
|
85
88
|
if (dbVersion !== this.#schema.version.toString()) {
|
|
86
89
|
throw new Error(
|
|
@@ -112,7 +115,7 @@ export class FragnoDatabase<const T extends AnySchema> {
|
|
|
112
115
|
return this.#schema;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
get adapter(): DatabaseAdapter {
|
|
118
|
+
get adapter(): DatabaseAdapter<TUOWConfig> {
|
|
116
119
|
return this.#adapter;
|
|
117
120
|
}
|
|
118
121
|
}
|
|
@@ -129,3 +132,5 @@ export {
|
|
|
129
132
|
type FragnoPublicConfigWithDatabase,
|
|
130
133
|
type DatabaseFragmentContext,
|
|
131
134
|
} from "./fragment";
|
|
135
|
+
|
|
136
|
+
export { decodeCursor, type CursorData } from "./query/cursor";
|