Integrate kysely with effect. Define your database tables with @effect/schema
and use effect-kysely
to query them with encoding and decoding support, or just use kysely
as a query builder for sqlfx.
This library is currently published only as an ES module.
Install effect-kysely
npm install effect-kysely
yarn add effect-kysely
pnpm add effect-kysely
Install all peer dependencies if not already installed
npm install kysely effect @effect/schema
yarn add kysely effect @effect/schema
pnpm add kysely effect @effect/schema
effect-kysely
provides some utility functions to define your database tables with @effect/schema
. As in kysely, you can define a schema specifying a different type for select, insert, and update operations.
import * as S from "@effect/schema/Schema";
import { columnType } from "effect-kysely/Schema.js";
const TodoId = S.number.pipe(S.brand("TodoId"));
const BooleanFromNumber = S.transform(
S.number,
S.boolean,
(n) => (n === 1 ? true : false),
(b) => (b ? 1 : 0),
);
const _Todo = S.struct({
// as in kysely, you can define a schema specifying a different type for select, insert, and update operations
id: columnType(TodoId, S.never, S.never),
content: S.string,
completed: BooleanFromNumber,
user_id: S.number,
created_at: columnType(S.DateFromString, S.never, S.never),
updated_at: columnType(S.DateFromString, S.never, S.DateFromString),
});
At the moment, effect-kysely
provides only the columnType
and generated
functions to define different schemas for a column. They have the same meaning as in kysely.
Note: A schema that uses these helpers is not meant to be used directly, but it can be used to derive different schemas for select, insert and update operations. If you try to decode/encode something with this schema, you will get an error.
You can derive a static type to be used with kysely from a schema using the S.Schema.Encoded
utility. The schema can be used to decode from the database and encode data to the database, so the type used with kysely is the the schema Encoded
type.
/*
type TodoTable = {
readonly id: ColumnType<number, never, never>;
readonly content: string;
readonly completed: number;
readonly user_id: number;
readonly created_at: ColumnType<string, never, never>;
readonly updated_at: ColumnType<string, never, string>;
}
*/
type TodoTable = S.Schema.Encoded<typeof _Todo>;
You can derive the select, insert and update schemas from a schema using the getSchemas
function. It returns an object with the Selectable
, Insertable
, and Updateable
schemas.
import { getSchemas } from "effect-kysely/Schema.js";
/*
Todo.Selectable has id, content, completed, user_id, created_at, updated_at
Todo.Insertable has content, completed, user_id
Todo.Updateable has content, completed, user_id, updated_at
*/
const Todo = getSchemas(_Todo);
You can also derive static types for the different schemas using the GetTypes
utility.
import { GetTypes } from "effect-kysely/Schema.js";
/*
Todo["Selectable"] = S.Schema.Type<Todo.Selectable>
Todo["Insertable"] = S.Schema.Type<Todo.Insertable>
Todo["Updateable"] = S.Schema.Type<Todo.Updateable>
*/
type Todo = GetTypes<typeof Todo>;
Define your database tables to be used with kysely and a tag to be used as an effect service:
import { Context } from "effect";
interface DbTables {
todo: TodoTable;
}
class DbTag extends Context.Tag("DbTag")<DbTag, Kysely<DbTables>>() {}
You can now create queries using effect-kysely
, with encoding and decoding support.
If you need to create a query encoding some data, you can use the withEncoder
function:
import { Effect } from "effect";
import { withEncoder } from "effect-kysely/Query.js";
const program = Effect.gen(function* (_) {
const db = yield* _(DbTag);
const insertQuery = withEncoder({
encoder: Todo.Insertable,
query: (todo) => db.insertInto("todo").values(todo).executeTakeFirstOrThrow(),
});
const result = yield* _(insertQuery({ content: "Buy milk", completed: false, user_id: 1 }));
return result;
});
const DbLive = new Kysely<DbTables>({ dialect: ... });
const runnable = program.pipe(Effect.provideService(DbTag, DbLive));
The value passed to the insertQuery
function will be encoded using the Todo.Insertable
schema (in this example, completed is encoded as a number). Kysely will type-check that the encoded value passed to the query is compatible with the Insertable
static type defined for the table.
In this case, result
will be an InsertResult
type from kysely
, and we are not interested in decoding it.
If you need to create a query decoding the result, you can use the withDecoder
function:
const selectAllTodos = withDecoder({
decoder: S.array(Todo.Selectable),
query: () => db.selectFrom("todo").selectAll().execute(),
});
const todos = yield * _(selectAllTodos());
In this case, the query does not take any parameter and we don't need an encoder. The result of the query will be decoded using the provided decoder
schema (id
is decoded as TodoId
, completed
is decoded as a boolean, created_at
and updated_at
are decoded as dates). Kysely generates a type for the result of the query, and withDecoder
checks that the input schema of the decoder is compatible with the query result.
If you need to create a query encoding some data and decoding the result, you can use the withCodec
function:
const insertTodo = withCodec({
encoder: Todo.Insertable,
decoder: S.struct({ id: TodoId }),
query: (todo) =>
db
.insertInto("todo")
.values(todo)
.returning("id")
.executeTakeFirstOrThrow(),
});
const { id } =
yield * _(insertTodo({ content: "Buy milk", completed: false, user_id: 1 }));
The effect returned by a query execution can fail with different errors:
QueryParseError
, if the encoding or decoding failsQueryError
, if the query execution fails. It contains the error message returned by Kysely.NotFoundError
, if you usedexecuteTakeFirstOrThrow()
and the query execution returns no result
effect-kysely
doesn't provide a specific way to handle transactions. Since the query passed to withEncoder
, withDecoder
or withCodec
is just a function that returns a Promise, you can write a query with a transaction using the method provided by Kysely.
const insertTodos = withEncoder({
encoder: S.tuple(Todo.Insertable, Todo.Insertable),
query: ([todo1, todo2]) =>
db.transaction().execute(async (trx) => {
await trx.insertInto("todo").values(todo1).executeTakeFirstOrThrow();
await trx.insertInto("todo").values(todo2).executeTakeFirstOrThrow();
}),
});
You need to:
- Define your database tables as described above
- create a
sqlfx
client - create a cold Kysely instance
At this point you can use createQuery
from effect-kysely/sqlfx.js
to create a query using kysely
as a query builder,
passing the sqlfx
client and a compilable kysely
query.
import { Config, Context, Effect } from "effect";
import {
DummyDriver,
Kysely,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler,
} from "kysely";
import * as Sql from "@sqlfx/sqlite/node";
import { createQuery } from "effect-kysely/sqlfx.js";
const program = Effect.gen(function* (_) {
const db = yield* _(DbTag);
const sql = yield* _(Sql.tag);
const InsertTodo = sql.resolver("InsertTodo", {
request: Todo.Insertable,
result: S.struct({ id: TodoId }),
run: (todo) =>
createQuery(sql, db.insertInto("todo").values(todo).returning("id")),
});
const GetTodoById = sql.resolverId("GetTodoById", {
id: S.number,
result: Todo.Selectable,
resultId: (_) => _.id,
run: (ids) =>
createQuery(
sql,
db.selectFrom("todo").selectAll().where("id", "in", ids),
),
});
const insertedTodos = yield* _(
Effect.all(
[
InsertTodo.execute({
content: "user1 todo1",
completed: false,
user_id: 1,
}),
InsertTodo.execute({
content: "user2 todo1",
completed: false,
user_id: 2,
}),
],
{ batching: true },
),
);
const todoIds = insertedTodos.map((t) => t.id);
const res = yield* _(
Effect.all(todoIds.map(GetTodoById.execute), { batching: true }),
);
return res;
});
const DbLive = new Kysely<DbTables>({
dialect: {
createAdapter: () => new SqliteAdapter(),
createDriver: () => new DummyDriver(),
createIntrospector: (db) => new SqliteIntrospector(db),
createQueryCompiler: () => new SqliteQueryCompiler(),
},
});
const SqlLive = Sql.makeLayer({
filename: Config.succeed("example.db"),
});
const runnable = program.pipe(
Effect.provideService(DbTag, DbLive),
Effect.provide(SqlLive),
);
If you use only effect-kysely
:
- You can use any database that has a Kysely dialect available
- The results of the queries are type-checked using the schemas you defined
- There is no support for batching and caching
If you use effect-kysely
with sqlfx
:
- You can use batching and caching
- You can use only the databases supported by
sqlfx
- The results of the queries are not type-checked using the schemas you defined
You can find more examples in the examples
folder.
MIT