1import { SqliteClient } from "@effect/sql-sqlite-node"
2import { assert, describe, it } from "@effect/vitest"
3import { Context, Effect, Schema, SchemaGetter } from "effect"
4import { Model } from "effect/schema"
5import { SqlClient, SqlModel, SqlResolver } from "effect/sql"
6
7class NameDecoder extends Context.Service<NameDecoder, { readonly prefix: string }>()("test/NameDecoder") {}
8class NameEncoder extends Context.Service<NameEncoder, { readonly prefix: string }>()("test/NameEncoder") {}
9const decodedName = Schema.String.pipe(Schema.decodeTo(Schema.String, {
10 decode: SchemaGetter.transformEffect((value: string) => Effect.map(NameDecoder, ({ prefix }) => prefix + value)),
11 encode: SchemaGetter.passthrough()
12}))
13const encodedName = Schema.String.pipe(Schema.decodeTo(Schema.String, {
14 decode: SchemaGetter.passthrough(),
15 encode: SchemaGetter.transformEffect((value: string) => Effect.map(NameEncoder, ({ prefix }) => prefix + value))
16}))
17class User extends Model.Class<User>("User")({ id: Schema.Int, name: decodedName }) {}
18class Both extends Model.Class<Both>("Both")({
19 id: Schema.Int,
20 name: Model.Field({ select: decodedName, insert: encodedName, update: encodedName })
21}) {}
22class Plain extends Model.Class<Plain>("Plain")({ id: Schema.Int, name: Schema.String }) {}
23const options = { tableName: "users", idColumn: "id", spanPrefix: "User" } as const
24const database = SqliteClient.layer({ filename: ":memory:" })
25const createTable = Effect.gen(function*() {
26 const sql = yield* SqlClient.SqlClient
27 yield* sql`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`
28 return sql
29})
30
31describe("SqlModel insert native node:sqlite runtime controls", () => {
32 it.effect("decoder is supplied at request execution, not resolver construction", () =>
33 Effect.gen(function*() {
34 assert.strictEqual(process.versions.bun, undefined)
35 assert.isString(process.versions.sqlite)
36 const sql = yield* createTable
37 const resolvers = yield* SqlModel.makeResolvers(User, options)
38 const result = yield* SqlResolver.request({ id: 1, name: "Ada" }, resolvers.insert).pipe(
39 Effect.provideService(NameDecoder, { prefix: "read:" })
40 )
41 assert.instanceOf(result, User)
42 assert.strictEqual(result.name, "read:Ada")
43 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: 1, name: "Ada" }])
44 const found = yield* SqlResolver.request(1, resolvers.findById).pipe(
45 Effect.provideService(NameDecoder, { prefix: "find:" })
46 )
47 assert.strictEqual(found.name, "find:Ada")
48 }).pipe(Effect.provide(database)))
49
50 it.effect("distinct encoder and decoder execute in their own directions", () =>
51 Effect.gen(function*() {
52 const sql = yield* createTable
53 const resolvers = yield* SqlModel.makeResolvers(Both, options)
54 const result = yield* SqlResolver.request({ id: 2, name: "Ada" }, resolvers.insert).pipe(
55 Effect.provideService(NameEncoder, { prefix: "write:" }),
56 Effect.provideService(NameDecoder, { prefix: "read:" })
57 )
58 assert.instanceOf(result, Both)
59 assert.strictEqual(result.name, "read:write:Ada")
60 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: 2, name: "write:Ada" }])
61 }).pipe(Effect.provide(database)))
62
63 it.effect("plain model insert is unchanged", () =>
64 Effect.gen(function*() {
65 const sql = yield* createTable
66 const resolvers = yield* SqlModel.makeResolvers(Plain, options)
67 const result = yield* SqlResolver.request({ id: 3, name: "plain" }, resolvers.insert)
68 assert.instanceOf(result, Plain)
69 assert.strictEqual(result.name, "plain")
70 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: 3, name: "plain" }])
71 }).pipe(Effect.provide(database)))
72
73 it.effect("insertVoid does not execute the model decoder", () =>
74 Effect.gen(function*() {
75 const sql = yield* createTable
76 const resolvers = yield* SqlModel.makeResolvers(User, options)
77 yield* SqlResolver.request({ id: 4, name: "void" }, resolvers.insertVoid)
78 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: 4, name: "void" }])
79 }).pipe(Effect.provide(database)))
80
81 it.effect("SQL failures keep their typed SqlError", () =>
82 Effect.gen(function*() {
83 const sql = yield* createTable
84 yield* sql`INSERT INTO users VALUES (5, 'existing')`
85 const resolvers = yield* SqlModel.makeResolvers(User, options)
86 const error = yield* SqlResolver.request({ id: 5, name: "duplicate" }, resolvers.insert).pipe(
87 Effect.provideService(NameDecoder, { prefix: "read:" }),
88 Effect.flip
89 )
90 assert.strictEqual(error._tag, "SqlError")
91 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: 5, name: "existing" }])
92 }).pipe(Effect.provide(database)))
93
94 it.effect("a real returned-row schema failure keeps its typed SchemaError", () =>
95 Effect.gen(function*() {
96 const sql = yield* SqlClient.SqlClient
97 yield* sql`CREATE TABLE users (id TEXT PRIMARY KEY, name TEXT NOT NULL)`
98 const resolvers = yield* SqlModel.makeResolvers(User, options)
99 const error = yield* SqlResolver.request({ id: 6, name: "invalid row" }, resolvers.insert).pipe(
100 Effect.provideService(NameDecoder, { prefix: "read:" }),
101 Effect.flip
102 )
103 assert.strictEqual(error._tag, "SchemaError")
104 assert.deepStrictEqual(yield* sql`SELECT * FROM users`, [{ id: "6.0", name: "invalid row" }])
105 }).pipe(Effect.provide(database)))
106})