import { assert, describe, it } from "@effect/vitest" import { Cause, Effect, Schema } from "effect" import { Model } from "effect/unstable/schema" import { SqlClient, SqlModel, SqlResolver } from "effect/unstable/sql" import { MysqlContainer } from "./utils.ts" class User extends Model.Class("User")({ id: Schema.Int.pipe( Model.FieldExcept(["insert"]) ), name: Schema.String, age: Schema.Int }) {} class SoftDeleteUser extends Model.Class("SoftDeleteUser")({ id: Schema.Int.pipe( Model.FieldExcept(["insert"]) ), name: Schema.String, deletedAt: Schema.NullOr(Schema.String).pipe( Model.FieldOnly(["select", "update"]) ) }) {} describe("SqlModel", () => { it.effect("insert returns result", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` const result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 })) assert.deepStrictEqual(result, new User({ id: 1, name: "Alice", age: 30 })) }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.effect("insert returns result with transforms", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` const result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 })) assert.deepStrictEqual(result, new User({ id: 1, name: "Alice", age: 30 })) }).pipe( Effect.provide(MysqlContainer.layerClientWithTransforms), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.effect("insertVoid", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` const result = yield* repo.insertVoid(User.insert.make({ name: "Alice", age: 30 })) assert.strictEqual(result, void 0) }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.live("insert data loader returns result", () => Effect.gen(function*() { const repo = yield* SqlModel.makeResolvers(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` const [alice, john] = yield* Effect.all([ SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert), SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert) ], { concurrency: "unbounded" }) assert.deepStrictEqual(alice.name, "Alice") assert.deepStrictEqual(john.name, "John") }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.live("findById data loader", () => Effect.gen(function*() { const repo = yield* SqlModel.makeResolvers(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` const alice = yield* SqlResolver.request(User.insert.make({ name: "Alice", age: 30 }), repo.insert) const john = yield* SqlResolver.request(User.insert.make({ name: "John", age: 30 }), repo.insert) const [alice2, john2] = yield* Effect.all([ SqlResolver.request(alice.id, repo.findById), SqlResolver.request(john.id, repo.findById) ], { concurrency: "unbounded" }) assert.deepStrictEqual(alice2.name, "Alice") assert.deepStrictEqual(john2.name, "John") }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.effect("findById ignores soft deleted rows", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(SoftDeleteUser, { tableName: "soft_delete_users", idColumn: "id", spanPrefix: "SoftDeleteUserRepository", softDeleteColumn: "deletedAt" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE soft_delete_users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)` const alice = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Alice" })) const bob = yield* repo.insert(SoftDeleteUser.insert.make({ name: "Bob" })) yield* sql`UPDATE soft_delete_users SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}` const aliceResult = yield* repo.findById(alice.id) assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null })) const error = yield* Effect.flip(repo.findById(bob.id)) assert.isTrue(Cause.isNoSuchElementError(error)) yield* repo.delete(alice.id) const rows = yield* sql< { deletedAt: string | null } >`SELECT deletedAt FROM soft_delete_users WHERE id = ${alice.id}` assert.strictEqual(rows.length, 1) assert.isNotNull(rows[0]!.deletedAt) const deletedError = yield* Effect.flip(repo.findById(alice.id)) assert.isTrue(Cause.isNoSuchElementError(deletedError)) }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.live("findById data loader ignores soft deleted rows", () => Effect.gen(function*() { const repo = yield* SqlModel.makeResolvers(SoftDeleteUser, { tableName: "soft_delete_user_resolvers", idColumn: "id", spanPrefix: "SoftDeleteUserRepository", softDeleteColumn: "deletedAt" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE soft_delete_user_resolvers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), deletedAt VARCHAR(255) NULL)` const alice = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Alice" }), repo.insert) const bob = yield* SqlResolver.request(SoftDeleteUser.insert.make({ name: "Bob" }), repo.insert) yield* sql`UPDATE soft_delete_user_resolvers SET deletedAt = CURRENT_TIMESTAMP WHERE id = ${bob.id}` const aliceResult = yield* SqlResolver.request(alice.id, repo.findById) assert.deepStrictEqual(aliceResult, new SoftDeleteUser({ id: alice.id, name: "Alice", deletedAt: null })) const error = yield* Effect.flip(SqlResolver.request(bob.id, repo.findById)) assert.isTrue(Cause.isNoSuchElementError(error)) yield* SqlResolver.request(alice.id, repo.delete) const rows = yield* sql< { deletedAt: string | null } >`SELECT deletedAt FROM soft_delete_user_resolvers WHERE id = ${alice.id}` assert.strictEqual(rows.length, 1) assert.isNotNull(rows[0]!.deletedAt) const deletedError = yield* Effect.flip(SqlResolver.request(alice.id, repo.findById)) assert.isTrue(Cause.isNoSuchElementError(deletedError)) }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.effect("update returns result", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 })) result = yield* repo.update(User.update.make({ ...result, name: "Bob" })) assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 })) }).pipe( Effect.provide(MysqlContainer.layerClient), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) it.effect("update returns result with transforms", () => Effect.gen(function*() { const repo = yield* SqlModel.makeRepository(User, { tableName: "users", idColumn: "id", spanPrefix: "UserRepository" }) const sql = yield* SqlClient.SqlClient yield* sql`CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), age INT)` let result = yield* repo.insert(User.insert.make({ name: "Alice", age: 30 })) result = yield* repo.update(User.update.make({ ...result, name: "Bob" })) assert.deepStrictEqual(result, new User({ id: 1, name: "Bob", age: 30 })) }).pipe( Effect.provide(MysqlContainer.layerClientWithTransforms), Effect.catchTag("ContainerError", () => Effect.void) ), { timeout: 60_000 }) })