@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
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
import { KyselyPGlite } from "kysely-pglite";
|
|
3
|
+
import { assert, beforeAll, beforeEach, describe, expect, expectTypeOf, it } from "vitest";
|
|
4
|
+
import { column, FragnoId, idColumn, referenceColumn, schema } from "../../schema/create";
|
|
5
|
+
import { fromKysely } from "./kysely-query";
|
|
6
|
+
import { createKyselyConnectionPool } from "./kysely-connection-pool";
|
|
7
|
+
import type { ConnectionPool } from "../../shared/connection-pool";
|
|
8
|
+
import type { CompiledQuery } from "kysely";
|
|
9
|
+
|
|
10
|
+
describe("kysely-query", () => {
|
|
11
|
+
const authSchema = schema((s) => {
|
|
12
|
+
return s
|
|
13
|
+
.addTable("user", (t) => {
|
|
14
|
+
return t
|
|
15
|
+
.addColumn("id", idColumn())
|
|
16
|
+
.addColumn("email", column("string"))
|
|
17
|
+
.addColumn("passwordHash", column("string"))
|
|
18
|
+
.addColumn(
|
|
19
|
+
"createdAt",
|
|
20
|
+
column("timestamp").defaultTo$((b) => b.now()),
|
|
21
|
+
)
|
|
22
|
+
.createIndex("idx_user_email", ["email"]);
|
|
23
|
+
})
|
|
24
|
+
.addTable("session", (t) => {
|
|
25
|
+
return t
|
|
26
|
+
.addColumn("id", idColumn())
|
|
27
|
+
.addColumn("userId", referenceColumn())
|
|
28
|
+
.addColumn("expiresAt", column("timestamp"))
|
|
29
|
+
.addColumn(
|
|
30
|
+
"createdAt",
|
|
31
|
+
column("timestamp").defaultTo$((b) => b.now()),
|
|
32
|
+
)
|
|
33
|
+
.createIndex("idx_session_user", ["userId"]);
|
|
34
|
+
})
|
|
35
|
+
.addReference("sessionOwner", {
|
|
36
|
+
from: {
|
|
37
|
+
table: "session",
|
|
38
|
+
column: "userId",
|
|
39
|
+
},
|
|
40
|
+
to: {
|
|
41
|
+
table: "user",
|
|
42
|
+
column: "id",
|
|
43
|
+
},
|
|
44
|
+
type: "one",
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
49
|
+
let kysely: Kysely<any>;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
let pool: ConnectionPool<Kysely<any>>;
|
|
52
|
+
let orm: ReturnType<typeof fromKysely<typeof authSchema>>;
|
|
53
|
+
|
|
54
|
+
const queries: CompiledQuery[] = [];
|
|
55
|
+
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
// Create Kysely instance with PGLite (in-memory Postgres)
|
|
58
|
+
const { dialect } = await KyselyPGlite.create();
|
|
59
|
+
kysely = new Kysely({
|
|
60
|
+
dialect,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Wrap in connection pool
|
|
64
|
+
pool = createKyselyConnectionPool(kysely);
|
|
65
|
+
orm = fromKysely(authSchema, pool, "postgresql", undefined, {
|
|
66
|
+
onQuery: (query) => {
|
|
67
|
+
queries.push(query);
|
|
68
|
+
},
|
|
69
|
+
dryRun: true,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
queries.splice(0, queries.length);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("findFirst", () => {
|
|
78
|
+
it("should find session with user join", async () => {
|
|
79
|
+
const someExternalId = "some-external-id";
|
|
80
|
+
|
|
81
|
+
// Find the session with user join
|
|
82
|
+
await orm.findFirst("session", (b) =>
|
|
83
|
+
b
|
|
84
|
+
.whereIndex("primary", (eb) => eb("id", "=", someExternalId))
|
|
85
|
+
.join((j) => j.sessionOwner((b) => b.select(["id", "email"]))),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const [query] = queries;
|
|
89
|
+
assert(query?.sql);
|
|
90
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
91
|
+
`"select "sessionOwner"."id" as "sessionOwner:id", "sessionOwner"."email" as "sessionOwner:email", "sessionOwner"."_internalId" as "sessionOwner:_internalId", "sessionOwner"."_version" as "sessionOwner:_version", "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session" left join "user" as "sessionOwner" on "session"."userId" = "sessionOwner"."_internalId" where "session"."id" = $1 limit $2"`,
|
|
92
|
+
);
|
|
93
|
+
expect(query?.parameters).toEqual([someExternalId, 1]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should find session without join", async () => {
|
|
97
|
+
const someExternalId = "some-external-id";
|
|
98
|
+
|
|
99
|
+
await orm.findFirst("session", (b) =>
|
|
100
|
+
b.whereIndex("primary", (eb) => eb("id", "=", someExternalId)),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const [query] = queries;
|
|
104
|
+
assert(query?.sql);
|
|
105
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
106
|
+
`"select "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session" where "session"."id" = $1 limit $2"`,
|
|
107
|
+
);
|
|
108
|
+
expect(query?.parameters).toEqual([someExternalId, 1]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should find user by email using custom index", async () => {
|
|
112
|
+
const email = "test@example.com";
|
|
113
|
+
|
|
114
|
+
await orm.findFirst("user", (b) =>
|
|
115
|
+
b.whereIndex("idx_user_email", (eb) => eb("email", "=", email)),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const [query] = queries;
|
|
119
|
+
assert(query?.sql);
|
|
120
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
121
|
+
`"select "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version" from "user" where "user"."email" = $1 limit $2"`,
|
|
122
|
+
);
|
|
123
|
+
expect(query?.parameters).toEqual([email, 1]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should find with select subset of columns", async () => {
|
|
127
|
+
const someExternalId = "some-external-id";
|
|
128
|
+
|
|
129
|
+
const res = await orm.findFirst("user", (b) =>
|
|
130
|
+
b.whereIndex("primary", (eb) => eb("id", "=", someExternalId)).select(["id", "email"]),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (res) {
|
|
134
|
+
expectTypeOf(res.email).toEqualTypeOf<string>();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const [query] = queries;
|
|
138
|
+
assert(query?.sql);
|
|
139
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
140
|
+
`"select "user"."id" as "id", "user"."email" as "email", "user"."_internalId" as "_internalId", "user"."_version" as "_version" from "user" where "user"."id" = $1 limit $2"`,
|
|
141
|
+
);
|
|
142
|
+
expect(query?.parameters).toEqual([someExternalId, 1]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("find", () => {
|
|
147
|
+
it("should find all sessions using primary index", async () => {
|
|
148
|
+
await orm.find("session", (b) => b.whereIndex("primary"));
|
|
149
|
+
|
|
150
|
+
const [query] = queries;
|
|
151
|
+
assert(query?.sql);
|
|
152
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
153
|
+
`"select "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session""`,
|
|
154
|
+
);
|
|
155
|
+
expect(query?.parameters).toEqual([]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should find sessions with user join", async () => {
|
|
159
|
+
await orm.find("session", (b) =>
|
|
160
|
+
b.whereIndex("primary").join((j) => j.sessionOwner((b) => b.select(["id", "email"]))),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const [query] = queries;
|
|
164
|
+
assert(query?.sql);
|
|
165
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
166
|
+
`"select "sessionOwner"."id" as "sessionOwner:id", "sessionOwner"."email" as "sessionOwner:email", "sessionOwner"."_internalId" as "sessionOwner:_internalId", "sessionOwner"."_version" as "sessionOwner:_version", "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session" left join "user" as "sessionOwner" on "session"."userId" = "sessionOwner"."_internalId""`,
|
|
167
|
+
);
|
|
168
|
+
expect(query?.parameters).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should find sessions with where clause using custom index", async () => {
|
|
172
|
+
const userId = "user-123";
|
|
173
|
+
|
|
174
|
+
await orm.find("session", (b) =>
|
|
175
|
+
b.whereIndex("idx_session_user", (eb) => eb("userId", "=", userId)),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const [query] = queries;
|
|
179
|
+
assert(query?.sql);
|
|
180
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
181
|
+
`"select "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session" where "session"."userId" = (select "_internalId" from "user" where "id" = $1 limit $2)"`,
|
|
182
|
+
);
|
|
183
|
+
expect(query?.parameters).toEqual([userId, 1]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should find with pageSize", async () => {
|
|
187
|
+
await orm.find("user", (b) => b.whereIndex("primary").pageSize(10));
|
|
188
|
+
|
|
189
|
+
const [query] = queries;
|
|
190
|
+
assert(query?.sql);
|
|
191
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
192
|
+
`"select "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version" from "user" limit $1"`,
|
|
193
|
+
);
|
|
194
|
+
expect(query?.parameters).toEqual([10]);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should find with select subset", async () => {
|
|
198
|
+
const _res = await orm.find("user", (b) => b.whereIndex("primary").select(["id", "email"]));
|
|
199
|
+
|
|
200
|
+
const [query] = queries;
|
|
201
|
+
assert(query?.sql);
|
|
202
|
+
expect(query?.sql).toMatchInlineSnapshot(
|
|
203
|
+
`"select "user"."id" as "id", "user"."email" as "email", "user"."_internalId" as "_internalId", "user"."_version" as "_version" from "user""`,
|
|
204
|
+
);
|
|
205
|
+
expect(query?.parameters).toEqual([]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("create", () => {
|
|
210
|
+
it("should create a new user", async () => {
|
|
211
|
+
const createdId = await orm.create("user", {
|
|
212
|
+
id: "user-123",
|
|
213
|
+
email: "test@example.com",
|
|
214
|
+
passwordHash: "hashed-password",
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Verify the operation succeeded by getting the created ID (FragnoId object)
|
|
218
|
+
expect(createdId).toBeDefined();
|
|
219
|
+
expect(typeof createdId).toBe("object");
|
|
220
|
+
|
|
221
|
+
// Verify the SQL query was captured
|
|
222
|
+
const [query] = queries;
|
|
223
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
224
|
+
`"insert into "user" ("id", "email", "passwordHash", "createdAt") values ($1, $2, $3, $4) returning "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version""`,
|
|
225
|
+
);
|
|
226
|
+
expect(query.parameters[0]).toEqual("user-123");
|
|
227
|
+
expect(query.parameters[1]).toEqual("test@example.com");
|
|
228
|
+
expect(query.parameters[2]).toEqual("hashed-password");
|
|
229
|
+
expect(query.parameters[3]).toBeInstanceOf(Date); // createdAt timestamp
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should create a new session", async () => {
|
|
233
|
+
const expiresAt = new Date("2025-12-31T23:59:59Z");
|
|
234
|
+
|
|
235
|
+
const createdId = await orm.create("session", {
|
|
236
|
+
id: "session-456",
|
|
237
|
+
userId: "user-123",
|
|
238
|
+
expiresAt,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Verify the operation succeeded by getting the created ID (FragnoId object)
|
|
242
|
+
expect(createdId).toBeDefined();
|
|
243
|
+
expect(typeof createdId).toBe("object");
|
|
244
|
+
|
|
245
|
+
// Verify the SQL query was captured
|
|
246
|
+
const [query] = queries;
|
|
247
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
248
|
+
`"insert into "session" ("id", "userId", "expiresAt", "createdAt") values ($1, (select "_internalId" from "user" where "id" = $2 limit $3), $4, $5) returning "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version""`,
|
|
249
|
+
);
|
|
250
|
+
expect(query.parameters[0]).toEqual("session-456");
|
|
251
|
+
expect(query.parameters[1]).toEqual("user-123"); // userId is resolved via subquery
|
|
252
|
+
expect(query.parameters[2]).toEqual(1); // Limit for subquery
|
|
253
|
+
expect(query.parameters[3]).toEqual(expiresAt);
|
|
254
|
+
expect(query.parameters[4]).toBeInstanceOf(Date); // createdAt timestamp
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("createMany", () => {
|
|
259
|
+
it("should create multiple users", async () => {
|
|
260
|
+
const createdIds = await orm.createMany("user", [
|
|
261
|
+
{
|
|
262
|
+
id: "user-1",
|
|
263
|
+
email: "user1@example.com",
|
|
264
|
+
passwordHash: "hash1",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "user-2",
|
|
268
|
+
email: "user2@example.com",
|
|
269
|
+
passwordHash: "hash2",
|
|
270
|
+
},
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
// Verify the operation succeeded by checking we got IDs back (FragnoId objects)
|
|
274
|
+
expect(createdIds).toHaveLength(2);
|
|
275
|
+
expect(typeof createdIds[0]).toBe("object");
|
|
276
|
+
expect(typeof createdIds[1]).toBe("object");
|
|
277
|
+
|
|
278
|
+
// Verify the SQL queries were captured
|
|
279
|
+
// createMany should generate one insert per record
|
|
280
|
+
expect(queries).toHaveLength(2);
|
|
281
|
+
|
|
282
|
+
// Check the first user insert
|
|
283
|
+
expect(queries[0].sql).toMatchInlineSnapshot(
|
|
284
|
+
`"insert into "user" ("id", "email", "passwordHash", "createdAt") values ($1, $2, $3, $4) returning "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version""`,
|
|
285
|
+
);
|
|
286
|
+
expect(queries[0].parameters[0]).toEqual("user-1");
|
|
287
|
+
expect(queries[0].parameters[1]).toEqual("user1@example.com");
|
|
288
|
+
expect(queries[0].parameters[2]).toEqual("hash1");
|
|
289
|
+
|
|
290
|
+
// Check the second user insert
|
|
291
|
+
expect(queries[1].sql).toMatchInlineSnapshot(
|
|
292
|
+
`"insert into "user" ("id", "email", "passwordHash", "createdAt") values ($1, $2, $3, $4) returning "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version""`,
|
|
293
|
+
);
|
|
294
|
+
expect(queries[1].parameters[0]).toEqual("user-2");
|
|
295
|
+
expect(queries[1].parameters[1]).toEqual("user2@example.com");
|
|
296
|
+
expect(queries[1].parameters[2]).toEqual("hash2");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("update", () => {
|
|
301
|
+
it("should update user by id", async () => {
|
|
302
|
+
const userId = "user-123";
|
|
303
|
+
|
|
304
|
+
await orm.update("user", userId, (b) =>
|
|
305
|
+
b.set({
|
|
306
|
+
email: "newemail@example.com",
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
// Verify the SQL query was captured
|
|
311
|
+
const [query] = queries;
|
|
312
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
313
|
+
`"update "user" set "email" = $1, "_version" = COALESCE(_version, 0) + 1 where "user"."id" = $2"`,
|
|
314
|
+
);
|
|
315
|
+
expect(query.parameters).toEqual(["newemail@example.com", userId]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should update session expiration", async () => {
|
|
319
|
+
const sessionId = "session-456";
|
|
320
|
+
const newExpiresAt = new Date("2026-01-01T00:00:00Z");
|
|
321
|
+
|
|
322
|
+
await orm.update("session", sessionId, (b) =>
|
|
323
|
+
b.set({
|
|
324
|
+
expiresAt: newExpiresAt,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Verify the SQL query was captured
|
|
329
|
+
const [query] = queries;
|
|
330
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
331
|
+
`"update "session" set "expiresAt" = $1, "_version" = COALESCE(_version, 0) + 1 where "session"."id" = $2"`,
|
|
332
|
+
);
|
|
333
|
+
expect(query.parameters).toEqual([newExpiresAt, sessionId]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should update with version check using FragnoId", async () => {
|
|
337
|
+
const userId = FragnoId.fromExternal("user-123", 5);
|
|
338
|
+
|
|
339
|
+
await orm.update("user", userId, (b) =>
|
|
340
|
+
b
|
|
341
|
+
.set({
|
|
342
|
+
email: "checked@example.com",
|
|
343
|
+
})
|
|
344
|
+
.check(),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Verify the SQL query includes version check in WHERE clause
|
|
348
|
+
const [query] = queries;
|
|
349
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
350
|
+
`"update "user" set "email" = $1, "_version" = COALESCE(_version, 0) + 1 where ("user"."id" = $2 and "user"."_version" = $3)"`,
|
|
351
|
+
);
|
|
352
|
+
expect(query.parameters).toEqual(["checked@example.com", "user-123", 5]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should throw when trying to check() with string ID", async () => {
|
|
356
|
+
await expect(
|
|
357
|
+
orm.update("user", "user-123", (b) => b.set({ email: "test@example.com" }).check()),
|
|
358
|
+
).rejects.toThrow(
|
|
359
|
+
'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("updateMany", () => {
|
|
365
|
+
it("should update multiple users by index", async () => {
|
|
366
|
+
await orm.updateMany("user", (b) =>
|
|
367
|
+
b
|
|
368
|
+
.whereIndex("idx_user_email", (eb) => eb("email", "=", "old@example.com"))
|
|
369
|
+
.set({ email: "new@example.com" }),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// updateMany first finds matching records, then updates them
|
|
373
|
+
expect(queries.length).toBeGreaterThan(0);
|
|
374
|
+
|
|
375
|
+
// Verify the find query that's executed first
|
|
376
|
+
const findQuery = queries[0];
|
|
377
|
+
assert(findQuery?.sql);
|
|
378
|
+
expect(findQuery?.sql).toMatchInlineSnapshot(
|
|
379
|
+
`"select "user"."id" as "id", "user"."email" as "email", "user"."passwordHash" as "passwordHash", "user"."createdAt" as "createdAt", "user"."_internalId" as "_internalId", "user"."_version" as "_version" from "user" where "user"."email" = $1"`,
|
|
380
|
+
);
|
|
381
|
+
expect(findQuery?.parameters).toEqual(["old@example.com"]);
|
|
382
|
+
|
|
383
|
+
// Note: In dryRun mode, no actual records are found, so no update queries are generated
|
|
384
|
+
// This is expected behavior - updateMany only generates update queries for found records
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("delete", () => {
|
|
389
|
+
it("should delete user by id", async () => {
|
|
390
|
+
const userId = "user-123";
|
|
391
|
+
|
|
392
|
+
await orm.delete("user", userId);
|
|
393
|
+
|
|
394
|
+
// Verify the SQL query was captured
|
|
395
|
+
const [query] = queries;
|
|
396
|
+
expect(query.sql).toMatchInlineSnapshot(`"delete from "user" where "user"."id" = $1"`);
|
|
397
|
+
expect(query.parameters).toEqual([userId]);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("should delete session by id", async () => {
|
|
401
|
+
const sessionId = "session-456";
|
|
402
|
+
|
|
403
|
+
await orm.delete("session", sessionId);
|
|
404
|
+
|
|
405
|
+
// Verify the SQL query was captured
|
|
406
|
+
const [query] = queries;
|
|
407
|
+
expect(query.sql).toMatchInlineSnapshot(`"delete from "session" where "session"."id" = $1"`);
|
|
408
|
+
expect(query.parameters).toEqual([sessionId]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("should delete with version check using FragnoId", async () => {
|
|
412
|
+
const userId = FragnoId.fromExternal("user-789", 3);
|
|
413
|
+
|
|
414
|
+
await orm.delete("user", userId, (b) => b.check());
|
|
415
|
+
|
|
416
|
+
// Verify the SQL query includes version check in WHERE clause
|
|
417
|
+
const [query] = queries;
|
|
418
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
419
|
+
`"delete from "user" where ("user"."id" = $1 and "user"."_version" = $2)"`,
|
|
420
|
+
);
|
|
421
|
+
expect(query.parameters).toEqual(["user-789", 3]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("should throw when trying to check() with string ID on delete", async () => {
|
|
425
|
+
await expect(orm.delete("user", "user-123", (b) => b.check())).rejects.toThrow(
|
|
426
|
+
'Cannot use check() with a string ID on table "user". Version checking requires a FragnoId with version information.',
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe("deleteMany", () => {
|
|
432
|
+
it("should delete sessions by userId using index", async () => {
|
|
433
|
+
const userId = "user-123";
|
|
434
|
+
|
|
435
|
+
await orm.deleteMany("session", (b) =>
|
|
436
|
+
b.whereIndex("idx_session_user", (eb) => eb("userId", "=", userId)),
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// deleteMany first finds matching records, then deletes them
|
|
440
|
+
expect(queries.length).toBeGreaterThan(0);
|
|
441
|
+
|
|
442
|
+
// Verify the find query that's executed first
|
|
443
|
+
const findQuery = queries[0];
|
|
444
|
+
assert(findQuery?.sql);
|
|
445
|
+
expect(findQuery?.sql).toMatchInlineSnapshot(
|
|
446
|
+
`"select "session"."id" as "id", "session"."userId" as "userId", "session"."expiresAt" as "expiresAt", "session"."createdAt" as "createdAt", "session"."_internalId" as "_internalId", "session"."_version" as "_version" from "session" where "session"."userId" = (select "_internalId" from "user" where "id" = $1 limit $2)"`,
|
|
447
|
+
);
|
|
448
|
+
expect(findQuery?.parameters).toEqual([userId, 1]);
|
|
449
|
+
|
|
450
|
+
// Note: In dryRun mode, no actual records are found, so no delete queries are generated
|
|
451
|
+
// This is expected behavior - deleteMany only generates delete queries for found records
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
describe("FragnoId support", () => {
|
|
456
|
+
it("should accept FragnoId in delete", async () => {
|
|
457
|
+
// Create a user first to get a FragnoId
|
|
458
|
+
const createdId = await orm.create("user", {
|
|
459
|
+
id: "fragno-user-123",
|
|
460
|
+
email: "fragno@example.com",
|
|
461
|
+
passwordHash: "hash",
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Clear queries from create
|
|
465
|
+
queries.splice(0, queries.length);
|
|
466
|
+
|
|
467
|
+
// Now delete using the FragnoId
|
|
468
|
+
await orm.delete("user", createdId);
|
|
469
|
+
|
|
470
|
+
// Verify the SQL query was captured with the external ID
|
|
471
|
+
const [query] = queries;
|
|
472
|
+
expect(query.sql).toMatchInlineSnapshot(`"delete from "user" where "user"."id" = $1"`);
|
|
473
|
+
expect(query.parameters).toEqual(["fragno-user-123"]);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("should accept FragnoId in update", async () => {
|
|
477
|
+
// Create a user first to get a FragnoId
|
|
478
|
+
const createdId = await orm.create("user", {
|
|
479
|
+
id: "fragno-user-456",
|
|
480
|
+
email: "update@example.com",
|
|
481
|
+
passwordHash: "hash",
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Clear queries from create
|
|
485
|
+
queries.splice(0, queries.length);
|
|
486
|
+
|
|
487
|
+
// Now update using the FragnoId
|
|
488
|
+
await orm.update("user", createdId, (b) => b.set({ email: "updated@example.com" }));
|
|
489
|
+
|
|
490
|
+
// Verify the SQL query was captured with the external ID
|
|
491
|
+
const [query] = queries;
|
|
492
|
+
expect(query.sql).toMatchInlineSnapshot(
|
|
493
|
+
`"update "user" set "email" = $1, "_version" = COALESCE(_version, 0) + 1 where "user"."id" = $2"`,
|
|
494
|
+
);
|
|
495
|
+
expect(query.parameters).toEqual(["updated@example.com", "fragno-user-456"]);
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
});
|