Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/fredx87/effect-kysely
kysely adapter for effect
https://github.com/fredx87/effect-kysely
effect kysely query schema sql
Last synced: 13 days ago
JSON representation
kysely adapter for effect
- Host: GitHub
- URL: https://github.com/fredx87/effect-kysely
- Owner: Fredx87
- License: mit
- Created: 2024-01-10T18:43:15.000Z (10 months ago)
- Default Branch: main
- Last Pushed: 2024-04-21T19:45:47.000Z (7 months ago)
- Last Synced: 2024-10-04T13:11:58.540Z (about 1 month ago)
- Topics: effect, kysely, query, schema, sql
- Language: TypeScript
- Homepage:
- Size: 113 KB
- Stars: 22
- Watchers: 3
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# effect-kysely
Integrate [kysely](https://kysely.dev/) with [effect](https://www.effect.website/). 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](https://github.com/tim-smart/sqlfx).
⚠️ **Warning: This library is still in development and the API is subject to change.**
This library is currently published only as an ES module.
## Getting started
Install `effect-kysely`
```sh
npm install effect-kysely
``````sh
yarn add effect-kysely
``````sh
pnpm add effect-kysely
```Install all peer dependencies if not already installed
```sh
npm install kysely effect @effect/schema
``````sh
yarn add kysely effect @effect/schema
``````sh
pnpm add kysely effect @effect/schema
```## Define your database tables
`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.
```ts
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.### Derive a static type to be used with kysely
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.
```ts
/*
type TodoTable = {
readonly id: ColumnType;
readonly content: string;
readonly completed: number;
readonly user_id: number;
readonly created_at: ColumnType;
readonly updated_at: ColumnType;
}
*/
type TodoTable = S.Schema.Encoded;
```### Derive select, insert and update schemas
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.
```ts
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.
```ts
import { GetTypes } from "effect-kysely/Schema.js";/*
Todo["Selectable"] = S.Schema.Type
Todo["Insertable"] = S.Schema.Type
Todo["Updateable"] = S.Schema.Type
*/
type Todo = GetTypes;
```### Define database tables and database service
Define your database tables to be used with kysely and a tag to be used as an effect service:
```ts
import { Context } from "effect";interface DbTables {
todo: TodoTable;
}class DbTag extends Context.Tag("DbTag")>() {}
```## Query your database with kysely
You can now create queries using `effect-kysely`, with encoding and decoding support.
### withEncoder
If you need to create a query encoding some data, you can use the `withEncoder` function:
```ts
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({ 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.
### withDecoder
If you need to create a query decoding the result, you can use the `withDecoder` function:
```ts
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.
### withCodec
If you need to create a query encoding some data and decoding the result, you can use the `withCodec` function:
```ts
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 }));
```### Errors
The effect returned by a query execution can fail with different errors:
- `QueryParseError`, if the encoding or decoding fails
- `QueryError`, if the query execution fails. It contains the error message returned by Kysely.
- `NotFoundError`, if you used `executeTakeFirstOrThrow()` and the query execution returns no result### Transactions
`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.
```ts
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();
}),
});
```## Use kysely as a query builder for sqlfx
You need to:
- Define your database tables as described above
- create a `sqlfx` client
- create a [cold Kysely instance](https://kysely.dev/docs/recipes/splitting-query-building-and-execution#cold-kysely-instances)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.```ts
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({
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),
);
```## FAQ
### What is the difference between using only this library and using it with `sqlfx`?
If you use only `effect-kysely`:
- You can use any database that has a [Kysely dialect](https://kysely.dev/docs/dialects) available
- The results of the queries are type-checked using the schemas you defined
- There is no support for batching and cachingIf 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## Examples
You can find more examples in the `examples` folder.
## License
MIT