1import { assert, it } from "@effect/vitest"
2import { Cause, Effect, Schema } from "effect"
3import { Model } from "effect/schema"
4import { SqlClient, SqlModel, SqlResolver } from "effect/sql"
5import { MysqlContainer } from "./utils.ts"
6
7class User extends Model.Class<User>("User")({
8 id: Schema.Int.pipe(
9 Model.FieldExcept(["insert"])
10 ),
11 name: Schema.String,
12 age: Schema.Int
13}) {}
14
15class SoftDeleteUser extends Model.Class<SoftDeleteUser>("SoftDeleteUser")({
16 id: Schema.Int.pipe(
17 Model.FieldExcept(["insert"])
18 ),
19 name: Schema.String,
20 deletedAt: Schema.NullOr(Schema.String).pipe(
21 Model.FieldOnly(["select", "update"])
22 )
23}) {}
24
25it.layer(MysqlContainer.layer, { timeout: "90 seconds", excludeTestServices: true })("SqlModel", (it) => {
26 it.effect("insert returns result", () =>
27 Effect.gen(function*() {
28 const repo = yield* SqlModel.makeRepository(User, {
29 tableName: "users_insert",
30 idColumn: "id",
31 spanPrefix: "UserRepository"
32 })
33 const sql = yield* SqlClient.SqlClient
34 yield* sql`CREATE TABLE users_insert (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
35
36 const result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
37 assert.deepStrictEqual(result, new User({ id: 1, name: "Alice", age: 30 }))
38 }).pipe(
39 Effect.provide(MysqlContainer.client)
40 ), { timeout: 60_000 })
41
42 it.effect("insert returns result with transforms", () =>
43 Effect.gen(function*() {
44 const repo = yield* SqlModel.makeRepository(User, {
45 tableName: "users_insert_transforms",
46 idColumn: "id",
47 spanPrefix: "UserRepository"
48 })
49 const sql = yield* SqlClient.SqlClient
50 yield* sql`CREATE TABLE users_insert_transforms (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
51
52 const result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
53 assert.deepStrictEqual(result, new User({ id: 1, name: "Alice", age: 30 }))
54 }).pipe(
55 Effect.provide(MysqlContainer.clientWithTransforms)
56 ), { timeout: 60_000 })
57
58 it.effect("insertVoid", () =>
59 Effect.gen(function*() {
60 const repo = yield* SqlModel.makeRepository(User, {
61 tableName: "users_insert_void",
62 idColumn: "id",
63 spanPrefix: "UserRepository"
64 })
65 const sql = yield* SqlClient.SqlClient
66 yield* sql`CREATE TABLE users_insert_void (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
67
68 const result = yield* repo.insertVoid(User.insert.make({ name: "Alice", age: 30 }))
69 assert.strictEqual(result, void 0)
70 }).pipe(
71 Effect.provide(MysqlContainer.client)
72 ), { timeout: 60_000 })
73
74 it.effect("insert data loader returns result", () =>
75 Effect.gen(function*() {
76 const repo = yield* SqlModel.makeResolvers(User, {
77 tableName: "users_insert_loader",
78 idColumn: "id",
79 spanPrefix: "UserRepository"
80 })
81 const sql = yield* SqlClient.SqlClient
82 yield* sql`CREATE TABLE users_insert_loader (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
83
84 const [alice, john] = yield* Effect.all([
85 SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert),
86 SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert)
87 ], { concurrency: "unbounded" })
88 assert.deepStrictEqual(alice.name, "Alice")
89 assert.deepStrictEqual(john.name, "John")
90 }).pipe(
91 Effect.provide(MysqlContainer.client)
92 ), { timeout: 60_000 })
93
94 it.effect("findById data loader", () =>
95 Effect.gen(function*() {
96 const repo = yield* SqlModel.makeResolvers(User, {
97 tableName: "users_find_loader",
98 idColumn: "id",
99 spanPrefix: "UserRepository"
100 })
101 const sql = yield* SqlClient.SqlClient
102 yield* sql`CREATE TABLE users_find_loader (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
103 const alice = yield* SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert)
104 const john = yield* SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert)
105
106 const [alice2, john2] = yield* Effect.all([
107 SqlResolver.request(alice.id, repo.findById),
108 SqlResolver.request(john.id, repo.findById)
109 ], { concurrency: "unbounded" })
110
111 assert.deepStrictEqual(alice2.name, "Alice")
112 assert.deepStrictEqual(john2.name, "John")
113 }).pipe(
114 Effect.provide(MysqlContainer.client)
115 ), { timeout: 60_000 })
116
117 it.effect("findById ignores soft deleted rows", () =>
118 Effect.gen(function*() {
119 const repo = yield* SqlModel.makeRepository(SoftDeleteUser, {
120 tableName: "soft_delete_users",
121 idColumn: "id",
122 spanPrefix: "SoftDeleteUserRepository",
123 softDeleteColumn: "deletedAt"
124 })
125 const sql = yield* SqlClient.SqlClient
126 yield* sql`CREATE TABLE soft_delete_users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)`
127
128 const alice = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Alice" }))
129 const bob = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Bob" }))
130 yield* sql`UPDATE soft_delete_users SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}`
131
132 const aliceResult = yield* repo.findById(alice.id)
133 assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null }))
134
135 const error = yield* Effect.flip(repo.findById(bob.id))
136 assert.isTrue(Cause.isNoSuchElementError(error))
137
138 yield* repo.delete(alice.id)
139 const rows = yield* sql<
140 { deletedAt: string | null }
141 >`SELECT deletedAt FROM soft_delete_users WHERE id = ${alice.id}`
142 assert.strictEqual(rows.length, 1)
143 assert.isNotNull(rows[0]!.deletedAt)
144
145 const deletedError = yield* Effect.flip(repo.findById(alice.id))
146 assert.isTrue(Cause.isNoSuchElementError(deletedError))
147 }).pipe(
148 Effect.provide(MysqlContainer.client)
149 ), { timeout: 60_000 })
150
151 it.effect("findById data loader ignores soft deleted rows", () =>
152 Effect.gen(function*() {
153 const repo = yield* SqlModel.makeResolvers(SoftDeleteUser, {
154 tableName: "soft_delete_user_resolvers",
155 idColumn: "id",
156 spanPrefix: "SoftDeleteUserRepository",
157 softDeleteColumn: "deletedAt"
158 })
159 const sql = yield* SqlClient.SqlClient
160 yield* sql`CREATE TABLE soft_delete_user_resolvers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)`
161
162 const alice = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Alice" }), repo.insert)
163 const bob = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Bob" }), repo.insert)
164 yield* sql`UPDATE soft_delete_user_resolvers SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}`
165
166 const aliceResult = yield* SqlResolver.request(alice.id, repo.findById)
167 assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null }))
168
169 const error = yield* Effect.flip(SqlResolver.request(bob.id, repo.findById))
170 assert.isTrue(Cause.isNoSuchElementError(error))
171
172 yield* SqlResolver.request(alice.id, repo.delete)
173 const rows = yield* sql<
174 { deletedAt: string | null }
175 >`SELECT deletedAt FROM soft_delete_user_resolvers WHERE id = ${alice.id}`
176 assert.strictEqual(rows.length, 1)
177 assert.isNotNull(rows[0]!.deletedAt)
178
179 const deletedError = yield* Effect.flip(SqlResolver.request(alice.id, repo.findById))
180 assert.isTrue(Cause.isNoSuchElementError(deletedError))
181 }).pipe(
182 Effect.provide(MysqlContainer.client)
183 ), { timeout: 60_000 })
184
185 it.effect("update returns result", () =>
186 Effect.gen(function*() {
187 const repo = yield* SqlModel.makeRepository(User, {
188 tableName: "users_update",
189 idColumn: "id",
190 spanPrefix: "UserRepository"
191 })
192 const sql = yield* SqlClient.SqlClient
193 yield* sql`CREATE TABLE users_update (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
194
195 let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
196 result = yield* repo.update(User.update.make({ ...result, name: "Bob" }))
197 assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 }))
198 }).pipe(
199 Effect.provide(MysqlContainer.client)
200 ), { timeout: 60_000 })
201
202 it.effect("update returns result with transforms", () =>
203 Effect.gen(function*() {
204 const repo = yield* SqlModel.makeRepository(User, {
205 tableName: "users_update_transforms",
206 idColumn: "id",
207 spanPrefix: "UserRepository"
208 })
209 const sql = yield* SqlClient.SqlClient
210 yield* sql`CREATE TABLE users_update_transforms (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)`
211
212 let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 }))
213 result = yield* repo.update(User.update.make({ ...result, name: "Bob" }))
214 assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 }))
215 }).pipe(
216 Effect.provide(MysqlContainer.clientWithTransforms)
217 ), { timeout: 60_000 })
218})