@fragno-dev/db 0.1.10 → 0.1.12
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 +40 -37
- package/CHANGELOG.md +19 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts +1 -0
- package/dist/adapters/drizzle/drizzle-query.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +41 -38
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +2 -0
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +13 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/shared.d.ts +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +3 -2
- 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-builder.js +23 -12
- package/dist/adapters/kysely/kysely-query-builder.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 +72 -50
- package/dist/adapters/kysely/kysely-query.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/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 +5 -5
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js.map +1 -1
- package/dist/query/query.d.ts +24 -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 +5 -4
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +2 -3
- 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 +2 -2
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +170 -50
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +89 -35
- package/src/adapters/drizzle/drizzle-query.test.ts +56 -6
- package/src/adapters/drizzle/drizzle-query.ts +68 -63
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +63 -3
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +27 -2
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +88 -0
- package/src/adapters/kysely/kysely-adapter.ts +6 -3
- package/src/adapters/kysely/kysely-query-builder.ts +35 -11
- package/src/adapters/kysely/kysely-query.test.ts +498 -0
- package/src/adapters/kysely/kysely-query.ts +137 -82
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +66 -0
- package/src/adapters/kysely/kysely-uow-executor.ts +5 -9
- package/src/migration-engine/generation-engine.ts +2 -1
- package/src/mod.ts +6 -6
- package/src/query/query-type.test.ts +34 -14
- package/src/query/query.ts +77 -36
- package/src/query/result-transform.test.ts +5 -5
- package/src/query/result-transform.ts +29 -11
- package/src/query/unit-of-work.ts +8 -11
- 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";
|
|
@@ -13,6 +18,53 @@ import type { SQLProvider } from "../../shared/providers";
|
|
|
13
18
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
19
|
type KyselyAny = Kysely<any>;
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Configuration options for creating a Kysely Unit of Work
|
|
23
|
+
*/
|
|
24
|
+
export interface KyselyUOWConfig {
|
|
25
|
+
/**
|
|
26
|
+
* Optional callback to receive compiled SQL queries for logging/debugging
|
|
27
|
+
* This callback is invoked for each query as it's compiled
|
|
28
|
+
*/
|
|
29
|
+
onQuery?: (query: CompiledQuery) => void;
|
|
30
|
+
/**
|
|
31
|
+
* If true, the query will not be executed and the query will be returned. Not respected for UOWs
|
|
32
|
+
* since those have to be manually executed.
|
|
33
|
+
*/
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Special builder for updateMany operations that captures configuration
|
|
39
|
+
*/
|
|
40
|
+
class UpdateManySpecialBuilder<TTable extends AnyTable> {
|
|
41
|
+
#indexName?: string;
|
|
42
|
+
#condition?: unknown;
|
|
43
|
+
#setValues?: TableToUpdateValues<TTable>;
|
|
44
|
+
|
|
45
|
+
whereIndex<TIndexName extends ValidIndexName<TTable>>(
|
|
46
|
+
indexName: TIndexName,
|
|
47
|
+
condition?: unknown,
|
|
48
|
+
): this {
|
|
49
|
+
this.#indexName = indexName as string;
|
|
50
|
+
this.#condition = condition;
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set(values: TableToUpdateValues<TTable>): this {
|
|
55
|
+
this.#setValues = values;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getConfig() {
|
|
60
|
+
return {
|
|
61
|
+
indexName: this.#indexName,
|
|
62
|
+
condition: this.#condition,
|
|
63
|
+
setValues: this.#setValues,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
16
68
|
/**
|
|
17
69
|
* Creates a Kysely-based query engine for the given schema.
|
|
18
70
|
*
|
|
@@ -42,12 +94,18 @@ export function fromKysely<T extends AnySchema>(
|
|
|
42
94
|
pool: ConnectionPool<KyselyAny>,
|
|
43
95
|
provider: SQLProvider,
|
|
44
96
|
mapper?: TableNameMapper,
|
|
45
|
-
|
|
46
|
-
|
|
97
|
+
uowConfig?: KyselyUOWConfig,
|
|
98
|
+
): AbstractQuery<T, KyselyUOWConfig> {
|
|
99
|
+
function createUOW(opts: { name?: string; config?: KyselyUOWConfig }) {
|
|
47
100
|
const uowCompiler = createKyselyUOWCompiler(schema, pool, provider, mapper);
|
|
48
101
|
|
|
49
102
|
const executor: UOWExecutor<CompiledQuery, unknown> = {
|
|
50
103
|
async executeRetrievalPhase(retrievalBatch: CompiledQuery[]) {
|
|
104
|
+
// In dryRun mode, skip execution and return empty results
|
|
105
|
+
if (opts.config?.dryRun) {
|
|
106
|
+
return retrievalBatch.map(() => []);
|
|
107
|
+
}
|
|
108
|
+
|
|
51
109
|
const conn = await pool.connect();
|
|
52
110
|
try {
|
|
53
111
|
return await executeKyselyRetrievalPhase(conn.db, retrievalBatch);
|
|
@@ -56,6 +114,14 @@ export function fromKysely<T extends AnySchema>(
|
|
|
56
114
|
}
|
|
57
115
|
},
|
|
58
116
|
async executeMutationPhase(mutationBatch: CompiledMutation<CompiledQuery>[]) {
|
|
117
|
+
// In dryRun mode, skip execution and return success with mock internal IDs
|
|
118
|
+
if (opts.config?.dryRun) {
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
createdInternalIds: mutationBatch.map(() => null),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
59
125
|
const conn = await pool.connect();
|
|
60
126
|
try {
|
|
61
127
|
return await executeKyselyMutationPhase(conn.db, mutationBatch);
|
|
@@ -97,13 +163,30 @@ export function fromKysely<T extends AnySchema>(
|
|
|
97
163
|
});
|
|
98
164
|
};
|
|
99
165
|
|
|
100
|
-
|
|
166
|
+
const { onQuery, ...restUowConfig } = opts.config ?? {};
|
|
167
|
+
|
|
168
|
+
return new UnitOfWork(schema, uowCompiler, executor, decoder, opts.name, {
|
|
169
|
+
...restUowConfig,
|
|
170
|
+
onQuery: (query) => {
|
|
171
|
+
// CompiledMutation has { query: CompiledQuery, expectedAffectedRows: number | null }
|
|
172
|
+
// CompiledQuery has { query: QueryAST, sql: string, parameters: unknown[] }
|
|
173
|
+
// Check for expectedAffectedRows to distinguish CompiledMutation from CompiledQuery
|
|
174
|
+
const actualQuery =
|
|
175
|
+
query && typeof query === "object" && "expectedAffectedRows" in query
|
|
176
|
+
? (query as CompiledMutation<CompiledQuery>).query
|
|
177
|
+
: (query as CompiledQuery);
|
|
178
|
+
|
|
179
|
+
opts.config?.onQuery?.(actualQuery);
|
|
180
|
+
},
|
|
181
|
+
});
|
|
101
182
|
}
|
|
102
183
|
|
|
103
184
|
return {
|
|
104
185
|
async find(tableName, builderFn) {
|
|
105
|
-
const uow = createUOW();
|
|
106
|
-
|
|
186
|
+
const uow = createUOW({ config: uowConfig });
|
|
187
|
+
// Safe: builderFn returns a FindBuilder (or void), which matches UnitOfWork signature
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
uow.find(tableName, builderFn as any);
|
|
107
190
|
// executeRetrieve returns an array of results (one per find operation)
|
|
108
191
|
// Since we only have one find, unwrap the first result
|
|
109
192
|
const [result]: unknown[][] = await uow.executeRetrieve();
|
|
@@ -111,9 +194,12 @@ export function fromKysely<T extends AnySchema>(
|
|
|
111
194
|
},
|
|
112
195
|
|
|
113
196
|
async findFirst(tableName, builderFn) {
|
|
114
|
-
const uow = createUOW();
|
|
197
|
+
const uow = createUOW({ config: uowConfig });
|
|
115
198
|
if (builderFn) {
|
|
116
|
-
uow.find(tableName, (b) =>
|
|
199
|
+
uow.find(tableName, (b) => {
|
|
200
|
+
builderFn(b);
|
|
201
|
+
return b.pageSize(1);
|
|
202
|
+
});
|
|
117
203
|
} else {
|
|
118
204
|
uow.find(tableName, (b) => b.whereIndex("primary").pageSize(1));
|
|
119
205
|
}
|
|
@@ -123,16 +209,15 @@ export function fromKysely<T extends AnySchema>(
|
|
|
123
209
|
},
|
|
124
210
|
|
|
125
211
|
async create(tableName, values) {
|
|
126
|
-
const uow = createUOW();
|
|
212
|
+
const uow = createUOW({ config: uowConfig });
|
|
127
213
|
uow.create(tableName, values);
|
|
128
214
|
const { success } = await uow.executeMutations();
|
|
129
215
|
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
216
|
throw new Error("Failed to create record");
|
|
133
217
|
}
|
|
134
218
|
|
|
135
|
-
const
|
|
219
|
+
const createdIds = uow.getCreatedIds();
|
|
220
|
+
const createdId = createdIds[0];
|
|
136
221
|
if (!createdId) {
|
|
137
222
|
throw new Error("Failed to get created ID");
|
|
138
223
|
}
|
|
@@ -140,7 +225,7 @@ export function fromKysely<T extends AnySchema>(
|
|
|
140
225
|
},
|
|
141
226
|
|
|
142
227
|
async createMany(tableName, valuesArray) {
|
|
143
|
-
const uow = createUOW();
|
|
228
|
+
const uow = createUOW({ config: uowConfig });
|
|
144
229
|
for (const values of valuesArray) {
|
|
145
230
|
uow.create(tableName, values);
|
|
146
231
|
}
|
|
@@ -153,8 +238,8 @@ export function fromKysely<T extends AnySchema>(
|
|
|
153
238
|
},
|
|
154
239
|
|
|
155
240
|
async update(tableName, id, builderFn) {
|
|
156
|
-
const uow = createUOW();
|
|
157
|
-
uow.update(tableName, id, builderFn
|
|
241
|
+
const uow = createUOW({ config: uowConfig });
|
|
242
|
+
uow.update(tableName, id, builderFn);
|
|
158
243
|
const { success } = await uow.executeMutations();
|
|
159
244
|
if (!success) {
|
|
160
245
|
throw new Error("Failed to update record (version conflict or record not found)");
|
|
@@ -162,51 +247,41 @@ export function fromKysely<T extends AnySchema>(
|
|
|
162
247
|
},
|
|
163
248
|
|
|
164
249
|
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
|
-
};
|
|
250
|
+
const table = schema.tables[tableName];
|
|
251
|
+
if (!table) {
|
|
252
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
253
|
+
}
|
|
179
254
|
|
|
255
|
+
const specialBuilder = new UpdateManySpecialBuilder<typeof table>();
|
|
180
256
|
builderFn(specialBuilder);
|
|
181
257
|
|
|
182
|
-
|
|
258
|
+
const { indexName, condition, setValues } = specialBuilder.getConfig();
|
|
259
|
+
|
|
260
|
+
if (!indexName) {
|
|
183
261
|
throw new Error("whereIndex() must be called in updateMany");
|
|
184
262
|
}
|
|
185
263
|
if (!setValues) {
|
|
186
264
|
throw new Error("set() must be called in updateMany");
|
|
187
265
|
}
|
|
188
266
|
|
|
189
|
-
|
|
190
|
-
const findUow = createUOW();
|
|
267
|
+
const findUow = createUOW({ config: uowConfig });
|
|
191
268
|
findUow.find(tableName, (b) => {
|
|
192
|
-
if (
|
|
193
|
-
|
|
269
|
+
if (condition) {
|
|
270
|
+
// Safe: condition is captured from whereIndex call with proper typing
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
272
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>, condition as any);
|
|
194
273
|
}
|
|
195
|
-
return b.whereIndex(
|
|
274
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>);
|
|
196
275
|
});
|
|
197
|
-
const
|
|
198
|
-
const records = findResults[0];
|
|
276
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
199
277
|
|
|
200
278
|
if (!records || records.length === 0) {
|
|
201
279
|
return;
|
|
202
280
|
}
|
|
203
281
|
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
updateUow.update(tableName as string, record.id as string, (b) =>
|
|
208
|
-
b.set(setValues as never),
|
|
209
|
-
);
|
|
282
|
+
const updateUow = createUOW({ config: uowConfig });
|
|
283
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
284
|
+
updateUow.update(tableName, record.id as string, (b) => b.set(setValues));
|
|
210
285
|
}
|
|
211
286
|
const { success } = await updateUow.executeMutations();
|
|
212
287
|
if (!success) {
|
|
@@ -215,8 +290,8 @@ export function fromKysely<T extends AnySchema>(
|
|
|
215
290
|
},
|
|
216
291
|
|
|
217
292
|
async delete(tableName, id, builderFn) {
|
|
218
|
-
const uow = createUOW();
|
|
219
|
-
uow.delete(tableName, id, builderFn
|
|
293
|
+
const uow = createUOW({ config: uowConfig });
|
|
294
|
+
uow.delete(tableName, id, builderFn);
|
|
220
295
|
const { success } = await uow.executeMutations();
|
|
221
296
|
if (!success) {
|
|
222
297
|
throw new Error("Failed to delete record (version conflict or record not found)");
|
|
@@ -224,43 +299,17 @@ export function fromKysely<T extends AnySchema>(
|
|
|
224
299
|
},
|
|
225
300
|
|
|
226
301
|
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);
|
|
302
|
+
const findUow = createUOW({ config: uowConfig });
|
|
303
|
+
findUow.find(tableName, builderFn);
|
|
304
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
239
305
|
|
|
240
|
-
if (!whereConfig.indexName) {
|
|
241
|
-
throw new Error("whereIndex() must be called in deleteMany");
|
|
242
|
-
}
|
|
243
|
-
|
|
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
306
|
if (!records || records.length === 0) {
|
|
257
307
|
return;
|
|
258
308
|
}
|
|
259
309
|
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
deleteUow.delete(tableName as string, record.id as string);
|
|
310
|
+
const deleteUow = createUOW({ config: uowConfig });
|
|
311
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
312
|
+
deleteUow.delete(tableName, record.id as string);
|
|
264
313
|
}
|
|
265
314
|
const { success } = await deleteUow.executeMutations();
|
|
266
315
|
if (!success) {
|
|
@@ -268,8 +317,14 @@ export function fromKysely<T extends AnySchema>(
|
|
|
268
317
|
}
|
|
269
318
|
},
|
|
270
319
|
|
|
271
|
-
createUnitOfWork(name) {
|
|
272
|
-
return createUOW(
|
|
320
|
+
createUnitOfWork(name, nestedUowConfig) {
|
|
321
|
+
return createUOW({
|
|
322
|
+
name,
|
|
323
|
+
config: {
|
|
324
|
+
...uowConfig,
|
|
325
|
+
...nestedUowConfig,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
273
328
|
},
|
|
274
|
-
} as AbstractQuery<T>;
|
|
329
|
+
} as AbstractQuery<T, KyselyUOWConfig>;
|
|
275
330
|
}
|
|
@@ -913,4 +913,70 @@ describe("kysely-uow-compiler", () => {
|
|
|
913
913
|
expect(compiled.mutationBatch).toHaveLength(1);
|
|
914
914
|
});
|
|
915
915
|
});
|
|
916
|
+
|
|
917
|
+
describe("create and use ID in same UOW", () => {
|
|
918
|
+
it("should support creating and using ID in same UOW", () => {
|
|
919
|
+
const uow = createTestUOW("create-user-and-post");
|
|
920
|
+
|
|
921
|
+
// Create user and capture the returned ID
|
|
922
|
+
const userId = uow.create("users", {
|
|
923
|
+
name: "John Doe",
|
|
924
|
+
email: "john@example.com",
|
|
925
|
+
age: 30,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// Use the returned FragnoId directly to create a post
|
|
929
|
+
// The compiler should extract externalId and generate a subquery
|
|
930
|
+
uow.create("posts", {
|
|
931
|
+
userId: userId,
|
|
932
|
+
title: "My First Post",
|
|
933
|
+
content: "This is my first post",
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
const compiler = createKyselyUOWCompiler(testSchema, pool, "postgresql");
|
|
937
|
+
const compiled = uow.compile(compiler);
|
|
938
|
+
|
|
939
|
+
// Should have no retrieval operations
|
|
940
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
941
|
+
|
|
942
|
+
// Should have 2 mutation operations (create user, create post)
|
|
943
|
+
expect(compiled.mutationBatch).toHaveLength(2);
|
|
944
|
+
|
|
945
|
+
const [userCreate, postCreate] = compiled.mutationBatch;
|
|
946
|
+
assert(userCreate);
|
|
947
|
+
assert(postCreate);
|
|
948
|
+
|
|
949
|
+
// Verify user create SQL
|
|
950
|
+
expect(userCreate.query.sql).toMatchInlineSnapshot(
|
|
951
|
+
`"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""`,
|
|
952
|
+
);
|
|
953
|
+
expect(userCreate.query.parameters).toMatchObject([
|
|
954
|
+
userId.externalId, // The generated ID
|
|
955
|
+
"John Doe",
|
|
956
|
+
"john@example.com",
|
|
957
|
+
30,
|
|
958
|
+
]);
|
|
959
|
+
expect(userCreate.expectedAffectedRows).toBeNull();
|
|
960
|
+
|
|
961
|
+
// Verify post create SQL - FragnoId generates subquery to lookup internal ID
|
|
962
|
+
expect(postCreate.query.sql).toMatchInlineSnapshot(
|
|
963
|
+
`"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""`,
|
|
964
|
+
);
|
|
965
|
+
expect(postCreate.query.parameters).toMatchObject([
|
|
966
|
+
expect.any(String), // generated post ID
|
|
967
|
+
"My First Post",
|
|
968
|
+
"This is my first post",
|
|
969
|
+
userId.externalId, // FragnoId's externalId is used in the subquery
|
|
970
|
+
1, // limit parameter
|
|
971
|
+
]);
|
|
972
|
+
expect(postCreate.expectedAffectedRows).toBeNull();
|
|
973
|
+
|
|
974
|
+
// Verify the returned FragnoId has the expected structure
|
|
975
|
+
expect(userId).toMatchObject({
|
|
976
|
+
externalId: expect.any(String),
|
|
977
|
+
version: 0,
|
|
978
|
+
internalId: undefined,
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
});
|
|
916
982
|
});
|
|
@@ -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
|
@@ -52,7 +52,7 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
|
|
|
52
52
|
/**
|
|
53
53
|
* Creates a FragnoDatabase instance by binding an adapter to this definition.
|
|
54
54
|
*/
|
|
55
|
-
create(adapter: DatabaseAdapter): FragnoDatabase<T> {
|
|
55
|
+
create<TUOWConfig = void>(adapter: DatabaseAdapter<TUOWConfig>): FragnoDatabase<T, TUOWConfig> {
|
|
56
56
|
return new FragnoDatabase({
|
|
57
57
|
namespace: this.#namespace,
|
|
58
58
|
schema: this.#schema,
|
|
@@ -65,12 +65,12 @@ export class FragnoDatabaseDefinition<const T extends AnySchema> {
|
|
|
65
65
|
* A Fragno database instance with a bound adapter.
|
|
66
66
|
* Created from a FragnoDatabaseDefinition by calling .create(adapter).
|
|
67
67
|
*/
|
|
68
|
-
export class FragnoDatabase<const T extends AnySchema> {
|
|
68
|
+
export class FragnoDatabase<const T extends AnySchema, TUOWConfig = void> {
|
|
69
69
|
#namespace: string;
|
|
70
70
|
#schema: T;
|
|
71
|
-
#adapter: DatabaseAdapter
|
|
71
|
+
#adapter: DatabaseAdapter<TUOWConfig>;
|
|
72
72
|
|
|
73
|
-
constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter }) {
|
|
73
|
+
constructor(options: { namespace: string; schema: T; adapter: DatabaseAdapter<TUOWConfig> }) {
|
|
74
74
|
this.#namespace = options.namespace;
|
|
75
75
|
this.#schema = options.schema;
|
|
76
76
|
this.#adapter = options.adapter;
|
|
@@ -80,7 +80,7 @@ export class FragnoDatabase<const T extends AnySchema> {
|
|
|
80
80
|
return fragnoDatabaseFakeSymbol;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
async createClient(): Promise<AbstractQuery<T>> {
|
|
83
|
+
async createClient(): Promise<AbstractQuery<T, TUOWConfig>> {
|
|
84
84
|
const dbVersion = await this.#adapter.getSchemaVersion(this.#namespace);
|
|
85
85
|
if (dbVersion !== this.#schema.version.toString()) {
|
|
86
86
|
throw new Error(
|
|
@@ -112,7 +112,7 @@ export class FragnoDatabase<const T extends AnySchema> {
|
|
|
112
112
|
return this.#schema;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
get adapter(): DatabaseAdapter {
|
|
115
|
+
get adapter(): DatabaseAdapter<TUOWConfig> {
|
|
116
116
|
return this.#adapter;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -111,7 +111,11 @@ describe("query type tests", () => {
|
|
|
111
111
|
it("should return selected columns only", () => {
|
|
112
112
|
const _query = {} as Query;
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
// Test type inference through builder pattern
|
|
115
|
+
function selectNameAndEmailFirst(q: Query) {
|
|
116
|
+
return q.findFirst("users", (b) => b.select(["name", "email"]));
|
|
117
|
+
}
|
|
118
|
+
type Result = Awaited<ReturnType<typeof selectNameAndEmailFirst>>;
|
|
115
119
|
|
|
116
120
|
expectTypeOf<Result>().toExtend<{
|
|
117
121
|
name: string;
|
|
@@ -122,10 +126,14 @@ describe("query type tests", () => {
|
|
|
122
126
|
it("should handle nullable columns correctly", () => {
|
|
123
127
|
const _query = {} as Query;
|
|
124
128
|
|
|
125
|
-
type
|
|
129
|
+
// Test type inference through builder pattern
|
|
130
|
+
function selectAge(q: Query) {
|
|
131
|
+
return q.findFirst("users", (b) => b.select(["age"]));
|
|
132
|
+
}
|
|
133
|
+
type Result = Awaited<ReturnType<typeof selectAge>>;
|
|
126
134
|
type NonNullResult = Exclude<Result, null>;
|
|
127
135
|
|
|
128
|
-
expectTypeOf<NonNullResult>().
|
|
136
|
+
expectTypeOf<NonNullResult>().toMatchObjectType<{ age: number | null }>();
|
|
129
137
|
});
|
|
130
138
|
});
|
|
131
139
|
|
|
@@ -148,14 +156,25 @@ describe("query type tests", () => {
|
|
|
148
156
|
it("should return array of selected columns only", () => {
|
|
149
157
|
const _query = {} as Query;
|
|
150
158
|
|
|
151
|
-
type
|
|
159
|
+
// Test type inference through builder pattern (mimics actual usage)
|
|
160
|
+
function selectNameAndEmail(q: Query) {
|
|
161
|
+
return q.find("users", (b) => b.select(["name", "email"]));
|
|
162
|
+
}
|
|
152
163
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
164
|
+
type Result = Awaited<ReturnType<typeof selectNameAndEmail>>;
|
|
165
|
+
type ResultElement = Result[number];
|
|
166
|
+
|
|
167
|
+
// Verify the result array contains the selected columns
|
|
168
|
+
expectTypeOf<ResultElement>().toMatchObjectType<{
|
|
169
|
+
name: string;
|
|
170
|
+
email: string;
|
|
171
|
+
}>();
|
|
172
|
+
|
|
173
|
+
// Verify that only selected columns exist (not age or isActive)
|
|
174
|
+
// @ts-expect-error - age should not exist on the result type
|
|
175
|
+
type _AgeType = ResultElement["age"];
|
|
176
|
+
// @ts-expect-error - isActive should not exist on the result type
|
|
177
|
+
type _IsActiveType = ResultElement["isActive"];
|
|
159
178
|
});
|
|
160
179
|
});
|
|
161
180
|
|
|
@@ -302,10 +321,11 @@ describe("query type tests", () => {
|
|
|
302
321
|
type PostResult = Awaited<ReturnType<typeof _query.create<"posts">>>;
|
|
303
322
|
expectTypeOf<PostResult>().toEqualTypeOf<FragnoId>();
|
|
304
323
|
|
|
305
|
-
// Find posts by user return type
|
|
306
|
-
type UserPostsResult =
|
|
307
|
-
|
|
308
|
-
|
|
324
|
+
// Find posts by user return type - type-only test
|
|
325
|
+
type UserPostsResult = {
|
|
326
|
+
title: string;
|
|
327
|
+
viewCount: number;
|
|
328
|
+
}[];
|
|
309
329
|
|
|
310
330
|
expectTypeOf<UserPostsResult>().toExtend<
|
|
311
331
|
{
|