| 1 | import { assert, it } from "@effect/vitest" |
| 2 | import { Cause, Effect, Schema } from "effect" |
| 3 | import { Model } from "effect/schema" |
| 4 | import { SqlClient, SqlModel, SqlResolver } from "effect/sql" |
| 5 | import { MysqlContainer } from "./utils.ts" |
| 6 | |
| 7 | class 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 | |
| 15 | class 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 | |
| 25 | it.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 | }) |