├── .npmignore ├── .madgerc ├── .env ├── .gitignore ├── docgen.json ├── tsconfig.src.json ├── tsconfig.build.json ├── tsconfig.json ├── .prettierrc.json ├── tsconfig.examples.json ├── tsconfig.test.json ├── .changeset └── config.json ├── src ├── index.ts ├── internal │ ├── context.ts │ ├── config.ts │ ├── layers.ts │ └── query.ts ├── PgContext.ts ├── PgError.ts ├── PgLayer.ts └── PgQuery.ts ├── vitest.config.ts ├── .github ├── workflows │ ├── check.yml │ ├── build.yml │ ├── pages.yml │ └── test.yml └── actions │ └── setup │ └── action.yml ├── renovate.json ├── examples └── example.ts ├── LICENSE ├── tsconfig.base.json ├── README.md ├── .eslintrc.cjs ├── package.json ├── test └── query.test.ts └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/ 3 | test/ 4 | -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "detectiveOptions": { 3 | "ts": { 4 | "skipTypeImports": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | TEST_POSTGRES_USER=postgres 2 | TEST_POSTGRES_DATABASE=effect_pg_test 3 | TEST_POSTGRES_PASSWORD=postgres 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.tsbuildinfo 3 | node_modules/ 4 | .DS_Store 5 | tmp/ 6 | dist/ 7 | build/ 8 | docs/ 9 | .direnv/ 10 | .idea -------------------------------------------------------------------------------- /docgen.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@effect/docgen/schema.json", 3 | "exclude": [ 4 | "src/internal/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "outDir": "build/src", 6 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 7 | "rootDir": "src" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 5 | "outDir": "build/esm", 6 | "declarationDir": "build/dts", 7 | "stripInternal": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { 6 | "path": "tsconfig.src.json" 7 | }, 8 | { 9 | "path": "tsconfig.test.json" 10 | }, 11 | { 12 | "path": "tsconfig.examples.json" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@trivago/prettier-plugin-sort-imports"], 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "importOrder": ["", "^(@)?effect(.*)$", "^[./]"], 8 | "importOrderSeparation": true, 9 | "importOrderSortSpecifiers": true 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["examples"], 4 | "references": [ 5 | { 6 | "path": "tsconfig.src.json" 7 | } 8 | ], 9 | "compilerOptions": { 10 | "tsBuildInfoFile": ".tsbuildinfo/examples.tsbuildinfo", 11 | "rootDir": "examples", 12 | "noEmit": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "test", 5 | ], 6 | "references": [ 7 | { 8 | "path": "tsconfig.src.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 13 | "rootDir": "test", 14 | "noEmit": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "sukovanej/effect-pg" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in context tags. 3 | * 4 | * @since 1.0.0 5 | */ 6 | export * as PgContext from "./PgContext.js" 7 | 8 | /** 9 | * Pg errors 10 | * @since 1.0.0 11 | */ 12 | export * as PgError from "./PgError.js" 13 | 14 | /** 15 | * Main module 16 | * 17 | * @since 1.0.0 18 | */ 19 | export * as PgLayer from "./PgLayer.js" 20 | 21 | /** 22 | * Querying 23 | * 24 | * @since 1.0.0 25 | */ 26 | export * as PgQuery from "./PgQuery.js" 27 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import { defineConfig } from "vitest/config" 3 | 4 | export default defineConfig({ 5 | test: { 6 | include: ["test/**/*.{test,spec}.?(c|m)[jt]s?(x)"], 7 | reporters: ["hanging-process", "default"], 8 | sequence: { 9 | concurrent: true 10 | }, 11 | alias: { 12 | "effect-pg": path.resolve(__dirname, "src") 13 | }, 14 | chaiConfig: { 15 | truncateThreshold: 10000 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [master] 7 | push: 8 | branches: [master] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | lint: 16 | name: Lint 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Install dependencies 22 | uses: ./.github/actions/setup 23 | - run: pnpm check 24 | - run: pnpm lint 25 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommitTypeAll(chore)" 6 | ], 7 | "packageRules": [ 8 | { 9 | "matchPackagePatterns": ["*"], 10 | "matchUpdateTypes": ["patch"], 11 | "groupName": "all patch dependencies", 12 | "groupSlug": "all-patch" 13 | }, 14 | { 15 | "matchPackagePatterns": ["*"], 16 | "matchUpdateTypes": ["minor"], 17 | "groupName": "all minor dependencies", 18 | "groupSlug": "all-minor" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Perform standard setup and install dependencies using pnpm. 3 | inputs: 4 | node-version: 5 | description: The version of Node.js to install 6 | required: true 7 | default: 21.5.0 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Install pnpm 13 | uses: pnpm/action-setup@v3 14 | - name: Install node 15 | uses: actions/setup-node@v4 16 | with: 17 | cache: pnpm 18 | node-version: ${{ inputs.node-version }} 19 | - name: Install dependencies 20 | shell: bash 21 | run: pnpm install --ignore-scripts 22 | -------------------------------------------------------------------------------- /src/internal/context.ts: -------------------------------------------------------------------------------- 1 | import type * as pg from "pg" 2 | 3 | import * as Context from "effect/Context" 4 | 5 | /** @internal */ 6 | export const ClientConfig = Context.GenericTag( 7 | "effect-pg/ClientConfig" 8 | ) 9 | 10 | /** @internal */ 11 | export const Client = Context.GenericTag("effect-pg/Client") 12 | 13 | /** @internal */ 14 | export const PoolConfig = Context.GenericTag("effect-pg/PoolConfig") 15 | 16 | /** @internal */ 17 | export const PoolClient = Context.GenericTag("effect-pg/PoolClient") 18 | 19 | /** @internal */ 20 | export const Pool = Context.GenericTag("effect-pg/Pool") 21 | 22 | /** @internal */ 23 | export const ClientBase = Context.GenericTag("effect-pg/Client") 24 | -------------------------------------------------------------------------------- /examples/example.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "@effect/schema" 2 | import { Effect, pipe } from "effect" 3 | import { PgLayer, PgQuery } from "effect-pg" 4 | 5 | const User = Schema.Struct({ name: Schema.String }) 6 | 7 | const createUsersTable = PgQuery.all( 8 | "CREATE TABLE IF NOT EXISTS users (name TEXT NOT NULL)" 9 | ) 10 | const insertUser = PgQuery.all("INSERT INTO users (name) VALUES ($1)") 11 | const selectUser = PgQuery.one("SELECT * FROM users", User) 12 | 13 | pipe( 14 | createUsersTable(), 15 | Effect.flatMap(() => insertUser("patrik")), 16 | Effect.flatMap(() => selectUser()), 17 | Effect.flatMap((result) => Effect.log(`User: ${JSON.stringify(result)}`)), 18 | Effect.provide(PgLayer.Client), 19 | Effect.provide(PgLayer.setConfig()), 20 | Effect.runPromise 21 | ) 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [master] 7 | push: 8 | branches: [master] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Install dependencies 24 | uses: ./.github/actions/setup 25 | - run: pnpm build 26 | - name: Check source state 27 | run: git add src && git diff-index --cached HEAD --exit-code src 28 | - name: Create Release Pull Request or Publish 29 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 30 | id: changesets 31 | uses: changesets/action@v1 32 | with: 33 | publish: pnpm changeset publish 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Milan Suk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/PgContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in context tags. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as pg from "pg" 7 | 8 | import type * as Context from "effect/Context" 9 | import * as internal_context from "./internal/context.js" 10 | 11 | /** 12 | * @category context 13 | * @since 1.0.0 14 | */ 15 | export const ClientConfig: Context.Tag = internal_context.ClientConfig 16 | 17 | /** 18 | * @category context 19 | * @since 1.0.0 20 | */ 21 | export const Client: Context.Tag = internal_context.Client 22 | 23 | /** 24 | * @category context 25 | * @since 1.0.0 26 | */ 27 | export const PoolConfig: Context.Tag = internal_context.PoolConfig 28 | 29 | /** 30 | * @category context 31 | * @since 1.0.0 32 | */ 33 | export const PoolClient: Context.Tag = internal_context.PoolClient 34 | 35 | /** 36 | * @category context 37 | * @since 1.0.0 38 | */ 39 | export const Pool: Context.Tag = internal_context.Pool 40 | 41 | /** 42 | * @category context 43 | * @since 1.0.0 44 | */ 45 | export const ClientBase: Context.Tag = internal_context.ClientBase 46 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "exactOptionalPropertyTypes": true, 5 | "moduleDetection": "force", 6 | "composite": true, 7 | "downlevelIteration": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": false, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "NodeNext", 15 | "lib": ["ES2022", "DOM"], 16 | "isolatedModules": true, 17 | "sourceMap": true, 18 | "declarationMap": true, 19 | "noImplicitReturns": false, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": false, 22 | "noFallthroughCasesInSwitch": true, 23 | "noEmitOnError": false, 24 | "noErrorTruncation": false, 25 | "allowJs": false, 26 | "checkJs": false, 27 | "forceConsistentCasingInFileNames": true, 28 | "noImplicitAny": true, 29 | "noImplicitThis": true, 30 | "noUncheckedIndexedAccess": false, 31 | "strictNullChecks": true, 32 | "baseUrl": ".", 33 | "target": "ES2022", 34 | "module": "NodeNext", 35 | "incremental": true, 36 | "removeComments": false, 37 | "paths": { 38 | "effect-pg": ["./src/index.js"], 39 | "effect-pg/*": ["./src/*.js"] 40 | }, 41 | "plugins": [ 42 | { 43 | "name": "@effect/language-service" 44 | } 45 | ], 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [master] 7 | push: 8 | branches: [master] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Install dependencies 23 | uses: ./.github/actions/setup 24 | - run: pnpm docgen 25 | - name: Build pages Jekyll 26 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 27 | uses: actions/jekyll-build-pages@v1 28 | with: 29 | source: ./docs 30 | destination: ./_site 31 | - name: Upload pages artifact 32 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 33 | uses: actions/upload-pages-artifact@v2 34 | 35 | deploy: 36 | name: Deploy 37 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 38 | runs-on: ubuntu-latest 39 | needs: build 40 | permissions: 41 | pages: write # To deploy to GitHub Pages 42 | id-token: write # To verify the deployment originates from an appropriate source 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | steps: 47 | - name: Deploy to GitHub Pages 48 | id: deployment 49 | uses: actions/deploy-pages@v2 50 | -------------------------------------------------------------------------------- /src/PgError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pg errors 3 | * @since 1.0.0 4 | */ 5 | import type * as ParseResult from "@effect/schema/ParseResult" 6 | import * as Data from "effect/Data" 7 | 8 | /** 9 | * @category models 10 | * @since 1.0.0 11 | */ 12 | export class PostgresUnknownError extends Data.TaggedError( 13 | "PostgresUnknownError" 14 | )<{ error: unknown }> {} 15 | 16 | /** 17 | * @category models 18 | * @since 1.0.0 19 | */ 20 | export class PostgresConnectionError extends Data.TaggedError( 21 | "PostgresConnectionError" 22 | )<{ error: unknown }> {} 23 | 24 | /** 25 | * @category models 26 | * @since 1.0.0 27 | */ 28 | export class PostgresTableDoesntExistError extends Data.TaggedError( 29 | "PostgresTableDoesntExistError" 30 | )<{ error: unknown }> {} 31 | 32 | /** 33 | * @category models 34 | * @since 1.0.0 35 | */ 36 | export class PostgresValidationError extends Data.TaggedError( 37 | "PostgresValidationError" 38 | )<{ error: ParseResult.ParseError }> {} 39 | 40 | /** 41 | * @category models 42 | * @since 1.0.0 43 | */ 44 | export class PostgresUnexpectedNumberOfRowsError extends Data.TaggedError( 45 | "PostgresUnexpectedNumberOfRowsError" 46 | )<{ expectedRows: number; receivedRows: number }> {} 47 | 48 | /** 49 | * @category models 50 | * @since 1.0.0 51 | */ 52 | export class PostgresDuplicateTableError extends Data.TaggedError( 53 | "PostgresDuplicateTableError" 54 | )<{ error: unknown }> {} 55 | 56 | /** 57 | * @category models 58 | * @since 1.0.0 59 | */ 60 | export class PostgresInvalidParametersError extends Data.TaggedError( 61 | "PostgresInvalidParametersError" 62 | )<{ error: unknown }> {} 63 | 64 | /** 65 | * @category models 66 | * @since 1.0.0 67 | */ 68 | export type PostgresQueryError = 69 | | PostgresTableDoesntExistError 70 | | PostgresUnknownError 71 | | PostgresDuplicateTableError 72 | | PostgresInvalidParametersError 73 | -------------------------------------------------------------------------------- /src/internal/config.ts: -------------------------------------------------------------------------------- 1 | import * as Config from "effect/Config" 2 | import * as Context from "effect/Context" 3 | import * as Effect from "effect/Effect" 4 | import { pipe } from "effect/Function" 5 | import * as Layer from "effect/Layer" 6 | import * as internal_context from "./context.js" 7 | 8 | import type { ConfigOptions } from "../PgLayer.js" 9 | 10 | /** @internal */ 11 | const defaultOptions: ConfigOptions = { 12 | namePrefix: "POSTGRES", 13 | defaultPort: 5432, 14 | defaultHost: "localhost" 15 | } 16 | 17 | /** @internal */ 18 | const withDefault = (defaultValue: A | undefined) => (c: Config.Config) => { 19 | if (defaultValue === undefined) { 20 | return c 21 | } 22 | 23 | return pipe(c, Config.withDefault(defaultValue)) 24 | } 25 | 26 | /** @internal */ 27 | export const makeConfig = (options?: Partial) => { 28 | const { namePrefix, ...defaults } = { ...defaultOptions, ...options } 29 | 30 | return Config.all({ 31 | host: Config.string(`${namePrefix}_HOST`).pipe( 32 | withDefault(defaults.defaultHost) 33 | ), 34 | port: Config.integer(`${namePrefix}_PORT`).pipe( 35 | withDefault(defaults.defaultPort) 36 | ), 37 | user: Config.string(`${namePrefix}_USER`).pipe( 38 | withDefault(defaults.defaultUser) 39 | ), 40 | password: Config.string(`${namePrefix}_PASSWORD`).pipe( 41 | withDefault(defaults.defaultPassword) 42 | ), 43 | database: Config.string(`${namePrefix}_DATABASE`).pipe( 44 | withDefault(defaults.defaultDatabase) 45 | ) 46 | }) 47 | } 48 | 49 | /** @internal */ 50 | export const setConfig = (options?: Partial) => 51 | pipe( 52 | makeConfig(options), 53 | Effect.map((config) => 54 | pipe( 55 | Context.make(internal_context.PoolConfig, config), 56 | Context.add(internal_context.ClientConfig, config) 57 | ) 58 | ), 59 | Layer.effectContext 60 | ) 61 | -------------------------------------------------------------------------------- /src/PgLayer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Main module 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as _Config from "effect/Config" 7 | import type * as ConfigError from "effect/ConfigError" 8 | import type * as Layer from "effect/Layer" 9 | import type * as pg from "pg" 10 | import * as internal_config from "./internal/config.js" 11 | import * as internal_layers from "./internal/layers.js" 12 | import type * as PgError from "./PgError.js" 13 | 14 | /** 15 | * @category models 16 | * @since 1.0.0 17 | */ 18 | export interface ConfigOptions { 19 | namePrefix: string 20 | defaultHost?: string 21 | defaultPort?: number 22 | defaultUser?: string 23 | defaultPassword?: string 24 | defaultDatabase?: string 25 | } 26 | 27 | /** 28 | * @category models 29 | * @since 1.0.0 30 | */ 31 | export interface Config { 32 | host: string 33 | port: number 34 | user: string 35 | password: string 36 | database: string 37 | } 38 | 39 | /** 40 | * @category config 41 | * @since 1.0.0 42 | */ 43 | export const makeConfig: ( 44 | options?: Partial 45 | ) => _Config.Config = internal_config.makeConfig 46 | 47 | /** 48 | * @category config 49 | * @since 1.0.0 50 | */ 51 | export const setConfig: ( 52 | options?: Partial 53 | ) => Layer.Layer = internal_config.setConfig 54 | 55 | /** 56 | * @category layer 57 | * @since 1.0.0 58 | */ 59 | export const Pool: Layer.Layer = internal_layers.pool 60 | 61 | /** 62 | * @category layer 63 | * @since 1.0.0 64 | */ 65 | export const Client: Layer.Layer< 66 | pg.ClientBase, 67 | PgError.PostgresConnectionError, 68 | pg.ClientConfig 69 | > = internal_layers.client 70 | 71 | /** 72 | * @category layer 73 | * @since 1.0.0 74 | */ 75 | export const PoolClient: Layer.Layer = 76 | internal_layers.poolClient 77 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [master] 7 | push: 8 | branches: [master] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | 16 | node: 17 | services: 18 | postgres: 19 | image: postgres 20 | env: 21 | POSTGRES_PASSWORD: postgres 22 | POSTGRES_USER: postgres 23 | POSTGRES_DB: postgres 24 | ports: 25 | - 5432:5432 26 | options: >- 27 | --health-cmd pg_isready 28 | --health-interval 10s 29 | --health-timeout 5s 30 | --health-retries 5 31 | 32 | name: Node 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 10 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Install dependencies 38 | uses: ./.github/actions/setup 39 | - run: pnpm test 40 | env: 41 | TEST_POSTGRES_USER: postgres 42 | TEST_POSTGRES_PASSWORD: postgres 43 | TEST_POSTGRES_DATABASE: postgres 44 | 45 | bun: 46 | services: 47 | postgres: 48 | image: postgres 49 | env: 50 | POSTGRES_PASSWORD: postgres 51 | POSTGRES_USER: postgres 52 | POSTGRES_DB: postgres 53 | ports: 54 | - 5432:5432 55 | options: >- 56 | --health-cmd pg_isready 57 | --health-interval 10s 58 | --health-timeout 5s 59 | --health-retries 5 60 | 61 | name: Bun 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | - name: Install dependencies 66 | uses: ./.github/actions/setup 67 | - name: Install bun 68 | uses: oven-sh/setup-bun@v1 69 | - run: bun vitest 70 | env: 71 | TEST_POSTGRES_USER: postgres 72 | TEST_POSTGRES_PASSWORD: postgres 73 | TEST_POSTGRES_DATABASE: postgres 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # effect-pg 2 | 3 | [node-pg](https://github.com/brianc/node-postgres) wrapper for 4 | [effect](https://github.com/Effect-TS). 5 | 6 | **Under development** 7 | 8 | ## Quickstart 9 | 10 | Install the package using 11 | 12 | ```bash 13 | pnpm install effect-pg effect 14 | ``` 15 | 16 | The following example assumes you have a postgres running. Setup the environment 17 | variables as follows. 18 | 19 | ``` 20 | $ export POSTGRES_USER= 21 | $ export POSTGRES_PASSWORD= 22 | $ export POSTGRES_DATABASE= 23 | ``` 24 | 25 | Optionally, also set `POSTGRES_HOST` and `POSTGRES_NAME`. The example below will 26 | create a new `users` table, insert a single row and fetch the row. The persisted 27 | row is immediately fetched and logged out. 28 | 29 | ```typescript 30 | import { Schema } from "@effect/schema" 31 | import { Effect, pipe } from "effect" 32 | import { PgLayer, PgQuery } from "effect-pg" 33 | 34 | const User = Schema.Struct({ name: Schema.String }) 35 | 36 | const createUsersTable = PgQuery.all( 37 | "CREATE TABLE IF NOT EXISTS users (name TEXT NOT NULL)" 38 | ) 39 | const insertUser = PgQuery.all("INSERT INTO users (name) VALUES ($1)") 40 | const selectUser = PgQuery.one("SELECT * FROM users", User) 41 | 42 | pipe( 43 | createUsersTable(), 44 | Effect.flatMap(() => insertUser("patrik")), 45 | Effect.flatMap(() => selectUser()), 46 | Effect.flatMap((result) => Effect.log(`User: ${JSON.stringify(result)}`)), 47 | Effect.provide(PgLayer.Client), 48 | Effect.provide(PgLayer.setConfig()), 49 | Effect.runPromise 50 | ) 51 | ``` 52 | 53 | ## Contributing 54 | 55 | ### Local development 56 | 57 | Spawn a postgres instance. 58 | 59 | ```bash 60 | $ docker run -d -p 5432:5432 --name test-postgres -e POSTGRES_PASSWORD=test -e POSTGRES_USER=test postgres 61 | ``` 62 | 63 | Create a `.env` file. 64 | 65 | ```bash 66 | TEST_POSTGRES_USER=test 67 | TEST_POSTGRES_DATABASE=test 68 | TEST_POSTGRES_PASSWORD=test 69 | ``` 70 | 71 | Run tests. 72 | 73 | ```bash 74 | pnpm test 75 | ``` 76 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | module.exports = { 3 | ignorePatterns: ["dist", "build", "docs", "*.md"], 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { 6 | ecmaVersion: 2018, 7 | sourceType: "module" 8 | }, 9 | settings: { 10 | "import/parsers": { 11 | "@typescript-eslint/parser": [".ts", ".tsx"] 12 | }, 13 | "import/resolver": { 14 | typescript: { 15 | alwaysTryTypes: true 16 | } 17 | } 18 | }, 19 | extends: [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/eslint-recommended", 22 | "plugin:@typescript-eslint/recommended", 23 | "plugin:@effect/recommended" 24 | ], 25 | plugins: [ 26 | "deprecation", 27 | "import", 28 | "sort-destructure-keys", 29 | "simple-import-sort", 30 | "codegen" 31 | ], 32 | rules: { 33 | "codegen/codegen": "error", 34 | "no-fallthrough": "off", 35 | "no-irregular-whitespace": "off", 36 | "object-shorthand": "error", 37 | "prefer-destructuring": "off", 38 | "sort-imports": "off", 39 | "no-unused-vars": "off", 40 | "prefer-rest-params": "off", 41 | "prefer-spread": "off", 42 | "import/first": "error", 43 | "import/no-cycle": "error", 44 | "import/newline-after-import": "error", 45 | "import/no-duplicates": "error", 46 | "import/no-unresolved": "off", 47 | "import/order": "off", 48 | "simple-import-sort/imports": "off", 49 | "sort-destructure-keys/sort-destructure-keys": "error", 50 | "deprecation/deprecation": "off", 51 | "@typescript-eslint/array-type": [ 52 | "warn", 53 | { default: "generic", readonly: "generic" } 54 | ], 55 | "@typescript-eslint/member-delimiter-style": 0, 56 | "@typescript-eslint/no-non-null-assertion": "off", 57 | "@typescript-eslint/ban-types": "off", 58 | "@typescript-eslint/no-explicit-any": "off", 59 | "@typescript-eslint/no-empty-interface": "off", 60 | "@typescript-eslint/consistent-type-imports": "warn", 61 | "@typescript-eslint/no-unused-vars": [ 62 | "error", 63 | { 64 | argsIgnorePattern: "^_", 65 | varsIgnorePattern: "^_" 66 | } 67 | ], 68 | "@typescript-eslint/ban-ts-comment": "off", 69 | "@typescript-eslint/camelcase": "off", 70 | "@typescript-eslint/explicit-function-return-type": "off", 71 | "@typescript-eslint/explicit-module-boundary-types": "off", 72 | "@typescript-eslint/interface-name-prefix": "off", 73 | "@typescript-eslint/no-array-constructor": "off", 74 | "@typescript-eslint/no-use-before-define": "off", 75 | "@typescript-eslint/no-namespace": "off", 76 | "@effect/dprint": [ 77 | "error", 78 | { 79 | config: { 80 | indentWidth: 2, 81 | lineWidth: 120, 82 | semiColons: "asi", 83 | quoteStyle: "alwaysDouble", 84 | trailingCommas: "never", 85 | operatorPosition: "maintain", 86 | "arrowFunction.useParentheses": "force" 87 | } 88 | } 89 | ] 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/internal/layers.ts: -------------------------------------------------------------------------------- 1 | import * as pg from "pg" 2 | 3 | import * as _Config from "effect/Config" 4 | import * as Context from "effect/Context" 5 | import * as Effect from "effect/Effect" 6 | import { pipe } from "effect/Function" 7 | import * as Layer from "effect/Layer" 8 | import * as PgError from "../PgError.js" 9 | import * as internal_context from "./context.js" 10 | 11 | /** @internal */ 12 | export const client = pipe( 13 | Effect.map(internal_context.ClientConfig, (config) => new pg.Client(config)), 14 | Effect.flatMap((client) => 15 | Effect.acquireRelease( 16 | pipe( 17 | Effect.tryPromise(() => client.connect()), 18 | Effect.mapError( 19 | (error) => new PgError.PostgresConnectionError({ error }) 20 | ), 21 | Effect.as(client), 22 | Effect.tap(() => Effect.logDebug("Postgres connection acquired")) 23 | ), 24 | (client) => 25 | pipe( 26 | Effect.sync(() => client.end()), 27 | Effect.tap(() => Effect.logDebug("Postgres connection released")) 28 | ) 29 | ) 30 | ), 31 | Layer.scoped(internal_context.Client), 32 | Layer.flatMap((client) => 33 | client.pipe( 34 | Context.add( 35 | internal_context.ClientBase, 36 | Context.get(client, internal_context.Client) 37 | ), 38 | Layer.succeedContext 39 | ) 40 | ) 41 | ) 42 | 43 | /** @internal */ 44 | export const pool = pipe( 45 | Effect.acquireRelease( 46 | pipe( 47 | Effect.map(internal_context.PoolConfig, (config) => new pg.Pool(config)), 48 | Effect.tap(() => Effect.logDebug("Postgres pool initialized")) 49 | ), 50 | (pool) => 51 | pipe( 52 | Effect.logDebug("Releasing postgres pool"), 53 | Effect.as([pool.idleCount, pool.waitingCount]), 54 | Effect.tap(() => Effect.tryPromise(() => pool.end())), 55 | Effect.tap(([idle, waiting]) => 56 | pipe( 57 | Effect.logDebug("Postgres pool ended"), 58 | Effect.annotateLogs("idleConnectionsCounts", `${idle}`), 59 | Effect.annotateLogs("waitingConnectionsCounts", `${waiting}`) 60 | ) 61 | ), 62 | Effect.orDie 63 | ) 64 | ), 65 | Layer.scoped(internal_context.Pool) 66 | ) 67 | 68 | /** @internal */ 69 | export const poolClient = pipe( 70 | Effect.flatMap(internal_context.Pool, (pool) => 71 | Effect.acquireRelease( 72 | pipe( 73 | Effect.tryPromise(() => pool.connect()), 74 | Effect.mapError( 75 | (error) => new PgError.PostgresConnectionError({ error }) 76 | ), 77 | Effect.tap(() => Effect.logDebug("Postgres connection acquired from pool")) 78 | ), 79 | (client) => 80 | pipe( 81 | Effect.sync(() => client.release()), 82 | Effect.tap(() => Effect.logDebug("Postgres connection released to pool")) 83 | ) 84 | )), 85 | Layer.scoped(internal_context.PoolClient), 86 | Layer.flatMap((poolClient) => 87 | poolClient.pipe( 88 | Context.add( 89 | internal_context.ClientBase, 90 | Context.get(poolClient, internal_context.PoolClient) 91 | ), 92 | Layer.succeedContext 93 | ) 94 | ) 95 | ) 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-pg", 3 | "type": "module", 4 | "version": "0.27.3", 5 | "license": "MIT", 6 | "author": "", 7 | "description": "node-pg wrapper for effect-ts", 8 | "homepage": "https://sukovanej.github.io/effect-pg", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sukovanej/effect-pg.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/sukovanej/effect-pg/issues" 15 | }, 16 | "packageManager": "pnpm@9.1.1", 17 | "publishConfig": { 18 | "access": "public", 19 | "directory": "dist" 20 | }, 21 | "scripts": { 22 | "build": "pnpm build-prepare && pnpm build-esm && pnpm build-cjs && pnpm build-annotate && build-utils pack-v2", 23 | "build-prepare": "build-utils prepare-v2", 24 | "build-esm": "tsc -b tsconfig.build.json", 25 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 26 | "build-annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", 27 | "clean": "rimraf build dist coverage .tsbuildinfo", 28 | "check": "tsc -b tsconfig.json", 29 | "check:watch": "tsc -b tsconfig.json --watch", 30 | "test": "vitest", 31 | "coverage": "vitest --run --coverage related", 32 | "coverage-all": "vitest --run --coverage", 33 | "circular": "madge --extensions ts --circular --no-color --no-spinner --warning src", 34 | "lint": "eslint src test examples", 35 | "lint-fix": "eslint src test examples --fix", 36 | "docgen": "docgen" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.24.6", 40 | "@babel/core": "^7.24.6", 41 | "@babel/plugin-transform-export-namespace-from": "^7.24.6", 42 | "@babel/plugin-transform-modules-commonjs": "^7.24.6", 43 | "@changesets/changelog-github": "^0.5.0", 44 | "@changesets/cli": "^2.27.3", 45 | "@effect/build-utils": "^0.7.6", 46 | "@effect/docgen": "^0.4.3", 47 | "@effect/eslint-plugin": "^0.1.2", 48 | "@effect/language-service": "^0.1.0", 49 | "@effect/platform": "^0.55.0", 50 | "@effect/platform-node": "^0.51.0", 51 | "@effect/schema": "^0.67.13", 52 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 53 | "@typescript-eslint/eslint-plugin": "^7.10.0", 54 | "@typescript-eslint/parser": "^7.10.0", 55 | "@vitest/coverage-v8": "^1.6.0", 56 | "babel-plugin-annotate-pure-calls": "^0.4.0", 57 | "effect": "3.2.5", 58 | "effect-dotenv": "^0.18.6", 59 | "eslint": "^8.57.0", 60 | "eslint-import-resolver-typescript": "^3.6.1", 61 | "eslint-plugin-codegen": "0.28.0", 62 | "eslint-plugin-deprecation": "^2.0.0", 63 | "eslint-plugin-import": "^2.29.1", 64 | "eslint-plugin-simple-import-sort": "^12.1.0", 65 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 66 | "madge": "^7.0.0", 67 | "prettier": "^3.2.5", 68 | "rimraf": "^5.0.7", 69 | "tsx": "^4.11.0", 70 | "typescript": "^5.4.5", 71 | "vitest": "^1.6.0" 72 | }, 73 | "dependencies": { 74 | "@types/pg": "^8.11.6", 75 | "@types/pg-cursor": "^2.7.2", 76 | "pg": "^8.11.5", 77 | "pg-cursor": "^2.10.5" 78 | }, 79 | "peerDependencies": { 80 | "@effect/schema": "^0.67.0", 81 | "effect": "^3.2.0" 82 | }, 83 | "pnpm": { 84 | "updateConfig": { 85 | "ignoreDependencies": [ 86 | "eslint" 87 | ] 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/PgQuery.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Querying 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Schema from "@effect/schema/Schema" 7 | import type * as Effect from "effect/Effect" 8 | import type * as Stream from "effect/Stream" 9 | import type * as pg from "pg" 10 | import * as internal_query from "./internal/query.js" 11 | import type * as PgError from "./PgError.js" 12 | 13 | /** 14 | * @category models 15 | * @since 1.0.0 16 | */ 17 | export interface QueryStreamOptions { 18 | maxRowsPerRead: number 19 | } 20 | 21 | /** 22 | * @category querying 23 | * @since 1.0.0 24 | */ 25 | export const all: { 26 | ( 27 | sql: string, 28 | schema: Schema.Schema 29 | ): ( 30 | ...values: Array 31 | ) => Effect.Effect, PgError.PostgresQueryError | PgError.PostgresValidationError, pg.ClientBase | R> 32 | 33 | ( 34 | sql: string 35 | ): ( 36 | ...values: Array 37 | ) => Effect.Effect, PgError.PostgresQueryError, pg.ClientBase> 38 | } = internal_query.all 39 | 40 | /** 41 | * @category querying 42 | * @since 1.0.0 43 | */ 44 | export const one: { 45 | ( 46 | sql: string 47 | ): ( 48 | ...values: Array 49 | ) => Effect.Effect 50 | 51 | ( 52 | sql: string, 53 | schema?: Schema.Schema 54 | ): ( 55 | ...values: Array 56 | ) => Effect.Effect< 57 | A, 58 | | PgError.PostgresQueryError 59 | | PgError.PostgresValidationError 60 | | PgError.PostgresUnexpectedNumberOfRowsError, 61 | pg.ClientBase | R 62 | > 63 | } = internal_query.one 64 | 65 | /** 66 | * @category querying 67 | * @since 1.0.0 68 | */ 69 | export const stream: { 70 | ( 71 | queryText: string, 72 | schema: Schema.Schema, 73 | options?: Partial 74 | ): ( 75 | ...values: Array 76 | ) => Stream.Stream< 77 | A, 78 | PgError.PostgresQueryError | PgError.PostgresValidationError, 79 | pg.ClientBase | R 80 | > 81 | 82 | ( 83 | queryText: string, 84 | options?: Partial 85 | ): ( 86 | ...values: Array 87 | ) => Stream.Stream 88 | } = internal_query.stream 89 | 90 | /** 91 | * @category transactions 92 | * @since 1.0.0 93 | */ 94 | export const begin: Effect.Effect = internal_query.begin 95 | 96 | /** 97 | * @category transactions 98 | * @since 1.0.0 99 | */ 100 | export const commit: Effect.Effect = internal_query.commit 101 | 102 | /** 103 | * @category transactions 104 | * @since 1.0.0 105 | */ 106 | export const rollback: Effect.Effect = internal_query.rollback 107 | 108 | /** 109 | * @category transactions 110 | * @since 1.0.0 111 | */ 112 | export const transaction: ( 113 | self: Effect.Effect 114 | ) => Effect.Effect = internal_query.transaction 115 | 116 | /** 117 | * @category transactions 118 | * @since 1.0.0 119 | */ 120 | export const transactionRollback: ( 121 | self: Effect.Effect 122 | ) => Effect.Effect = internal_query.transactionRollback 123 | -------------------------------------------------------------------------------- /test/query.test.ts: -------------------------------------------------------------------------------- 1 | import type * as pg from "pg" 2 | import { assert, describe, expect, test } from "vitest" 3 | 4 | import { NodeContext } from "@effect/platform-node" 5 | import { Schema } from "@effect/schema" 6 | import { Array, Chunk, Effect, Either, pipe, Stream } from "effect" 7 | import { DotEnv } from "effect-dotenv" 8 | import { PgError, PgLayer, PgQuery } from "effect-pg" 9 | 10 | export const setTestConfig = PgLayer.setConfig({ namePrefix: "TEST_POSTGRES" }) 11 | 12 | const runTest = (self: Effect.Effect) => 13 | pipe( 14 | self, 15 | PgQuery.transactionRollback, 16 | Effect.provide(PgLayer.Client), 17 | Effect.provide(setTestConfig), 18 | Effect.provide(DotEnv.setConfigProvider()), 19 | Effect.provide(NodeContext.layer), 20 | Effect.runPromise 21 | ) 22 | 23 | const User = Schema.Struct({ id: Schema.Number, name: Schema.String }) 24 | 25 | const createUsersTable = PgQuery.all( 26 | "CREATE TABLE users (id SERIAL, name TEXT NOT NULL)" 27 | ) 28 | const insertUser = PgQuery.all("INSERT INTO users (name) VALUES ($1::TEXT)", User) 29 | const selectUser = PgQuery.one("SELECT * FROM users", User) 30 | const selectUsers = PgQuery.all("SELECT * FROM users", User) 31 | 32 | test("Simple test 1", async () => { 33 | await pipe( 34 | createUsersTable(), 35 | Effect.flatMap(() => insertUser("milan")), 36 | Effect.flatMap(() => selectUser()), 37 | Effect.flatMap((row) => 38 | Effect.sync(() => { 39 | expect(row.name).toEqual("milan") 40 | }) 41 | ), 42 | Effect.flatMap(() => selectUsers()), 43 | Effect.map((rows) => { 44 | expect(rows.map((user) => user.name)).toEqual(["milan"]) 45 | }), 46 | runTest 47 | ) 48 | }) 49 | 50 | test("Simple test 2", async () => { 51 | const result = await pipe( 52 | createUsersTable(), 53 | Effect.flatMap(() => Effect.all(Array.replicate(insertUser("milan"), 3))), 54 | Effect.flatMap(() => selectUser()), 55 | Effect.either, 56 | runTest 57 | ) 58 | 59 | expect(result).toEqual( 60 | Either.left( 61 | new PgError.PostgresUnexpectedNumberOfRowsError({ 62 | expectedRows: 1, 63 | receivedRows: 3 64 | }) 65 | ) 66 | ) 67 | }) 68 | 69 | test("Table doesnt exist error", async () => { 70 | await pipe( 71 | selectUsers(), 72 | Effect.map(() => { 73 | assert.fail("Expected failure") 74 | }), 75 | Effect.catchAll((error) => { 76 | expect(error._tag).toEqual("PostgresTableDoesntExistError") 77 | return Effect.void 78 | }), 79 | runTest 80 | ) 81 | }) 82 | 83 | test("Duplicate table error", async () => { 84 | await pipe( 85 | createUsersTable(), 86 | Effect.flatMap(() => createUsersTable()), 87 | Effect.map(() => { 88 | assert.fail("Expected failure") 89 | }), 90 | Effect.catchAll((error) => { 91 | expect(error._tag).toEqual("PostgresDuplicateTableError") 92 | return Effect.void 93 | }), 94 | runTest 95 | ) 96 | }) 97 | 98 | test("Pool", async () => { 99 | await pipe( 100 | createUsersTable(), 101 | Effect.flatMap(() => createUsersTable()), 102 | Effect.map(() => { 103 | assert.fail("Expected failure") 104 | }), 105 | Effect.catchAll((error) => { 106 | expect(error._tag).toEqual("PostgresDuplicateTableError") 107 | return Effect.void 108 | }), 109 | PgQuery.transactionRollback, 110 | Effect.provide(PgLayer.PoolClient), 111 | Effect.provide(PgLayer.Pool), 112 | Effect.provide(setTestConfig), 113 | Effect.provide(DotEnv.setConfigProvider()), 114 | Effect.provide(NodeContext.layer), 115 | Effect.runPromise 116 | ) 117 | }) 118 | 119 | describe("streaming", () => { 120 | test("sequence", async () => { 121 | const queryNumSequence = PgQuery.stream( 122 | "SELECT * FROM generate_series(1, $1) n", 123 | Schema.Struct({ n: Schema.Number }) 124 | ) 125 | 126 | const result = await pipe( 127 | queryNumSequence(10), 128 | Stream.map(({ n }) => n), 129 | Stream.runCollect, 130 | runTest 131 | ) 132 | 133 | expect(Chunk.toReadonlyArray(result)).toEqual(Array.range(1, 10)) 134 | }) 135 | 136 | test("take", async () => { 137 | const queryNumSequence = PgQuery.stream( 138 | "SELECT * FROM generate_series(1, $1) n", 139 | Schema.Struct({ n: Schema.Number }) 140 | ) 141 | 142 | const result = await pipe( 143 | queryNumSequence(20), 144 | Stream.map(({ n }) => n), 145 | Stream.take(10), 146 | Stream.runCollect, 147 | runTest 148 | ) 149 | 150 | expect(Chunk.toReadonlyArray(result)).toEqual(Array.range(1, 10)) 151 | }) 152 | 153 | test("maxRowsPerRead", async () => { 154 | const queryNumSequence = PgQuery.stream( 155 | "SELECT * FROM generate_series(1, $1) n", 156 | Schema.Struct({ n: Schema.Number }), 157 | { maxRowsPerRead: 20 } 158 | ) 159 | 160 | const result = await pipe( 161 | queryNumSequence(20), 162 | Stream.map(({ n }) => n), 163 | Stream.runCollect, 164 | runTest 165 | ) 166 | 167 | expect(Chunk.toReadonlyArray(result)).toEqual(Array.range(1, 20)) 168 | }) 169 | }) 170 | -------------------------------------------------------------------------------- /src/internal/query.ts: -------------------------------------------------------------------------------- 1 | import type * as pg from "pg" 2 | import Cursor from "pg-cursor" 3 | 4 | import * as Schema from "@effect/schema/Schema" 5 | import type * as Cause from "effect/Cause" 6 | import * as Chunk from "effect/Chunk" 7 | import * as Effect from "effect/Effect" 8 | import * as Exit from "effect/Exit" 9 | import { flow, pipe } from "effect/Function" 10 | import * as Option from "effect/Option" 11 | import * as Predicate from "effect/Predicate" 12 | import * as Stream from "effect/Stream" 13 | import * as PgError from "../PgError.js" 14 | import type * as PgQuery from "../PgQuery.js" 15 | import * as internal_context from "./context.js" 16 | 17 | /** @internal */ 18 | const isObjectWithCode = (code: string, object: unknown) => 19 | Predicate.isRecord(object) && "code" in object && object["code"] === code 20 | 21 | /** @internal */ 22 | const convertError = (exception: Cause.UnknownException) => { 23 | const error = exception.error 24 | 25 | if (isObjectWithCode("42P01", error)) { 26 | return new PgError.PostgresTableDoesntExistError({ error }) 27 | } else if (isObjectWithCode("42P07", error)) { 28 | return new PgError.PostgresDuplicateTableError({ error }) 29 | } else if (isObjectWithCode("08P01", error)) { 30 | return new PgError.PostgresInvalidParametersError({ error }) 31 | } 32 | 33 | return new PgError.PostgresUnknownError({ error }) 34 | } 35 | 36 | /** @internal */ 37 | export const all: { 38 | ( 39 | sql: string, 40 | schema: Schema.Schema 41 | ): ( 42 | ...values: Array 43 | ) => Effect.Effect, PgError.PostgresQueryError | PgError.PostgresValidationError, R | pg.ClientBase> 44 | 45 | ( 46 | sql: string 47 | ): ( 48 | ...values: Array 49 | ) => Effect.Effect, PgError.PostgresQueryError, pg.ClientBase> 50 | } = (sql: string, schema?: Schema.Schema) => { 51 | const parse = schema ? Schema.decodeUnknown(schema) : undefined 52 | 53 | return (...values: Array): Effect.Effect => 54 | pipe( 55 | Effect.flatMap(internal_context.ClientBase, (client) => Effect.tryPromise(() => client.query(sql, values))), 56 | Effect.mapError(convertError), 57 | Effect.flatMap((result) => { 58 | if (parse === undefined) { 59 | return Effect.succeed(result.rows) 60 | } 61 | 62 | return pipe( 63 | result.rows, 64 | Effect.forEach((row) => parse(row)), 65 | Effect.mapError( 66 | (error) => new PgError.PostgresValidationError({ error }) 67 | ) 68 | ) 69 | }) 70 | ) 71 | } 72 | 73 | /** @internal */ 74 | export const one: { 75 | ( 76 | sql: string 77 | ): ( 78 | ...values: Array 79 | ) => Effect.Effect 80 | 81 | ( 82 | sql: string, 83 | schema?: Schema.Schema 84 | ): ( 85 | ...values: Array 86 | ) => Effect.Effect< 87 | A, 88 | | PgError.PostgresQueryError 89 | | PgError.PostgresValidationError 90 | | PgError.PostgresUnexpectedNumberOfRowsError, 91 | pg.ClientBase | R 92 | > 93 | } = (sql: string, schema?: Schema.Schema) => { 94 | const runQuery = schema ? all(sql, schema) : all(sql) 95 | 96 | return (...values: Array): Effect.Effect => 97 | runQuery(...values).pipe( 98 | Effect.filterOrFail( 99 | (rows) => rows.length === 1, 100 | (rows) => 101 | new PgError.PostgresUnexpectedNumberOfRowsError({ 102 | expectedRows: 1, 103 | receivedRows: rows.length 104 | }) 105 | ), 106 | Effect.map((rows) => rows[0] as unknown) 107 | ) 108 | } 109 | 110 | /** @internal */ 111 | const defaultQueryStreamOptions: PgQuery.QueryStreamOptions = { 112 | maxRowsPerRead: 10 113 | } 114 | 115 | /** @internal */ 116 | export const stream: { 117 | ( 118 | queryText: string, 119 | schema: Schema.Schema, 120 | options?: Partial 121 | ): ( 122 | ...values: Array 123 | ) => Stream.Stream 124 | 125 | ( 126 | queryText: string, 127 | options?: Partial 128 | ): ( 129 | ...values: Array 130 | ) => Stream.Stream 131 | } = ( 132 | sql: string, 133 | ...args: 134 | | [ 135 | schema: Schema.Schema, 136 | options?: Partial | undefined 137 | ] 138 | | [options?: Partial | undefined] 139 | ) => { 140 | const { options, parse } = Schema.isSchema(args[0]) 141 | ? { 142 | parse: Schema.decodeUnknown(args[0]), 143 | options: { ...defaultQueryStreamOptions, ...args[1] } 144 | } 145 | : { 146 | parse: undefined, 147 | options: { ...defaultQueryStreamOptions, ...args[0] } 148 | } 149 | 150 | return (...values: Array) => 151 | pipe( 152 | Effect.flatMap(internal_context.ClientBase, (client) => 153 | Effect.acquireRelease( 154 | Effect.succeed(client.query(new Cursor(sql, values))), 155 | (cursor) => Effect.tryPromise(() => cursor.close()).pipe(Effect.orDie) 156 | )), 157 | Effect.map((cursor) => 158 | pipe( 159 | Effect.tryPromise(() => cursor.read(options.maxRowsPerRead)), 160 | Effect.mapError(flow(convertError, Option.some)), 161 | Effect.flatMap((rows) => { 162 | if (parse === undefined) { 163 | return Effect.succeed(rows) 164 | } 165 | 166 | return pipe( 167 | rows, 168 | Effect.forEach((row) => parse(row)), 169 | Effect.mapError((error) => 170 | Option.some( 171 | new PgError.PostgresValidationError({ 172 | error 173 | }) as unknown as PgError.PostgresUnknownError 174 | ) 175 | ) 176 | ) 177 | }), 178 | Effect.map(Chunk.fromIterable), 179 | Effect.filterOrFail(Chunk.isNonEmpty, () => Option.none()) 180 | ) 181 | ), 182 | Stream.fromPull 183 | ) 184 | } 185 | 186 | /** @internal */ 187 | export const begin = all("BEGIN")() 188 | 189 | /** @internal */ 190 | export const commit = all("COMMIT")() 191 | 192 | /** @internal */ 193 | export const rollback = all("ROLLBACK")() 194 | 195 | /** @internal */ 196 | export const transaction = ( 197 | self: Effect.Effect 198 | ): Effect.Effect => 199 | Effect.acquireUseRelease( 200 | begin, 201 | () => self, 202 | (_, exit) => 203 | pipe( 204 | exit, 205 | Exit.match({ 206 | onSuccess: () => commit, 207 | onFailure: () => rollback 208 | }), 209 | Effect.orDie 210 | ) 211 | ) 212 | 213 | /** @internal */ 214 | export const transactionRollback = ( 215 | self: Effect.Effect 216 | ): Effect.Effect => 217 | Effect.acquireUseRelease( 218 | begin, 219 | () => self, 220 | () => Effect.orDie(rollback) 221 | ) 222 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # effect-pg 2 | 3 | ## 0.27.3 4 | 5 | ### Patch Changes 6 | 7 | - [`5f4e4bf`](https://github.com/sukovanej/effect-pg/commit/5f4e4bf9e440c9e77eea0fb6d56ccb2917b69bdd) Thanks [@sukovanej](https://github.com/sukovanej)! - Update effect. 8 | 9 | ## 0.27.2 10 | 11 | ### Patch Changes 12 | 13 | - [`9feb01f`](https://github.com/sukovanej/effect-pg/commit/9feb01f00ee4230094de63ab1ec6d74172bc24ca) Thanks [@sukovanej](https://github.com/sukovanej)! - Update /schema. 14 | 15 | ## 0.27.1 16 | 17 | ### Patch Changes 18 | 19 | - [`73690bd`](https://github.com/sukovanej/effect-pg/commit/73690bd92344a02f1de1f3c18dedf46764fba8c7) Thanks [@sukovanej](https://github.com/sukovanej)! - Update effect. 20 | 21 | ## 0.27.0 22 | 23 | ### Minor Changes 24 | 25 | - bccdad0: Update to effect 3.0 26 | 27 | ## 0.26.0 28 | 29 | ### Minor Changes 30 | 31 | - 960f384: Update effect. 32 | 33 | ## 0.25.17 34 | 35 | ### Patch Changes 36 | 37 | - 9ad6a5d: Update effect. 38 | 39 | ## 0.25.16 40 | 41 | ### Patch Changes 42 | 43 | - 272b2d8: Update effect. 44 | 45 | ## 0.25.15 46 | 47 | ### Patch Changes 48 | 49 | - 8c5167c: Update @effect/schema 50 | 51 | ## 0.25.14 52 | 53 | ### Patch Changes 54 | 55 | - 006a1bb: Update effect. 56 | 57 | ## 0.25.13 58 | 59 | ### Patch Changes 60 | 61 | - 58495bc: Update effect. 62 | 63 | ## 0.25.12 64 | 65 | ### Patch Changes 66 | 67 | - 38a0763: Update effect. 68 | 69 | ## 0.25.11 70 | 71 | ### Patch Changes 72 | 73 | - e493424: Update effect. 74 | 75 | ## 0.25.10 76 | 77 | ### Patch Changes 78 | 79 | - 81df21f: Update effect. 80 | 81 | ## 0.25.9 82 | 83 | ### Patch Changes 84 | 85 | - 35e0234: Update effect. 86 | 87 | ## 0.25.8 88 | 89 | ### Patch Changes 90 | 91 | - d6200e1: Update effect. 92 | 93 | ## 0.25.7 94 | 95 | ### Patch Changes 96 | 97 | - f937472: Update effect. 98 | 99 | ## 0.25.6 100 | 101 | ### Patch Changes 102 | 103 | - 227aee5: Update effect. 104 | 105 | ## 0.25.5 106 | 107 | ### Patch Changes 108 | 109 | - e8e1139: Update effect. 110 | 111 | ## 0.25.4 112 | 113 | ### Patch Changes 114 | 115 | - c3724f9: Update effect. 116 | - 23eee07: Use namespace imports. 117 | 118 | ## 0.25.3 119 | 120 | ### Patch Changes 121 | 122 | - 58de301: Update effect packages. 123 | 124 | ## 0.25.2 125 | 126 | ### Patch Changes 127 | 128 | - 990689b: Update effect. 129 | 130 | ## 0.25.1 131 | 132 | ### Patch Changes 133 | 134 | - 3ca3609: Update effect. 135 | 136 | ## 0.25.0 137 | 138 | ### Minor Changes 139 | 140 | - b6ea148: Minor effect update. Align setup with the effect ecosystem. 141 | 142 | ## 0.24.1 143 | 144 | ### Patch Changes 145 | 146 | - e3fcba3: Fix @effect/schema peer dependency version. 147 | 148 | ## 0.24.0 149 | 150 | ### Minor Changes 151 | 152 | - a312c3a: Update @effect/schema. 153 | 154 | ## 0.23.5 155 | 156 | ### Patch Changes 157 | 158 | - ec401f3: Update effect. 159 | 160 | ## 0.23.4 161 | 162 | ### Patch Changes 163 | 164 | - 3866673: Update effect. 165 | 166 | ## 0.23.3 167 | 168 | ### Patch Changes 169 | 170 | - 4d254bd: Update dependencies. 171 | 172 | ## 0.23.2 173 | 174 | ### Patch Changes 175 | 176 | - 925915d: Update @effect/schema. 177 | 178 | ## 0.23.1 179 | 180 | ### Patch Changes 181 | 182 | - ce76f84: Update @effect/schema. 183 | 184 | ## 0.23.0 185 | 186 | ### Minor Changes 187 | 188 | - a4146b8: Update effect. 189 | 190 | ## 0.22.3 191 | 192 | ### Patch Changes 193 | 194 | - 2bd8007: Update effect + @effect/schema. 195 | 196 | ## 0.22.2 197 | 198 | ### Patch Changes 199 | 200 | - c56ea61: Update @effect/schema. 201 | 202 | ## 0.22.1 203 | 204 | ### Patch Changes 205 | 206 | - 6f07f73: Update effect. 207 | 208 | ## 0.22.0 209 | 210 | ### Minor Changes 211 | 212 | - 3f366e0: Update effect. 213 | 214 | ## 0.21.0 215 | 216 | ### Minor Changes 217 | 218 | - a4bb6aa: Update effect. 219 | 220 | ## 0.20.1 221 | 222 | ### Patch Changes 223 | 224 | - 862e61b: Update effect. 225 | 226 | ## 0.20.0 227 | 228 | ### Minor Changes 229 | 230 | - 5e34ba1: Update effect. 231 | 232 | ## 0.19.0 233 | 234 | ### Minor Changes 235 | 236 | - dc61162: Update @effect/schema. 237 | 238 | ## 0.18.0 239 | 240 | ### Minor Changes 241 | 242 | - a7c16c4: Update dependencies. 243 | 244 | ## 0.17.4 245 | 246 | ### Patch Changes 247 | 248 | - 46eff6a: Update dependencies. 249 | 250 | ## 0.17.3 251 | 252 | ### Patch Changes 253 | 254 | - 437142c: Update dependencies. 255 | 256 | ## 0.17.2 257 | 258 | ### Patch Changes 259 | 260 | - e8e5ce0: Update effect. 261 | 262 | ## 0.17.1 263 | 264 | ### Patch Changes 265 | 266 | - 623cb84: Update effect. 267 | 268 | ## 0.17.0 269 | 270 | ### Minor Changes 271 | 272 | - 4038ddc: Update effect. 273 | 274 | ## 0.16.0 275 | 276 | ### Minor Changes 277 | 278 | - 7fafea0: Update public API. `Pg` and `PgError` modules are exported. 279 | 280 | ## 0.15.0 281 | 282 | ### Minor Changes 283 | 284 | - 34d852e: Update effect. 285 | 286 | ## 0.14.0 287 | 288 | ### Minor Changes 289 | 290 | - 0701454: Update effect. 291 | 292 | ## 0.13.1 293 | 294 | ### Patch Changes 295 | 296 | - 3e14229: Update @effect/schema and @types/pg. 297 | 298 | ## 0.13.0 299 | 300 | ### Minor Changes 301 | 302 | - 742e78e: Update effect. 303 | - 2ed02d9: Use Data.TaggedError for errors. 304 | 305 | ## 0.12.10 306 | 307 | ### Patch Changes 308 | 309 | - 950dc31: Remove @effect/match. 310 | 311 | ## 0.12.9 312 | 313 | ### Patch Changes 314 | 315 | - 41d4b7c: Update effect. 316 | 317 | ## 0.12.8 318 | 319 | ### Patch Changes 320 | 321 | - 4a8d7a7: Update effect. 322 | 323 | ## 0.12.7 324 | 325 | ### Patch Changes 326 | 327 | - df6b3be: Update effect dependencies. 328 | 329 | ## 0.12.6 330 | 331 | ### Patch Changes 332 | 333 | - 3d8043d: Update @effect/schema. 334 | 335 | ## 0.12.5 336 | 337 | ### Patch Changes 338 | 339 | - 96de43d: Update effect, @effect/schema and @effect/match peer dependencies. 340 | 341 | ## 0.12.4 342 | 343 | ### Patch Changes 344 | 345 | - 65a4020: Update effect. 346 | 347 | ## 0.12.3 348 | 349 | ### Patch Changes 350 | 351 | - edf5b2c: Update effect. 352 | 353 | ## 0.12.2 354 | 355 | ### Patch Changes 356 | 357 | - f5e671d: Update effect. 358 | 359 | ## 0.12.1 360 | 361 | ### Patch Changes 362 | 363 | - 0de0d24: Update schema. 364 | 365 | ## 0.12.0 366 | 367 | ### Minor Changes 368 | 369 | - 27f19bf: Update effect + `Pg` export. 370 | 371 | ## 0.11.0 372 | 373 | ### Minor Changes 374 | 375 | - f32f5dd: Fix queryStream. 376 | 377 | ## 0.10.1 378 | 379 | ### Patch Changes 380 | 381 | - cd4ea01: Include @types/pg. 382 | 383 | ## 0.10.0 384 | 385 | ### Minor Changes 386 | 387 | - dcc00e2: Update API. 388 | - 0f60e2f: Update API. 389 | 390 | ## 0.9.0 391 | 392 | ### Minor Changes 393 | 394 | - ae60b4e: Update API, add `queryStream`. 395 | 396 | ### Patch Changes 397 | 398 | - 2b00c84: Update /io. 399 | 400 | ## 0.8.1 401 | 402 | ### Patch Changes 403 | 404 | - 923d764: Update /data and /io. 405 | 406 | ## 0.8.0 407 | 408 | ### Minor Changes 409 | 410 | - a627f29: Update effect dependencies. 411 | 412 | ## 0.7.0 413 | 414 | ### Minor Changes 415 | 416 | - a21de48: Update effect dependencies. 417 | 418 | ## 0.6.1 419 | 420 | ### Patch Changes 421 | 422 | - 58a939e: Update /match. 423 | 424 | ## 0.6.0 425 | 426 | ### Minor Changes 427 | 428 | - 61f1df3: Update /data, /io and /match. 429 | - f843b8b: Fix ESM. 430 | 431 | ## 0.5.1 432 | 433 | ### Patch Changes 434 | 435 | - c2caeca: Update dependecies. 436 | 437 | ## 0.5.0 438 | 439 | ### Minor Changes 440 | 441 | - 058f3ed: @effect/\* as peer dependencies 442 | - 657b164: Update dist. 443 | 444 | ## 0.4.0 445 | 446 | ### Minor Changes 447 | 448 | - 882deff: Update /io and /match. 449 | 450 | ## 0.3.0 451 | 452 | ### Minor Changes 453 | 454 | - c133507: Update /io and /match. 455 | 456 | ## 0.2.4 457 | 458 | ### Patch Changes 459 | 460 | - 2d7b87d: Update dependencies. 461 | - d914065: Update /io and /match. 462 | 463 | ## 0.2.3 464 | 465 | ### Patch Changes 466 | 467 | - 7726dcd: Update /data. 468 | - fdbb369: Update /data 469 | 470 | ## 0.2.2 471 | 472 | ### Patch Changes 473 | 474 | - bbc0506: Update /data and /io. 475 | 476 | ## 0.2.1 477 | 478 | ### Patch Changes 479 | 480 | - 2741e1d: Update /io. 481 | 482 | ## 0.2.0 483 | 484 | ### Minor Changes 485 | 486 | - eda5105: Update dependencies. 487 | 488 | ## 0.1.0 489 | 490 | ### Minor Changes 491 | 492 | - c31a4c6: Update dependencies. 493 | 494 | ## 0.0.23 495 | 496 | ### Patch Changes 497 | 498 | - 5be50ad: Update dependencies. 499 | 500 | ## 0.0.22 501 | 502 | ### Patch Changes 503 | 504 | - a9abbde: Update dependencies 505 | 506 | ## 0.0.21 507 | 508 | ### Patch Changes 509 | 510 | - 08afcc5: Update dependencies. 511 | 512 | ## 0.0.20 513 | 514 | ### Patch Changes 515 | 516 | - dd69b94: Update dependencies. 517 | 518 | ## 0.0.19 519 | 520 | ### Patch Changes 521 | 522 | - 45772d2: Update dependencies. 523 | 524 | ## 0.0.18 525 | 526 | ### Patch Changes 527 | 528 | - f260af5: Update dependencies. 529 | 530 | ## 0.0.17 531 | 532 | ### Patch Changes 533 | 534 | - bbdf472: Update /data. 535 | 536 | ## 0.0.16 537 | 538 | ### Patch Changes 539 | 540 | - a6874ce: Update dependencies. 541 | 542 | ## 0.0.15 543 | 544 | ### Patch Changes 545 | 546 | - 4868c5e: Update dependencies. 547 | 548 | ## 0.0.14 549 | 550 | ### Patch Changes 551 | 552 | - 8361319: Update dependencies 553 | 554 | ## 0.0.13 555 | 556 | ### Patch Changes 557 | 558 | - 4abc1de: Update dependencies 559 | 560 | ## 0.0.12 561 | 562 | ### Patch Changes 563 | 564 | - 7c66b5a: Update dependencies 565 | 566 | ## 0.0.11 567 | 568 | ### Patch Changes 569 | 570 | - 87a74dc: update dependencies 571 | 572 | ## 0.0.10 573 | 574 | ### Patch Changes 575 | 576 | - 6b301f3: Make poolLayer scoped, add logging. 577 | 578 | ## 0.0.9 579 | 580 | ### Patch Changes 581 | 582 | - 245fec6: update dependencies 583 | 584 | ## 0.0.8 585 | 586 | ### Patch Changes 587 | 588 | - a17466e: use /io and /data as direct dependencies 589 | - d6e4c2c: fix transaction 590 | 591 | ## 0.0.7 592 | 593 | ### Patch Changes 594 | 595 | - 3b3ef33: update dependencies 596 | 597 | ## 0.0.6 598 | 599 | ### Patch Changes 600 | 601 | - f80a2d9: simplify interface 602 | 603 | ## 0.0.5 604 | 605 | ### Patch Changes 606 | 607 | - Update peer deps 608 | 609 | ## 0.0.4 610 | 611 | ### Patch Changes 612 | 613 | - a199cc2: Export used underlaying pg types 614 | - d575a62: Remove unnecessary context combinators 615 | 616 | ## 0.0.3 617 | 618 | ### Patch Changes 619 | 620 | - change pool to layer 621 | 622 | ## 0.0.2 623 | 624 | ### Patch Changes 625 | 626 | - fix: add dist 627 | 628 | ## 0.0.1 629 | 630 | ### Patch Changes 631 | 632 | - 888d885: Initial release 633 | --------------------------------------------------------------------------------