Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/tim-smart/sqlfx
A SQL toolkit for Effect-TS
https://github.com/tim-smart/sqlfx
effect-ts postgres sql
Last synced: about 20 hours ago
JSON representation
A SQL toolkit for Effect-TS
- Host: GitHub
- URL: https://github.com/tim-smart/sqlfx
- Owner: tim-smart
- License: mit
- Created: 2023-05-10T02:03:40.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-03-28T01:51:51.000Z (9 months ago)
- Last Synced: 2024-04-14T09:06:00.035Z (9 months ago)
- Topics: effect-ts, postgres, sql
- Language: TypeScript
- Homepage: https://tim-smart.github.io/sqlfx/
- Size: 2.91 MB
- Stars: 88
- Watchers: 5
- Forks: 10
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# sqlfx
A SQL toolkit for Effect-TS
https://tim-smart.github.io/sqlfx
## Basic example
```ts
import { pipe } from "effect/Function"
import * as Config from "effect/Config"
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"const PgLive = Pg.makeLayer({
database: Config.succeed("effect_pg_dev"),
})const program = Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const people = yield* _(
sql<{
readonly id: number
readonly name: string
}>`SELECT id, name FROM people`,
)yield* _(Effect.log(`Got ${people.length} results!`))
})pipe(program, Effect.provideLayer(PgLive), Effect.runPromise)
```## INSERT resolver
```ts
import { pipe } from "effect/Function"
import * as Effect from "effect/Effect"
import * as Schema from "@effect/schema/Schema"
import * as Pg from "@sqlfx/pg"class Person extends Schema.class({
id: Schema.number,
name: Schema.string,
createdAt: Schema.DateFromSelf,
updatedAt: Schema.DateFromSelf,
}) {}const InsertPersonSchema = pipe(
Person.schemaStruct(),
Schema.omit("id", "createdAt", "updatedAt"),
)export const makePersonService = Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const insert = sql.resolver(
"InsertPerson",
InsertPersonSchema,
Person.schema(),
requests =>
sql`
INSERT INTO people
${sql.insert(requests)}
RETURNING people.*
`,
).executereturn { insert }
})
```## SELECT resolver
```ts
import * as Effect from "effect/Effect"
import * as Schema from "@effect/schema/Schema"
import * as Pg from "@sqlfx/pg"class Person extends Schema.Class({
id: Schema.number,
name: Schema.string,
createdAt: Schema.DateFromSelf,
updatedAt: Schema.DateFromSelf,
}) {}export const makePersonService = Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const getByIdResolver = sql.idResolver(
"GetPersonById",
Schema.number,
Person.schema(),
_ => _.id,
ids => sql`SELECT * FROM people WHERE id IN ${sql(ids)}`,
)const getById = (id: number) =>
Effect.withRequestCaching("on")(getByIdResolver.execute(id))return { getById }
})
```## Building queries
### Safe interpolation
```ts
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"export const make = (limit: number) =>
Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const statement = sql`SELECT * FROM people LIMIT ${limit}`
// e.g. SELECT * FROM people LIMIT ?
})
```### Unsafe interpolation
```ts
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"type OrderBy = "id" | "created_at" | "updated_at"
type SortOrder = "ASC" | "DESC"export const make = (orderBy: OrderBy, sortOrder: SortOrder) =>
Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const statement = sql`SELECT * FROM people ORDER BY ${sql(orderBy)} ${sql.unsafe(sortOrder)}`
// e.g. SELECT * FROM people ORDER BY `id` ASC
})
```### Where clause combinators
#### AND
```ts
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"export const make = (names: string[], cursor: string) =>
Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const statement = sql`SELECT * FROM people WHERE ${sql.and([
sql`name IN ${sql(names)}`,
sql`created_at < ${sql(cursor)}`,
])}`
// SELECT * FROM people WHERE (name IN ? AND created_at < ?)
})
```#### OR
```ts
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"export const make = (names: string[], cursor: Date) =>
Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const statement = sql`SELECT * FROM people WHERE ${sql.or([
sql`name IN ${sql(names)}`,
sql`created_at < ${sql(cursor)}`,
])}`
// SELECT * FROM people WHERE (name IN ? OR created_at < ?)
})
```#### Mixed
```ts
import * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"export const make = (names: string[], afterCursor: Date, beforeCursor: Date) =>
Effect.gen(function* (_) {
const sql = yield* _(Pg.tag)const statement = sql`SELECT * FROM people WHERE ${sql.or([
sql`name IN ${sql(names)}`,
sql.and([
`created_at >${sql(afterCursor)}`,
`created_at < ${sql(beforeCursor)}`,
]),
])}`
// SELECT * FROM people WHERE (name IN ? OR (created_at > ? AND created_at < ?))
})
```## Migrations
A `Migrator` module is provided, for running migrations.
Migrations are forward-only, and are written in Typescript as Effect's.
Here is an example migration:
```ts
// src/migrations/0001_add_users.tsimport * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"export default Effect.flatMap(
Pg.tag,
sql => sql`
CREATE TABLE users (
id serial PRIMARY KEY,
name varchar(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
)
`,
)
```To run your migrations:
```ts
// src/main.tsimport * as Effect from "effect/Effect"
import * as Pg from "@sqlfx/pg"
import * as Migrator from "@sqlfx/pg/Migrator"
import * as Config from "effect/Config"
import { fileURLToPath } from "node:url"
import * as Layer from "effect/Layer"
import { pipe } from "effect/Function"const program = Effect.gen(function* (_) {
// ...
})const PgLive = Pg.makeLayer({
database: Config.succeed("example_database"),
})const MigratorLive = Layer.provide(
Migrator.makeLayer({
directory: fileURLToPath(new URL("migrations", import.meta.url)),
// Where to put the `_schema.sql` file
schemaDirectory: "src/migrations",
}),
PgLive,
)const EnvLive = Layer.mergeAll(PgLive, MigratorLive)
pipe(
program,
Effect.provideLayer(EnvLive),
Effect.tapErrorCause(Effect.logErrorCause),
Effect.runFork,
)
```