Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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

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 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

## Examples

You can find more examples in the `examples` folder.

## License

MIT