Skip to content

Commit

Permalink
fix mysql support for Model.makeRepository (#3599)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart authored Sep 13, 2024
1 parent 8632f7d commit d8aff79
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-kings-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/sql": patch
---

fix mysql support for Model.makeRepository
5 changes: 5 additions & 0 deletions .changeset/rare-ducks-clean.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/sql": patch
---

add insertVoid & updateVoid to Model repository
69 changes: 69 additions & 0 deletions packages/sql-mysql2/test/Model.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Schema } from "@effect/schema"
import { Model, SqlClient } from "@effect/sql"
import { assert, describe, it } from "@effect/vitest"
import { Effect } from "effect"
import { MysqlContainer } from "./utils.js"

class User extends Model.Class<User>("User")({
id: Model.Generated(Schema.Int),
name: Schema.String,
age: Schema.Int
}) {}

describe("Model", () => {
it.effect("insert returns result", () =>
Effect.gen(function*() {
const repo = yield* Model.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.ClientLive),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })

it.effect("insertVoid", () =>
Effect.gen(function*() {
const repo = yield* Model.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.ClientLive),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 60_000 })

it.scopedLive("insert data loader returns result", () =>
Effect.gen(function*() {
const repo = yield* Model.makeDataLoaders(User, {
tableName: "users",
idColumn: "id",
spanPrefix: "UserRepository",
window: 10
})
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([
repo.insert(User.insert.make({ name: "Alice", age: 30 })),
repo.insert(User.insert.make({ name: "John", age: 30 }))
], { batching: true })
assert.deepStrictEqual(alice.name, "Alice")
assert.deepStrictEqual(john.name, "John")
}).pipe(
Effect.provide(MysqlContainer.ClientLive),
Effect.catchTag("ContainerError", () => Effect.void)
), { timeout: 20_000 })
})
4 changes: 2 additions & 2 deletions packages/sql-mysql2/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export class MysqlContainer extends Context.Tag("test/MysqlContainer")<
)

static ClientLive = Layer.unwrapEffect(
Effect.gen(function*(_) {
const container = yield* _(MysqlContainer)
Effect.gen(function*() {
const container = yield* MysqlContainer
return MysqlClient.layer({
url: Config.succeed(Redacted.make(container.getConnectionUri()))
})
Expand Down
117 changes: 105 additions & 12 deletions packages/sql/src/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,15 @@ export const makeRepository = <
readonly insert: (
insert: S["insert"]["Type"]
) => Effect.Effect<S["Type"], never, S["Context"] | S["insert"]["Context"]>
readonly insertVoid: (
insert: S["insert"]["Type"]
) => Effect.Effect<void, never, S["Context"] | S["insert"]["Context"]>
readonly update: (
update: S["update"]["Type"]
) => Effect.Effect<S["Type"], never, S["Context"] | S["update"]["Context"]>
readonly updateVoid: (
update: S["update"]["Type"]
) => Effect.Effect<void, never, S["Context"] | S["update"]["Context"]>
readonly findById: (
id: Schema.Schema.Type<S["fields"][Id]>
) => Effect.Effect<Option.Option<S["Type"]>, never, S["Context"] | Schema.Schema.Context<S["fields"][Id]>>
Expand All @@ -637,11 +643,20 @@ export const makeRepository = <
Effect.gen(function*() {
const sql = yield* SqlClient
const idSchema = Model.fields[options.idColumn] as Schema.Schema.Any
const idColumn = options.idColumn as string

const insertSchema = SqlSchema.single({
Request: Model.insert,
Result: Model,
execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
execute: (request) =>
sql.onDialectOrElse({
mysql: () =>
sql`insert into ${sql(options.tableName)} ${sql.insert(request)};
select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID();`.unprepared.pipe(
Effect.map(([, results]) => results as any)
),
orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
})
})
const insert = (
insert: S["insert"]["Type"]
Expand All @@ -654,13 +669,42 @@ export const makeRepository = <
})
) as any

const insertVoidSchema = SqlSchema.void({
Request: Model.insert,
execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}`
})
const insertVoid = (
insert: S["insert"]["Type"]
): Effect.Effect<void, never, S["Context"] | S["insert"]["Context"]> =>
insertVoidSchema(insert).pipe(
Effect.orDie,
Effect.withSpan(`${options.spanPrefix}.insertVoid`, {
captureStackTrace: false,
attributes: { insert }
})
) as any

const updateSchema = SqlSchema.single({
Request: Model.update,
Result: Model,
execute: (request) =>
sql`update ${sql(options.tableName)} set ${sql.update(request, [options.idColumn])} where ${
sql(options.idColumn as string)
} = ${sql(request[options.idColumn])} returning *`
sql.onDialectOrElse({
mysql: () =>
sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${
request[idColumn]
}`.raw.pipe(
Effect.zipRight(
sql`select * from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${
request[idColumn]
}`
),
sql.withTransaction
),
orElse: () =>
sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${
request[idColumn]
} returning *`
})
})
const update = (
update: S["update"]["Type"]
Expand All @@ -673,10 +717,28 @@ export const makeRepository = <
})
) as any

const updateVoidSchema = SqlSchema.void({
Request: Model.update,
execute: (request) =>
sql`update ${sql(options.tableName)} set ${sql.update(request, [idColumn])} where ${sql(idColumn)} = ${
request[idColumn]
}`
})
const updateVoid = (
update: S["update"]["Type"]
): Effect.Effect<void, never, S["Context"] | S["update"]["Context"]> =>
updateVoidSchema(update).pipe(
Effect.orDie,
Effect.withSpan(`${options.spanPrefix}.updateVoid`, {
captureStackTrace: false,
attributes: { update }
})
) as any

const findByIdSchema = SqlSchema.findOne({
Request: idSchema,
Result: Model,
execute: (id) => sql`select * from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${id}`
execute: (id) => sql`select * from ${sql(options.tableName)} where ${sql(idColumn)} = ${id}`
})
const findById = (
id: Schema.Schema.Type<S["fields"][Id]>
Expand All @@ -691,7 +753,7 @@ export const makeRepository = <

const deleteSchema = SqlSchema.void({
Request: idSchema,
execute: (id) => sql`delete from ${sql(options.tableName)} where ${sql(options.idColumn as string)} = ${id}`
execute: (id) => sql`delete from ${sql(options.tableName)} where ${sql(idColumn)} = ${id}`
})
const delete_ = (
id: Schema.Schema.Type<S["fields"][Id]>
Expand All @@ -704,7 +766,7 @@ export const makeRepository = <
})
) as any

return { insert, update, findById, delete: delete_ } as const
return { insert, insertVoid, update, updateVoid, findById, delete: delete_ } as const
})

/**
Expand All @@ -728,6 +790,7 @@ export const makeDataLoaders = <
): Effect.Effect<
{
readonly insert: (insert: S["insert"]["Type"]) => Effect.Effect<S["Type"]>
readonly insertVoid: (insert: S["insert"]["Type"]) => Effect.Effect<void>
readonly findById: (id: Schema.Schema.Type<S["fields"][Id]>) => Effect.Effect<Option.Option<S["Type"]>>
readonly delete: (id: Schema.Schema.Type<S["fields"][Id]>) => Effect.Effect<void>
},
Expand All @@ -737,11 +800,21 @@ export const makeDataLoaders = <
Effect.gen(function*() {
const sql = yield* SqlClient
const idSchema = Model.fields[options.idColumn] as Schema.Schema.Any
const idColumn = options.idColumn as string

const insertResolver = yield* SqlResolver.ordered(`${options.spanPrefix}/insert`, {
Request: Model.insert,
Result: Model,
execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
execute: (request) =>
sql.onDialectOrElse({
mysql: () =>
Effect.forEach(request, (request) =>
sql`insert into ${sql(options.tableName)} ${sql.insert(request)};
select * from ${sql(options.tableName)} where ${sql(idColumn)} = LAST_INSERT_ID();`.unprepared.pipe(
Effect.map(([, results]) => results[0] as any)
), { concurrency: 10 }),
orElse: () => sql`insert into ${sql(options.tableName)} ${sql.insert(request).returning("*")}`
})
})
const insertLoader = yield* RRX.dataLoader(insertResolver, {
window: options.window,
Expand All @@ -759,16 +832,36 @@ export const makeDataLoaders = <
})
) as any

const insertVoidResolver = yield* SqlResolver.void(`${options.spanPrefix}/insertVoid`, {
Request: Model.insert,
execute: (request) => sql`insert into ${sql(options.tableName)} ${sql.insert(request)}`
})
const insertVoidLoader = yield* RRX.dataLoader(insertVoidResolver, {
window: options.window,
maxBatchSize: options.maxBatchSize!
})
const insertVoidExecute = insertVoidResolver.makeExecute(insertVoidLoader)
const insertVoid = (
insert: S["insert"]["Type"]
): Effect.Effect<void, never, S["Context"] | S["insert"]["Context"]> =>
insertVoidExecute(insert).pipe(
Effect.orDie,
Effect.withSpan(`${options.spanPrefix}.insertVoid`, {
captureStackTrace: false,
attributes: { insert }
})
) as any

const findByIdResolver = yield* SqlResolver.grouped(`${options.spanPrefix}/findById`, {
Request: idSchema,
RequestGroupKey(id) {
return id
},
Result: Model,
ResultGroupKey(request) {
return request[options.idColumn]
return request[idColumn]
},
execute: (ids) => sql`select * from ${sql(options.tableName)} where ${sql.in(options.idColumn as string, ids)}`
execute: (ids) => sql`select * from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
})
const findByIdLoader = yield* RRX.dataLoader(findByIdResolver, {
window: options.window,
Expand All @@ -786,7 +879,7 @@ export const makeDataLoaders = <

const deleteResolver = yield* SqlResolver.void(`${options.spanPrefix}/delete`, {
Request: idSchema,
execute: (ids) => sql`delete from ${sql(options.tableName)} where ${sql.in(options.idColumn as string, ids)}`
execute: (ids) => sql`delete from ${sql(options.tableName)} where ${sql.in(idColumn, ids)}`
})
const deleteLoader = yield* RRX.dataLoader(deleteResolver, {
window: options.window,
Expand All @@ -802,5 +895,5 @@ export const makeDataLoaders = <
})
) as any

return { insert, findById, delete: delete_ } as const
return { insert, insertVoid, findById, delete: delete_ } as const
})

0 comments on commit d8aff79

Please sign in to comment.