@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,6 @@
|
|
|
1
1
|
import { drizzle } from "drizzle-orm/pglite";
|
|
2
|
-
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { column, idColumn, referenceColumn, schema } from "../../schema/create";
|
|
2
|
+
import { beforeAll, beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
|
3
|
+
import { column, FragnoId, idColumn, referenceColumn, schema } from "../../schema/create";
|
|
4
4
|
import type { DBType } from "./shared";
|
|
5
5
|
import { writeAndLoadSchema } from "./test-utils";
|
|
6
6
|
import { fromDrizzle } from "./drizzle-query";
|
|
@@ -134,10 +134,14 @@ describe("drizzle-query", () => {
|
|
|
134
134
|
it("should find with select subset of columns", async () => {
|
|
135
135
|
const someExternalId = "some-external-id";
|
|
136
136
|
|
|
137
|
-
await orm.findFirst("user", (b) =>
|
|
137
|
+
const res = await orm.findFirst("user", (b) =>
|
|
138
138
|
b.whereIndex("primary", (eb) => eb("id", "=", someExternalId)).select(["id", "email"]),
|
|
139
139
|
);
|
|
140
140
|
|
|
141
|
+
if (res) {
|
|
142
|
+
expectTypeOf(res.email).toEqualTypeOf<string>();
|
|
143
|
+
}
|
|
144
|
+
|
|
141
145
|
const [query] = queries;
|
|
142
146
|
expect(query.sql).toMatchInlineSnapshot(
|
|
143
147
|
`"select "id", "email", "_internalId", "_version" from "user" "user" where "user"."id" = $1 limit $2"`,
|
|
@@ -194,7 +198,7 @@ describe("drizzle-query", () => {
|
|
|
194
198
|
});
|
|
195
199
|
|
|
196
200
|
it("should find with select subset", async () => {
|
|
197
|
-
await orm.find("user", (b) => b.whereIndex("primary").select(["id", "email"]));
|
|
201
|
+
const _res = await orm.find("user", (b) => b.whereIndex("primary").select(["id", "email"]));
|
|
198
202
|
|
|
199
203
|
const [query] = queries;
|
|
200
204
|
expect(query.sql).toMatchInlineSnapshot(
|
|
@@ -331,6 +335,33 @@ describe("drizzle-query", () => {
|
|
|
331
335
|
);
|
|
332
336
|
expect(query.params).toEqual([newExpiresAt.toISOString(), sessionId]);
|
|
333
337
|
});
|
|
338
|
+
|
|
339
|
+
it("should update with version check using FragnoId", async () => {
|
|
340
|
+
const userId = FragnoId.fromExternal("user-123", 5);
|
|
341
|
+
|
|
342
|
+
await orm.update("user", userId, (b) =>
|
|
343
|
+
b
|
|
344
|
+
.set({
|
|
345
|
+
email: "checked@example.com",
|
|
346
|
+
})
|
|
347
|
+
.check(),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Verify the SQL query includes version check in WHERE clause
|
|
351
|
+
const [query] = queries;
|
|
352
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
353
|
+
`"update "user" set "email" = $1, "_version" = COALESCE(_version, 0) + 1 where ("user"."id" = $2 and "user"."_version" = $3)"`,
|
|
354
|
+
);
|
|
355
|
+
expect(query.params).toEqual(["checked@example.com", "user-123", 5]);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should throw when trying to check() with string ID", async () => {
|
|
359
|
+
await expect(
|
|
360
|
+
orm.update("user", "user-123", (b) => b.set({ email: "test@example.com" }).check()),
|
|
361
|
+
).rejects.toThrow(
|
|
362
|
+
'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
|
|
363
|
+
);
|
|
364
|
+
});
|
|
334
365
|
});
|
|
335
366
|
|
|
336
367
|
describe("updateMany", () => {
|
|
@@ -378,6 +409,25 @@ describe("drizzle-query", () => {
|
|
|
378
409
|
expect(query.sql).toMatchInlineSnapshot(`"delete from "session" where "session"."id" = $1"`);
|
|
379
410
|
expect(query.params).toEqual([sessionId]);
|
|
380
411
|
});
|
|
412
|
+
|
|
413
|
+
it("should delete with version check using FragnoId", async () => {
|
|
414
|
+
const userId = FragnoId.fromExternal("user-789", 3);
|
|
415
|
+
|
|
416
|
+
await orm.delete("user", userId, (b) => b.check());
|
|
417
|
+
|
|
418
|
+
// Verify the SQL query includes version check in WHERE clause
|
|
419
|
+
const [query] = queries;
|
|
420
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
421
|
+
`"delete from "user" where ("user"."id" = $1 and "user"."_version" = $2)"`,
|
|
422
|
+
);
|
|
423
|
+
expect(query.params).toEqual(["user-789", 3]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("should throw when trying to check() with string ID on delete", async () => {
|
|
427
|
+
await expect(orm.delete("user", "user-123", (b) => b.check())).rejects.toThrow(
|
|
428
|
+
'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
|
|
429
|
+
);
|
|
430
|
+
});
|
|
381
431
|
});
|
|
382
432
|
|
|
383
433
|
describe("deleteMany", () => {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AbstractQuery } from "../../query/query";
|
|
2
|
-
import type { AnySchema } from "../../schema/create";
|
|
3
|
-
import type { CompiledMutation, UOWExecutor } from "../../query/unit-of-work";
|
|
2
|
+
import type { AnySchema, AnyTable } from "../../schema/create";
|
|
3
|
+
import type { CompiledMutation, UOWExecutor, ValidIndexName } from "../../query/unit-of-work";
|
|
4
4
|
import { createDrizzleUOWCompiler, type DrizzleCompiledQuery } from "./drizzle-uow-compiler";
|
|
5
5
|
import { executeDrizzleRetrievalPhase, executeDrizzleMutationPhase } from "./drizzle-uow-executor";
|
|
6
6
|
import { UnitOfWork } from "../../query/unit-of-work";
|
|
7
7
|
import { parseDrizzle, type DrizzleResult, type TableNameMapper, type DBType } from "./shared";
|
|
8
8
|
import { createDrizzleUOWDecoder } from "./drizzle-uow-decoder";
|
|
9
9
|
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
10
|
+
import type { TableToUpdateValues } from "../../query/query";
|
|
11
|
+
import type { CursorResult } from "../../query/cursor";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Configuration options for creating a Drizzle Unit of Work
|
|
@@ -24,6 +26,37 @@ export interface DrizzleUOWConfig {
|
|
|
24
26
|
dryRun?: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Special builder for updateMany operations that captures configuration
|
|
31
|
+
*/
|
|
32
|
+
class UpdateManySpecialBuilder<TTable extends AnyTable> {
|
|
33
|
+
#indexName?: string;
|
|
34
|
+
#condition?: unknown;
|
|
35
|
+
#setValues?: TableToUpdateValues<TTable>;
|
|
36
|
+
|
|
37
|
+
whereIndex<TIndexName extends ValidIndexName<TTable>>(
|
|
38
|
+
indexName: TIndexName,
|
|
39
|
+
condition?: unknown,
|
|
40
|
+
): this {
|
|
41
|
+
this.#indexName = indexName as string;
|
|
42
|
+
this.#condition = condition;
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set(values: TableToUpdateValues<TTable>): this {
|
|
47
|
+
this.#setValues = values;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getConfig() {
|
|
52
|
+
return {
|
|
53
|
+
indexName: this.#indexName,
|
|
54
|
+
condition: this.#condition,
|
|
55
|
+
setValues: this.#setValues,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
/**
|
|
28
61
|
* Creates a Drizzle-based query engine for the given schema.
|
|
29
62
|
*
|
|
@@ -114,15 +147,28 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
114
147
|
|
|
115
148
|
return {
|
|
116
149
|
async find(tableName, builderFn) {
|
|
117
|
-
|
|
150
|
+
// Safe: builderFn returns a FindBuilder (or void), which matches UnitOfWork signature
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
const uow = createUOW({ config: uowConfig }).find(tableName, builderFn as any);
|
|
118
153
|
const [result] = await uow.executeRetrieve();
|
|
119
154
|
return result;
|
|
120
155
|
},
|
|
121
156
|
|
|
157
|
+
async findWithCursor(tableName, builderFn) {
|
|
158
|
+
// Safe: builderFn returns a FindBuilder, which matches UnitOfWork signature
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
+
const uow = createUOW({ config: uowConfig }).findWithCursor(tableName, builderFn as any);
|
|
161
|
+
const [result] = await uow.executeRetrieve();
|
|
162
|
+
return result as CursorResult<unknown>;
|
|
163
|
+
},
|
|
164
|
+
|
|
122
165
|
async findFirst(tableName, builderFn) {
|
|
123
166
|
const uow = createUOW({ config: uowConfig });
|
|
124
167
|
if (builderFn) {
|
|
125
|
-
uow.find(tableName, (b) =>
|
|
168
|
+
uow.find(tableName, (b) => {
|
|
169
|
+
builderFn(b);
|
|
170
|
+
return b.pageSize(1);
|
|
171
|
+
});
|
|
126
172
|
} else {
|
|
127
173
|
uow.find(tableName, (b) => b.whereIndex("primary").pageSize(1));
|
|
128
174
|
}
|
|
@@ -133,7 +179,7 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
133
179
|
|
|
134
180
|
async create(tableName, values) {
|
|
135
181
|
const uow = createUOW({ config: uowConfig });
|
|
136
|
-
uow.create(tableName
|
|
182
|
+
uow.create(tableName, values);
|
|
137
183
|
const { success } = await uow.executeMutations();
|
|
138
184
|
if (!success) {
|
|
139
185
|
throw new Error("Failed to create record");
|
|
@@ -150,7 +196,7 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
150
196
|
async createMany(tableName, valuesArray) {
|
|
151
197
|
const uow = createUOW({ config: uowConfig });
|
|
152
198
|
for (const values of valuesArray) {
|
|
153
|
-
uow.create(tableName
|
|
199
|
+
uow.create(tableName, values);
|
|
154
200
|
}
|
|
155
201
|
const { success } = await uow.executeMutations();
|
|
156
202
|
if (!success) {
|
|
@@ -162,7 +208,7 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
162
208
|
|
|
163
209
|
async update(tableName, id, builderFn) {
|
|
164
210
|
const uow = createUOW({ config: uowConfig });
|
|
165
|
-
uow.update(tableName
|
|
211
|
+
uow.update(tableName, id, builderFn);
|
|
166
212
|
const { success } = await uow.executeMutations();
|
|
167
213
|
if (!success) {
|
|
168
214
|
throw new Error("Failed to update record (version conflict or record not found)");
|
|
@@ -170,25 +216,17 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
170
216
|
},
|
|
171
217
|
|
|
172
218
|
async updateMany(tableName, builderFn) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const specialBuilder = {
|
|
179
|
-
whereIndex(indexName: string, condition?: unknown) {
|
|
180
|
-
whereConfig = { indexName, condition };
|
|
181
|
-
return this;
|
|
182
|
-
},
|
|
183
|
-
set(values: unknown) {
|
|
184
|
-
setValues = values;
|
|
185
|
-
return this;
|
|
186
|
-
},
|
|
187
|
-
};
|
|
219
|
+
const table = schema.tables[tableName];
|
|
220
|
+
if (!table) {
|
|
221
|
+
throw new Error(`Table ${tableName} not found in schema`);
|
|
222
|
+
}
|
|
188
223
|
|
|
224
|
+
const specialBuilder = new UpdateManySpecialBuilder<typeof table>();
|
|
189
225
|
builderFn(specialBuilder);
|
|
190
226
|
|
|
191
|
-
|
|
227
|
+
const { indexName, condition, setValues } = specialBuilder.getConfig();
|
|
228
|
+
|
|
229
|
+
if (!indexName) {
|
|
192
230
|
throw new Error("whereIndex() must be called in updateMany");
|
|
193
231
|
}
|
|
194
232
|
if (!setValues) {
|
|
@@ -197,24 +235,22 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
197
235
|
|
|
198
236
|
const findUow = createUOW({ config: uowConfig });
|
|
199
237
|
findUow.find(tableName, (b) => {
|
|
200
|
-
if (
|
|
201
|
-
|
|
238
|
+
if (condition) {
|
|
239
|
+
// Safe: condition is captured from whereIndex call with proper typing
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
241
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>, condition as any);
|
|
202
242
|
}
|
|
203
|
-
return b.whereIndex(
|
|
243
|
+
return b.whereIndex(indexName as ValidIndexName<typeof table>);
|
|
204
244
|
});
|
|
205
|
-
const
|
|
206
|
-
const records = (findResults as unknown as [unknown])[0];
|
|
245
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
207
246
|
|
|
208
|
-
// @ts-expect-error - Type narrowing doesn't work through unknown cast
|
|
209
247
|
if (!records || records.length === 0) {
|
|
210
248
|
return;
|
|
211
249
|
}
|
|
212
250
|
|
|
213
251
|
const updateUow = createUOW({ config: uowConfig });
|
|
214
|
-
for (const record of records as
|
|
215
|
-
updateUow.update(tableName
|
|
216
|
-
b.set(setValues as never),
|
|
217
|
-
);
|
|
252
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
253
|
+
updateUow.update(tableName, record.id as string, (b) => b.set(setValues));
|
|
218
254
|
}
|
|
219
255
|
const { success } = await updateUow.executeMutations();
|
|
220
256
|
if (!success) {
|
|
@@ -224,7 +260,7 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
224
260
|
|
|
225
261
|
async delete(tableName, id, builderFn) {
|
|
226
262
|
const uow = createUOW({ config: uowConfig });
|
|
227
|
-
uow.delete(tableName
|
|
263
|
+
uow.delete(tableName, id, builderFn);
|
|
228
264
|
const { success } = await uow.executeMutations();
|
|
229
265
|
if (!success) {
|
|
230
266
|
throw new Error("Failed to delete record (version conflict or record not found)");
|
|
@@ -232,39 +268,17 @@ export function fromDrizzle<T extends AnySchema>(
|
|
|
232
268
|
},
|
|
233
269
|
|
|
234
270
|
async deleteMany(tableName, builderFn) {
|
|
235
|
-
let whereConfig: { indexName?: string; condition?: unknown } = {};
|
|
236
|
-
|
|
237
|
-
const specialBuilder = {
|
|
238
|
-
whereIndex(indexName: string, condition?: unknown) {
|
|
239
|
-
whereConfig = { indexName, condition };
|
|
240
|
-
return this;
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
builderFn(specialBuilder as never);
|
|
245
|
-
|
|
246
|
-
if (!whereConfig.indexName) {
|
|
247
|
-
throw new Error("whereIndex() must be called in deleteMany");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
271
|
const findUow = createUOW({ config: uowConfig });
|
|
251
|
-
findUow.find(tableName
|
|
252
|
-
|
|
253
|
-
return b.whereIndex(whereConfig.indexName as never, whereConfig.condition as never);
|
|
254
|
-
}
|
|
255
|
-
return b.whereIndex(whereConfig.indexName as never);
|
|
256
|
-
});
|
|
257
|
-
const findResults2 = await findUow.executeRetrieve();
|
|
258
|
-
const records = (findResults2 as unknown as [unknown])[0];
|
|
272
|
+
findUow.find(tableName, builderFn);
|
|
273
|
+
const [records]: unknown[][] = await findUow.executeRetrieve();
|
|
259
274
|
|
|
260
|
-
// @ts-expect-error - Type narrowing doesn't work through unknown cast
|
|
261
275
|
if (!records || records.length === 0) {
|
|
262
276
|
return;
|
|
263
277
|
}
|
|
264
278
|
|
|
265
279
|
const deleteUow = createUOW({ config: uowConfig });
|
|
266
|
-
for (const record of records as
|
|
267
|
-
deleteUow.delete(tableName
|
|
280
|
+
for (const record of records as Array<{ id: unknown }>) {
|
|
281
|
+
deleteUow.delete(tableName, record.id as string);
|
|
268
282
|
}
|
|
269
283
|
const { success } = await deleteUow.executeMutations();
|
|
270
284
|
if (!success) {
|
|
@@ -14,6 +14,7 @@ import { UnitOfWork, type UOWDecoder } from "../../query/unit-of-work";
|
|
|
14
14
|
import { writeAndLoadSchema } from "./test-utils";
|
|
15
15
|
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
16
16
|
import { createDrizzleConnectionPool } from "./drizzle-connection-pool";
|
|
17
|
+
import { Cursor } from "../../query/cursor";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Integration tests for Drizzle UOW compiler and executor.
|
|
@@ -233,7 +234,12 @@ describe("drizzle-uow-compiler", () => {
|
|
|
233
234
|
|
|
234
235
|
it("should compile find operation with cursor pagination using after", () => {
|
|
235
236
|
const uow = createTestUOW();
|
|
236
|
-
const cursor =
|
|
237
|
+
const cursor = new Cursor({
|
|
238
|
+
indexName: "idx_name",
|
|
239
|
+
orderDirection: "asc",
|
|
240
|
+
pageSize: 10,
|
|
241
|
+
indexValues: { name: "Alice" },
|
|
242
|
+
});
|
|
237
243
|
uow.find("users", (b) =>
|
|
238
244
|
b.whereIndex("idx_name").orderByIndex("idx_name", "asc").after(cursor).pageSize(10),
|
|
239
245
|
);
|
|
@@ -250,7 +256,12 @@ describe("drizzle-uow-compiler", () => {
|
|
|
250
256
|
|
|
251
257
|
it("should compile find operation with cursor pagination using before", () => {
|
|
252
258
|
const uow = createTestUOW();
|
|
253
|
-
const cursor =
|
|
259
|
+
const cursor = new Cursor({
|
|
260
|
+
indexName: "idx_name",
|
|
261
|
+
orderDirection: "desc",
|
|
262
|
+
pageSize: 10,
|
|
263
|
+
indexValues: { name: "Bob" },
|
|
264
|
+
});
|
|
254
265
|
uow.find("users", (b) =>
|
|
255
266
|
b.whereIndex("idx_name").orderByIndex("idx_name", "desc").before(cursor).pageSize(10),
|
|
256
267
|
);
|
|
@@ -267,7 +278,12 @@ describe("drizzle-uow-compiler", () => {
|
|
|
267
278
|
|
|
268
279
|
it("should compile find operation with cursor pagination and additional where conditions", () => {
|
|
269
280
|
const uow = createTestUOW();
|
|
270
|
-
const cursor =
|
|
281
|
+
const cursor = new Cursor({
|
|
282
|
+
indexName: "idx_name",
|
|
283
|
+
orderDirection: "asc",
|
|
284
|
+
pageSize: 5,
|
|
285
|
+
indexValues: { name: "Alice" },
|
|
286
|
+
});
|
|
271
287
|
uow.find("users", (b) =>
|
|
272
288
|
b
|
|
273
289
|
.whereIndex("idx_name", (eb) => eb("name", "starts with", "John"))
|
|
@@ -457,10 +473,9 @@ describe("drizzle-uow-compiler", () => {
|
|
|
457
473
|
const [batch] = compiled.mutationBatch;
|
|
458
474
|
assert(batch);
|
|
459
475
|
expect(batch.expectedAffectedRows).toBeNull();
|
|
460
|
-
// FragnoId should
|
|
461
|
-
// But since we don't have the internal ID populated in the test, it should serialize
|
|
476
|
+
// FragnoId should generate a subquery to lookup the internal ID from external ID
|
|
462
477
|
expect(batch.query.sql).toMatchInlineSnapshot(
|
|
463
|
-
`"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values ($1, $2, $3, $4, default, default, default)"`,
|
|
478
|
+
`"insert into "posts" ("id", "title", "content", "userId", "viewCount", "_internalId", "_version") values ($1, $2, $3, (select "_internalId" from "users" where "id" = $4 limit 1), default, default, default)"`,
|
|
464
479
|
);
|
|
465
480
|
});
|
|
466
481
|
|
|
@@ -1303,5 +1318,66 @@ describe("drizzle-uow-compiler", () => {
|
|
|
1303
1318
|
);
|
|
1304
1319
|
expect(compiled.retrievalBatch[0].params).toEqual([1, sessionId]);
|
|
1305
1320
|
});
|
|
1321
|
+
|
|
1322
|
+
it("should support creating and using ID in same UOW", () => {
|
|
1323
|
+
const uow = createAuthUOW("create-user-and-session");
|
|
1324
|
+
|
|
1325
|
+
// Create user and capture the returned ID
|
|
1326
|
+
const userId = uow.create("user", {
|
|
1327
|
+
email: "test@example.com",
|
|
1328
|
+
passwordHash: "hashed_password",
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// Use the returned FragnoId directly to create a session
|
|
1332
|
+
// The compiler should extract externalId and generate a subquery
|
|
1333
|
+
uow.create("session", {
|
|
1334
|
+
userId: userId,
|
|
1335
|
+
expiresAt: new Date("2025-12-31"),
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
const compiler = createDrizzleUOWCompiler(authSchema, authPool, "postgresql");
|
|
1339
|
+
const compiled = uow.compile(compiler);
|
|
1340
|
+
|
|
1341
|
+
// Should have no retrieval operations
|
|
1342
|
+
expect(compiled.retrievalBatch).toHaveLength(0);
|
|
1343
|
+
|
|
1344
|
+
// Should have 2 mutation operations (create user, create session)
|
|
1345
|
+
expect(compiled.mutationBatch).toHaveLength(2);
|
|
1346
|
+
|
|
1347
|
+
const [userCreate, sessionCreate] = compiled.mutationBatch;
|
|
1348
|
+
assert(userCreate);
|
|
1349
|
+
assert(sessionCreate);
|
|
1350
|
+
|
|
1351
|
+
// Verify user create SQL
|
|
1352
|
+
expect(userCreate.query.sql).toMatchInlineSnapshot(
|
|
1353
|
+
`"insert into "user" ("id", "email", "passwordHash", "createdAt", "_internalId", "_version") values ($1, $2, $3, $4, default, default)"`,
|
|
1354
|
+
);
|
|
1355
|
+
expect(userCreate.query.params).toMatchObject([
|
|
1356
|
+
userId.externalId, // The generated ID
|
|
1357
|
+
"test@example.com",
|
|
1358
|
+
"hashed_password",
|
|
1359
|
+
expect.any(String), // timestamp
|
|
1360
|
+
]);
|
|
1361
|
+
expect(userCreate.expectedAffectedRows).toBeNull();
|
|
1362
|
+
|
|
1363
|
+
// Verify session create SQL - FragnoId generates subquery to lookup internal ID
|
|
1364
|
+
expect(sessionCreate.query.sql).toMatchInlineSnapshot(
|
|
1365
|
+
`"insert into "session" ("id", "userId", "expiresAt", "createdAt", "_internalId", "_version") values ($1, (select "_internalId" from "user" where "id" = $2 limit 1), $3, $4, default, default)"`,
|
|
1366
|
+
);
|
|
1367
|
+
expect(sessionCreate.query.params).toMatchObject([
|
|
1368
|
+
expect.any(String), // generated session ID
|
|
1369
|
+
userId.externalId, // FragnoId's externalId is used in the subquery
|
|
1370
|
+
expect.any(String), // expiresAt timestamp
|
|
1371
|
+
expect.any(String), // createdAt timestamp
|
|
1372
|
+
]);
|
|
1373
|
+
expect(sessionCreate.expectedAffectedRows).toBeNull();
|
|
1374
|
+
|
|
1375
|
+
// Verify the returned FragnoId has the expected structure
|
|
1376
|
+
expect(userId).toMatchObject({
|
|
1377
|
+
externalId: expect.any(String),
|
|
1378
|
+
version: 0,
|
|
1379
|
+
internalId: undefined,
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1306
1382
|
});
|
|
1307
1383
|
});
|
|
@@ -423,8 +423,9 @@ export function createDrizzleUOWCompiler<TSchema extends AnySchema>(
|
|
|
423
423
|
// Add cursor-based pagination conditions
|
|
424
424
|
if ((after || before) && indexColumns.length > 0) {
|
|
425
425
|
const cursor = after || before;
|
|
426
|
-
|
|
427
|
-
const
|
|
426
|
+
// Decode cursor if it's a string, otherwise use it as-is
|
|
427
|
+
const cursorObj = typeof cursor === "string" ? decodeCursor(cursor!) : cursor!;
|
|
428
|
+
const serializedValues = serializeCursorValues(cursorObj, indexColumns, provider);
|
|
428
429
|
|
|
429
430
|
// Build tuple comparison for cursor pagination
|
|
430
431
|
// For "after" with "asc": (col1, col2, ...) > (val1, val2, ...)
|
|
@@ -4,6 +4,7 @@ import type { RetrievalOperation, UOWDecoder } from "../../query/unit-of-work";
|
|
|
4
4
|
import { decodeResult } from "../../query/result-transform";
|
|
5
5
|
import { getOrderedJoinColumns } from "./join-column-utils";
|
|
6
6
|
import type { DrizzleResult } from "./shared";
|
|
7
|
+
import { createCursorFromRecord, Cursor, type CursorResult } from "../../query/cursor";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Join information with nested join support
|
|
@@ -173,10 +174,48 @@ export function createDrizzleUOWDecoder<TSchema extends AnySchema>(
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
// Handle find operations - decode each row
|
|
176
|
-
|
|
177
|
+
const decodedRows = result.rows.map((row) => {
|
|
177
178
|
const transformedRow = transformJoinArraysToObjects(row, op, provider);
|
|
178
179
|
return decodeResult(transformedRow, op.table, provider);
|
|
179
180
|
});
|
|
181
|
+
|
|
182
|
+
// If cursor generation is requested, wrap in CursorResult
|
|
183
|
+
if (op.withCursor) {
|
|
184
|
+
let cursor: Cursor | undefined;
|
|
185
|
+
|
|
186
|
+
// Generate cursor from last item if results exist
|
|
187
|
+
if (decodedRows.length > 0 && op.options.orderByIndex && op.options.pageSize) {
|
|
188
|
+
const lastItem = decodedRows[decodedRows.length - 1];
|
|
189
|
+
const indexName = op.options.orderByIndex.indexName;
|
|
190
|
+
|
|
191
|
+
// Get index columns
|
|
192
|
+
let indexColumns;
|
|
193
|
+
if (indexName === "_primary") {
|
|
194
|
+
indexColumns = [op.table.getIdColumn()];
|
|
195
|
+
} else {
|
|
196
|
+
const index = op.table.indexes[indexName];
|
|
197
|
+
if (index) {
|
|
198
|
+
indexColumns = index.columns;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (indexColumns && lastItem) {
|
|
203
|
+
cursor = createCursorFromRecord(lastItem as Record<string, unknown>, indexColumns, {
|
|
204
|
+
indexName: op.options.orderByIndex.indexName,
|
|
205
|
+
orderDirection: op.options.orderByIndex.direction,
|
|
206
|
+
pageSize: op.options.pageSize,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cursorResult: CursorResult<unknown> = {
|
|
212
|
+
items: decodedRows,
|
|
213
|
+
cursor,
|
|
214
|
+
};
|
|
215
|
+
return cursorResult;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return decodedRows;
|
|
180
219
|
});
|
|
181
220
|
};
|
|
182
221
|
}
|