├── .changeset └── config.json ├── .envrc ├── .github ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── build.yml │ ├── check.yml │ ├── pages.yml │ └── test.yml ├── .gitignore ├── .madgerc ├── .prettierignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── example-headers-openapi-ui.png ├── example-openapi-ui.png └── example-server-openapi-ui.png ├── docs ├── _config.yml └── index.md ├── eslint.config.mjs ├── flake.lock ├── flake.nix ├── package.json ├── packages ├── effect-http-node │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── docgen.json │ ├── examples │ │ ├── 100-299_success-channel.ts │ │ ├── bun-server.ts │ │ ├── conflict-error-example.ts │ │ ├── custom-response.ts │ │ ├── description.ts │ │ ├── example-server.ts │ │ ├── example.ts │ │ ├── form-data.ts │ │ ├── groups.ts │ │ ├── handle-raw.ts │ │ ├── headers-client.ts │ │ ├── headers.ts │ │ ├── input-example.ts │ │ ├── mock-client.ts │ │ ├── multiple-responses.ts │ │ ├── new-api.ts │ │ ├── no-content.ts │ │ ├── optional-query-parameter.ts │ │ ├── pattern-example.ts │ │ ├── plain-text.ts │ │ ├── readme-headers.ts │ │ ├── readme-quickstart.ts │ │ ├── readme-security-basic.ts │ │ ├── readme-security-complex.ts │ │ ├── readme-security-custom.ts │ │ ├── readme-security.ts │ │ ├── request-example.ts │ │ ├── request-validation-optional-parameter.ts │ │ ├── request-validation.ts │ │ ├── resource-example.ts │ │ ├── schema-with-optional-field.ts │ │ ├── scoped-resources.ts │ │ ├── static-files-from-root.ts │ │ └── unexpected-error.ts │ ├── package.json │ ├── src │ │ ├── NodeServer.ts │ │ ├── NodeSwaggerFiles.ts │ │ ├── NodeTesting.ts │ │ ├── index.ts │ │ └── internal │ │ │ ├── node-server.ts │ │ │ ├── node-swagger-files.ts │ │ │ └── node-testing.ts │ ├── test │ │ ├── client.test.ts │ │ ├── example-server.test.ts │ │ ├── examples.ts │ │ ├── handler.test.ts │ │ ├── router-builder.test.ts │ │ ├── security.test.ts │ │ ├── server.test.ts │ │ ├── swagger-router.test.ts │ │ └── testing-client.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.examples.json │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts └── effect-http │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── docgen.json │ ├── dtslint │ ├── Api.tst.ts │ ├── ApiEndpoint.tst.ts │ ├── ApiGroup.tst.ts │ ├── Handler.tst.ts │ └── tsconfig.json │ ├── package.json │ ├── src │ ├── Api.ts │ ├── ApiEndpoint.ts │ ├── ApiGroup.ts │ ├── ApiRequest.ts │ ├── ApiResponse.ts │ ├── ApiSchema.ts │ ├── Client.ts │ ├── ClientError.ts │ ├── ExampleServer.ts │ ├── Handler.ts │ ├── HttpError.ts │ ├── Middlewares.ts │ ├── MockClient.ts │ ├── OpenApi.ts │ ├── OpenApiTypes.ts │ ├── QuerySchema.ts │ ├── Representation.ts │ ├── RouterBuilder.ts │ ├── Security.ts │ ├── SwaggerRouter.ts │ ├── index.ts │ └── internal │ │ ├── api-endpoint.ts │ │ ├── api-group.ts │ │ ├── api-request.ts │ │ ├── api-response.ts │ │ ├── api-schema.ts │ │ ├── api.ts │ │ ├── circular.ts │ │ ├── client-error.ts │ │ ├── client.ts │ │ ├── clientRequestEncoder.ts │ │ ├── clientResponseParser.ts │ │ ├── example-compiler.ts │ │ ├── example-server.ts │ │ ├── handler.ts │ │ ├── http-error.ts │ │ ├── middlewares.ts │ │ ├── mock-client.ts │ │ ├── open-api.ts │ │ ├── query-schema.ts │ │ ├── representation.ts │ │ ├── router-builder.ts │ │ ├── security.ts │ │ ├── serverRequestParser.ts │ │ ├── serverResponseEncoder.ts │ │ ├── swagger-router.ts │ │ └── utils.ts │ ├── test │ ├── api.test.ts │ ├── client-error.test.ts │ ├── example-compiler.test.ts │ ├── examples.ts │ ├── mock-client.test.ts │ ├── openapi.test.ts │ ├── representation.test.ts │ └── security.test.ts │ ├── tsconfig.build.json │ ├── tsconfig.examples.json │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── patches └── @changesets__assemble-release-plan@6.0.4.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── scripts ├── clean.mjs └── docs.mjs ├── tsconfig.base.json ├── tsconfig.build.json ├── tsconfig.json ├── tstyche.config.json ├── vitest.config.ts ├── vitest.shared.ts └── vitest.workspace.ts /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "sukovanej/effect-http" }], 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [], 10 | "snapshot": { 11 | "useCalculatedVersion": false, 12 | "prereleaseTemplate": "{tag}-{commit}" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.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@v4 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 | -------------------------------------------------------------------------------- /.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 packages/*/src && git diff-index --cached HEAD --exit-code packages/*/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 | -------------------------------------------------------------------------------- /.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 | - run: pnpm test-types 26 | -------------------------------------------------------------------------------- /.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@v3 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 | -------------------------------------------------------------------------------- /.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 | node: 16 | name: Node 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 test 24 | 25 | bun: 26 | name: Bun 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Install dependencies 31 | uses: ./.github/actions/setup 32 | - name: Install bun 33 | uses: oven-sh/setup-bun@v1 34 | - run: bun vitest 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.tsbuildinfo 3 | node_modules/ 4 | .DS_Store 5 | tmp/ 6 | dist/ 7 | build/ 8 | docs/ 9 | .direnv/ 10 | .idea -------------------------------------------------------------------------------- /.madgerc: -------------------------------------------------------------------------------- 1 | { 2 | "detectiveOptions": { 3 | "ts": { 4 | "skipTypeImports": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.ts 3 | *.cjs 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "typescript.enablePromptUseWorkspaceTsdk": true, 5 | "editor.formatOnSave": true, 6 | "eslint.format.enable": true, 7 | "[json]": { 8 | "editor.defaultFormatter": "vscode.json-language-features" 9 | }, 10 | "[markdown]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 15 | }, 16 | "[javascriptreact]": { 17 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 21 | }, 22 | "[typescriptreact]": { 23 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 24 | }, 25 | "eslint.validate": ["markdown", "javascript", "typescript"], 26 | "editor.codeActionsOnSave": { 27 | "source.fixAll.eslint": "explicit" 28 | }, 29 | "editor.quickSuggestions": { 30 | "other": true, 31 | "comments": false, 32 | "strings": false 33 | }, 34 | "editor.acceptSuggestionOnCommitCharacter": true, 35 | "editor.acceptSuggestionOnEnter": "on", 36 | "editor.quickSuggestionsDelay": 10, 37 | "editor.suggestOnTriggerCharacters": true, 38 | "editor.tabCompletion": "off", 39 | "editor.suggest.localityBonus": true, 40 | "editor.suggestSelection": "recentlyUsed", 41 | "editor.wordBasedSuggestions": "matchingDocuments", 42 | "editor.parameterHints.enabled": true, 43 | "files.insertFinalNewline": true 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /assets/example-headers-openapi-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sukovanej/effect-http/f251f6cf73d0b8c81130226f9f71e597c56dfd7b/assets/example-headers-openapi-ui.png -------------------------------------------------------------------------------- /assets/example-openapi-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sukovanej/effect-http/f251f6cf73d0b8c81130226f9f71e597c56dfd7b/assets/example-openapi-ui.png -------------------------------------------------------------------------------- /assets/example-server-openapi-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sukovanej/effect-http/f251f6cf73d0b8c81130226f9f71e597c56dfd7b/assets/example-server-openapi-ui.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: mikearnaldi/just-the-docs 2 | search_enabled: true 3 | aux_links: 4 | "GitHub": 5 | - "//github.com/sukovanej/effect-http" 6 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | permalink: / 4 | nav_order: 1 5 | has_children: false 6 | has_toc: false 7 | --- 8 | 9 | ## Work In Progress 10 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupPluginRules } from "@eslint/compat" 2 | import { FlatCompat } from "@eslint/eslintrc" 3 | import js from "@eslint/js" 4 | import tsParser from "@typescript-eslint/parser" 5 | import codegen from "eslint-plugin-codegen" 6 | import _import from "eslint-plugin-import" 7 | import simpleImportSort from "eslint-plugin-simple-import-sort" 8 | import sortDestructureKeys from "eslint-plugin-sort-destructure-keys" 9 | import path from "node:path" 10 | import { fileURLToPath } from "node:url" 11 | 12 | const __filename = fileURLToPath(import.meta.url) 13 | const __dirname = path.dirname(__filename) 14 | const compat = new FlatCompat({ 15 | baseDirectory: __dirname, 16 | recommendedConfig: js.configs.recommended, 17 | allConfig: js.configs.all 18 | }) 19 | 20 | export default [ 21 | { 22 | ignores: ["**/dist", "**/build", "**/docs", "**/*.md"] 23 | }, 24 | ...compat.extends( 25 | "eslint:recommended", 26 | "plugin:@typescript-eslint/eslint-recommended", 27 | "plugin:@typescript-eslint/recommended", 28 | "plugin:@effect/recommended" 29 | ), 30 | { 31 | plugins: { 32 | import: fixupPluginRules(_import), 33 | "sort-destructure-keys": sortDestructureKeys, 34 | "simple-import-sort": simpleImportSort, 35 | codegen 36 | }, 37 | 38 | languageOptions: { 39 | parser: tsParser, 40 | ecmaVersion: 2018, 41 | sourceType: "module" 42 | }, 43 | 44 | settings: { 45 | "import/parsers": { 46 | "@typescript-eslint/parser": [".ts", ".tsx"] 47 | }, 48 | 49 | "import/resolver": { 50 | typescript: { 51 | alwaysTryTypes: true 52 | } 53 | } 54 | }, 55 | 56 | rules: { 57 | "codegen/codegen": "error", 58 | "no-fallthrough": "off", 59 | "no-irregular-whitespace": "off", 60 | "object-shorthand": "error", 61 | "prefer-destructuring": "off", 62 | "sort-imports": "off", 63 | 64 | "no-restricted-syntax": [ 65 | "error", 66 | { 67 | selector: "CallExpression[callee.property.name='push'] > SpreadElement.arguments", 68 | message: "Do not use spread arguments in Array.push" 69 | } 70 | ], 71 | 72 | "no-unused-vars": "off", 73 | "require-yield": "off", 74 | "prefer-rest-params": "off", 75 | "prefer-spread": "off", 76 | "import/first": "error", 77 | "import/newline-after-import": "error", 78 | "import/no-duplicates": "error", 79 | "import/no-unresolved": "off", 80 | "import/order": "off", 81 | "simple-import-sort/imports": "off", 82 | "sort-destructure-keys/sort-destructure-keys": "error", 83 | "deprecation/deprecation": "off", 84 | 85 | "@typescript-eslint/array-type": [ 86 | "warn", 87 | { 88 | default: "generic", 89 | readonly: "generic" 90 | } 91 | ], 92 | 93 | "@typescript-eslint/member-delimiter-style": 0, 94 | "@typescript-eslint/no-non-null-assertion": "off", 95 | "@typescript-eslint/ban-types": "off", 96 | "@typescript-eslint/no-explicit-any": "off", 97 | "@typescript-eslint/no-empty-interface": "off", 98 | "@typescript-eslint/consistent-type-imports": "warn", 99 | 100 | "@typescript-eslint/no-unused-vars": [ 101 | "error", 102 | { 103 | argsIgnorePattern: "^_", 104 | varsIgnorePattern: "^_" 105 | } 106 | ], 107 | 108 | "@typescript-eslint/ban-ts-comment": "off", 109 | "@typescript-eslint/camelcase": "off", 110 | "@typescript-eslint/explicit-function-return-type": "off", 111 | "@typescript-eslint/explicit-module-boundary-types": "off", 112 | "@typescript-eslint/interface-name-prefix": "off", 113 | "@typescript-eslint/no-array-constructor": "off", 114 | "@typescript-eslint/no-use-before-define": "off", 115 | "@typescript-eslint/no-namespace": "off", 116 | 117 | "@effect/dprint": [ 118 | "error", 119 | { 120 | config: { 121 | indentWidth: 2, 122 | lineWidth: 120, 123 | semiColons: "asi", 124 | quoteStyle: "alwaysDouble", 125 | trailingCommas: "never", 126 | operatorPosition: "maintain", 127 | "arrowFunction.useParentheses": "force" 128 | } 129 | } 130 | ] 131 | } 132 | }, 133 | { 134 | files: ["packages/*/src/**/*", "packages/*/test/**/*"], 135 | rules: { 136 | "no-console": "error" 137 | } 138 | } 139 | ] 140 | 141 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1701680307, 9 | "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1702938738, 24 | "narHash": "sha256-O7Vb0xC9s4Dmgxj8APEpuuMj7HsLgPbpy1UKvNVJp7o=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "dd8e82f3b4017b8faa52c2b1897a38d53c3c26cb", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs = { 4 | url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 | }; 6 | 7 | flake-utils = { 8 | url = "github:numtide/flake-utils"; 9 | }; 10 | }; 11 | 12 | outputs = { 13 | self, 14 | nixpkgs, 15 | flake-utils, 16 | ... 17 | }: 18 | flake-utils.lib.eachDefaultSystem (system: let 19 | pkgs = nixpkgs.legacyPackages.${system}; 20 | corepackEnable = pkgs.runCommand "corepack-enable" {} '' 21 | mkdir -p $out/bin 22 | ${pkgs.nodejs_21}/bin/corepack enable --install-directory $out/bin 23 | ''; 24 | in { 25 | formatter = pkgs.alejandra; 26 | 27 | devShells = { 28 | default = with pkgs; 29 | mkShell { 30 | buildInputs = [ 31 | corepackEnable 32 | bun 33 | nodejs_21 34 | ]; 35 | }; 36 | }; 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "packageManager": "pnpm@9.1.1", 5 | "scripts": { 6 | "clean": "node scripts/clean.mjs", 7 | "codegen": "pnpm --recursive --parallel run codegen", 8 | "build": "tsc -b tsconfig.build.json && pnpm --recursive --parallel run build", 9 | "circular": "madge --extensions ts --circular --no-color --no-spinner packages/*/src", 10 | "test": "vitest", 11 | "coverage": "vitest --coverage", 12 | "check": "tsc -b tsconfig.json", 13 | "check:watch": "tsc -b tsconfig.json -w", 14 | "check-recursive": "pnpm --recursive exec tsc -b tsconfig.json", 15 | "lint": "eslint \"**/{src,test,examples,scripts,dtslint}/**/*.{ts,mjs}\"", 16 | "lint-fix": "pnpm lint --fix", 17 | "docgen": "pnpm --recursive --parallel exec docgen && node scripts/docs.mjs", 18 | "test-types": "tstyche", 19 | "changeset-publish": "pnpm build && changeset publish" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.26.4", 23 | "@babel/core": "^7.26.9", 24 | "@babel/plugin-transform-export-namespace-from": "^7.25.9", 25 | "@babel/plugin-transform-modules-commonjs": "^7.26.3", 26 | "@changesets/changelog-github": "^0.5.0", 27 | "@changesets/cli": "^2.27.9", 28 | "@effect/build-utils": "^0.7.9", 29 | "@effect/docgen": "^0.5.2", 30 | "@effect/eslint-plugin": "^0.2.0", 31 | "@effect/language-service": "^0.2.0", 32 | "@effect/vitest": "^0.19.2", 33 | "@eslint/compat": "^1.1.1", 34 | "@eslint/eslintrc": "^3.1.0", 35 | "@eslint/js": "^9.9.1", 36 | "@types/node": "^22.13.8", 37 | "@types/swagger-ui-dist": "^3.30.5", 38 | "@typescript-eslint/eslint-plugin": "^8.27.0", 39 | "@typescript-eslint/parser": "^8.27.0", 40 | "@vitest/coverage-v8": "^3.0.7", 41 | "babel-plugin-annotate-pure-calls": "^0.5.0", 42 | "eslint": "^9.9.1", 43 | "eslint-import-resolver-typescript": "^3.6.3", 44 | "eslint-plugin-codegen": "^0.28.0", 45 | "eslint-plugin-import": "^2.30.0", 46 | "eslint-plugin-simple-import-sort": "^12.1.1", 47 | "eslint-plugin-sort-destructure-keys": "^2.0.0", 48 | "glob": "^11.0.1", 49 | "madge": "^8.0.0", 50 | "prettier": "^3.5.2", 51 | "rimraf": "^6.0.1", 52 | "tstyche": "^3.5.0", 53 | "tsx": "^4.19.3", 54 | "typescript": "^5.8.2", 55 | "vitest": "^3.0.7" 56 | }, 57 | "pnpm": { 58 | "updateConfig": { 59 | "ignoreDependencies": [ 60 | "eslint" 61 | ] 62 | }, 63 | "patchedDependencies": { 64 | "@changesets/assemble-release-plan@6.0.4": "patches/@changesets__assemble-release-plan@6.0.4.patch" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/effect-http-node/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 | -------------------------------------------------------------------------------- /packages/effect-http-node/README.md: -------------------------------------------------------------------------------- 1 | # effect-http-node 2 | 3 | This is the node-specific package. Please refer to the main readme. 4 | -------------------------------------------------------------------------------- /packages/effect-http-node/docgen.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@effect/docgen/schema.json", 3 | "exclude": [ 4 | "src/internal/**/*.ts" 5 | ], 6 | "examplesCompilerOptions": { 7 | "noEmit": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "Bundler", 11 | "module": "ES2022", 12 | "target": "ES2022", 13 | "lib": [ 14 | "ES2022", 15 | "DOM" 16 | ], 17 | "paths": { 18 | "effect-http": [ 19 | "../../../effect-http/src/index.js" 20 | ], 21 | "effect-http/*": [ 22 | "../../../effect-http/src/*.js" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/100-299_success-channel.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Schema } from "effect" 2 | import { Api, ApiResponse, Client } from "effect-http" 3 | 4 | export const api = Api.make().pipe( 5 | Api.addEndpoint( 6 | Api.post("test", "/test").pipe( 7 | Api.setResponseBody(Schema.String), 8 | Api.addResponse(ApiResponse.make(400)) 9 | ) 10 | ) 11 | ) 12 | 13 | const client = Client.make(api) 14 | 15 | const log200 = (body: string) => Effect.log(body) 16 | 17 | Effect.tap(client.test({}), log200) 18 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/bun-server.ts: -------------------------------------------------------------------------------- 1 | import { HttpServer } from "@effect/platform" 2 | import { BunContext, BunHttpServer, BunRuntime } from "@effect/platform-bun" 3 | import { Effect, Layer, Logger, pipe, Schema } from "effect" 4 | import { Api, Handler, RouterBuilder } from "effect-http" 5 | import { NodeSwaggerFiles } from "effect-http-node" 6 | 7 | const Response = Schema.Struct({ 8 | name: Schema.String, 9 | id: pipe(Schema.Number, Schema.int(), Schema.positive()) 10 | }) 11 | const Query = Schema.Struct({ id: Schema.NumberFromString }) 12 | 13 | const getUserEndpoint = Api.get("getUser", "/user").pipe( 14 | Api.setResponseBody(Response), 15 | Api.setRequestQuery(Query) 16 | ) 17 | 18 | const getUserHandler = Handler.make(getUserEndpoint, ({ query }) => Effect.succeed({ name: "milan", id: query.id })) 19 | 20 | const api = pipe( 21 | Api.make({ title: "Users API" }), 22 | Api.addEndpoint(getUserEndpoint) 23 | ) 24 | 25 | const app = pipe( 26 | RouterBuilder.make(api), 27 | RouterBuilder.handle(getUserHandler), 28 | RouterBuilder.build 29 | ) 30 | 31 | const server = pipe( 32 | HttpServer.serve(app), 33 | HttpServer.withLogAddress, 34 | Layer.provide(NodeSwaggerFiles.SwaggerFilesLive), 35 | Layer.provide(BunHttpServer.layer({ port: 3000 })), 36 | Layer.provide(BunContext.layer), 37 | Layer.provide(Logger.pretty), 38 | Layer.launch 39 | ) 40 | 41 | BunRuntime.runMain(server) 42 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/conflict-error-example.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, pipe, Schema } from "effect" 3 | import { Api, Handler, HttpError, Middlewares, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | class UserRepository extends Effect.Tag("UserRepository") Effect.Effect 9 | storeUser: (user: string) => Effect.Effect 10 | }>() { 11 | static dummy = this.of({ 12 | userExistsByName: () => Effect.succeed(true), 13 | storeUser: () => Effect.void 14 | }) 15 | } 16 | 17 | const storeUserEndpoint = Api.post("storeUser", "/users").pipe( 18 | Api.setResponseBody(Schema.String), 19 | Api.setRequestBody(Schema.Struct({ name: Schema.String })) 20 | ) 21 | 22 | const api = pipe( 23 | Api.make({ title: "Users API" }), 24 | Api.addEndpoint(storeUserEndpoint) 25 | ) 26 | 27 | const storeUserHandler = Handler.make(storeUserEndpoint, ({ body }) => 28 | Effect.gen(function*(_) { 29 | const userRepository = yield* UserRepository 30 | 31 | if (yield* userRepository.userExistsByName(body.name)) { 32 | return yield* HttpError.conflict(`User "${body.name}" already exists.`) 33 | } 34 | 35 | yield* userRepository.storeUser(body.name) 36 | return `User "${body.name}" stored.` 37 | })) 38 | 39 | const app = RouterBuilder.make(api).pipe( 40 | RouterBuilder.handle(storeUserHandler), 41 | RouterBuilder.build, 42 | Middlewares.errorLog 43 | ) 44 | 45 | app.pipe( 46 | NodeServer.listen({ port: 3000 }), 47 | Effect.provideService(UserRepository, UserRepository.dummy), 48 | Effect.provide(Logger.pretty), 49 | NodeRuntime.runMain 50 | ) 51 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/custom-response.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const api = Api.make().pipe( 8 | Api.addEndpoint( 9 | Api.get("hello", "/hello").pipe( 10 | Api.setResponseStatus(201), 11 | Api.setResponseBody(Schema.Number), 12 | Api.setResponseHeaders(Schema.Struct({ "x-hello-world": Schema.String })) 13 | ) 14 | ) 15 | ) 16 | 17 | const app = pipe( 18 | RouterBuilder.make(api), 19 | RouterBuilder.handle( 20 | "hello", 21 | () => Effect.succeed({ body: 12, headers: { "x-hello-world": "test" }, status: 201 as const }) 22 | ), 23 | RouterBuilder.build 24 | ) 25 | 26 | pipe( 27 | app, 28 | NodeServer.listen({ port: 3000 }), 29 | Effect.provide(Logger.pretty), 30 | Logger.withMinimumLogLevel(LogLevel.All), 31 | NodeRuntime.runMain 32 | ) 33 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/description.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const Response = pipe( 7 | Schema.Struct({ 8 | name: Schema.String, 9 | id: pipe(Schema.Number, Schema.int(), Schema.positive()) 10 | }), 11 | Schema.annotations({ description: "User" }) 12 | ) 13 | const Query = Schema.Struct({ 14 | id: pipe(Schema.NumberFromString, Schema.annotations({ description: "User id" })) 15 | }) 16 | 17 | const api = pipe( 18 | Api.make({ title: "Users API" }), 19 | Api.addEndpoint( 20 | Api.get("getUser", "/user", { description: "Returns a User by id" }).pipe( 21 | Api.setResponseBody(Response), 22 | Api.setRequestQuery(Query) 23 | ) 24 | ) 25 | ) 26 | 27 | const app = pipe( 28 | RouterBuilder.make(api), 29 | RouterBuilder.handle("getUser", ({ query }) => Effect.succeed({ name: "mike", id: query.id })), 30 | RouterBuilder.build 31 | ) 32 | 33 | pipe( 34 | app, 35 | NodeServer.listen({ port: 3000 }), 36 | Effect.provide(Logger.pretty), 37 | Logger.withMinimumLogLevel(LogLevel.All), 38 | NodeRuntime.runMain 39 | ) 40 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/example-server.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, ExampleServer, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const Response = Schema.Struct({ 7 | name: Schema.String, 8 | value: Schema.Number 9 | }) 10 | 11 | const api = pipe( 12 | Api.make({ servers: ["http://localhost:3000", { description: "hello", url: "/api/" }] }), 13 | Api.addEndpoint( 14 | Api.get("test", "/test").pipe(Api.setResponseBody(Response)) 15 | ) 16 | ) 17 | 18 | pipe( 19 | ExampleServer.make(api), 20 | RouterBuilder.build, 21 | NodeServer.listen({ port: 3000 }), 22 | Effect.provide(Logger.pretty), 23 | Logger.withMinimumLogLevel(LogLevel.All), 24 | NodeRuntime.runMain 25 | ) 26 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/example.ts: -------------------------------------------------------------------------------- 1 | import { Effect, Layer, Logger, LogLevel, Schema } from "effect" 2 | import { Api, Handler, Middlewares, RouterBuilder, Security } from "effect-http" 3 | 4 | import { NodeRuntime } from "@effect/platform-node" 5 | import { NodeServer } from "effect-http-node" 6 | 7 | class StuffService extends Effect.Tag("@services/StuffService")() { 10 | static dummy = Layer.succeed(this, this.of({ value: 42 })) 11 | } 12 | 13 | const HumanSchema = Schema.Struct({ 14 | height: Schema.Number, 15 | name: Schema.String 16 | }) 17 | const Lesnek = Schema.Struct({ name: Schema.String }) 18 | const Standa = Schema.Record({ 19 | key: Schema.String, 20 | value: Schema.Union(Schema.String, Schema.Number) 21 | }) 22 | 23 | const getLesnekEndpoint = Api.get("getLesnek", "/lesnek").pipe( 24 | Api.setResponseBody(Schema.String), 25 | Api.setRequestQuery(Lesnek), 26 | Api.setSecurity(Security.bearer({ name: "myAwesomeBearerAuth", bearerFormat: "JWT" })) 27 | ) 28 | const getMilanEndpoint = Api.get("getMilan", "/milan").pipe(Api.setResponseBody(Schema.String)) 29 | const testEndpoint = Api.get("test", "/test").pipe(Api.setResponseBody(Standa), Api.setRequestQuery(Lesnek)) 30 | const standaEndpoint = Api.post("standa", "/standa").pipe(Api.setResponseBody(Standa), Api.setRequestBody(Standa)) 31 | const handleMilanEndpoint = Api.post("handleMilan", "/petr").pipe( 32 | Api.setResponseBody(HumanSchema), 33 | Api.setRequestBody(HumanSchema) 34 | ) 35 | const callStandaEndpoint = Api.put("callStanda", "/api/zdar").pipe( 36 | Api.setResponseBody(Schema.String), 37 | Api.setRequestBody(Schema.Struct({ zdar: Schema.Literal("zdar") })) 38 | ) 39 | 40 | const api = Api.make({ title: "My awesome pets API", version: "1.0.0" }).pipe( 41 | Api.addEndpoint(getLesnekEndpoint), 42 | Api.addEndpoint(getMilanEndpoint), 43 | Api.addEndpoint(testEndpoint), 44 | Api.addEndpoint(standaEndpoint), 45 | Api.addEndpoint(handleMilanEndpoint), 46 | Api.addEndpoint(callStandaEndpoint) 47 | ) 48 | 49 | const getLesnekHandler = Handler.make(getLesnekEndpoint, ({ query }) => 50 | Effect.succeed(`hello ${query.name}`).pipe( 51 | Effect.tap(() => Effect.logDebug("hello world")) 52 | )) 53 | const handleMilanHandler = Handler.make(handleMilanEndpoint, ({ body }) => 54 | Effect.map(StuffService, ({ value }) => ({ 55 | ...body, 56 | randomValue: body.height + value 57 | }))) 58 | const getMilanHandler = Handler.make(getMilanEndpoint, () => Effect.succeed("Milan")) 59 | const testHandler = Handler.make(testEndpoint, ({ query }) => Effect.succeed(query)) 60 | const standaHandler = Handler.make(standaEndpoint, ({ body }) => Effect.succeed(body)) 61 | const callStandaHandler = Handler.make(callStandaEndpoint, ({ body }) => Effect.succeed(body.zdar)) 62 | 63 | const app = RouterBuilder.make(api, { parseOptions: { errors: "all" } }).pipe( 64 | RouterBuilder.handle(getLesnekHandler), 65 | RouterBuilder.handle(handleMilanHandler), 66 | RouterBuilder.handle(getMilanHandler), 67 | RouterBuilder.handle(testHandler), 68 | RouterBuilder.handle(standaHandler), 69 | RouterBuilder.handle(callStandaHandler), 70 | RouterBuilder.build 71 | ) 72 | 73 | app.pipe( 74 | Middlewares.accessLog(LogLevel.Debug), 75 | NodeServer.listen({ port: 4000 }), 76 | Effect.provide(StuffService.dummy), 77 | Effect.provide(Logger.pretty), 78 | Logger.withMinimumLogLevel(LogLevel.All), 79 | NodeRuntime.runMain 80 | ) 81 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/form-data.ts: -------------------------------------------------------------------------------- 1 | import { NodeContext, NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, HttpError, Representation, RouterBuilder } from "effect-http" 4 | 5 | import type { Multipart } from "@effect/platform" 6 | import { FileSystem, HttpServerRequest } from "@effect/platform" 7 | import { NodeServer } from "effect-http-node" 8 | 9 | const api = pipe( 10 | Api.make(), 11 | Api.addEndpoint( 12 | Api.post("upload", "/upload").pipe( 13 | Api.setRequestBody(Api.FormData), 14 | Api.setResponseBody(Schema.String), 15 | Api.setResponseRepresentations([Representation.plainText]) 16 | ) 17 | ) 18 | ) 19 | 20 | const app = pipe( 21 | RouterBuilder.make(api), 22 | RouterBuilder.handle("upload", () => 23 | Effect.gen(function*() { 24 | const request = yield* HttpServerRequest.HttpServerRequest 25 | const formData = yield* request.multipart 26 | 27 | const file = formData["file"] 28 | 29 | if (typeof file === "string") { 30 | return yield* HttpError.badRequest("File not found") 31 | } 32 | 33 | const fs = yield* FileSystem.FileSystem 34 | return yield* fs.readFileString((file[0] as Multipart.PersistedFile).path) 35 | }).pipe(Effect.scoped)), 36 | RouterBuilder.build 37 | ) 38 | 39 | pipe( 40 | app, 41 | NodeServer.listen({ port: 3000 }), 42 | Effect.provide(Logger.pretty), 43 | Logger.withMinimumLogLevel(LogLevel.All), 44 | Effect.provide(NodeContext.layer), 45 | NodeRuntime.runMain 46 | ) 47 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/groups.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, ApiGroup, ExampleServer, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const Response = Schema.Struct({ name: Schema.String }) 8 | 9 | const testApi = pipe( 10 | ApiGroup.make("test", { 11 | description: "Test description", 12 | externalDocs: { 13 | description: "Test external doc", 14 | url: "https://www.google.com/search?q=effect-http" 15 | } 16 | }), 17 | ApiGroup.addEndpoint( 18 | ApiGroup.get("test", "/test").pipe(Api.setResponseBody(Response)) 19 | ) 20 | ) 21 | 22 | const userApi = pipe( 23 | ApiGroup.make("Users", { 24 | description: "All about users", 25 | externalDocs: { 26 | url: "https://www.google.com/search?q=effect-http" 27 | } 28 | }), 29 | ApiGroup.addEndpoint( 30 | ApiGroup.get("getUser", "/user").pipe(Api.setResponseBody(Response)) 31 | ), 32 | ApiGroup.addEndpoint( 33 | ApiGroup.post("storeUser", "/user").pipe(Api.setResponseBody(Response)) 34 | ), 35 | ApiGroup.addEndpoint( 36 | ApiGroup.put("updateUser", "/user").pipe(Api.setResponseBody(Response)) 37 | ), 38 | ApiGroup.addEndpoint( 39 | ApiGroup.delete("deleteUser", "/user").pipe(Api.setResponseBody(Response)) 40 | ) 41 | ) 42 | 43 | const categoriesApi = ApiGroup.make("Categories").pipe( 44 | ApiGroup.addEndpoint( 45 | ApiGroup.get("getCategory", "/category").pipe(Api.setResponseBody(Response)) 46 | ), 47 | ApiGroup.addEndpoint( 48 | ApiGroup.post("storeCategory", "/category").pipe(Api.setResponseBody(Response)) 49 | ), 50 | ApiGroup.addEndpoint( 51 | ApiGroup.put("updateCategory", "/category").pipe(Api.setResponseBody(Response)) 52 | ), 53 | ApiGroup.addEndpoint( 54 | ApiGroup.delete("deleteCategory", "/category").pipe(Api.setResponseBody(Response)) 55 | ) 56 | ) 57 | 58 | const api = Api.make().pipe( 59 | Api.addGroup(testApi), 60 | Api.addGroup(userApi), 61 | Api.addGroup(categoriesApi) 62 | ) 63 | 64 | ExampleServer.make(api).pipe( 65 | RouterBuilder.build, 66 | NodeServer.listen({ port: 3000 }), 67 | Effect.provide(Logger.pretty), 68 | Logger.withMinimumLogLevel(LogLevel.All), 69 | NodeRuntime.runMain 70 | ) 71 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/handle-raw.ts: -------------------------------------------------------------------------------- 1 | import { Headers, HttpServerResponse } from "@effect/platform" 2 | import { NodeRuntime } from "@effect/platform-node" 3 | import { Effect, Logger, Schema } from "effect" 4 | import { Api, RouterBuilder } from "effect-http" 5 | import { NodeServer } from "effect-http-node" 6 | 7 | export const api = Api.make({ title: "Example API" }).pipe( 8 | Api.addEndpoint( 9 | Api.get("root", "/").pipe( 10 | Api.setResponseBody(Schema.String), 11 | Api.setResponseHeaders(Schema.Struct({ "Content-Type": Schema.String })) 12 | ) 13 | ) 14 | ) 15 | 16 | export const app = RouterBuilder.make(api).pipe( 17 | RouterBuilder.handleRaw( 18 | "root", 19 | HttpServerResponse.text("Hello World!", { 20 | status: 200 as const, 21 | headers: Headers.fromInput({ "content-type": "text/plain" }) 22 | }) 23 | ), 24 | RouterBuilder.build 25 | ) 26 | 27 | const program = app.pipe( 28 | NodeServer.listen({ port: 3000 }), 29 | Effect.provide(Logger.pretty) 30 | ) 31 | 32 | NodeRuntime.runMain(program) 33 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/headers-client.ts: -------------------------------------------------------------------------------- 1 | import { Array, Effect, pipe, Schema } from "effect" 2 | import { Api, Client } from "effect-http" 3 | 4 | // Example client triggering the API from `examples/headers.ts` 5 | // Running the script call the `/hello` endpoint 1000k times 6 | 7 | export const api = pipe( 8 | Api.make(), 9 | Api.addEndpoint( 10 | Api.post("hello", "/hello").pipe( 11 | Api.setResponseBody(Schema.String), 12 | Api.setRequestBody(Schema.Struct({ value: Schema.Number })), 13 | Api.setRequestHeaders(Schema.Struct({ "x-client-id": Schema.String })) 14 | ) 15 | ) 16 | ) 17 | 18 | const client = Client.make(api, { baseUrl: "http://localhost:3000" }) 19 | 20 | Effect.all( 21 | client.hello({ body: { value: 1 }, headers: { "x-client-id": "abc" } }).pipe( 22 | Effect.flatMap((r) => Effect.logInfo(`Success ${r}`)), 23 | Effect.catchAll((e) => Effect.logInfo(`Error ${JSON.stringify(e)}`)), 24 | Array.replicate(1000000) 25 | ) 26 | ).pipe(Effect.runFork) 27 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/headers.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Array, Context, Effect, Logger, LogLevel, pipe, Ref, Schema } from "effect" 3 | import { Api, HttpError, Middlewares, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | interface Clients { 8 | hasAccess: (clientId: string) => Effect.Effect 9 | getRemainingUsage: (clientId: string) => Effect.Effect 10 | recordUsage: (aipKey: string) => Effect.Effect 11 | } 12 | 13 | const ClientsService = Context.GenericTag("@services/ClientsService") 14 | 15 | type ClientUsage = { 16 | clientId: string 17 | timestamp: number 18 | } 19 | 20 | const RATE_WINDOW = 1000 * 30 // 30s 21 | const ALLOWED_USAGES_PER_WINDOW = 5 22 | 23 | const clients = ClientsService.of({ 24 | hasAccess: (clientId) => Effect.succeed(clientId === "abc"), 25 | getRemainingUsage: (clientId) => 26 | pipe( 27 | Effect.all( 28 | [ 29 | Effect.flatMap(UsagesService, Ref.get), 30 | Effect.clockWith((clock) => clock.currentTimeMillis) 31 | ] as const 32 | ), 33 | Effect.map(([usages, timestamp]) => 34 | pipe( 35 | usages, 36 | Array.filter( 37 | (usage) => 38 | usage.clientId === clientId && 39 | usage.timestamp > timestamp - RATE_WINDOW 40 | ), 41 | Array.length, 42 | (usagesPerWindow) => ALLOWED_USAGES_PER_WINDOW - usagesPerWindow 43 | ) 44 | ) 45 | ), 46 | recordUsage: (clientId) => 47 | pipe( 48 | Effect.all( 49 | [ 50 | UsagesService, 51 | Effect.clockWith((clock) => clock.currentTimeMillis) 52 | ] as const 53 | ), 54 | Effect.flatMap(([usages, timestamp]) => Ref.update(usages, (usages) => [...usages, { clientId, timestamp }])) 55 | ) 56 | }) 57 | 58 | type Usages = Ref.Ref> 59 | 60 | const UsagesService = Context.GenericTag("@services/UsagesService") 61 | 62 | export const api = pipe( 63 | Api.make(), 64 | Api.addEndpoint( 65 | Api.post("hello", "/hello").pipe( 66 | Api.setResponseBody(Schema.String), 67 | Api.setRequestBody(Schema.Struct({ value: Schema.Number })), 68 | Api.setRequestHeaders(Schema.Struct({ "x-client-id": Schema.String })) 69 | ) 70 | ) 71 | ) 72 | 73 | const app = pipe( 74 | RouterBuilder.make(api), 75 | RouterBuilder.handle("hello", ({ headers: { "x-client-id": clientId } }) => 76 | pipe( 77 | Effect.filterOrFail( 78 | Effect.flatMap(ClientsService, (clients) => clients.hasAccess(clientId)), 79 | (hasAccess) => hasAccess, 80 | () => HttpError.unauthorized("Wrong api key") 81 | ), 82 | Effect.flatMap(() => Effect.flatMap(ClientsService, (client) => client.getRemainingUsage(clientId))), 83 | Effect.tap((remainingUsages) => Effect.log(`Remaining ${remainingUsages} usages.`)), 84 | Effect.filterOrFail( 85 | (remainingUsages) => remainingUsages > 0, 86 | () => HttpError.tooManyRequests("Rate limit exceeded") 87 | ), 88 | Effect.flatMap(() => Effect.flatMap(ClientsService, (client) => client.recordUsage(clientId))), 89 | Effect.as("hello there") 90 | )), 91 | RouterBuilder.build, 92 | Middlewares.errorLog, 93 | Middlewares.accessLog(LogLevel.Debug) 94 | ) 95 | 96 | pipe( 97 | app, 98 | NodeServer.listen({ port: 3000 }), 99 | Effect.provideService(ClientsService, clients), 100 | Effect.provideServiceEffect(UsagesService, Ref.make([] as Array)), 101 | Effect.provide(Logger.pretty), 102 | Logger.withMinimumLogLevel(LogLevel.All), 103 | NodeRuntime.runMain 104 | ) 105 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/input-example.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, HttpError, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const api = pipe( 8 | Api.make({ title: "My api" }), 9 | Api.addEndpoint( 10 | Api.get("stuff", "/stuff").pipe( 11 | Api.setResponseBody(Schema.String), 12 | Api.setRequestQuery(Schema.Struct({ value: Schema.String })) 13 | ) 14 | ) 15 | ) 16 | 17 | const stuffHandler = RouterBuilder.handler(api, "stuff", ({ query }) => 18 | pipe( 19 | Effect.fail(HttpError.notFound("I didnt find it")), 20 | Effect.tap(() => Effect.log(`Received ${query.value}`)) 21 | )) 22 | 23 | const app = pipe( 24 | RouterBuilder.make(api), 25 | RouterBuilder.handle(stuffHandler), 26 | RouterBuilder.build 27 | ) 28 | 29 | pipe( 30 | app, 31 | NodeServer.listen({ port: 3000 }), 32 | Effect.provide(Logger.pretty), 33 | Logger.withMinimumLogLevel(LogLevel.All), 34 | NodeRuntime.runMain 35 | ) 36 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/mock-client.ts: -------------------------------------------------------------------------------- 1 | import { Effect, pipe, Schema } from "effect" 2 | import { Api, MockClient } from "effect-http" 3 | 4 | export const exampleApiGet = Api.make().pipe( 5 | Api.addEndpoint( 6 | Api.get("getValue", "/get-value").pipe( 7 | Api.setResponseBody(Schema.Number) 8 | ) 9 | ) 10 | ) 11 | 12 | const client = MockClient.make(exampleApiGet) 13 | 14 | const program = pipe( 15 | client.getValue({}), 16 | Effect.tap(Effect.log) 17 | ) 18 | 19 | Effect.runFork(program) 20 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/multiple-responses.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Random, Schema } from "effect" 3 | import { Api, ApiResponse, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const helloEndpoint = Api.post("hello", "/hello").pipe( 7 | Api.setResponseBody(Schema.Number), 8 | Api.setResponseHeaders(Schema.Struct({ 9 | "my-header": pipe( 10 | Schema.NumberFromString, 11 | Schema.annotations({ description: "My header" }) 12 | ) 13 | })), 14 | Api.addResponse(ApiResponse.make(201, Schema.Number)), 15 | Api.addResponse({ status: 204, headers: Schema.Struct({ "x-another": Schema.NumberFromString }) }) 16 | ) 17 | 18 | const api = pipe( 19 | Api.make(), 20 | Api.addEndpoint(helloEndpoint) 21 | ) 22 | 23 | const randomChoice = >(...values: A) => 24 | Random.nextIntBetween(0, values.length).pipe(Effect.map((i) => values[i])) 25 | 26 | const app = pipe( 27 | RouterBuilder.make(api), 28 | RouterBuilder.handle("hello", () => 29 | randomChoice( 30 | { status: 200, body: 12, headers: { "my-header": 69 } }, 31 | { status: 201, body: 12 }, 32 | { status: 204, headers: { "x-another": 12 } } 33 | )), 34 | RouterBuilder.build 35 | ) 36 | 37 | pipe( 38 | app, 39 | NodeServer.listen({ port: 3000 }), 40 | Effect.provide(Logger.pretty), 41 | Logger.withMinimumLogLevel(LogLevel.All), 42 | NodeRuntime.runMain 43 | ) 44 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/new-api.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-object-type */ 2 | import { pipe, Schema } from "effect" 3 | import { Api, ApiGroup, ApiResponse, Security } from "effect-http" 4 | 5 | interface MyRequirement {} 6 | interface AnotherDep {} 7 | 8 | const schema1: Schema.Schema = Schema.NumberFromString 9 | const schema2: Schema.Schema = Schema.NumberFromString 10 | 11 | class MyRequest extends Schema.Class("Schema1")({ 12 | name: schema1 13 | }) {} 14 | 15 | class TestPathParams extends Schema.Class("TestPathParams")({ 16 | path: schema2 17 | }) {} 18 | 19 | const group1 = pipe( 20 | ApiGroup.make("my group"), 21 | ApiGroup.addEndpoint( 22 | pipe( 23 | ApiGroup.get("groupTest", "/test", { description: "test description" }), 24 | ApiGroup.setRequestBody(MyRequest), 25 | ApiGroup.setRequestPath(TestPathParams) 26 | ) 27 | ) 28 | ) 29 | 30 | const test = pipe( 31 | Api.get("test", "/test"), 32 | Api.setRequestBody(MyRequest), 33 | Api.setRequestPath(TestPathParams), 34 | Api.setSecurity( 35 | Security.bearer({ name: "mySecurity", description: "test" }) 36 | ), 37 | Api.setResponseBody(MyRequest), 38 | Api.addResponse(ApiResponse.make(201, TestPathParams, MyRequest)) 39 | ) 40 | 41 | export const api = pipe( 42 | Api.make({ title: "my api" }), 43 | Api.addEndpoint(test), 44 | Api.addEndpoint( 45 | pipe( 46 | Api.get("another", "/hello-there"), 47 | Api.setRequestBody(MyRequest), 48 | Api.setRequestPath(TestPathParams) 49 | ) 50 | ), 51 | Api.addGroup(group1) 52 | ) 53 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/no-content.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Schema } from "effect" 3 | import { Api, ExampleServer, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | export const api = Api.make().pipe( 7 | Api.addEndpoint( 8 | Api.post("test", "/test").pipe(Api.setRequestBody(Schema.String)) 9 | ) 10 | ) 11 | 12 | const app = ExampleServer.make(api).pipe( 13 | RouterBuilder.build 14 | ) 15 | 16 | app.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain) 17 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/optional-query-parameter.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const SchemaBooleanFromString = Schema.transformLiterals(["true", true], ["false", false]) 8 | 9 | export const api = pipe( 10 | Api.make(), 11 | Api.addEndpoint( 12 | Api.get("userById", "/api/users/:userId").pipe( 13 | Api.setResponseBody(Schema.Struct({ name: Schema.String })), 14 | Api.setRequestPath(Schema.Struct({ userId: Schema.String })), 15 | Api.setRequestQuery( 16 | Schema.Struct({ include_deleted: Schema.optional(SchemaBooleanFromString) }) 17 | ), 18 | Api.setRequestHeaders(Schema.Struct({ authorization: Schema.String })) 19 | ) 20 | ) 21 | ) 22 | 23 | const app = pipe( 24 | RouterBuilder.make(api), 25 | RouterBuilder.handle("userById", ({ query: { include_deleted } }) => 26 | Effect.succeed({ 27 | name: `include_deleted = ${include_deleted ?? "[not set]"}` 28 | })), 29 | RouterBuilder.build 30 | ) 31 | 32 | app.pipe( 33 | NodeServer.listen({ port: 3000 }), 34 | Effect.provide(Logger.pretty), 35 | Logger.withMinimumLogLevel(LogLevel.All), 36 | NodeRuntime.runMain 37 | ) 38 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/pattern-example.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const api = pipe( 8 | Api.make({ title: "My awesome pets API", version: "1.0.0" }), 9 | Api.addEndpoint( 10 | Api.get("test", "/test").pipe( 11 | Api.setResponseBody(Schema.String), 12 | Api.setRequestQuery(Schema.Struct({ value: Schema.String })) 13 | ) 14 | ) 15 | ) 16 | 17 | const app = pipe( 18 | RouterBuilder.make(api), 19 | RouterBuilder.handle("test", ({ query }) => Effect.succeed(`test ${query.value}`)), 20 | RouterBuilder.build 21 | ) 22 | 23 | pipe( 24 | app, 25 | NodeServer.listen({ port: 4000 }), 26 | Effect.provide(Logger.pretty), 27 | Logger.withMinimumLogLevel(LogLevel.All), 28 | NodeRuntime.runMain 29 | ) 30 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/plain-text.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, Schema } from "effect" 3 | import { Api, Representation, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | export const api = Api.make({ title: "Example API" }).pipe( 7 | Api.addEndpoint( 8 | Api.get("root", "/").pipe( 9 | Api.setResponseBody(Schema.Unknown), 10 | Api.setResponseRepresentations([Representation.plainText, Representation.json]) 11 | ) 12 | ) 13 | ) 14 | 15 | export const app = RouterBuilder.make(api).pipe( 16 | RouterBuilder.handle("root", () => Effect.succeed({ content: { hello: "world" }, status: 200 as const })), 17 | RouterBuilder.build 18 | ) 19 | 20 | app.pipe( 21 | NodeServer.listen({ port: 3000 }), 22 | Effect.provide(Logger.pretty), 23 | NodeRuntime.runMain 24 | ) 25 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-headers.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { pipe, Schema } from "effect" 3 | import { Api, ExampleServer, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const api = Api.make().pipe( 7 | Api.addEndpoint( 8 | Api.get("hello", "/hello").pipe( 9 | Api.setResponseBody(Schema.String), 10 | Api.setRequestHeaders(Schema.Struct({ "x-client-id": Schema.String })) 11 | ) 12 | ) 13 | ) 14 | 15 | pipe( 16 | ExampleServer.make(api), 17 | RouterBuilder.build, 18 | NodeServer.listen({ port: 3000 }), 19 | NodeRuntime.runMain 20 | ) 21 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-quickstart.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, pipe, Schema } from "effect" 3 | import { Api, Client, Handler, QuerySchema, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const UserResponse = Schema.Struct({ 7 | name: Schema.String, 8 | id: Schema.Int.pipe(Schema.positive()) 9 | }) 10 | const GetUserQuery = Schema.Struct({ id: QuerySchema.Number }) 11 | 12 | const getUserEndpoint = Api.get("getUser", "/user").pipe( 13 | Api.setResponseBody(UserResponse), 14 | Api.setRequestQuery(GetUserQuery) 15 | ) 16 | 17 | const getUserHandler = Handler.make( 18 | getUserEndpoint, 19 | ({ query }) => Effect.succeed({ name: "milan", id: query.id }) 20 | ) 21 | 22 | const api = Api.make({ title: "Users API" }).pipe( 23 | Api.addEndpoint(getUserEndpoint) 24 | ) 25 | 26 | const app = pipe( 27 | RouterBuilder.make(api), 28 | RouterBuilder.handle(getUserHandler), 29 | RouterBuilder.build 30 | ) 31 | 32 | app.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain) 33 | 34 | // Another file 35 | 36 | const client = Client.make(api, { baseUrl: "http://localhost:3000" }) 37 | 38 | const program = pipe( 39 | client.getUser({ query: { id: 12 } }), 40 | Effect.flatMap((user) => Effect.log(`Got ${user.name}, nice!`)) 41 | ) 42 | 43 | Effect.runFork(program) 44 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-security-basic.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Schema } from "effect" 3 | import { Api, RouterBuilder, Security } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const api = Api.make().pipe( 7 | Api.addEndpoint( 8 | Api.post("mySecuredEndpoint", "/my-secured-endpoint").pipe( 9 | Api.setResponseBody(Schema.String), 10 | Api.setSecurity(Security.basic()) 11 | ) 12 | ) 13 | ) 14 | 15 | const app = RouterBuilder.make(api).pipe( 16 | RouterBuilder.handle( 17 | "mySecuredEndpoint", 18 | (_, security) => Effect.succeed(`Accessed as ${security.user}`) 19 | ), 20 | RouterBuilder.build 21 | ) 22 | 23 | app.pipe(NodeServer.listen({ port: 3000 }), NodeRuntime.runMain) 24 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-security-complex.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Fiber, Layer, pipe, Schedule, Schema } from "effect" 3 | import { Api, Client, Middlewares, RouterBuilder, Security } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | interface UserInfo { 7 | email: string 8 | } 9 | 10 | class UserStorage extends Effect.Tag("UserStorage")< 11 | UserStorage, 12 | { getInfo: (user: string) => Effect.Effect } 13 | >() { 14 | static dummy = Layer.succeed( 15 | this, 16 | { getInfo: (_: string) => Effect.succeed({ email: "email@gmail.com" }) } 17 | ) 18 | } 19 | 20 | const mySecurity = pipe( 21 | Security.basic({ description: "My basic auth" }), 22 | Security.map((creds) => creds.user), 23 | Security.mapEffect((user) => UserStorage.getInfo(user)) 24 | ) 25 | 26 | const api = Api.make().pipe( 27 | Api.addEndpoint( 28 | pipe( 29 | Api.post("endpoint", "/endpoint"), 30 | Api.setResponseBody(Schema.String), 31 | Api.setSecurity(mySecurity) 32 | ) 33 | ) 34 | ) 35 | 36 | const app = RouterBuilder.make(api).pipe( 37 | RouterBuilder.handle( 38 | "endpoint", 39 | (_, security) => Effect.succeed(`Logged as ${security.email}`) 40 | ), 41 | RouterBuilder.build, 42 | Middlewares.errorLog 43 | ) 44 | 45 | Effect.gen(function*(_) { 46 | const fiber = yield* pipe(app, NodeServer.listen({ port: 3000 }), Effect.fork) 47 | 48 | const client = Client.make(api, { baseUrl: "http://localhost:3000" }) 49 | 50 | yield* pipe( 51 | client.endpoint({}, Client.setBasic("patrik", "slepice")), 52 | Effect.catchAllDefect(Effect.fail), 53 | Effect.onError((e) => Effect.logWarning(`Api call failed with ${e}`)), 54 | Effect.retry({ schedule: Schedule.spaced("1 second") }), 55 | Effect.flatMap((response) => Effect.log(`Api call succeeded with ${response}`)) 56 | ) 57 | 58 | yield* Fiber.join(fiber) 59 | }).pipe( 60 | Effect.provide(UserStorage.dummy), 61 | NodeRuntime.runMain 62 | ) 63 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-security-custom.ts: -------------------------------------------------------------------------------- 1 | import { HttpServerRequest } from "@effect/platform" 2 | import { NodeRuntime } from "@effect/platform-node" 3 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 4 | import { Api, HttpError, Middlewares, RouterBuilder, Security } from "effect-http" 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const customSecurity = Security.make( 8 | pipe( 9 | HttpServerRequest.schemaHeaders(Schema.Struct({ "x-api-key": Schema.String })), 10 | Effect.mapError(() => HttpError.unauthorized("Expected valid X-API-KEY header")), 11 | Effect.map((headers) => headers["x-api-key"]) 12 | ), 13 | { "myApiKey": { name: "X-API-KEY", type: "apiKey", in: "header", description: "My API key" } } 14 | ) 15 | 16 | const api = Api.make().pipe( 17 | Api.addEndpoint( 18 | Api.post("myRoute", "/my-route").pipe( 19 | Api.setResponseBody(Schema.String), 20 | Api.setSecurity(customSecurity) 21 | ) 22 | ) 23 | ) 24 | 25 | const app = RouterBuilder.make(api).pipe( 26 | RouterBuilder.handle("myRoute", (_, apiKey) => Effect.succeed(`Logged as ${apiKey}`)), 27 | RouterBuilder.build, 28 | Middlewares.errorLog 29 | ) 30 | 31 | app.pipe( 32 | NodeServer.listen({ port: 3000 }), 33 | Effect.provide(Logger.pretty), 34 | Logger.withMinimumLogLevel(LogLevel.All), 35 | NodeRuntime.runMain 36 | ) 37 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/readme-security.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, Option, Schema } from "effect" 3 | import { Api, Middlewares, RouterBuilder, Security } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | const mySecurity = Security.or( 7 | Security.asSome(Security.basic()), 8 | Security.as(Security.unit, Option.none()) 9 | ) 10 | 11 | const mySecuredEnpoint = Api.post("security", "/testSecurity").pipe( 12 | Api.setResponseBody(Schema.String), 13 | Api.setSecurity(mySecurity) 14 | ) 15 | 16 | const api = Api.make().pipe( 17 | Api.addEndpoint(mySecuredEnpoint) 18 | ) 19 | 20 | const app = RouterBuilder.make(api).pipe( 21 | RouterBuilder.handle("security", (_, security) => 22 | Effect.succeed( 23 | Option.match(security, { 24 | onSome: (creds) => `logged as ${creds.user}`, 25 | onNone: () => "not logged" 26 | }) 27 | )), 28 | RouterBuilder.build, 29 | Middlewares.errorLog 30 | ) 31 | 32 | app.pipe( 33 | NodeServer.listen({ port: 3000 }), 34 | Effect.provide(Logger.pretty), 35 | Logger.withMinimumLogLevel(LogLevel.All), 36 | NodeRuntime.runMain 37 | ) 38 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/request-example.ts: -------------------------------------------------------------------------------- 1 | import type { Error } from "@effect/platform" 2 | import { FileSystem } from "@effect/platform" 3 | import { NodeRuntime } from "@effect/platform-node" 4 | import { Array, Context, Duration, Effect, Logger, LogLevel, pipe, Request, RequestResolver, Schema } from "effect" 5 | import { Api, HttpError, RouterBuilder } from "effect-http" 6 | import { NodeServer } from "effect-http-node" 7 | 8 | interface GetValue extends Request.Request { 9 | readonly _tag: "GetValue" 10 | } 11 | const GetValue = Request.tagged("GetValue") 12 | 13 | const GetValueCache = Context.GenericTag("@services/GetValueCache") 14 | 15 | const GetValueResolver = Effect.map( 16 | FileSystem.FileSystem, 17 | ({ readFileString }) => 18 | RequestResolver.fromEffect((_: GetValue) => 19 | readFileString("test-file").pipe( 20 | Effect.tap(() => Effect.logDebug("Value read from file")) 21 | ) 22 | ) 23 | ) 24 | 25 | const requestMyValue = Effect.flatMap(GetValueCache, (getValueCache) => 26 | pipe( 27 | Effect.request(GetValue({}), GetValueResolver), 28 | Effect.withRequestCache(getValueCache), 29 | Effect.withRequestCaching(true) 30 | )) 31 | 32 | const api = pipe( 33 | Api.make(), 34 | Api.addEndpoint( 35 | Api.get("getValue", "/value").pipe(Api.setResponseBody(Schema.String)) 36 | ) 37 | ) 38 | 39 | const app = pipe( 40 | RouterBuilder.make(api), 41 | RouterBuilder.handle("getValue", () => 42 | Effect.flatMap(GetValueCache, (getValueCache) => 43 | pipe( 44 | Effect.all(Array.replicate(requestMyValue, 10), { 45 | concurrency: 10 46 | }), 47 | Effect.mapError(() => HttpError.notFound("File not found")), 48 | Effect.withRequestCache(getValueCache), 49 | Effect.withRequestCaching(true), 50 | Effect.map((values) => values.join(", ")) 51 | ))), 52 | RouterBuilder.build 53 | ) 54 | 55 | pipe( 56 | app, 57 | NodeServer.listen({ port: 3000 }), 58 | Effect.provideServiceEffect( 59 | GetValueCache, 60 | Request.makeCache({ capacity: 100, timeToLive: Duration.seconds(5) }) 61 | ), 62 | Effect.provide(Logger.pretty), 63 | Logger.withMinimumLogLevel(LogLevel.All), 64 | NodeRuntime.runMain 65 | ) 66 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/request-validation-optional-parameter.ts: -------------------------------------------------------------------------------- 1 | import { pipe, Schema } from "effect" 2 | import { Api } from "effect-http" 3 | 4 | const Stuff = Schema.Struct({ value: Schema.Number }) 5 | const StuffParams = Schema.Struct({ 6 | param: Schema.String, 7 | another: Schema.optional(Schema.String) 8 | }) 9 | 10 | export const api = pipe( 11 | Api.make({ title: "My api" }), 12 | Api.addEndpoint( 13 | pipe( 14 | Api.get("stuff", "/stuff/:param/:another?"), 15 | Api.setResponseBody(Stuff), 16 | Api.setRequestPath(StuffParams) 17 | ) 18 | ) 19 | ) 20 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/request-validation.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "effect" 2 | import { Api } from "effect-http" 3 | 4 | const Stuff = Schema.Struct({ value: Schema.Number }) 5 | const StuffRequest = Schema.Struct({ field: Schema.Array(Schema.String) }) 6 | const StuffQuery = Schema.Struct({ value: Schema.String }) 7 | const StuffPath = Schema.Struct({ param: Schema.String }) 8 | 9 | export const api = Api.make({ title: "My api" }).pipe( 10 | Api.addEndpoint( 11 | Api.post("stuff", "/stuff/:param").pipe( 12 | Api.setRequestBody(StuffRequest), 13 | Api.setRequestQuery(StuffQuery), 14 | Api.setRequestPath(StuffPath), 15 | Api.setResponseBody(Stuff) 16 | ) 17 | ) 18 | ) 19 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/resource-example.ts: -------------------------------------------------------------------------------- 1 | import type { Error } from "@effect/platform" 2 | import { FileSystem } from "@effect/platform" 3 | import { NodeContext, NodeRuntime } from "@effect/platform-node" 4 | import { Array, Context, Duration, Effect, Logger, LogLevel, pipe, Resource, Schedule, Schema } from "effect" 5 | import { Api, HttpError, RouterBuilder } from "effect-http" 6 | 7 | import { NodeServer } from "effect-http-node" 8 | 9 | const MyValue = Context.GenericTag>("@services/MyValue") 10 | 11 | const readMyValue = Effect.flatMap(MyValue, Resource.get) 12 | 13 | const api = Api.make().pipe( 14 | Api.addEndpoint( 15 | Api.get("getValue", "/value").pipe( 16 | Api.setResponseBody(Schema.String) 17 | ) 18 | ) 19 | ) 20 | 21 | const app = pipe( 22 | RouterBuilder.make(api), 23 | RouterBuilder.handle("getValue", () => 24 | pipe( 25 | Effect.all(Array.replicate(readMyValue, 10), { concurrency: 10 }), 26 | Effect.mapError(() => HttpError.notFound("File not found")), 27 | Effect.map((values) => values.join(", ")) 28 | )), 29 | RouterBuilder.build 30 | ) 31 | 32 | pipe( 33 | app, 34 | NodeServer.listen({ port: 3000 }), 35 | Effect.provideServiceEffect( 36 | MyValue, 37 | Resource.auto( 38 | FileSystem.FileSystem.pipe( 39 | Effect.flatMap(({ readFileString }) => readFileString("test-file")), 40 | Effect.tap(() => Effect.logDebug("MyValue refreshed from file")) 41 | ), 42 | Schedule.fixed(Duration.seconds(5)) 43 | ) 44 | ), 45 | Effect.scoped, 46 | Effect.provide(Logger.pretty), 47 | Logger.withMinimumLogLevel(LogLevel.All), 48 | Effect.provide(NodeContext.layer), 49 | NodeRuntime.runMain 50 | ) 51 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/schema-with-optional-field.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, Option, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | const Response = Schema.Struct({ 8 | foo: Schema.optionalWith(Schema.String, { as: "Option" }), 9 | bar: Schema.Option(Schema.String) 10 | }) 11 | 12 | const api = pipe( 13 | Api.make(), 14 | Api.addEndpoint( 15 | Api.get("hello", "/hello").pipe(Api.setResponseBody(Response)) 16 | ) 17 | ) 18 | 19 | const app = pipe( 20 | RouterBuilder.make(api), 21 | RouterBuilder.handle("hello", () => Effect.succeed({ foo: Option.none(), bar: Option.none() })), 22 | RouterBuilder.build 23 | ) 24 | 25 | pipe( 26 | app, 27 | NodeServer.listen({ port: 4000 }), 28 | Effect.provide(Logger.pretty), 29 | Logger.withMinimumLogLevel(LogLevel.All), 30 | NodeRuntime.runMain 31 | ) 32 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/scoped-resources.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Context, Effect, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { NodeServer } from "effect-http-node" 6 | 7 | interface Resource { 8 | value: number 9 | } 10 | 11 | const ResourceService = Context.GenericTag("@services/ResourceService") 12 | 13 | const resource = Effect.acquireRelease( 14 | pipe( 15 | Effect.log("Acquried resource"), 16 | Effect.as({ value: 2 } satisfies Resource) 17 | ), 18 | () => Effect.log("Released resource") 19 | ) 20 | 21 | const api = pipe( 22 | Api.make(), 23 | Api.addEndpoint( 24 | Api.get("test", "/test").pipe(Api.setResponseBody(Schema.String)) 25 | ) 26 | ) 27 | 28 | const app = pipe( 29 | RouterBuilder.make(api), 30 | RouterBuilder.handle("test", () => Effect.map(ResourceService, ({ value }) => `There you go: ${value}`)), 31 | RouterBuilder.build 32 | ) 33 | 34 | pipe( 35 | app, 36 | NodeServer.listen({ port: 3000 }), 37 | Effect.provideServiceEffect(ResourceService, resource), 38 | Effect.scoped, 39 | NodeRuntime.runMain 40 | ) 41 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/static-files-from-root.ts: -------------------------------------------------------------------------------- 1 | import { NodeContext, NodeRuntime } from "@effect/platform-node" 2 | import { Effect, Logger, LogLevel, pipe, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | 5 | import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "@effect/platform" 6 | import { NodeServer } from "effect-http-node" 7 | 8 | const api = Api.make().pipe( 9 | Api.addEndpoint( 10 | Api.get("handle", "/api/handle").pipe(Api.setResponseBody(Schema.String)) 11 | ) 12 | ) 13 | 14 | const StaticFilesMiddleware = HttpMiddleware.make((app) => 15 | Effect.gen(function*() { 16 | const request = yield* HttpServerRequest.HttpServerRequest 17 | 18 | if (request.url.startsWith("/api")) { 19 | return yield* app 20 | } 21 | 22 | return yield* pipe( 23 | HttpServerResponse.file(request.url.replace("/", "")), 24 | Effect.orElse(() => HttpServerResponse.text("Not found", { status: 404 })) 25 | ) 26 | }) 27 | ) 28 | 29 | const app = RouterBuilder.make(api, { enableDocs: true }).pipe( 30 | RouterBuilder.handle("handle", () => Effect.succeed("Hello World")), 31 | RouterBuilder.build, 32 | StaticFilesMiddleware 33 | ) 34 | 35 | app.pipe( 36 | NodeServer.listen({ port: 3000 }), 37 | Effect.provide(NodeContext.layer), 38 | Effect.provide(Logger.pretty), 39 | Logger.withMinimumLogLevel(LogLevel.All), 40 | NodeRuntime.runMain 41 | ) 42 | -------------------------------------------------------------------------------- /packages/effect-http-node/examples/unexpected-error.ts: -------------------------------------------------------------------------------- 1 | import { NodeRuntime } from "@effect/platform-node" 2 | import { Data, Effect, Logger, Schema } from "effect" 3 | import { Api, RouterBuilder } from "effect-http" 4 | import { NodeServer } from "effect-http-node" 5 | 6 | export const api = Api.make({ title: "Example API" }).pipe( 7 | Api.addEndpoint( 8 | Api.get("root", "/").pipe(Api.setResponseBody(Schema.String)) 9 | ) 10 | ) 11 | 12 | class MyError extends Data.TaggedError("MyError")<{ message: string }> {} 13 | 14 | export const app = RouterBuilder.make(api).pipe( 15 | RouterBuilder.handle( 16 | "root", 17 | () => Effect.fail(new MyError({ message: "Unexpected error" })) 18 | ), 19 | RouterBuilder.build 20 | ) 21 | 22 | const program = app.pipe( 23 | NodeServer.listen({ port: 3000 }), 24 | Effect.provide(Logger.pretty) 25 | ) 26 | 27 | NodeRuntime.runMain(program) 28 | -------------------------------------------------------------------------------- /packages/effect-http-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-http-node", 3 | "type": "module", 4 | "version": "0.27.0", 5 | "license": "MIT", 6 | "author": "Milan Suk ", 7 | "description": "High-level declarative HTTP API for effect-ts", 8 | "homepage": "https://sukovanej.github.io/effect-http", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sukovanej/effect-http.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/sukovanej/effect-http/issues" 15 | }, 16 | "packageManager": "pnpm@9.1.1", 17 | "publishConfig": { 18 | "access": "public", 19 | "directory": "dist" 20 | }, 21 | "scripts": { 22 | "codegen": "build-utils prepare-v2", 23 | "build": "pnpm codegen && pnpm build-esm && pnpm build-cjs && pnpm build-annotate && build-utils pack-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 | "check": "tsc -b tsconfig.json", 28 | "test": "vitest", 29 | "coverage": "vitest --coverage" 30 | }, 31 | "dependencies": { 32 | "swagger-ui-dist": "^5.20.1" 33 | }, 34 | "peerDependencies": { 35 | "@effect/platform": "^0.80.0", 36 | "@effect/platform-node": "^0.76.0", 37 | "effect": "^3.14.0", 38 | "effect-http": "workspace:^" 39 | }, 40 | "devDependencies": { 41 | "@effect/platform": "^0.80.1", 42 | "@effect/platform-bun": "^0.60.2", 43 | "@effect/platform-node": "^0.76.2", 44 | "@types/node": "^22.13.8", 45 | "effect": "^3.14.1", 46 | "effect-http": "workspace:^" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/NodeServer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified way to run a node server. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as NodeContext from "@effect/platform-node/NodeContext" 7 | import type * as Etag from "@effect/platform/Etag" 8 | import type * as HttpApp from "@effect/platform/HttpApp" 9 | import type * as HttpPlatform from "@effect/platform/HttpPlatform" 10 | import type * as HttpServer from "@effect/platform/HttpServer" 11 | import type * as HttpServerError from "@effect/platform/HttpServerError" 12 | import type * as HttpServerRequest from "@effect/platform/HttpServerRequest" 13 | import type * as SwaggerRouter from "effect-http/SwaggerRouter" 14 | import type * as Effect from "effect/Effect" 15 | import type * as Scope from "effect/Scope" 16 | 17 | import * as internal from "./internal/node-server.js" 18 | 19 | /** 20 | * @category models 21 | * @since 1.0.0 22 | */ 23 | export interface Options { 24 | port: number | undefined 25 | } 26 | 27 | /** 28 | * @category combinators 29 | * @since 1.0.0 30 | */ 31 | export const listen: ( 32 | options?: Partial 33 | ) => ( 34 | router: HttpApp.Default 35 | ) => Effect.Effect< 36 | never, 37 | HttpServerError.ServeError, 38 | Exclude< 39 | Exclude< 40 | Exclude, 41 | HttpServer.HttpServer | HttpPlatform.HttpPlatform | Etag.Generator | NodeContext.NodeContext 42 | >, 43 | SwaggerRouter.SwaggerFiles 44 | > 45 | > = internal.listen 46 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/NodeSwaggerFiles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a router serving Swagger files. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as FileSystem from "@effect/platform/FileSystem" 7 | import type * as Path from "@effect/platform/Path" 8 | import type * as SwaggerRouter from "effect-http/SwaggerRouter" 9 | import type * as Layer from "effect/Layer" 10 | import * as internal from "./internal/node-swagger-files.js" 11 | 12 | /** 13 | * @category context 14 | * @since 1.0.0 15 | */ 16 | export const SwaggerFilesLive: Layer.Layer = 17 | internal.SwaggerFilesLive 18 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/NodeTesting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Testing of the `Server` implementation. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as NodeContext from "@effect/platform-node/NodeContext" 7 | import type * as Etag from "@effect/platform/Etag" 8 | import type * as HttpApp from "@effect/platform/HttpApp" 9 | import type * as HttpClient from "@effect/platform/HttpClient" 10 | import type * as HttpPlatform from "@effect/platform/HttpPlatform" 11 | import type * as HttpServer from "@effect/platform/HttpServer" 12 | import type * as HttpServerRequest from "@effect/platform/HttpServerRequest" 13 | import type * as Effect from "effect/Effect" 14 | import type * as Scope from "effect/Scope" 15 | 16 | import type * as Api from "effect-http/Api" 17 | import type * as ApiEndpoint from "effect-http/ApiEndpoint" 18 | import type * as Client from "effect-http/Client" 19 | import type * as Handler from "effect-http/Handler" 20 | import type * as SwaggerRouter from "effect-http/SwaggerRouter" 21 | 22 | import * as internal from "./internal/node-testing.js" 23 | 24 | /** 25 | * Create a testing client for the `Server`. 26 | * 27 | * @category constructors 28 | * @since 1.0.0 29 | */ 30 | export const make: ( 31 | app: HttpApp.Default, 32 | api: A, 33 | options?: Partial 34 | ) => Effect.Effect< 35 | Client.Client, 36 | never, 37 | | Scope.Scope 38 | | Exclude< 39 | Exclude< 40 | Exclude< 41 | Exclude, 42 | HttpServer.HttpServer | HttpPlatform.HttpPlatform | Etag.Generator | NodeContext.NodeContext 43 | >, 44 | SwaggerRouter.SwaggerFiles 45 | >, 46 | NodeContext.NodeContext 47 | > 48 | > = internal.make 49 | 50 | /** 51 | * Create a testing client for the `Server`. Instead of the `Client.Client` interface 52 | * it returns a raw *@effect/platform/Http/Client* `Client` with base url set. 53 | * 54 | * @category constructors 55 | * @since 1.0.0 56 | */ 57 | export const makeRaw: ( 58 | app: HttpApp.Default 59 | ) => Effect.Effect< 60 | HttpClient.HttpClient, 61 | never, 62 | | Scope.Scope 63 | | Exclude< 64 | Exclude< 65 | Exclude< 66 | Exclude, 67 | HttpServer.HttpServer | HttpPlatform.HttpPlatform | Etag.Generator | NodeContext.NodeContext 68 | >, 69 | SwaggerRouter.SwaggerFiles 70 | >, 71 | NodeContext.NodeContext 72 | > 73 | > = internal.makeRaw 74 | 75 | /** 76 | * Testing of `Handler.Handler`. 77 | * 78 | * @example 79 | * import { HttpClientRequest } from "@effect/platform" 80 | * import { Schema } from "effect" 81 | * import { Effect } from "effect" 82 | * import { Api, Handler } from "effect-http" 83 | * import { NodeTesting } from "effect-http-node" 84 | * 85 | * const myEndpoint = Api.get("myEndpoint", "/my-endpoint").pipe( 86 | * Api.setResponseBody(Schema.Struct({ hello: Schema.String })) 87 | * ) 88 | * 89 | * const myHandler = Handler.make(myEndpoint, () => Effect.succeed({ hello: "world" })) 90 | * 91 | * Effect.gen(function*() { 92 | * const client = yield* NodeTesting.handler(myHandler) 93 | * const response = yield* client.execute(HttpClientRequest.get("/my-endpoint")) 94 | * 95 | * assert.deepStrictEqual(response.status, 200) 96 | * assert.deepStrictEqual(yield* response.json, { hello: "world" }) 97 | * }).pipe(Effect.scoped, Effect.runFork) 98 | * 99 | * @category constructors 100 | * @since 1.0.0 101 | */ 102 | export const handler: ( 103 | app: Handler.Handler 104 | ) => Effect.Effect< 105 | HttpClient.HttpClient, 106 | never, 107 | | Scope.Scope 108 | | Exclude< 109 | Exclude< 110 | Exclude, 111 | HttpServer.HttpServer | HttpPlatform.HttpPlatform | Etag.Generator | NodeContext.NodeContext 112 | >, 113 | NodeContext.NodeContext 114 | > 115 | > = internal.handler 116 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Simplified way to run a node server. 3 | * 4 | * @since 1.0.0 5 | */ 6 | export * as NodeServer from "./NodeServer.js" 7 | 8 | /** 9 | * Create a router serving Swagger files. 10 | * 11 | * @since 1.0.0 12 | */ 13 | export * as NodeSwaggerFiles from "./NodeSwaggerFiles.js" 14 | 15 | /** 16 | * Testing of the `Server` implementation. 17 | * 18 | * @since 1.0.0 19 | */ 20 | export * as NodeTesting from "./NodeTesting.js" 21 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/internal/node-server.ts: -------------------------------------------------------------------------------- 1 | import * as NodeContext from "@effect/platform-node/NodeContext" 2 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" 3 | import type * as HttpApp from "@effect/platform/HttpApp" 4 | import * as HttpServer from "@effect/platform/HttpServer" 5 | import * as Effect from "effect/Effect" 6 | import { pipe } from "effect/Function" 7 | import * as Layer from "effect/Layer" 8 | import { createServer } from "http" 9 | 10 | import type * as NodeServer from "../NodeServer.js" 11 | import * as NodeSwaggerFiles from "../NodeSwaggerFiles.js" 12 | 13 | /** @internal */ 14 | const DEFAULT_LISTEN_OPTIONS: NodeServer.Options = { 15 | port: undefined 16 | } 17 | 18 | /** @internal */ 19 | export const listen = (options?: Partial) => (router: HttpApp.Default) => 20 | pipe( 21 | HttpServer.logAddress, 22 | Effect.flatMap(() => Layer.launch(HttpServer.serve(router))), 23 | Effect.scoped, 24 | Effect.provide(NodeHttpServer.layer(() => createServer(), { ...DEFAULT_LISTEN_OPTIONS, ...options })), 25 | Effect.provide(NodeSwaggerFiles.SwaggerFilesLive), 26 | Effect.provide(NodeContext.layer) 27 | ) 28 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/internal/node-swagger-files.ts: -------------------------------------------------------------------------------- 1 | import * as FileSystem from "@effect/platform/FileSystem" 2 | import * as Path from "@effect/platform/Path" 3 | import * as SwaggerRouter from "effect-http/SwaggerRouter" 4 | import * as Effect from "effect/Effect" 5 | import * as Layer from "effect/Layer" 6 | import * as Record from "effect/Record" 7 | import { getAbsoluteFSPath } from "swagger-ui-dist" 8 | 9 | /** @internal */ 10 | const readFile = (path: string) => Effect.flatMap(FileSystem.FileSystem, (fs) => fs.readFile(path)) 11 | 12 | /** @internal */ 13 | const SWAGGER_FILE_NAMES = [ 14 | "index.css", 15 | "swagger-ui.css", 16 | "swagger-ui-bundle.js", 17 | "swagger-ui-standalone-preset.js", 18 | "favicon-32x32.png", 19 | "favicon-16x16.png" 20 | ] 21 | 22 | /** @internal */ 23 | const readSwaggerFile = (swaggerBasePath: string, file: string) => 24 | Effect.flatMap(Path.Path, (path) => readFile(path.resolve(swaggerBasePath, file)).pipe(Effect.orDie)) 25 | 26 | /** @internal */ 27 | export const SwaggerFilesLive = Effect.gen(function*(_) { 28 | const absolutePath = getAbsoluteFSPath() 29 | 30 | const files = yield* _( 31 | SWAGGER_FILE_NAMES, 32 | Effect.forEach((path) => Effect.zip(Effect.succeed(path), readSwaggerFile(absolutePath, path))), 33 | Effect.map(Record.fromEntries) 34 | ) 35 | 36 | const size = Object.entries(files).reduce( 37 | (acc, [_, content]) => acc + content.byteLength, 38 | 0 39 | ) 40 | const sizeMb = (size / 1024 / 1024).toFixed(1) 41 | 42 | yield* _(Effect.logDebug(`Static swagger UI files loaded (${sizeMb}MB)`)) 43 | 44 | return { files } 45 | }).pipe(Layer.effect(SwaggerRouter.SwaggerFiles)) 46 | -------------------------------------------------------------------------------- /packages/effect-http-node/src/internal/node-testing.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from "http" 2 | 3 | import * as NodeContext from "@effect/platform-node/NodeContext" 4 | import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer" 5 | import * as FetchHttpClient from "@effect/platform/FetchHttpClient" 6 | import type * as HttpApp from "@effect/platform/HttpApp" 7 | import * as HttpClient from "@effect/platform/HttpClient" 8 | import * as HttpClientRequest from "@effect/platform/HttpClientRequest" 9 | import * as HttpServer from "@effect/platform/HttpServer" 10 | import * as Context from "effect/Context" 11 | import * as Deferred from "effect/Deferred" 12 | import * as Effect from "effect/Effect" 13 | import * as Layer from "effect/Layer" 14 | 15 | import type * as Api from "effect-http/Api" 16 | import type * as ApiEndpoint from "effect-http/ApiEndpoint" 17 | import * as Client from "effect-http/Client" 18 | import * as Handler from "effect-http/Handler" 19 | import type * as SwaggerRouter from "effect-http/SwaggerRouter" 20 | 21 | import * as NodeSwaggerFiles from "../NodeSwaggerFiles.js" 22 | 23 | /** @internal */ 24 | const defaultHttpClient = FetchHttpClient.layer.pipe( 25 | Layer.build, 26 | Effect.map(Context.get(HttpClient.HttpClient)), 27 | Effect.scoped, 28 | Effect.runSync 29 | ) 30 | 31 | /** @internal */ 32 | export const make = ( 33 | app: HttpApp.Default, 34 | api: A, 35 | options?: Partial 36 | ) => 37 | startTestServer(app).pipe( 38 | Effect.map((url) => 39 | Client.make(api, { 40 | ...options, 41 | httpClient: makeHttpClient(options?.httpClient ?? defaultHttpClient, url) 42 | }) 43 | ), 44 | Effect.provide(NodeSwaggerFiles.SwaggerFilesLive), 45 | Effect.provide(NodeContext.layer) 46 | ) 47 | 48 | /** @internal */ 49 | export const makeRaw = ( 50 | app: HttpApp.Default 51 | ) => 52 | startTestServer(app).pipe( 53 | Effect.map((url) => makeHttpClient(defaultHttpClient, url)), 54 | Effect.provide(NodeSwaggerFiles.SwaggerFilesLive), 55 | Effect.provide(NodeContext.layer) 56 | ) 57 | 58 | /** @internal */ 59 | export const handler = ( 60 | handler: Handler.Handler 61 | ) => 62 | startTestServer(Handler.getRouter(handler)).pipe( 63 | Effect.map((url) => makeHttpClient(defaultHttpClient, url)), 64 | Effect.provide(NodeContext.layer) 65 | ) 66 | 67 | /** @internal */ 68 | const startTestServer = ( 69 | app: HttpApp.Default 70 | ) => 71 | Effect.flatMap(Deferred.make(), (allocatedUrl) => 72 | serverUrl.pipe( 73 | Effect.flatMap((url) => Deferred.succeed(allocatedUrl, url)), 74 | Effect.flatMap(() => Layer.launch(HttpServer.serve(app))), 75 | Effect.provide(NodeHttpServer.layer(createServer, { port: undefined })), 76 | Effect.forkScoped, 77 | Effect.flatMap(() => Deferred.await(allocatedUrl)) 78 | )) 79 | 80 | /** @internal */ 81 | const makeHttpClient = (client: HttpClient.HttpClient, url: string) => 82 | client.pipe( 83 | HttpClient.mapRequest(HttpClientRequest.prependUrl(url)), 84 | HttpClient.transformResponse( 85 | Effect.mapInputContext((ctx: Context.Context) => { 86 | const init = ctx.unsafeMap.get(FetchHttpClient.RequestInit.key) ?? {} 87 | return Context.add(ctx, FetchHttpClient.RequestInit, { keepalive: false, ...init }) 88 | }) 89 | ) 90 | ) 91 | 92 | /** @internal */ 93 | const serverUrl = Effect.map(HttpServer.HttpServer, (server) => { 94 | const address = server.address 95 | 96 | if (address._tag === "UnixAddress") { 97 | return address.path 98 | } 99 | 100 | return `http://localhost:${address.port}` 101 | }) 102 | -------------------------------------------------------------------------------- /packages/effect-http-node/test/example-server.test.ts: -------------------------------------------------------------------------------- 1 | import * as it from "@effect/vitest" 2 | import { Effect } from "effect" 3 | import { ExampleServer, RouterBuilder } from "effect-http" 4 | import { NodeTesting } from "effect-http-node" 5 | import { expect } from "vitest" 6 | import { exampleApiFullResponse, exampleApiGet } from "./examples.js" 7 | 8 | it.scoped( 9 | "example server", 10 | () => 11 | Effect.gen(function*(_) { 12 | const app = ExampleServer.make(exampleApiGet) 13 | 14 | const response = yield* _( 15 | NodeTesting.make(RouterBuilder.build(app), exampleApiGet), 16 | Effect.flatMap((client) => client.getValue({})) 17 | ) 18 | 19 | expect(typeof response).toEqual("number") 20 | }) 21 | ) 22 | 23 | it.scoped( 24 | "handle", 25 | () => 26 | Effect.gen(function*(_) { 27 | const app = RouterBuilder.make(exampleApiFullResponse).pipe( 28 | RouterBuilder.handle("another", () => Effect.succeed(69)), 29 | ExampleServer.handle("hello") 30 | ) 31 | 32 | const response = yield* _( 33 | NodeTesting.make(RouterBuilder.build(app), exampleApiFullResponse), 34 | Effect.flatMap((client) => client.hello({})) 35 | ) 36 | 37 | expect(response.status).toEqual(200) 38 | expect(typeof response.body).toEqual("number") 39 | expect(typeof response.headers["my-header"]).toEqual("string") 40 | }) 41 | ) 42 | 43 | it.scoped( 44 | "handleRemaining", 45 | () => 46 | Effect.gen(function*(_) { 47 | const app = RouterBuilder.make(exampleApiFullResponse).pipe( 48 | RouterBuilder.handle("another", () => Effect.succeed(69)), 49 | ExampleServer.handleRemaining 50 | ) 51 | 52 | const response = yield* _( 53 | NodeTesting.make(RouterBuilder.build(app), exampleApiFullResponse), 54 | Effect.flatMap((client) => client.hello({})) 55 | ) 56 | 57 | expect(response.status).toEqual(200) 58 | expect(typeof response.body).toEqual("number") 59 | expect(typeof response.headers["my-header"]).toEqual("string") 60 | }) 61 | ) 62 | -------------------------------------------------------------------------------- /packages/effect-http-node/test/security.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientRequest } from "@effect/platform" 2 | import * as it from "@effect/vitest" 3 | import { Effect, flow, pipe, Schema } from "effect" 4 | import { Api, Client, RouterBuilder, Security } from "effect-http" 5 | import { NodeTesting } from "effect-http-node" 6 | import { expect } from "vitest" 7 | 8 | const xApiKey = Security.apiKey({ key: "x-api-key", in: "header" }) 9 | 10 | const parameters = [ 11 | { 12 | security: pipe(Security.basic(), Security.map((auth) => auth.user)), 13 | name: "Security.basic", 14 | setAuth: Client.setBasic("user", "pass"), 15 | expected: "user" 16 | }, 17 | { 18 | security: xApiKey, 19 | name: "Security.apiKey", 20 | setAuth: Client.setApiKey("x-api-key", "header", "api-key"), 21 | expected: "api-key" 22 | }, 23 | { 24 | security: Security.bearer(), 25 | name: "Security.bearer", 26 | setAuth: Client.setBearer("token"), 27 | expected: "token" 28 | }, 29 | { 30 | security: pipe(Security.bearer(), Security.as("my-value")), 31 | name: "Security.as", 32 | setAuth: Client.setBearer("whatever"), 33 | expected: "my-value" 34 | }, 35 | { 36 | security: pipe(Security.bearer(), Security.map((token) => token + "1")), 37 | name: "Security.map", 38 | setAuth: Client.setBearer("token"), 39 | expected: "token1" 40 | }, 41 | { 42 | security: pipe(Security.bearer(), Security.mapEffect((token) => Effect.succeed(token + "1"))), 43 | name: "Security.mapEffect", 44 | setAuth: Client.setBearer("token"), 45 | expected: "token1" 46 | }, 47 | { 48 | security: pipe(Security.bearer(), Security.and(xApiKey), Security.map(([a, b]) => `${a}-${b}`)), 49 | name: "Security.and", 50 | setAuth: flow(Client.setBearer("token"), Client.setApiKey("x-api-key", "header", "api-key")), 51 | expected: "token-api-key" 52 | }, 53 | { 54 | security: pipe(Security.bearer(), Security.or(xApiKey)), 55 | name: "Security.or", 56 | setAuth: Client.setBearer("token"), 57 | expected: "token" 58 | }, 59 | { 60 | security: pipe(Security.bearer(), Security.or(xApiKey)), 61 | name: "Security.or", 62 | setAuth: Client.setApiKey("x-api-key", "header", "api-key"), 63 | expected: "api-key" 64 | }, 65 | { 66 | security: Security.apiKey({ in: "query", key: "key" }), 67 | name: "Security.apiKey in query", 68 | setAuth: HttpClientRequest.setUrlParam("key", "api-key"), 69 | expected: "api-key" 70 | } 71 | ] 72 | 73 | parameters.forEach(({ expected, name, security, setAuth }) => 74 | it.scoped( 75 | `security ${name}`, 76 | () => 77 | Effect.gen(function*(_) { 78 | const api = Api.make().pipe( 79 | Api.addEndpoint( 80 | Api.post("test", "/test").pipe(Api.setResponseBody(Schema.String), Api.setSecurity(security)) 81 | ) 82 | ) 83 | 84 | const app = pipe( 85 | RouterBuilder.make(api), 86 | RouterBuilder.handle("test", (_, security) => Effect.succeed(security)), 87 | RouterBuilder.build 88 | ) 89 | 90 | const result = yield* _( 91 | NodeTesting.make(app, api), 92 | Effect.flatMap((client) => client.test({}, setAuth)) 93 | ) 94 | 95 | expect(result).toBe(expected) 96 | }) 97 | ) 98 | ) 99 | -------------------------------------------------------------------------------- /packages/effect-http-node/test/swagger-router.test.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientRequest, HttpRouter } from "@effect/platform" 2 | import { expect, it } from "@effect/vitest" 3 | import { Array, Effect } from "effect" 4 | import { SwaggerRouter } from "effect-http" 5 | import { NodeTesting } from "effect-http-node" 6 | 7 | const docsUrls = [ 8 | "/api/docs", 9 | "/api/docs/index.html", 10 | "/api/docs/swagger-ui.css", 11 | "/api/docs/swagger-ui-bundle.js", 12 | "/api/docs/swagger-ui-standalone-preset.js", 13 | "/api/docs/favicon-32x32.png" 14 | ] 15 | 16 | it.scoped("swagger-router mount", () => 17 | Effect.gen(function*(_) { 18 | const router = HttpRouter.empty.pipe( 19 | HttpRouter.mount("/api/docs", SwaggerRouter.make({})) 20 | ) 21 | const client = yield* _(NodeTesting.makeRaw(router)) 22 | const responses = yield* _( 23 | docsUrls, 24 | Array.map((url) => HttpClientRequest.get(url)), 25 | Effect.forEach(client.execute) 26 | ) 27 | 28 | expect(responses.map((response) => response.status)).toStrictEqual(Array.replicate(200, docsUrls.length)) 29 | 30 | for (const indexResponse of responses.slice(0, 2)) { 31 | const html = yield* _(indexResponse.text) 32 | expect(html).includes("/api/docs/swagger-ui.css") 33 | } 34 | })) 35 | 36 | it.scoped("swagger-router mountApp", () => 37 | Effect.gen(function*(_) { 38 | const router = HttpRouter.empty.pipe( 39 | HttpRouter.mountApp("/api/docs", SwaggerRouter.make({})) 40 | ) 41 | const client = yield* _(NodeTesting.makeRaw(router)) 42 | const responses = yield* _( 43 | docsUrls, 44 | Array.map((url) => HttpClientRequest.get(url)), 45 | Effect.forEach(client.execute) 46 | ) 47 | 48 | expect(responses.map((response) => response.status)).toStrictEqual(Array.replicate(200, docsUrls.length)) 49 | 50 | for (const indexResponse of responses.slice(0, 2)) { 51 | const html = yield* _(indexResponse.text) 52 | expect(html).includes("/api/docs/swagger-ui.css") 53 | } 54 | })) 55 | -------------------------------------------------------------------------------- /packages/effect-http-node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.src.json", 3 | "references": [ 4 | { "path": "../effect-http" }, 5 | ], 6 | "compilerOptions": { 7 | "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", 8 | "outDir": "build/esm", 9 | "declarationDir": "build/dts", 10 | "stripInternal": true, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/effect-http-node/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 | -------------------------------------------------------------------------------- /packages/effect-http-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "../effect-http" }, 6 | { "path": "tsconfig.src.json" }, 7 | { "path": "tsconfig.test.json" }, 8 | { "path": "tsconfig.examples.json" }, 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/effect-http-node/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "references": [ 5 | { "path": "../effect-http" }, 6 | ], 7 | "compilerOptions": { 8 | "types": ["node"], 9 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 10 | "rootDir": "src", 11 | "outDir": "build/src", 12 | "exactOptionalPropertyTypes": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/effect-http-node/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "../effect-http" }, 7 | ], 8 | "compilerOptions": { 9 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 10 | "rootDir": "test", 11 | "noEmit": true, 12 | "exactOptionalPropertyTypes": false 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/effect-http-node/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /packages/effect-http/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 | -------------------------------------------------------------------------------- /packages/effect-http/README.md: -------------------------------------------------------------------------------- 1 | # effect-http 2 | 3 | This is the platform-independent package. Please refer to the main readme. 4 | -------------------------------------------------------------------------------- /packages/effect-http/docgen.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@effect/docgen/schema.json", 3 | "exclude": [ 4 | "src/internal/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /packages/effect-http/dtslint/Api.tst.ts: -------------------------------------------------------------------------------- 1 | import type { ApiEndpoint } from "effect-http" 2 | import { Api } from "effect-http" 3 | import { describe, expect, it } from "tstyche" 4 | 5 | describe("Api", () => { 6 | it("make", () => { 7 | expect(Api.make().pipe(Api.setOptions({ title: "My API" }))).type.toBe>() 8 | }) 9 | 10 | it("make with addEndpoint", () => { 11 | expect( 12 | Api.make().pipe( 13 | Api.addEndpoint(Api.get("hello", "/hello")), 14 | Api.setOptions({ title: "My API" }) 15 | ) 16 | ).type.toBe>>() 17 | 18 | expect( 19 | Api.make().pipe( 20 | Api.addEndpoint( 21 | Api.get("hello", "/hello").pipe( 22 | Api.setEndpointOptions({ description: "My endpoint" }) 23 | ) 24 | ), 25 | Api.setOptions({ title: "My API" }) 26 | ) 27 | ).type.toBe>>() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/effect-http/dtslint/ApiEndpoint.tst.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "effect" 2 | import type { ApiRequest, ApiResponse, ApiSchema, Security } from "effect-http" 3 | import { ApiEndpoint } from "effect-http" 4 | import { describe, expect, it } from "tstyche" 5 | 6 | describe("ApiGroup", () => { 7 | it("make", () => { 8 | expect( 9 | ApiEndpoint.get("hello", "/hello").pipe( 10 | ApiEndpoint.setOptions({ description: "my endpoint" }) 11 | ) 12 | ).type.toBe>() 13 | }) 14 | 15 | it("correct request types", () => { 16 | ApiEndpoint.get("hello", "/hello").pipe( 17 | ApiEndpoint.setRequestQuery( 18 | // @ts-expect-error 19 | Schema.string 20 | ) 21 | ) 22 | 23 | ApiEndpoint.get("hello", "/hello").pipe( 24 | ApiEndpoint.setRequestPath( 25 | // @ts-expect-error 26 | Schema.string 27 | ) 28 | ) 29 | 30 | ApiEndpoint.get("hello", "/hello").pipe( 31 | ApiEndpoint.setRequestHeaders( 32 | // @ts-expect-error 33 | Schema.string 34 | ) 35 | ) 36 | 37 | ApiEndpoint.get("hello", "/hello").pipe( 38 | ApiEndpoint.setRequestQuery( 39 | // @ts-expect-error expecting accepting a string | undefined | string[] 40 | Schema.Struct({ field: Schema.Number }) 41 | ) 42 | ) 43 | 44 | ApiEndpoint.get("hello", "/hello").pipe( 45 | ApiEndpoint.setRequestPath( 46 | // @ts-expect-error expecting accepting a string | undefined | string[] 47 | Schema.Struct({ field: Schema.Number }) 48 | ) 49 | ) 50 | 51 | ApiEndpoint.get("hello", "/hello").pipe( 52 | ApiEndpoint.setRequestHeaders( 53 | // @ts-expect-error expecting accepting a string | undefined | string[] 54 | Schema.Struct({ field: Schema.Number }) 55 | ) 56 | ) 57 | }) 58 | 59 | it("array is supported for query", () => { 60 | expect( 61 | ApiEndpoint.get("hello", "/hello").pipe( 62 | ApiEndpoint.setRequestQuery( 63 | Schema.Struct({ field: Schema.Array(Schema.String) }) 64 | ) 65 | ) 66 | ).type.toBe< 67 | ApiEndpoint.ApiEndpoint< 68 | "hello", 69 | ApiRequest.ApiRequest< 70 | ApiSchema.Ignored, 71 | ApiSchema.Ignored, 72 | { readonly field: ReadonlyArray }, 73 | ApiSchema.Ignored, 74 | never 75 | >, 76 | ApiResponse.ApiResponse.Default, 77 | Security.Security.Default 78 | > 79 | >() 80 | }) 81 | }) 82 | 83 | // array is supported for query 84 | -------------------------------------------------------------------------------- /packages/effect-http/dtslint/ApiGroup.tst.ts: -------------------------------------------------------------------------------- 1 | import type { ApiEndpoint } from "effect-http" 2 | import { ApiGroup } from "effect-http" 3 | import { describe, expect, it } from "tstyche" 4 | 5 | describe("ApiGroup", () => { 6 | it("make", () => { 7 | expect(ApiGroup.make("myGroup").pipe(ApiGroup.setOptions({ description: "My group" }))).type.toBe< 8 | ApiGroup.ApiGroup 9 | >() 10 | 11 | expect( 12 | ApiGroup.make("myGroup").pipe( 13 | ApiGroup.addEndpoint( 14 | ApiGroup.get("hello", "/hello").pipe( 15 | ApiGroup.setEndpointOptions({ description: "My endpoint" }) 16 | ) 17 | ), 18 | ApiGroup.setOptions({ description: "My group" }) 19 | ) 20 | ).type.toBe< 21 | ApiGroup.ApiGroup> 22 | >() 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/effect-http/dtslint/Handler.tst.ts: -------------------------------------------------------------------------------- 1 | import { Effect } from "effect" 2 | import type { ApiEndpoint } from "effect-http" 3 | import { Api, Handler } from "effect-http" 4 | import { describe, expect, it } from "tstyche" 5 | 6 | const endpoint1 = Api.get("endpoint1", "/endpoint1") 7 | const endpoint2 = Api.get("endpoint2", "/endpoint2") 8 | const endpoint3 = Api.get("endpoint3", "/endpoint3") 9 | 10 | declare const eff1: Effect.Effect 11 | declare const eff2: Effect.Effect 12 | declare const eff3: Effect.Effect 13 | 14 | describe("Handler", () => { 15 | it("make", () => { 16 | expect(Handler.make(endpoint1, () => Effect.void)).type.toBe< 17 | Handler.Handler, never, never> 18 | >() 19 | 20 | expect(endpoint1.pipe(Handler.make(() => Effect.void))).type.toBe< 21 | Handler.Handler, never, never> 22 | >() 23 | }) 24 | 25 | it("concat", () => { 26 | expect(Handler.concat( 27 | Handler.make(endpoint1, () => Effect.void), 28 | Handler.make(endpoint2, () => Effect.void) 29 | )).type.toBe< 30 | Handler.Handler< 31 | ApiEndpoint.ApiEndpoint.Default<"endpoint1"> | ApiEndpoint.ApiEndpoint.Default<"endpoint2">, 32 | never, 33 | never 34 | > 35 | >() 36 | 37 | expect( 38 | Handler.make(endpoint1, () => Effect.void).pipe(Handler.concat( 39 | Handler.make(endpoint2, () => Effect.void) 40 | )) 41 | ).type.toBe< 42 | Handler.Handler< 43 | ApiEndpoint.ApiEndpoint.Default<"endpoint1"> | ApiEndpoint.ApiEndpoint.Default<"endpoint2">, 44 | never, 45 | never 46 | > 47 | >() 48 | }) 49 | 50 | it("concat with errors and context", () => { 51 | expect( 52 | Handler.concat( 53 | Handler.make(endpoint1, () => eff1), 54 | Handler.make(endpoint2, () => Effect.void) 55 | ) 56 | ).type.toBe< 57 | Handler.Handler< 58 | ApiEndpoint.ApiEndpoint.Default<"endpoint1"> | ApiEndpoint.ApiEndpoint.Default<"endpoint2">, 59 | "E1", 60 | "R1" 61 | > 62 | >() 63 | 64 | expect( 65 | Handler.concat( 66 | Handler.make(endpoint1, () => eff1), 67 | Handler.make(endpoint2, () => eff2) 68 | ) 69 | ).type.toBe< 70 | Handler.Handler< 71 | ApiEndpoint.ApiEndpoint.Default<"endpoint1"> | ApiEndpoint.ApiEndpoint.Default<"endpoint2">, 72 | "E1" | "E2", 73 | "R1" | "R2" 74 | > 75 | >() 76 | 77 | expect( 78 | Handler.concatAll( 79 | Handler.make(endpoint1, () => eff1), 80 | Handler.make(endpoint2, () => eff2), 81 | Handler.make(endpoint3, () => eff3) 82 | ) 83 | ).type.toBe< 84 | Handler.Handler< 85 | | ApiEndpoint.ApiEndpoint.Default<"endpoint1"> 86 | | ApiEndpoint.ApiEndpoint.Default<"endpoint2"> 87 | | ApiEndpoint.ApiEndpoint.Default<"endpoint3">, 88 | "E1" | "E2" | "E3", 89 | "R1" | "R2" | "R3" 90 | > 91 | >() 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /packages/effect-http/dtslint/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "include": ["."], 4 | "compilerOptions": { 5 | "incremental": false, 6 | "composite": false, 7 | "noUnusedLocals": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/effect-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effect-http", 3 | "type": "module", 4 | "version": "0.87.0", 5 | "license": "MIT", 6 | "author": "Milan Suk ", 7 | "description": "High-level declarative HTTP API for effect-ts", 8 | "homepage": "https://sukovanej.github.io/effect-http", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/sukovanej/effect-http.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/sukovanej/effect-http/issues" 15 | }, 16 | "packageManager": "pnpm@9.1.1", 17 | "publishConfig": { 18 | "access": "public", 19 | "directory": "dist" 20 | }, 21 | "scripts": { 22 | "check": "tsc -b tsconfig.json", 23 | "check:watch": "tsc -b tsconfig.json --watch", 24 | "codegen": "build-utils prepare-v2", 25 | "build": "pnpm codegen && pnpm build-esm && pnpm build-cjs && pnpm build-annotate && build-utils pack-v2", 26 | "build-esm": "tsc -b tsconfig.build.json", 27 | "build-cjs": "babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps", 28 | "build-annotate": "babel build --plugins annotate-pure-calls --out-dir build --source-maps", 29 | "test": "vitest", 30 | "coverage": "vitest --coverage" 31 | }, 32 | "peerDependencies": { 33 | "@effect/platform": "^0.80.0", 34 | "effect": "^3.14.0" 35 | }, 36 | "devDependencies": { 37 | "@apidevtools/swagger-parser": "^10.1.1", 38 | "@effect/platform": "^0.80.1", 39 | "effect": "^3.14.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/effect-http/src/ApiGroup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Api groups. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Pipeable from "effect/Pipeable" 7 | import type * as Types from "effect/Types" 8 | 9 | import type * as ApiEndpoint from "./ApiEndpoint.js" 10 | import * as internal from "./internal/api-group.js" 11 | 12 | /** 13 | * @since 1.0.0 14 | * @category type id 15 | */ 16 | export const TypeId: unique symbol = internal.TypeId 17 | 18 | /** 19 | * @since 1.0.0 20 | * @category type id 21 | */ 22 | export type TypeId = typeof TypeId 23 | 24 | /** 25 | * @category models 26 | * @since 1.0.0 27 | */ 28 | export interface Options { 29 | readonly description?: string 30 | readonly externalDocs?: { 31 | readonly description?: string 32 | readonly url: string 33 | } 34 | } 35 | 36 | /** 37 | * @category models 38 | * @since 1.0.0 39 | */ 40 | export interface ApiGroup extends Pipeable.Pipeable, ApiGroup.Variance { 41 | readonly name: string 42 | readonly endpoints: ReadonlyArray 43 | readonly options: Options 44 | } 45 | 46 | /** 47 | * @category models 48 | * @since 1.0.0 49 | */ 50 | export declare namespace ApiGroup { 51 | /** 52 | * @category models 53 | * @since 1.0.0 54 | */ 55 | export interface Variance { 56 | readonly [TypeId]: { 57 | readonly _A: Types.Covariant 58 | } 59 | } 60 | 61 | /** 62 | * Any api group with `Endpoint = Endpoint.Any`. 63 | * 64 | * @category models 65 | * @since 1.0.0 66 | */ 67 | export type Any = ApiGroup 68 | 69 | /** 70 | * Default api group spec. 71 | * 72 | * @category models 73 | * @since 1.0.0 74 | */ 75 | export type Empty = ApiGroup 76 | 77 | /** 78 | * @category models 79 | * @since 1.0.0 80 | */ 81 | export type Context = [Endpoint] extends [ApiGroup] ? ApiEndpoint.ApiEndpoint.Context 82 | : never 83 | } 84 | 85 | /** 86 | * @category constructors 87 | * @since 1.0.0 88 | */ 89 | export const make: ( 90 | name: string, 91 | options?: Partial 92 | ) => ApiGroup.Empty = internal.make 93 | 94 | /** 95 | * @category modifications 96 | * @since 1.0.0 97 | */ 98 | export const addEndpoint: ( 99 | endpoint: E2 100 | ) => (api: ApiGroup) => ApiGroup = internal.addEndpoint 101 | 102 | export { 103 | /** 104 | * @category modifications 105 | * @since 1.0.0 106 | */ 107 | addResponse, 108 | /** 109 | * @category constructors 110 | * @since 1.0.0 111 | */ 112 | delete, 113 | /** 114 | * @category constructors 115 | * @since 1.0.0 116 | */ 117 | get, 118 | /** 119 | * @category constructors 120 | * @since 1.0.0 121 | */ 122 | make as endpoint, 123 | /** 124 | * @category constructors 125 | * @since 1.0.0 126 | */ 127 | patch, 128 | /** 129 | * @category constructors 130 | * @since 1.0.0 131 | */ 132 | post, 133 | /** 134 | * @category constructors 135 | * @since 1.0.0 136 | */ 137 | put, 138 | /** 139 | * @category modifications 140 | * @since 1.0.0 141 | */ 142 | setOptions as setEndpointOptions, 143 | /** 144 | * @category constructors 145 | * @since 1.0.0 146 | */ 147 | setRequest, 148 | /** 149 | * @category modifications 150 | * @since 1.0.0 151 | */ 152 | setRequestBody, 153 | /** 154 | * @category modifications 155 | * @since 1.0.0 156 | */ 157 | setRequestHeaders, 158 | /** 159 | * @category modifications 160 | * @since 1.0.0 161 | */ 162 | setRequestPath, 163 | /** 164 | * @category modifications 165 | * @since 1.0.0 166 | */ 167 | setRequestQuery, 168 | /** 169 | * @category modifications 170 | * @since 1.0.0 171 | */ 172 | setResponse, 173 | /** 174 | * @category modifications 175 | * @since 1.0.0 176 | */ 177 | setResponseBody, 178 | /** 179 | * @category modifications 180 | * @since 1.0.0 181 | */ 182 | setResponseHeaders, 183 | /** 184 | * @category modifications 185 | * @since 1.0.0 186 | */ 187 | setResponseRepresentations, 188 | /** 189 | * @category modifications 190 | * @since 1.0.0 191 | */ 192 | setResponseStatus, 193 | /** 194 | * @category modifications 195 | * @since 1.0.0 196 | */ 197 | setSecurity 198 | } from "./ApiEndpoint.js" 199 | 200 | /** 201 | * @category modifications 202 | * @since 1.0.0 203 | */ 204 | export const setOptions: ( 205 | options: Partial 206 | ) => (api: ApiGroup) => ApiGroup = internal.setOptions 207 | 208 | /** 209 | * @category refinements 210 | * @since 1.0.0 211 | */ 212 | export const isApiGroup: (u: unknown) => u is ApiGroup.Any = internal.isApiGroup 213 | -------------------------------------------------------------------------------- /packages/effect-http/src/ApiRequest.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP request declaration. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Schema from "effect/Schema" 7 | import type * as Types from "effect/Types" 8 | import type * as ApiSchema from "./ApiSchema.js" 9 | import * as internal from "./internal/api-request.js" 10 | 11 | /** 12 | * @category type id 13 | * @since 1.0.0 14 | */ 15 | export const TypeId: unique symbol = internal.TypeId 16 | 17 | /** 18 | * @category type id 19 | * @since 1.0.0 20 | */ 21 | export type TypeId = typeof TypeId 22 | 23 | /** 24 | * @category models 25 | * @since 1.0.0 26 | */ 27 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 28 | export interface ApiRequest extends ApiRequest.Variance {} 29 | 30 | /** 31 | * @category models 32 | * @since 1.0.0 33 | */ 34 | export declare namespace ApiRequest { 35 | /** 36 | * @category models 37 | * @since 1.0.0 38 | */ 39 | export interface Variance { 40 | readonly [TypeId]: { 41 | readonly _B: Types.Invariant 42 | readonly _P: Types.Invariant

43 | readonly _Q: Types.Invariant 44 | readonly _H: Types.Invariant 45 | readonly _R: Types.Covariant 46 | } 47 | } 48 | 49 | /** 50 | * Any request with all `Body`, `Path`, `Query` and `Headers` set to `any`. 51 | * 52 | * @category models 53 | * @since 1.0.0 54 | */ 55 | export type Any = ApiRequest 56 | 57 | /** 58 | * Default request. 59 | * 60 | * @category models 61 | * @since 1.0.0 62 | */ 63 | export type Default = ApiRequest 64 | 65 | /** 66 | * @category models 67 | * @since 1.0.0 68 | */ 69 | export type Body = [Request] extends [ApiRequest] ? B 70 | : never 71 | 72 | /** 73 | * @category models 74 | * @since 1.0.0 75 | */ 76 | export type Path = [Request] extends [ApiRequest] ? P 77 | : never 78 | 79 | /** 80 | * @category models 81 | * @since 1.0.0 82 | */ 83 | export type Query = [Request] extends [ApiRequest] ? Q 84 | : never 85 | 86 | /** 87 | * @category models 88 | * @since 1.0.0 89 | */ 90 | export type Headers = [Request] extends [ApiRequest] ? H 91 | : never 92 | 93 | /** 94 | * @category models 95 | * @since 1.0.0 96 | */ 97 | export type Context = [Request] extends [ApiRequest] ? R 98 | : never 99 | } 100 | 101 | /** 102 | * @category getters 103 | * @since 1.0.0 104 | */ 105 | export const getBodySchema: ( 106 | request: ApiRequest 107 | ) => Schema.Schema | ApiSchema.Ignored = internal.getBodySchema 108 | 109 | /** 110 | * @category getters 111 | * @since 1.0.0 112 | */ 113 | export const getPathSchema: ( 114 | request: ApiRequest 115 | ) => Schema.Schema | ApiSchema.Ignored = internal.getPathSchema 116 | 117 | /** 118 | * @category getters 119 | * @since 1.0.0 120 | */ 121 | export const getQuerySchema: ( 122 | request: ApiRequest 123 | ) => Schema.Schema | ApiSchema.Ignored = internal.getQuerySchema 124 | 125 | /** 126 | * @category getters 127 | * @since 1.0.0 128 | */ 129 | export const getHeadersSchema: ( 130 | request: ApiRequest 131 | ) => Schema.Schema | ApiSchema.Ignored = internal.getHeadersSchema 132 | 133 | /** 134 | * @category modifications 135 | * @since 1.0.0 136 | */ 137 | export const setBody: ( 138 | schema: Schema.Schema 139 | ) => <_, P, Q, H, R1>( 140 | endpoint: ApiRequest<_, P, Q, H, R1> 141 | ) => ApiRequest = internal.setBody 142 | 143 | /** 144 | * @category modifications 145 | * @since 1.0.0 146 | */ 147 | export const setPath: ( 148 | schema: Schema.Schema 149 | ) => ( 150 | endpoint: ApiRequest 151 | ) => ApiRequest = internal.setPath 152 | 153 | /** 154 | * @category modifications 155 | * @since 1.0.0 156 | */ 157 | export const setQuery: ( 158 | schema: Schema.Schema 159 | ) => ( 160 | endpoint: ApiRequest 161 | ) => ApiRequest = internal.setQuery 162 | 163 | /** 164 | * @category modifications 165 | * @since 1.0.0 166 | */ 167 | export const setHeaders: ( 168 | schema: Schema.Schema 169 | ) => ( 170 | endpoint: ApiRequest 171 | ) => ApiRequest = internal.setHeaders 172 | -------------------------------------------------------------------------------- /packages/effect-http/src/ApiResponse.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP response declaration. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Array from "effect/Array" 7 | import type * as Pipeable from "effect/Pipeable" 8 | import type * as Schema from "effect/Schema" 9 | import type * as Types from "effect/Types" 10 | import type * as ApiSchema from "./ApiSchema.js" 11 | import * as internal from "./internal/api-response.js" 12 | import type * as Representation from "./Representation.js" 13 | 14 | /** 15 | * @since 1.0.0 16 | * @category type id 17 | */ 18 | export const TypeId: unique symbol = internal.TypeId 19 | 20 | /** 21 | * @since 1.0.0 22 | * @category type id 23 | */ 24 | export type TypeId = typeof TypeId 25 | 26 | /** 27 | * @category models 28 | * @since 1.0.0 29 | */ 30 | export interface ApiResponse 31 | extends ApiResponse.Variance, Pipeable.Pipeable 32 | {} 33 | 34 | /** 35 | * @category models 36 | * @since 1.0.0 37 | */ 38 | export declare namespace ApiResponse { 39 | /** 40 | * @category models 41 | * @since 1.0.0 42 | */ 43 | export interface Variance { 44 | readonly [TypeId]: { 45 | readonly _S: Types.Covariant 46 | readonly _B: Types.Invariant 47 | readonly _H: Types.Invariant 48 | readonly _R: Types.Covariant 49 | } 50 | } 51 | 52 | /** 53 | * @category models 54 | * @since 1.0.0 55 | */ 56 | export type AnyStatus = number 57 | 58 | /** 59 | * Any request with all `Body`, `Path`, `Query` and `Headers` set to `Schema.Schema.Any`. 60 | * 61 | * @category models 62 | * @since 1.0.0 63 | */ 64 | export type Any = ApiResponse 65 | 66 | /** 67 | * Default response. 68 | * 69 | * @category models 70 | * @since 1.0.0 71 | */ 72 | export type Default = ApiResponse<200, ApiSchema.Ignored, ApiSchema.Ignored, never> 73 | 74 | /** 75 | * @category models 76 | * @since 1.0.0 77 | */ 78 | export type Status = [Request] extends [ApiResponse] ? S 79 | : never 80 | 81 | /** 82 | * @category models 83 | * @since 1.0.0 84 | */ 85 | export type Body = [Request] extends [ApiResponse] ? B 86 | : never 87 | 88 | /** 89 | * @category models 90 | * @since 1.0.0 91 | */ 92 | export type Headers = [Request] extends [ApiResponse] ? H 93 | : never 94 | 95 | /** 96 | * @category models 97 | * @since 1.0.0 98 | */ 99 | export type Context = [Request] extends [ApiResponse] ? R 100 | : never 101 | } 102 | 103 | /** 104 | * @category refinements 105 | * @since 1.0.0 106 | */ 107 | export const isApiResponse: ( 108 | u: unknown 109 | ) => u is ApiResponse = internal.isApiResponse 110 | 111 | /** 112 | * @category constructors 113 | * @since 1.0.0 114 | */ 115 | export const make: ( 116 | status: S, 117 | body?: Schema.Schema | ApiSchema.Ignored, 118 | headers?: Schema.Schema | ApiSchema.Ignored, 119 | representations?: Array.NonEmptyReadonlyArray 120 | ) => ApiResponse = internal.make 121 | 122 | /** 123 | * @category modifications 124 | * @since 1.0.0 125 | */ 126 | export const setStatus: (schema: S) => <_ extends ApiResponse.AnyStatus, B, H, R>( 127 | response: ApiResponse<_, B, H, R> 128 | ) => ApiResponse = internal.setStatus 129 | 130 | /** 131 | * @category modifications 132 | * @since 1.0.0 133 | */ 134 | export const setBody: (schema: Schema.Schema) => ( 135 | response: ApiResponse 136 | ) => ApiResponse = internal.setBody 137 | 138 | /** 139 | * @category modifications 140 | * @since 1.0.0 141 | */ 142 | export const setHeaders: (schema: Schema.Schema) => ( 143 | response: ApiResponse 144 | ) => ApiResponse = internal.setHeaders 145 | 146 | /** 147 | * @category modifications 148 | * @since 1.0.0 149 | */ 150 | export const setRepresentations: ( 151 | representations: Array.NonEmptyReadonlyArray 152 | ) => ( 153 | response: ApiResponse 154 | ) => ApiResponse = internal.setRepresentations 155 | 156 | /** 157 | * @category getters 158 | * @since 1.0.0 159 | */ 160 | export const getStatus: ( 161 | response: ApiResponse 162 | ) => S = internal.getStatus 163 | 164 | /** 165 | * @category getters 166 | * @since 1.0.0 167 | */ 168 | export const getBodySchema: ( 169 | response: ApiResponse 170 | ) => Schema.Schema | ApiSchema.Ignored = internal.getBodySchema 171 | 172 | /** 173 | * @category getters 174 | * @since 1.0.0 175 | */ 176 | export const getHeadersSchema: ( 177 | response: ApiResponse 178 | ) => Schema.Schema | ApiSchema.Ignored = internal.getHeadersSchema 179 | 180 | /** 181 | * @category getters 182 | * @since 1.0.0 183 | */ 184 | export const getRepresentations: ( 185 | response: ApiResponse 186 | ) => Array.NonEmptyReadonlyArray = internal.getRepresentations 187 | -------------------------------------------------------------------------------- /packages/effect-http/src/ApiSchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @since 1.0.0 3 | */ 4 | import type * as Schema from "effect/Schema" 5 | import * as internal from "./internal/api-schema.js" 6 | 7 | /** 8 | * @category type id 9 | * @since 1.0.0 10 | */ 11 | export const IgnoredId: unique symbol = internal.IgnoredId 12 | 13 | /** 14 | * @category models 15 | * @since 1.0.0 16 | */ 17 | export type IgnoredId = typeof IgnoredId 18 | 19 | /** 20 | * @category type id 21 | * @since 1.0.0 22 | */ 23 | export const Ignored: Ignored = internal.Ignored 24 | 25 | /** 26 | * @category type id 27 | * @since 1.0.0 28 | */ 29 | export interface Ignored { 30 | readonly [IgnoredId]: IgnoredId 31 | } 32 | 33 | /** 34 | * @category refinements 35 | * @since 1.0.0 36 | */ 37 | export const isIgnored: (u: unknown) => u is Ignored = internal.isIgnored 38 | 39 | /** 40 | * FormData schema 41 | * 42 | * @category schemas 43 | * @since 1.0.0 44 | */ 45 | export const FormData: Schema.Schema = internal.formDataSchema 46 | -------------------------------------------------------------------------------- /packages/effect-http/src/Client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module exposes the `client` combinator which accepts an `Api` instance 3 | * and it generates a client-side implementation. The generated implementation 4 | * is type-safe and guarantees compatibility of the client and server side. 5 | * 6 | * @since 1.0.0 7 | */ 8 | import type * as HttpClient from "@effect/platform/HttpClient" 9 | import type * as HttpClientRequest from "@effect/platform/HttpClientRequest" 10 | import type * as Effect from "effect/Effect" 11 | import type * as Types from "effect/Types" 12 | 13 | import type * as Api from "./Api.js" 14 | import type * as ApiEndpoint from "./ApiEndpoint.js" 15 | import type * as ApiResponse from "./ApiResponse.js" 16 | import type * as ClientError from "./ClientError.js" 17 | import type * as Handler from "./Handler.js" 18 | import * as internal from "./internal/client.js" 19 | 20 | /** 21 | * @category models 22 | * @since 1.0.0 23 | */ 24 | export type Client = Types.Simplify< 25 | { 26 | [Id in Api.Api.Ids]: Client.Function> 27 | } 28 | > 29 | 30 | /** 31 | * @category models 32 | * @since 1.0.0 33 | */ 34 | export interface Options { 35 | httpClient?: HttpClient.HttpClient 36 | baseUrl?: string 37 | } 38 | 39 | /** 40 | * @category models 41 | * @since 1.0.0 42 | */ 43 | export declare namespace Client { 44 | /** 45 | * @category models 46 | * @since 1.0.0 47 | */ 48 | export type Function = ( 49 | input: Handler.Handler.ToRequest>, 50 | map?: (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 51 | ) => Effect.Effect< 52 | Handler.Handler.ToResponse>, 53 | ClientError.ClientError, 54 | ApiEndpoint.ApiEndpoint.ContextNoSecurity 55 | > 56 | } 57 | 58 | /** 59 | * @category constructors 60 | * @since 1.0.0 61 | */ 62 | export const endpointClient: >( 63 | id: Id, 64 | api: A, 65 | options: Partial 66 | ) => Client.Function> = internal.endpointClient 67 | 68 | /** 69 | * Derive client implementation from the `Api` 70 | * 71 | * @category constructors 72 | * @since 1.0.0 73 | */ 74 | export const make: ( 75 | api: A, 76 | options?: Partial 77 | ) => Client = internal.make 78 | 79 | /** 80 | * @category auth 81 | * @since 1.0.0 82 | */ 83 | export const setBasic: { 84 | (user: string, pass: string): (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 85 | (request: HttpClientRequest.HttpClientRequest, user: string, pass: string): HttpClientRequest.HttpClientRequest 86 | } = internal.setBasicAuth 87 | 88 | /** 89 | * @category auth 90 | * @since 1.0.0 91 | */ 92 | export const setBearer: { 93 | (token: string): (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 94 | (request: HttpClientRequest.HttpClientRequest, token: string): HttpClientRequest.HttpClientRequest 95 | } = internal.setBearer 96 | 97 | /** 98 | * @category auth 99 | * @since 1.0.0 100 | */ 101 | export const setApiKey: { 102 | ( 103 | key: string, 104 | _in: "query" | "header", 105 | apiKey: string 106 | ): (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 107 | ( 108 | request: HttpClientRequest.HttpClientRequest, 109 | key: string, 110 | _in: "query" | "header", 111 | apiKey: string 112 | ): HttpClientRequest.HttpClientRequest 113 | } = internal.setApiKey 114 | 115 | /** 116 | * @category models 117 | * @since 1.0.0 118 | */ 119 | export interface Response { 120 | status: S 121 | body: B 122 | headers: H 123 | } 124 | -------------------------------------------------------------------------------- /packages/effect-http/src/ClientError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Models for errors being created on the client side. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Cause from "effect/Cause" 7 | import type * as ParseResult from "effect/ParseResult" 8 | 9 | import * as internal from "./internal/client-error.js" 10 | 11 | /** 12 | * @since 1.0.0 13 | * @category type id 14 | */ 15 | export const ClientSideErrorTypeId: unique symbol = internal.ClientSideErrorTypeId 16 | 17 | /** 18 | * @since 1.0.0 19 | * @category type id 20 | */ 21 | export type ClientSideErrorTypeId = typeof ClientSideErrorTypeId 22 | 23 | /** 24 | * @since 1.0.0 25 | * @category type id 26 | */ 27 | export const ServerSideErrorTypeId: unique symbol = internal.ServerSideErrorTypeId 28 | 29 | /** 30 | * @since 1.0.0 31 | * @category type id 32 | */ 33 | export type ServerSideErrorTypeId = typeof ServerSideErrorTypeId 34 | 35 | /** 36 | * @category models 37 | * @since 1.0.0 38 | */ 39 | export type ClientError = 40 | | ClientErrorClientSide 41 | | ClientErrorServerSide 42 | 43 | /** 44 | * @category models 45 | * @since 1.0.0 46 | */ 47 | export interface ClientErrorClientSide extends Cause.YieldableError { 48 | readonly [ClientSideErrorTypeId]: object 49 | readonly _tag: "ClientError" 50 | readonly message: string 51 | readonly error: unknown 52 | readonly side: "client" 53 | } 54 | 55 | /** 56 | * @category models 57 | * @since 1.0.0 58 | */ 59 | export interface ClientErrorServerSide extends Cause.YieldableError { 60 | readonly [ServerSideErrorTypeId]: object 61 | readonly _tag: "ClientError" 62 | readonly message: string 63 | readonly error: unknown 64 | readonly status: S 65 | readonly side: "server" 66 | } 67 | 68 | /** 69 | * @category constructors 70 | * @since 1.0.0 71 | */ 72 | export const makeClientSide: (error: unknown, message?: string) => ClientErrorClientSide = internal.makeClientSide 73 | 74 | /** 75 | * @category constructors 76 | * @since 1.0.0 77 | */ 78 | export const makeServerSide: ( 79 | error: unknown, 80 | status: S, 81 | message?: string 82 | ) => ClientErrorServerSide = internal.makeServerSide 83 | 84 | /** 85 | * @category constructors 86 | * @since 1.0.0 87 | */ 88 | export const makeClientSideRequestValidation: ( 89 | location: string 90 | ) => (error: ParseResult.ParseError) => ClientErrorClientSide = internal.makeClientSideRequestValidation 91 | 92 | /** 93 | * @category constructors 94 | * @since 1.0.0 95 | */ 96 | export const makeClientSideResponseValidation: ( 97 | location: string 98 | ) => (error: ParseResult.ParseError) => ClientErrorClientSide = internal.makeClientSideResponseValidation 99 | 100 | /** 101 | * @category refinements 102 | * @since 1.0.0 103 | */ 104 | export const isClientSideError: (u: unknown) => u is ClientErrorClientSide = internal.isClientSideError 105 | 106 | /** 107 | * @category refinements 108 | * @since 1.0.0 109 | */ 110 | export const isServerSideError: (u: unknown) => u is ClientErrorServerSide = internal.isServerSideError 111 | -------------------------------------------------------------------------------- /packages/effect-http/src/ExampleServer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The `ExampleServer.make` function generates a `RouterBuilder` implementation based 3 | * on an instance of `Api`. The listening server will perform all the 4 | * request and response validations similarly to a real implementation. 5 | * 6 | * Responses returned from the server are generated randomly using the 7 | * response `Schema`. 8 | * 9 | * @since 1.0.0 10 | */ 11 | import type * as Effect from "effect/Effect" 12 | import type * as Schema from "effect/Schema" 13 | 14 | import type * as Api from "./Api.js" 15 | import type * as ApiEndpoint from "./ApiEndpoint.js" 16 | import * as internal from "./internal/example-server.js" 17 | import type * as RouterBuilder from "./RouterBuilder.js" 18 | 19 | /** 20 | * Generate an example `RouterBuilder` implementation. 21 | * 22 | * @category utils 23 | * @since 1.0.0 24 | */ 25 | export const make: ( 26 | api: A 27 | ) => RouterBuilder.RouterBuilder, never, never> = internal.make 28 | 29 | /** 30 | * Create an example implementation for a single endpoint. 31 | * 32 | * @category utils 33 | * @since 1.0.0 34 | */ 35 | export const handle: < 36 | A extends ApiEndpoint.ApiEndpoint.Any, 37 | Id extends ApiEndpoint.ApiEndpoint.Id 38 | >( 39 | id: Id 40 | ) => ( 41 | routerBuilder: RouterBuilder.RouterBuilder 42 | ) => RouterBuilder.RouterBuilder< 43 | ApiEndpoint.ApiEndpoint.ExcludeById, 44 | E, 45 | R | ApiEndpoint.ApiEndpoint.Context> 46 | > = internal.handle 47 | 48 | /** 49 | * Create an example implementation for all remaining endpoints. 50 | * 51 | * @category utils 52 | * @since 1.0.0 53 | */ 54 | export const handleRemaining: ( 55 | routerBuilder: RouterBuilder.RouterBuilder 56 | ) => RouterBuilder.RouterBuilder> = internal.handleRemaining 57 | 58 | /** 59 | * Generate an example value for the given schema. 60 | * 61 | * @category utils 62 | * @since 1.0.0 63 | */ 64 | export const makeSchema: ( 65 | schema: Schema.Schema 66 | ) => Effect.Effect = internal.makeSchema 67 | -------------------------------------------------------------------------------- /packages/effect-http/src/Middlewares.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mechanism for extendning behaviour of all handlers on the server. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as HttpApp from "@effect/platform/HttpApp" 7 | import type * as LogLevel from "effect/LogLevel" 8 | 9 | import * as internal from "./internal/middlewares.js" 10 | 11 | /** 12 | * Add access logs for handled requests. The log runs before each request. 13 | * Optionally configure log level using the first argument. The default log level 14 | * is `Debug`. 15 | * 16 | * @category logging 17 | * @since 1.0.0 18 | */ 19 | export const accessLog: ( 20 | level?: LogLevel.LogLevel 21 | ) => (app: HttpApp.Default) => HttpApp.Default = internal.accessLog 22 | 23 | /** 24 | * Annotate request logs using generated UUID. The default annotation key is `requestId`. 25 | * The annotation key is configurable using the first argument. 26 | * 27 | * Note that in order to apply the annotation also for access logging, you should 28 | * make sure the `accessLog` middleware is plugged after the `uuidLogAnnotation`. 29 | * 30 | * @category logging 31 | * @since 1.0.0 32 | */ 33 | export const uuidLogAnnotation: ( 34 | logAnnotationKey?: string 35 | ) => (app: HttpApp.Default) => HttpApp.Default = internal.uuidLogAnnotation 36 | 37 | /** 38 | * Logs out a handler failure. 39 | * 40 | * @category logging 41 | * @since 1.0.0 42 | */ 43 | export const errorLog: (app: HttpApp.Default) => HttpApp.Default = internal.errorLog 44 | -------------------------------------------------------------------------------- /packages/effect-http/src/MockClient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `Client` implementation derivation for testing purposes. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Api from "./Api.js" 7 | import type * as ApiEndpoint from "./ApiEndpoint.js" 8 | import type * as Client from "./Client.js" 9 | import type * as Handler from "./Handler.js" 10 | import * as internal from "./internal/mock-client.js" 11 | 12 | /** 13 | * @category models 14 | * @since 1.0.0 15 | */ 16 | export type Options = { 17 | responses: { 18 | [Id in Api.Api.Ids]: Handler.Handler.ToResponse< 19 | ApiEndpoint.ApiEndpoint.Response> 20 | > 21 | } 22 | } 23 | 24 | /** 25 | * Derive mock client implementation from the `Api` 26 | * 27 | * @category constructors 28 | * @since 1.0.0 29 | */ 30 | export const make: ( 31 | api: A, 32 | option?: Partial> 33 | ) => Client.Client = internal.make 34 | -------------------------------------------------------------------------------- /packages/effect-http/src/OpenApi.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Derivation of `OpenApi` schema from an instance of `Api`. 3 | * 4 | * @since 1.0.0 5 | */ 6 | 7 | import type * as Schema from "effect/Schema" 8 | 9 | import type * as Api from "./Api.js" 10 | import * as internal from "./internal/open-api.js" 11 | import type * as OpenApiTypes from "./OpenApiTypes.js" 12 | 13 | /** 14 | * Generate OpenApi specification for the Api. 15 | * 16 | * @category constructors 17 | * @since 1.0.0 18 | */ 19 | export const make: (api: Api.Api.Any) => OpenApiTypes.OpenAPISpec = internal.make 20 | 21 | /** 22 | * Generate OpenApi specification for the Api. 23 | * 24 | * @category constructors 25 | * @since 1.0.0 26 | */ 27 | export const makeSchema: (api: Schema.Schema.AnyNoContext) => OpenApiTypes.OpenAPISchemaType = internal.makeSchema 28 | 29 | /** 30 | * Set up an OpenAPI for the given schema. 31 | * 32 | * @category combining 33 | * @since 1.0.0 34 | */ 35 | export const annotate: ( 36 | annotation: ( 37 | compiler: (schema: Schema.Schema.Any) => OpenApiTypes.OpenAPISchemaType 38 | ) => OpenApiTypes.OpenAPISchemaType 39 | ) => (self: S) => Schema.Annotable.Self = internal.annotate 40 | -------------------------------------------------------------------------------- /packages/effect-http/src/QuerySchema.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP query parameters. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as Schema from "effect/Schema" 7 | 8 | import * as internal from "./internal/query-schema.js" 9 | 10 | /** 11 | * @category schema 12 | * @since 1.0.0 13 | */ 14 | export const Number: Schema.Schema = internal.Number 15 | 16 | /** 17 | * @category schema 18 | * @since 1.0.0 19 | */ 20 | export const number: (schema: Schema.Schema) => Schema.Schema = internal.number 21 | 22 | /** 23 | * @category schema 24 | * @since 1.0.0 25 | */ 26 | export const Int: Schema.Schema = internal.Int 27 | 28 | /** 29 | * @category schema 30 | * @since 1.0.0 31 | */ 32 | export const int: (schema: Schema.Schema) => Schema.Schema = internal.int 33 | 34 | /** 35 | * @category schema 36 | * @since 1.0.0 37 | */ 38 | export const Array: ( 39 | schema: Schema.Schema 40 | ) => Schema.optionalWith< 41 | Schema.Schema, string | ReadonlyArray, R>, 42 | { exact: true; default: () => [] } 43 | > = internal.Array 44 | 45 | /** 46 | * @category schema 47 | * @since 1.0.0 48 | */ 49 | export const NonEmptyArray: ( 50 | schema: Schema.Schema 51 | ) => Schema.Schema], string | readonly [string, ...Array], R> = internal.NonEmptyArray 52 | -------------------------------------------------------------------------------- /packages/effect-http/src/Representation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * `Representation` is a data structure holding information about how to 3 | * serialize and deserialize a server response for a given conten type. 4 | * 5 | * @since 1.0.0 6 | */ 7 | import type * as Cause from "effect/Cause" 8 | import type * as Effect from "effect/Effect" 9 | import type * as Pipeable from "effect/Pipeable" 10 | 11 | import * as internal from "./internal/representation.js" 12 | 13 | /** 14 | * @category type id 15 | * @since 1.0.0 16 | */ 17 | export const TypeId: unique symbol = internal.TypeId 18 | 19 | /** 20 | * @category type id 21 | * @since 1.0.0 22 | */ 23 | export type TypeId = typeof TypeId 24 | 25 | /** 26 | * @category models 27 | * @since 1.0.0 28 | */ 29 | export interface Representation extends Pipeable.Pipeable { 30 | readonly [TypeId]: TypeId 31 | readonly stringify: ( 32 | input: unknown 33 | ) => Effect.Effect 34 | readonly parse: ( 35 | input: string 36 | ) => Effect.Effect 37 | contentType: string 38 | } 39 | 40 | /** 41 | * @category errors 42 | * @since 1.0.0 43 | */ 44 | export interface RepresentationError extends Cause.YieldableError { 45 | readonly _tag: "RepresentationError" 46 | readonly message: string 47 | } 48 | 49 | /** 50 | * @category constructors 51 | * @since 1.0.0 52 | */ 53 | export const make: (fields: Omit) => Representation = internal.make 54 | 55 | /** 56 | * @category representations 57 | * @since 1.0.0 58 | */ 59 | export const json: Representation = internal.json 60 | 61 | /** 62 | * @category representations 63 | * @since 1.0.0 64 | */ 65 | export const plainText: Representation = internal.plainText 66 | -------------------------------------------------------------------------------- /packages/effect-http/src/SwaggerRouter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a router serving Swagger files. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import type * as HttpRouter from "@effect/platform/HttpRouter" 7 | import type * as Context from "effect/Context" 8 | 9 | import * as internal from "./internal/swagger-router.js" 10 | 11 | /** 12 | * @category models 13 | * @since 1.0.0 14 | */ 15 | export interface SwaggerFiles { 16 | files: Record 17 | } 18 | 19 | /** 20 | * @category context 21 | * @since 1.0.0 22 | */ 23 | export const SwaggerFiles: Context.Tag = internal.SwaggerFiles 24 | 25 | /** 26 | * @category constructors 27 | * @since 1.0.0 28 | */ 29 | export const make: (spec: unknown) => HttpRouter.HttpRouter = internal.make 30 | -------------------------------------------------------------------------------- /packages/effect-http/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP API service declaration. 3 | * 4 | * @since 1.0.0 5 | */ 6 | export * as Api from "./Api.js" 7 | 8 | /** 9 | * HTTP endpoint declaration. 10 | * 11 | * @since 1.0.0 12 | */ 13 | export * as ApiEndpoint from "./ApiEndpoint.js" 14 | 15 | /** 16 | * Api groups. 17 | * 18 | * @since 1.0.0 19 | */ 20 | export * as ApiGroup from "./ApiGroup.js" 21 | 22 | /** 23 | * HTTP request declaration. 24 | * 25 | * @since 1.0.0 26 | */ 27 | export * as ApiRequest from "./ApiRequest.js" 28 | 29 | /** 30 | * HTTP response declaration. 31 | * 32 | * @since 1.0.0 33 | */ 34 | export * as ApiResponse from "./ApiResponse.js" 35 | 36 | /** 37 | * @since 1.0.0 38 | */ 39 | export * as ApiSchema from "./ApiSchema.js" 40 | 41 | /** 42 | * This module exposes the `client` combinator which accepts an `Api` instance 43 | * and it generates a client-side implementation. The generated implementation 44 | * is type-safe and guarantees compatibility of the client and server side. 45 | * 46 | * @since 1.0.0 47 | */ 48 | export * as Client from "./Client.js" 49 | 50 | /** 51 | * Models for errors being created on the client side. 52 | * 53 | * @since 1.0.0 54 | */ 55 | export * as ClientError from "./ClientError.js" 56 | 57 | /** 58 | * The `ExampleServer.make` function generates a `RouterBuilder` implementation based 59 | * on an instance of `Api`. The listening server will perform all the 60 | * request and response validations similarly to a real implementation. 61 | * 62 | * Responses returned from the server are generated randomly using the 63 | * response `Schema`. 64 | * 65 | * @since 1.0.0 66 | */ 67 | export * as ExampleServer from "./ExampleServer.js" 68 | 69 | /** 70 | * Server side handler implementation of an endpoint. 71 | * 72 | * @since 1.0.0 73 | */ 74 | export * as Handler from "./Handler.js" 75 | 76 | /** 77 | * HTTP errors and redirection messages based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Status. 78 | * 79 | * @since 1.0.0 80 | */ 81 | export * as HttpError from "./HttpError.js" 82 | 83 | /** 84 | * Mechanism for extendning behaviour of all handlers on the server. 85 | * 86 | * @since 1.0.0 87 | */ 88 | export * as Middlewares from "./Middlewares.js" 89 | 90 | /** 91 | * `Client` implementation derivation for testing purposes. 92 | * 93 | * @since 1.0.0 94 | */ 95 | export * as MockClient from "./MockClient.js" 96 | 97 | /** 98 | * Derivation of `OpenApi` schema from an instance of `Api`. 99 | * 100 | * @since 1.0.0 101 | */ 102 | export * as OpenApi from "./OpenApi.js" 103 | 104 | /** 105 | * @category models 106 | * @since 1.0.0 107 | */ 108 | export * as OpenApiTypes from "./OpenApiTypes.js" 109 | 110 | /** 111 | * HTTP query parameters. 112 | * 113 | * @since 1.0.0 114 | */ 115 | export * as QuerySchema from "./QuerySchema.js" 116 | 117 | /** 118 | * `Representation` is a data structure holding information about how to 119 | * serialize and deserialize a server response for a given conten type. 120 | * 121 | * @since 1.0.0 122 | */ 123 | export * as Representation from "./Representation.js" 124 | 125 | /** 126 | * Build a `Router` satisfying an `Api.Api`. 127 | * 128 | * @since 1.0.0 129 | */ 130 | export * as RouterBuilder from "./RouterBuilder.js" 131 | 132 | /** 133 | * Authentication and authorization. 134 | * @since 1.0.0 135 | */ 136 | export * as Security from "./Security.js" 137 | 138 | /** 139 | * Create a router serving Swagger files. 140 | * 141 | * @since 1.0.0 142 | */ 143 | export * as SwaggerRouter from "./SwaggerRouter.js" 144 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/api-group.ts: -------------------------------------------------------------------------------- 1 | import * as Pipeable from "effect/Pipeable" 2 | 3 | import type * as Api from "../Api.js" 4 | import * as ApiEndpoint from "../ApiEndpoint.js" 5 | import type * as ApiGroup from "../ApiGroup.js" 6 | import * as api_endpoint from "./api-endpoint.js" 7 | 8 | /** @internal */ 9 | export const TypeId: ApiGroup.TypeId = Symbol.for( 10 | "effect-http/Api/ApiGroupTypeId" 11 | ) as ApiGroup.TypeId 12 | 13 | /** @internal */ 14 | export const variance = { 15 | /* c8 ignore next */ 16 | _A: (_: never) => _ 17 | } 18 | 19 | /** @internal */ 20 | export class ApiGroupImpl implements ApiGroup.ApiGroup { 21 | readonly [TypeId] = variance 22 | 23 | constructor( 24 | readonly name: string, 25 | readonly endpoints: ReadonlyArray, 26 | readonly options: ApiGroup.Options 27 | ) {} 28 | 29 | pipe() { 30 | return Pipeable.pipeArguments(this, arguments) 31 | } 32 | } 33 | 34 | /** @internal */ 35 | export const isApiGroup = (u: unknown): u is ApiGroup.ApiGroup.Any => typeof u === "object" && u !== null && TypeId in u 36 | 37 | /** @internal */ 38 | export const make = (name: string, options?: Partial): ApiGroup.ApiGroup.Empty => 39 | new ApiGroupImpl(name, [], { ...options }) 40 | 41 | /** @internal */ 42 | export const validateNewEndpoint = ( 43 | self: ApiGroup.ApiGroup.Any, 44 | endpoint: ApiEndpoint.ApiEndpoint.Any 45 | ) => { 46 | if (self.endpoints.some((e) => ApiEndpoint.getId(e) === ApiEndpoint.getId(endpoint))) { 47 | throw new Error(`Endpoint with id ${ApiEndpoint.getId(endpoint)} already exists`) 48 | } 49 | } 50 | 51 | /** @internal */ 52 | export const addEndpoint = 53 | (endpoint: E2) => 54 | (self: ApiGroup.ApiGroup): ApiGroup.ApiGroup => { 55 | api_endpoint.validateEndpoint(endpoint) 56 | validateNewEndpoint(self, endpoint) 57 | 58 | return new ApiGroupImpl(self.name, [...self.endpoints, endpoint], self.options) 59 | } 60 | 61 | /** @internal */ 62 | export const setOptions = 63 | (options: Partial) => 64 | (self: ApiGroup.ApiGroup): ApiGroup.ApiGroup => 65 | new ApiGroupImpl(self.name, self.endpoints, { ...self.options, ...options }) 66 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/api-request.ts: -------------------------------------------------------------------------------- 1 | import * as Pipeable from "effect/Pipeable" 2 | import type * as Schema from "effect/Schema" 3 | import type * as ApiRequest from "../ApiRequest.js" 4 | import * as ApiSchema from "../ApiSchema.js" 5 | 6 | /** @internal */ 7 | export const TypeId: ApiRequest.TypeId = Symbol.for( 8 | "effect-http/Api/RequestTypeId" 9 | ) as ApiRequest.TypeId 10 | 11 | /** @internal */ 12 | export const variance = { 13 | /* c8 ignore next */ 14 | _B: (_: any) => _, 15 | /* c8 ignore next */ 16 | _P: (_: any) => _, 17 | /* c8 ignore next */ 18 | _Q: (_: any) => _, 19 | /* c8 ignore next */ 20 | _H: (_: any) => _, 21 | /* c8 ignore next */ 22 | _R: (_: never) => _ 23 | } 24 | 25 | /** @internal */ 26 | export class ApiRequestImpl implements ApiRequest.ApiRequest { 27 | readonly [TypeId] = variance 28 | 29 | constructor( 30 | readonly body: Schema.Schema | ApiSchema.Ignored, 31 | readonly path: Schema.Schema | ApiSchema.Ignored, 32 | readonly query: Schema.Schema | ApiSchema.Ignored, 33 | readonly headers: Schema.Schema | ApiSchema.Ignored 34 | ) {} 35 | 36 | pipe() { 37 | return Pipeable.pipeArguments(this, arguments) 38 | } 39 | } 40 | 41 | /** @internal */ 42 | export const defaultRequest: ApiRequest.ApiRequest.Default = new ApiRequestImpl( 43 | ApiSchema.Ignored, 44 | ApiSchema.Ignored, 45 | ApiSchema.Ignored, 46 | ApiSchema.Ignored 47 | ) 48 | 49 | /** @internal */ 50 | export const getBodySchema = ( 51 | request: ApiRequest.ApiRequest 52 | ): Schema.Schema | ApiSchema.Ignored => (request as ApiRequestImpl).body 53 | 54 | /** @internal */ 55 | export const getPathSchema = ( 56 | request: ApiRequest.ApiRequest 57 | ): Schema.Schema | ApiSchema.Ignored => (request as ApiRequestImpl).path 58 | 59 | /** @internal */ 60 | export const getQuerySchema = ( 61 | request: ApiRequest.ApiRequest 62 | ): Schema.Schema | ApiSchema.Ignored => (request as ApiRequestImpl).query 63 | 64 | /** @internal */ 65 | export const getHeadersSchema = ( 66 | request: ApiRequest.ApiRequest 67 | ): Schema.Schema | ApiSchema.Ignored => (request as ApiRequestImpl).headers 68 | 69 | /** @internal */ 70 | export const setBody = 71 | (schema: Schema.Schema) => 72 | <_, P, Q, H, R1>(request: ApiRequest.ApiRequest<_, P, Q, H, R1>): ApiRequest.ApiRequest => 73 | new ApiRequestImpl( 74 | schema, 75 | getPathSchema(request), 76 | getQuerySchema(request), 77 | getHeadersSchema(request) 78 | ) 79 | 80 | /** @internal */ 81 | export const setPath = (schema: Schema.Schema) => 82 | ( 83 | request: ApiRequest.ApiRequest 84 | ): ApiRequest.ApiRequest => 85 | new ApiRequestImpl( 86 | getBodySchema(request), 87 | schema, 88 | getQuerySchema(request), 89 | getHeadersSchema(request) 90 | ) 91 | 92 | /** @internal */ 93 | export const setQuery = 94 | (schema: Schema.Schema) => 95 | (request: ApiRequest.ApiRequest): ApiRequest.ApiRequest => 96 | new ApiRequestImpl( 97 | getBodySchema(request), 98 | getPathSchema(request), 99 | schema, 100 | getHeadersSchema(request) 101 | ) 102 | 103 | /** @internal */ 104 | export const setHeaders = (schema: Schema.Schema) => 105 | ( 106 | request: ApiRequest.ApiRequest 107 | ): ApiRequest.ApiRequest => 108 | new ApiRequestImpl( 109 | getBodySchema(request), 110 | getPathSchema(request), 111 | getQuerySchema(request), 112 | schema // TODO: transform schema properties to lowercase 113 | ) 114 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/api-response.ts: -------------------------------------------------------------------------------- 1 | import type * as Array from "effect/Array" 2 | import * as Pipeable from "effect/Pipeable" 3 | import * as Predicate from "effect/Predicate" 4 | import type * as Schema from "effect/Schema" 5 | 6 | import type * as ApiResponse from "../ApiResponse.js" 7 | import * as ApiSchema from "../ApiSchema.js" 8 | import * as Representation from "../Representation.js" 9 | 10 | /** @internal */ 11 | export const TypeId: ApiResponse.TypeId = Symbol.for( 12 | "effect-http/Api/ResponseTypeId" 13 | ) as ApiResponse.TypeId 14 | 15 | /** @internal */ 16 | export const variance = { 17 | /* c8 ignore next */ 18 | _S: (_: any) => _, 19 | /* c8 ignore next */ 20 | _B: (_: any) => _, 21 | /* c8 ignore next */ 22 | _H: (_: any) => _, 23 | /* c8 ignore next */ 24 | _R: (_: never) => _ 25 | } 26 | 27 | /** @internal */ 28 | class ApiResponseImpl 29 | implements ApiResponse.ApiResponse 30 | { 31 | readonly [TypeId] = variance 32 | 33 | constructor( 34 | readonly status: S, 35 | readonly body: Schema.Schema | ApiSchema.Ignored, 36 | readonly headers: Schema.Schema | ApiSchema.Ignored, 37 | readonly representations: Array.NonEmptyReadonlyArray 38 | ) {} 39 | 40 | pipe() { 41 | return Pipeable.pipeArguments(this, arguments) 42 | } 43 | } 44 | 45 | /** @internal */ 46 | const defaultRepresentations = [Representation.json] as const 47 | 48 | /** @internal */ 49 | export const defaultResponse: ApiResponse.ApiResponse.Default = new ApiResponseImpl( 50 | 200, 51 | ApiSchema.Ignored, 52 | ApiSchema.Ignored, 53 | defaultRepresentations 54 | ) 55 | 56 | /** @internal */ 57 | export const isApiResponse = ( 58 | u: unknown 59 | ): u is ApiResponse.ApiResponse => Predicate.hasProperty(u, TypeId) && Predicate.isObject(u[TypeId]) 60 | 61 | /** @internal */ 62 | export const make = ( 63 | status: S, 64 | body?: Schema.Schema | ApiSchema.Ignored, 65 | headers?: Schema.Schema | ApiSchema.Ignored, 66 | representations?: Array.NonEmptyReadonlyArray 67 | ): ApiResponse.ApiResponse => 68 | new ApiResponseImpl( 69 | status, 70 | body ?? ApiSchema.Ignored, 71 | headers ?? ApiSchema.Ignored, 72 | representations ?? defaultRepresentations 73 | ) 74 | 75 | /** @internal */ 76 | export const setStatus = 77 | (status: S) => 78 | <_ extends ApiResponse.ApiResponse.AnyStatus, B, H, R>( 79 | response: ApiResponse.ApiResponse<_, B, H, R> 80 | ): ApiResponse.ApiResponse => 81 | make(status, getBodySchema(response), getHeadersSchema(response), getRepresentations(response)) 82 | 83 | /** @internal */ 84 | export const setBody = 85 | (schema: Schema.Schema) => 86 | ( 87 | response: ApiResponse.ApiResponse 88 | ): ApiResponse.ApiResponse => 89 | make(getStatus(response), schema, getHeadersSchema(response), getRepresentations(response)) 90 | 91 | /** @internal */ 92 | export const setHeaders = 93 | (schema: Schema.Schema) => 94 | ( 95 | response: ApiResponse.ApiResponse 96 | ): ApiResponse.ApiResponse => 97 | make( 98 | getStatus(response), 99 | getBodySchema(response), 100 | schema, 101 | getRepresentations(response) 102 | ) 103 | 104 | /** @internal */ 105 | export const setRepresentations = 106 | (representations: Array.NonEmptyReadonlyArray) => 107 | ( 108 | response: ApiResponse.ApiResponse 109 | ): ApiResponse.ApiResponse => 110 | make(getStatus(response), getBodySchema(response), getHeadersSchema(response), representations) 111 | 112 | /** @internal */ 113 | export const getStatus = ( 114 | response: ApiResponse.ApiResponse 115 | ): S => (response as ApiResponseImpl).status 116 | 117 | /** @internal */ 118 | export const getHeadersSchema = ( 119 | response: ApiResponse.ApiResponse 120 | ): Schema.Schema | ApiSchema.Ignored => (response as ApiResponseImpl).headers 121 | 122 | /** @internal */ 123 | export const getBodySchema = ( 124 | response: ApiResponse.ApiResponse 125 | ): Schema.Schema | ApiSchema.Ignored => (response as ApiResponseImpl).body 126 | 127 | /** @internal */ 128 | export const getRepresentations = ( 129 | response: ApiResponse.ApiResponse 130 | ) => (response as ApiResponseImpl).representations 131 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/api-schema.ts: -------------------------------------------------------------------------------- 1 | import * as Predicate from "effect/Predicate" 2 | import * as Schema from "effect/Schema" 3 | 4 | import type * as ApiSchema from "../ApiSchema.js" 5 | import * as circular from "./circular.js" 6 | 7 | /** @internal */ 8 | export const IgnoredId: ApiSchema.IgnoredId = Symbol.for( 9 | "effect-http/ignored-id" 10 | ) as ApiSchema.IgnoredId 11 | 12 | /** @internal */ 13 | export const Ignored: ApiSchema.Ignored = { [IgnoredId]: IgnoredId } 14 | 15 | /** @internal */ 16 | export const formDataSchema = Schema.instanceOf(FormData).pipe( 17 | circular.annotate(() => ({ 18 | type: "string", 19 | format: "binary", 20 | description: "Multipart form data" 21 | })) 22 | ) 23 | 24 | /** @internal */ 25 | export const isIgnored = (u: unknown): u is ApiSchema.Ignored => 26 | Predicate.hasProperty(u, IgnoredId) && u[IgnoredId] === IgnoredId 27 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/api.ts: -------------------------------------------------------------------------------- 1 | import * as Array from "effect/Array" 2 | import * as Pipeable from "effect/Pipeable" 3 | import type * as Api from "../Api.js" 4 | import * as ApiEndpoint from "../ApiEndpoint.js" 5 | import * as ApiGroup from "../ApiGroup.js" 6 | import * as api_endpoint from "./api-endpoint.js" 7 | import * as api_group from "./api-group.js" 8 | 9 | /** @internal */ 10 | export const TypeId: Api.TypeId = Symbol.for( 11 | "effect-http/Api/TypeId" 12 | ) as Api.TypeId 13 | 14 | /** @internal */ 15 | export const variance = { 16 | /* c8 ignore next */ 17 | _A: (_: never) => _ 18 | } 19 | 20 | /** @internal */ 21 | class ApiImpl implements Api.Api { 22 | readonly [TypeId] = variance 23 | 24 | constructor( 25 | readonly groups: ReadonlyArray>, 26 | readonly options: Api.Api.Any["options"] 27 | ) {} 28 | 29 | pipe() { 30 | return Pipeable.pipeArguments(this, arguments) 31 | } 32 | } 33 | 34 | /** @internal */ 35 | const DEFAULT_API_OPTIONS: Api.Api.Any["options"] = { 36 | title: "Api", 37 | version: "1.0.0" 38 | } 39 | 40 | /** @internal */ 41 | export const isApi = (u: unknown): u is Api.Api.Any => typeof u === "object" && u !== null && TypeId in u 42 | 43 | /** @internal */ 44 | export const make = (options?: Partial): Api.Api.Empty => 45 | new ApiImpl([], { ...DEFAULT_API_OPTIONS, ...options }) 46 | 47 | /** @internal */ 48 | export const addEndpoint = 49 | (endpoint: E2) => 50 | (api: Api.Api): Api.Api => { 51 | api_endpoint.validateEndpoint(endpoint) 52 | api.groups.forEach((group) => api_group.validateNewEndpoint(group, endpoint)) 53 | 54 | const defaultGroup = api.groups.find((group) => group.name === "default") ?? ApiGroup.make("default") 55 | const groupsWithoutDefault = api.groups.filter((group) => group.name !== "default") 56 | const newDefaultGroup = new api_group.ApiGroupImpl( 57 | "default", 58 | [...defaultGroup.endpoints, endpoint], 59 | defaultGroup.options 60 | ) 61 | return new ApiImpl([...groupsWithoutDefault, newDefaultGroup], api.options) as any 62 | } 63 | 64 | /** @internal */ 65 | export const addGroup = ( 66 | group: ApiGroup.ApiGroup 67 | ) => 68 | (self: Api.Api): Api.Api => { 69 | const current = self.groups.flatMap((group) => group.endpoints.map(ApiEndpoint.getId)) 70 | const incomming = group.endpoints.map(ApiEndpoint.getId) 71 | 72 | if (Array.intersection(current, incomming).length > 0) { 73 | throw new Error(`Duplicate endpoint ids found in the group`) 74 | } 75 | 76 | return new ApiImpl([...self.groups, group], self.options) 77 | } 78 | 79 | /** @internal */ 80 | export const getEndpoint = >( 81 | api: A, 82 | id: Id 83 | ): Api.Api.EndpointById => { 84 | const endpoint = api.groups.flatMap((group) => group.endpoints).find((endpoint) => ApiEndpoint.getId(endpoint) === id) 85 | 86 | if (endpoint === undefined) { 87 | throw new Error(`Endpoint with id ${id} not found`) 88 | } 89 | 90 | return endpoint as Api.Api.EndpointById 91 | } 92 | 93 | /** @internal */ 94 | export const setOptions = 95 | (options: Partial) => (self: Api.Api): Api.Api => 96 | new ApiImpl(self.groups, { ...self.options, ...options }) 97 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/circular.ts: -------------------------------------------------------------------------------- 1 | import * as Schema from "effect/Schema" 2 | 3 | import type * as OpenApiTypes from "../OpenApiTypes.js" 4 | 5 | /** @internal */ 6 | export const OpenApiId = Symbol.for("effect-http/OpenApi") 7 | 8 | /** @internal */ 9 | export const annotate = ( 10 | annotation: ( 11 | compiler: (schema: Schema.Schema.Any) => OpenApiTypes.OpenAPISchemaType 12 | ) => OpenApiTypes.OpenAPISchemaType 13 | ) => 14 | (self: S): Schema.Annotable.Self => 15 | Schema.annotations(self, { [OpenApiId]: annotation }) 16 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/client-error.ts: -------------------------------------------------------------------------------- 1 | import * as Data from "effect/Data" 2 | import type * as ParseResult from "effect/ParseResult" 3 | import * as Predicate from "effect/Predicate" 4 | import type * as ClientError from "../ClientError.js" 5 | 6 | /** @internal */ 7 | export const ClientSideErrorTypeId: ClientError.ClientSideErrorTypeId = Symbol.for( 8 | "effect-http/ClientError/ClientSideErrorTypeId" 9 | ) as ClientError.ClientSideErrorTypeId 10 | 11 | /** @internal */ 12 | export const ServerSideErrorTypeId: ClientError.ServerSideErrorTypeId = Symbol.for( 13 | "effect-http/ClientError/ServerSideErrorTypeId" 14 | ) as ClientError.ServerSideErrorTypeId 15 | 16 | /** @internal */ 17 | export class ClientErrorServerSideImpl extends Data.TaggedError("ClientError")<{ 18 | message: string 19 | error: unknown 20 | status: S 21 | side: "server" 22 | }> implements ClientError.ClientErrorServerSide { 23 | readonly [ServerSideErrorTypeId] = {} 24 | } 25 | 26 | /** @internal */ 27 | export class ClientErrorClientSideImpl extends Data.TaggedError("ClientError")<{ 28 | message: string 29 | error: unknown 30 | side: "client" 31 | }> implements ClientError.ClientErrorClientSide { 32 | readonly [ClientSideErrorTypeId] = {} 33 | } 34 | 35 | /** @internal */ 36 | export const makeClientSide = (error: unknown, message?: string) => { 37 | return new ClientErrorClientSideImpl({ 38 | message: message ?? (typeof error === "string" ? error : JSON.stringify(error)), 39 | error, 40 | side: "client" 41 | }) 42 | } 43 | 44 | /** @internal */ 45 | const getMessage = (error: unknown) => { 46 | if (typeof error === "string") { 47 | return error 48 | } 49 | 50 | if (error instanceof Error) { 51 | return error.message 52 | } 53 | 54 | if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") { 55 | return error.message 56 | } 57 | 58 | return JSON.stringify(error) 59 | } 60 | 61 | /** @internal */ 62 | export const makeServerSide = ( 63 | error: unknown, 64 | status: S, 65 | message?: string 66 | ) => { 67 | return new ClientErrorServerSideImpl({ 68 | message: message ?? getMessage(error), 69 | error, 70 | status, 71 | side: "server" 72 | }) 73 | } 74 | 75 | /** @internal */ 76 | export const makeClientSideRequestValidation = (location: string) => (error: ParseResult.ParseError) => 77 | new ClientErrorClientSideImpl({ 78 | message: `Failed to encode ${location}. ${error.message}`, 79 | error, 80 | side: "client" 81 | }) 82 | 83 | /** @internal */ 84 | export const makeClientSideResponseValidation = (location: string) => (error: ParseResult.ParseError) => 85 | new ClientErrorClientSideImpl({ 86 | message: `Failed to validate response ${location}. ${error.message}`, 87 | error, 88 | side: "client" 89 | }) 90 | 91 | /** @internal */ 92 | export const isClientSideError = (u: unknown): u is ClientError.ClientErrorClientSide => 93 | Predicate.isTagged(u, "ClientError") && ClientSideErrorTypeId in u 94 | 95 | /** @internal */ 96 | export const isServerSideError = (u: unknown): u is ClientError.ClientErrorServerSide => 97 | Predicate.isTagged(u, "ClientError") && ServerSideErrorTypeId in u 98 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/client.ts: -------------------------------------------------------------------------------- 1 | import * as FetchHttpClient from "@effect/platform/FetchHttpClient" 2 | import * as HttpClient from "@effect/platform/HttpClient" 3 | import * as HttpClientRequest from "@effect/platform/HttpClientRequest" 4 | import * as Context from "effect/Context" 5 | import * as Effect from "effect/Effect" 6 | import * as Encoding from "effect/Encoding" 7 | import { dual, identity, pipe } from "effect/Function" 8 | import * as Layer from "effect/Layer" 9 | import * as Api from "../Api.js" 10 | import * as ApiEndpoint from "../ApiEndpoint.js" 11 | import type * as Client from "../Client.js" 12 | import * as ClientError from "../ClientError.js" 13 | import * as ClientRequestEncoder from "./clientRequestEncoder.js" 14 | import * as ClientResponseParser from "./clientResponseParser.js" 15 | 16 | /** @internal */ 17 | const defaultHttpClient = FetchHttpClient.layer.pipe( 18 | Layer.build, 19 | Effect.map(Context.get(HttpClient.HttpClient)), 20 | Effect.scoped, 21 | Effect.runSync 22 | ) 23 | 24 | /** @internal */ 25 | export const endpointClient = >( 26 | id: Id, 27 | api: A, 28 | options: Partial 29 | ): Client.Client.Function> => { 30 | const endpoint = Api.getEndpoint(api, id) 31 | const responseParser = ClientResponseParser.create(endpoint) 32 | const requestEncoder = ClientRequestEncoder.create(endpoint) 33 | 34 | const httpClient = (options.httpClient ?? defaultHttpClient).pipe( 35 | options.baseUrl ? 36 | HttpClient.mapRequest(HttpClientRequest.prependUrl(options.baseUrl)) : 37 | identity 38 | ) 39 | 40 | return (( 41 | args: unknown, 42 | mapper?: (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 43 | ) => 44 | pipe( 45 | requestEncoder.encodeRequest(args), 46 | Effect.map(mapper ?? identity), 47 | Effect.flatMap(httpClient.execute), 48 | Effect.catchTags({ 49 | RequestError: (err) => ClientError.makeClientSide(err.cause, err.message), 50 | ResponseError: (err) => ClientError.makeServerSide(err.cause, err.response.status, err.message), 51 | ClientError: (err) => Effect.fail(err) 52 | }), 53 | Effect.flatMap(responseParser.parseResponse), 54 | Effect.annotateLogs("clientOperationId", ApiEndpoint.getId(endpoint)) 55 | )) 56 | } 57 | 58 | /** @internal */ 59 | export const make = ( 60 | api: A, 61 | options?: Partial 62 | ): Client.Client => 63 | api.groups.flatMap((group) => group.endpoints).reduce( 64 | (client, endpoint) => ({ 65 | ...client as any, 66 | [ApiEndpoint.getId(endpoint)]: endpointClient(ApiEndpoint.getId(endpoint) as any, api, options ?? {}) 67 | }), 68 | {} as Client.Client 69 | ) 70 | 71 | /** @internal */ 72 | export const setBasicAuth = dual( 73 | 3, 74 | (request: HttpClientRequest.HttpClientRequest, user: string, pass: string): HttpClientRequest.HttpClientRequest => 75 | HttpClientRequest.setHeader(request, "Authorization", `Basic ${Encoding.encodeBase64(`${user}:${pass}`)}`) 76 | ) 77 | 78 | /** @internal */ 79 | export const setBearer = dual( 80 | 2, 81 | (request: HttpClientRequest.HttpClientRequest, token: string): HttpClientRequest.HttpClientRequest => 82 | HttpClientRequest.setHeader(request, "Authorization", `Bearer ${token}`) 83 | ) 84 | 85 | /** @internal */ 86 | export const setApiKey = dual(4, ( 87 | request: HttpClientRequest.HttpClientRequest, 88 | key: string, 89 | _in: "query" | "header", 90 | apiKey: string 91 | ): HttpClientRequest.HttpClientRequest => 92 | _in === "query" 93 | ? HttpClientRequest.setUrlParam(request, key, apiKey) 94 | : HttpClientRequest.setHeader(request, key, apiKey)) 95 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/clientResponseParser.ts: -------------------------------------------------------------------------------- 1 | import type * as HttpClientResponse from "@effect/platform/HttpClientResponse" 2 | import * as Array from "effect/Array" 3 | import * as Effect from "effect/Effect" 4 | import { flow, pipe } from "effect/Function" 5 | import * as Option from "effect/Option" 6 | import * as Schema from "effect/Schema" 7 | import * as Unify from "effect/Unify" 8 | import * as ApiEndpoint from "../ApiEndpoint.js" 9 | import * as ApiResponse from "../ApiResponse.js" 10 | import * as ApiSchema from "../ApiSchema.js" 11 | import * as ClientError from "../ClientError.js" 12 | import type * as Representation from "../Representation.js" 13 | 14 | /** @internal */ 15 | interface ClientResponseParser { 16 | parseResponse: ( 17 | response: HttpClientResponse.HttpClientResponse 18 | ) => Effect.Effect 19 | } 20 | 21 | /** @internal */ 22 | const make = ( 23 | parseResponse: ClientResponseParser["parseResponse"] 24 | ): ClientResponseParser => ({ parseResponse }) 25 | 26 | /** @internal */ 27 | export const create = ( 28 | endpoint: ApiEndpoint.ApiEndpoint.Any 29 | ): ClientResponseParser => { 30 | const responses = ApiEndpoint.getResponse(endpoint) 31 | const isFullResponse = ApiEndpoint.isFullResponse(endpoint) 32 | const statusToSchema = responses.reduce( 33 | (obj, schemas) => ({ ...obj, [ApiResponse.getStatus(schemas)]: schemas }), 34 | {} as Record 35 | ) 36 | 37 | return make((response) => 38 | Effect.gen(function*(_) { 39 | yield* _(handleUnsucessful(response)) 40 | 41 | if (!(response.status in statusToSchema)) { 42 | const allowedStatuses = Object.keys(statusToSchema) 43 | 44 | return yield* _( 45 | ClientError.makeClientSide( 46 | `Unexpected status ${response.status}. Allowed ones are ${allowedStatuses}.` 47 | ) 48 | ) 49 | } 50 | 51 | const responseSpec = statusToSchema[response.status] 52 | const _parseBody = parseBody( 53 | ApiResponse.getBodySchema(responseSpec) as Schema.Schema, 54 | ApiResponse.getRepresentations(responseSpec) 55 | ) 56 | const body = yield* _(_parseBody(response)) 57 | 58 | if (!isFullResponse) { 59 | return body 60 | } 61 | 62 | const headersSchema = ApiResponse.getHeadersSchema(responseSpec) 63 | 64 | const headers = ApiSchema.isIgnored(headersSchema) 65 | ? undefined 66 | : yield* _( 67 | response.headers, 68 | Schema.decodeUnknown(headersSchema as Schema.Schema), 69 | Effect.mapError( 70 | ClientError.makeClientSideResponseValidation("headers") 71 | ) 72 | ) 73 | 74 | return { status: response.status, body, headers } 75 | }) 76 | ) 77 | } 78 | 79 | /** @internal */ 80 | const handleUnsucessful = Unify.unify( 81 | (response: HttpClientResponse.HttpClientResponse) => { 82 | if (response.status >= 300) { 83 | return response.json.pipe( 84 | Effect.orElse(() => response.text), 85 | Effect.orElseSucceed(() => "No body provided"), 86 | Effect.flatMap((error) => Effect.fail(ClientError.makeServerSide(error, response.status))) 87 | ) 88 | } 89 | 90 | return Effect.void 91 | } 92 | ) 93 | 94 | /** @internal */ 95 | const representationFromResponse = ( 96 | representations: Array.NonEmptyReadonlyArray, 97 | response: HttpClientResponse.HttpClientResponse 98 | ): Representation.Representation => { 99 | if (representations.length === 0) { 100 | return representations[0] 101 | } 102 | 103 | const contentType = response.headers["content-type"] 104 | 105 | // TODO: this logic needs to be improved a lot! 106 | return pipe( 107 | representations, 108 | Array.filter( 109 | (representation) => representation.contentType === contentType 110 | ), 111 | Array.head, 112 | Option.getOrElse(() => representations[0]) 113 | ) 114 | } 115 | 116 | /** @internal */ 117 | const decodeBody = ( 118 | schema: Schema.Schema, 119 | representations: Array.NonEmptyReadonlyArray 120 | ) => { 121 | const parse = Schema.decodeUnknown(schema) 122 | 123 | return (response: HttpClientResponse.HttpClientResponse) => { 124 | const representation = representationFromResponse( 125 | representations, 126 | response 127 | ) 128 | 129 | return response.text.pipe( 130 | Effect.mapError((error) => ClientError.makeClientSide(error, `Invalid response: ${error.reason}`)), 131 | Effect.flatMap( 132 | flow( 133 | representation.parse, 134 | Effect.mapError((error) => 135 | ClientError.makeClientSide( 136 | error, 137 | `Invalid response: ${error.message}` 138 | ) 139 | ) 140 | ) 141 | ), 142 | Effect.flatMap( 143 | flow( 144 | parse, 145 | Effect.mapError(ClientError.makeClientSideResponseValidation("body")) 146 | ) 147 | ) 148 | ) 149 | } 150 | } 151 | 152 | /** @internal */ 153 | const parseBody: ( 154 | schema: Schema.Schema | ApiSchema.Ignored, 155 | representations: Array.NonEmptyReadonlyArray 156 | ) => ( 157 | response: HttpClientResponse.HttpClientResponse 158 | ) => Effect.Effect = ( 159 | schema, 160 | representations 161 | ) => { 162 | if (ApiSchema.isIgnored(schema)) { 163 | return () => Effect.succeed(undefined) 164 | } 165 | 166 | return decodeBody(schema, representations) 167 | } 168 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/example-server.ts: -------------------------------------------------------------------------------- 1 | import * as Array from "effect/Array" 2 | import * as Effect from "effect/Effect" 3 | import { pipe } from "effect/Function" 4 | import type * as Schema from "effect/Schema" 5 | import * as Unify from "effect/Unify" 6 | 7 | import * as Api from "../Api.js" 8 | import * as ApiEndpoint from "../ApiEndpoint.js" 9 | import * as HttpError from "../HttpError.js" 10 | import * as RouterBuilder from "../RouterBuilder.js" 11 | import * as example_compiler from "./example-compiler.js" 12 | import * as utils from "./utils.js" 13 | 14 | /** @internal */ 15 | export const make = ( 16 | api: A 17 | ): RouterBuilder.RouterBuilder< 18 | Api.Api.Context, 19 | never, 20 | never 21 | > => handleRemaining(RouterBuilder.make(api)) 22 | 23 | /** @internal */ 24 | export const handle = < 25 | A extends ApiEndpoint.ApiEndpoint.Any, 26 | Id extends ApiEndpoint.ApiEndpoint.Id 27 | >(id: Id) => 28 | ( 29 | routerBuilder: RouterBuilder.RouterBuilder 30 | ): RouterBuilder.RouterBuilder< 31 | ApiEndpoint.ApiEndpoint.ExcludeById, 32 | E, 33 | R | ApiEndpoint.ApiEndpoint.Context> 34 | > => { 35 | const endpoint = Api.getEndpoint(routerBuilder.api, id) 36 | 37 | return pipe( 38 | routerBuilder, 39 | RouterBuilder.handle(id, createExampleHandler(endpoint) as any) 40 | ) as any 41 | } 42 | 43 | /** @internal */ 44 | export const handleRemaining = ( 45 | routerBuilder: RouterBuilder.RouterBuilder 46 | ): RouterBuilder.RouterBuilder> => 47 | pipe( 48 | routerBuilder.remainingEndpoints, 49 | Array.reduce( 50 | routerBuilder as RouterBuilder.RouterBuilder>, 51 | (server, endpoint) => 52 | pipe( 53 | server, 54 | RouterBuilder.handle(ApiEndpoint.getId(endpoint) as any, createExampleHandler(endpoint) as any) 55 | ) as any 56 | ) 57 | ) as RouterBuilder.RouterBuilder> 58 | 59 | /** @internal */ 60 | const createExampleHandler = (endpoint: ApiEndpoint.ApiEndpoint.Any) => { 61 | const responseSchema = utils.createResponseSchema(endpoint) 62 | 63 | return () => 64 | pipe( 65 | Unify.unify(responseSchema && example_compiler.randomExample(responseSchema) || Effect.void), 66 | Effect.mapError((error) => 67 | HttpError.internalServerError( 68 | `Sorry, I don't have any example response. ${JSON.stringify(error)}` 69 | ) 70 | ) 71 | ) 72 | } 73 | 74 | export const makeSchema = ( 75 | schema: Schema.Schema 76 | ) => Effect.orDie(example_compiler.randomExample(schema)) 77 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/http-error.ts: -------------------------------------------------------------------------------- 1 | import * as Cookies from "@effect/platform/Cookies" 2 | import * as Headers from "@effect/platform/Headers" 3 | import * as HttpBody from "@effect/platform/HttpBody" 4 | import * as HttpServerResponse from "@effect/platform/HttpServerResponse" 5 | import * as Data from "effect/Data" 6 | import { absurd, identity, pipe } from "effect/Function" 7 | import * as Pipeable from "effect/Pipeable" 8 | import * as Stream from "effect/Stream" 9 | 10 | import type * as HttpError from "../HttpError.js" 11 | 12 | /** @internal */ 13 | export const TypeId: HttpError.TypeId = Symbol.for("effect-http/HttpError/TypeId") as HttpError.TypeId 14 | 15 | /** @internal */ 16 | export const variance = {} 17 | 18 | /** @internal */ 19 | const unsafeIsStream = (u: unknown): u is Stream.Stream => 20 | typeof u === "object" && u !== null && Stream.StreamTypeId in u 21 | 22 | /** @internal */ 23 | export class HttpErrorImpl extends Data.TaggedError("HttpError")<{ 24 | readonly status: number 25 | readonly content: HttpBody.HttpBody 26 | readonly headers: Headers.Headers 27 | readonly cookies: Cookies.Cookies 28 | }> implements HttpError.HttpError { 29 | readonly [TypeId] = variance 30 | 31 | static unsafeFromUnknown(status: number, body: unknown, options?: Partial) { 32 | let content = undefined 33 | 34 | if (body === undefined) { 35 | content = HttpBody.empty 36 | } else if (body instanceof Uint8Array) { 37 | content = HttpBody.uint8Array(body) 38 | } else if (typeof body === "string") { 39 | content = HttpBody.text(body) 40 | } else if (unsafeIsStream(body)) { 41 | content = HttpBody.stream(body) 42 | } else { 43 | content = HttpBody.unsafeJson(body) 44 | } 45 | 46 | return new HttpErrorImpl({ 47 | status, 48 | content, 49 | headers: options?.headers ?? Headers.empty, 50 | cookies: options?.cookies ?? Cookies.empty 51 | }) 52 | } 53 | 54 | pipe() { 55 | return Pipeable.pipeArguments(this, arguments) 56 | } 57 | } 58 | 59 | /** @internal */ 60 | export const toResponse = (error: HttpError.HttpError) => { 61 | const options: HttpServerResponse.Options.WithContentType = { 62 | status: error.status, 63 | contentType: error.content.contentType, 64 | headers: pipe( 65 | error.headers, 66 | error.content.contentLength ? Headers.set("Content-Length", error.content.contentLength.toString()) : identity 67 | ), 68 | cookies: error.cookies 69 | } 70 | 71 | const content = error.content 72 | 73 | if (content._tag === "Empty") { 74 | return HttpServerResponse.empty(options) 75 | } else if (content._tag === "Uint8Array") { 76 | return HttpServerResponse.uint8Array(content.body, options) 77 | } else if (content._tag === "Raw") { 78 | return HttpServerResponse.raw(content.body, options) 79 | } else if (content._tag === "Stream") { 80 | return HttpServerResponse.stream(content.stream, options) 81 | } else if (content._tag === "FormData") { 82 | return HttpServerResponse.formData(content.formData, options) 83 | } 84 | 85 | return absurd(content) 86 | } 87 | 88 | /** @internal */ 89 | export const make = (status: number, body?: unknown, options?: Partial) => 90 | HttpErrorImpl.unsafeFromUnknown(status, body, options) 91 | 92 | export const makeStatus = (status: number) => (body?: unknown, options?: Partial) => 93 | HttpErrorImpl.unsafeFromUnknown(status, body, options) 94 | 95 | /** @internal */ 96 | export const isHttpError = (error: unknown): error is HttpError.HttpError => 97 | typeof error === "object" && error !== null && TypeId in error 98 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/middlewares.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mechanism for extendning behaviour of all handlers on the server. 3 | * 4 | * @since 1.0.0 5 | */ 6 | 7 | import * as HttpMiddleware from "@effect/platform/HttpMiddleware" 8 | import * as HttpServerRequest from "@effect/platform/HttpServerRequest" 9 | import * as Effect from "effect/Effect" 10 | import * as FiberRef from "effect/FiberRef" 11 | import { pipe } from "effect/Function" 12 | import * as HashMap from "effect/HashMap" 13 | import * as LogLevel from "effect/LogLevel" 14 | 15 | /** @internal */ 16 | export const accessLog = (level: LogLevel.LogLevel = LogLevel.Info) => 17 | HttpMiddleware.make((app) => 18 | pipe( 19 | HttpServerRequest.HttpServerRequest, 20 | Effect.flatMap((request) => Effect.logWithLevel(level, `${request.method} ${request.url}`)), 21 | Effect.flatMap(() => app) 22 | ) 23 | ) 24 | 25 | /** @internal */ 26 | export const uuidLogAnnotation = (logAnnotationKey = "requestId") => 27 | HttpMiddleware.make((app) => 28 | pipe( 29 | Effect.sync(() => crypto.randomUUID()), 30 | Effect.flatMap((uuid) => 31 | FiberRef.update( 32 | FiberRef.currentLogAnnotations, 33 | HashMap.set(logAnnotationKey, uuid) 34 | ) 35 | ), 36 | Effect.flatMap(() => app) 37 | ) 38 | ) 39 | 40 | /** @internal */ 41 | export const errorLog = HttpMiddleware.make((app) => 42 | Effect.gen(function*(_) { 43 | const request = yield* _(HttpServerRequest.HttpServerRequest) 44 | 45 | const response = yield* _(app, Effect.tapErrorCause(Effect.logError)) 46 | 47 | if (response.status >= 400 && response.status < 500) { 48 | yield* _( 49 | Effect.logWarning( 50 | `${request.method.toUpperCase()} ${request.url} client error ${response.status}` 51 | ) 52 | ) 53 | } else if (response.status >= 500) { 54 | yield* _( 55 | Effect.logError( 56 | `${request.method.toUpperCase()} ${request.url} server error ${response.status}` 57 | ) 58 | ) 59 | } 60 | 61 | return response 62 | }) 63 | ) 64 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/mock-client.ts: -------------------------------------------------------------------------------- 1 | import type * as HttpClientRequest from "@effect/platform/HttpClientRequest" 2 | import * as Effect from "effect/Effect" 3 | import { identity, pipe } from "effect/Function" 4 | 5 | import type * as Api from "../Api.js" 6 | import * as ApiEndpoint from "../ApiEndpoint.js" 7 | import type * as Client from "../Client.js" 8 | import type * as MockClient from "../MockClient.js" 9 | import * as ClientRequestEncoder from "./clientRequestEncoder.js" 10 | import * as example_compiler from "./example-compiler.js" 11 | import * as utils from "./utils.js" 12 | 13 | /** @internal */ 14 | export const make = ( 15 | api: A, 16 | option?: Partial> 17 | ): Client.Client => 18 | api.groups.flatMap((group) => group.endpoints).reduce((client, endpoint) => { 19 | const requestEncoder = ClientRequestEncoder.create(endpoint) 20 | const responseSchema = utils.createResponseSchema(endpoint) 21 | 22 | const customResponses = option?.responses 23 | const customResponse = customResponses && (customResponses as any)[ApiEndpoint.getId(endpoint)] 24 | 25 | const fn = ( 26 | args: unknown, 27 | mapRequest: (request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest 28 | ) => { 29 | return pipe( 30 | requestEncoder.encodeRequest(args), 31 | Effect.map(mapRequest ?? identity), 32 | Effect.flatMap(() => { 33 | if (customResponse !== undefined) { 34 | return Effect.succeed(customResponse) 35 | } else if (responseSchema === undefined) { 36 | return Effect.void 37 | } 38 | 39 | return example_compiler.randomExample(responseSchema) 40 | }) 41 | ) 42 | } 43 | 44 | return { ...client as any, [ApiEndpoint.getId(endpoint)]: fn } 45 | }, {} as Client.Client) 46 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/query-schema.ts: -------------------------------------------------------------------------------- 1 | import * as Schema from "effect/Schema" 2 | import * as circular from "./circular.js" 3 | 4 | /** @internal */ 5 | export const Number = Schema.NumberFromString.pipe( 6 | circular.annotate(() => ({ type: "number", description: "a number" })) 7 | ) 8 | 9 | export const number = (schema: Schema.Schema) => 10 | Schema.compose(Schema.NumberFromString, schema, { strict: true }).pipe( 11 | circular.annotate(() => ({ type: "number", description: "a number" })) 12 | ) 13 | 14 | /** @internal */ 15 | export const Int = Schema.compose(Schema.NumberFromString, Schema.Int).pipe( 16 | circular.annotate(() => ({ type: "integer", description: "an integer" })) 17 | ) 18 | 19 | export const int = (schema: Schema.Schema) => 20 | Schema.compose(Int, schema, { strict: true }).pipe( 21 | circular.annotate(() => ({ type: "integer", description: "an integer" })) 22 | ) 23 | 24 | /** @internal */ 25 | export const Array = ( 26 | schema: Schema.Schema 27 | ): Schema.optionalWith< 28 | Schema.Schema, string | ReadonlyArray, R>, 29 | { exact: true; default: () => [] } 30 | > => { 31 | const stringToStringArraySchema = Schema.transform(Schema.String, Schema.Array(Schema.String), { 32 | decode: (i) => [i], 33 | encode: (i) => i[0] 34 | }) 35 | const arraySchema = Schema.Array(schema) 36 | const stringToArraySchema = Schema.compose(stringToStringArraySchema, arraySchema, { strict: true }) 37 | 38 | return Schema.optionalWith( 39 | Schema.Union(stringToArraySchema, arraySchema).pipe( 40 | circular.annotate((compile) => ({ type: "array", items: compile(schema) })) 41 | ), 42 | { exact: true, default: () => [] } 43 | ) 44 | } 45 | 46 | /** @internal */ 47 | export const NonEmptyArray = ( 48 | schema: Schema.Schema 49 | ): Schema.Schema], string | readonly [string, ...Array], R> => { 50 | const stringToStringArraySchema = Schema.transform(Schema.String, Schema.NonEmptyArray(Schema.String), { 51 | decode: (i) => [i] as const, 52 | encode: (i) => i[0] 53 | }) 54 | const arraySchema = Schema.NonEmptyArray(schema) 55 | const stringToArraySchema = Schema.compose(stringToStringArraySchema, arraySchema, { strict: true }) 56 | 57 | return Schema.Union(stringToArraySchema, arraySchema).pipe( 58 | circular.annotate((compile) => ({ type: "array", items: compile(schema) })) 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/representation.ts: -------------------------------------------------------------------------------- 1 | import * as Data from "effect/Data" 2 | import * as Effect from "effect/Effect" 3 | import * as Pipeable from "effect/Pipeable" 4 | import type * as Representation from "../Representation.js" 5 | 6 | /** @internal */ 7 | export const TypeId: Representation.TypeId = Symbol.for( 8 | "effect-http/Representation/Representation" 9 | ) as Representation.TypeId 10 | 11 | /** @internal */ 12 | class RepresentationErrorImpl extends Data.TaggedError("RepresentationError")<{ message: string }> 13 | implements Representation.RepresentationError 14 | {} 15 | 16 | /** @internal */ 17 | const representationProto = { 18 | [TypeId]: TypeId, 19 | pipe() { 20 | return Pipeable.pipeArguments(this, arguments) 21 | } 22 | } 23 | 24 | /** @internal */ 25 | export const make = ( 26 | fields: Omit 27 | ): Representation.Representation => { 28 | const representation = Object.create(representationProto) 29 | return Object.assign(representation, fields) 30 | } 31 | 32 | /** @internal */ 33 | export const json = make({ 34 | stringify: (input) => 35 | Effect.try({ 36 | try: () => JSON.stringify(input), 37 | catch: (error) => 38 | new RepresentationErrorImpl({ 39 | message: `JSON parsing failed with ${error}` 40 | }) 41 | }), 42 | parse: (input) => 43 | Effect.try({ 44 | try: () => JSON.parse(input), 45 | catch: (error) => 46 | new RepresentationErrorImpl({ 47 | message: `JSON stringify failed with ${error}` 48 | }) 49 | }), 50 | contentType: "application/json" 51 | }) 52 | 53 | /** @internal */ 54 | export const plainText = make({ 55 | stringify: (input) => { 56 | if (typeof input === "string") { 57 | return Effect.succeed(input) 58 | } else if (typeof input === "number") { 59 | return Effect.succeed(String(input)) 60 | } 61 | 62 | return json.stringify(input) 63 | }, 64 | parse: Effect.succeed, 65 | contentType: "text/plain" 66 | }) 67 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/serverRequestParser.ts: -------------------------------------------------------------------------------- 1 | import * as HttpRouter from "@effect/platform/HttpRouter" 2 | import * as HttpServerRequest from "@effect/platform/HttpServerRequest" 3 | import * as Effect from "effect/Effect" 4 | import * as Schema from "effect/Schema" 5 | import type * as AST from "effect/SchemaAST" 6 | 7 | import * as ApiEndpoint from "../ApiEndpoint.js" 8 | import * as ApiRequest from "../ApiRequest.js" 9 | import * as ApiSchema from "../ApiSchema.js" 10 | import * as HttpError from "../HttpError.js" 11 | import * as Security from "../Security.js" 12 | 13 | /** @internal */ 14 | interface ServerRequestParser { 15 | parseRequest: Effect.Effect< 16 | { query: any; path: any; body: any; headers: any; security: any }, 17 | HttpError.HttpError, 18 | HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams | HttpRouter.RouteContext 19 | > 20 | } 21 | 22 | /** @internal */ 23 | const createError = ( 24 | location: "query" | "path" | "body" | "headers", 25 | message: string 26 | ) => 27 | HttpError.make(400, { 28 | error: "Request validation error", 29 | location, 30 | message 31 | }) 32 | 33 | /** @internal */ 34 | const make = ( 35 | parseRequest: ServerRequestParser["parseRequest"] 36 | ): ServerRequestParser => ({ parseRequest }) 37 | 38 | /** @internal */ 39 | export const create = ( 40 | endpoint: ApiEndpoint.ApiEndpoint.Any, 41 | parseOptions?: AST.ParseOptions 42 | ): ServerRequestParser => 43 | make( 44 | Effect.all({ 45 | body: parseBody(endpoint, parseOptions), 46 | query: parseQuery(endpoint, parseOptions), 47 | path: parsePath(endpoint, parseOptions), 48 | headers: parseHeaders(endpoint, parseOptions), 49 | security: parseSecurity(endpoint) 50 | }) as any 51 | ) 52 | 53 | /** @internal */ 54 | const parseBody = ( 55 | endpoint: ApiEndpoint.ApiEndpoint.Any, 56 | parseOptions?: AST.ParseOptions 57 | ) => { 58 | const schema = ApiRequest.getBodySchema(ApiEndpoint.getRequest(endpoint)) 59 | 60 | if (ApiSchema.isIgnored(schema)) { 61 | return Effect.succeed(undefined) 62 | } 63 | 64 | const parse = Schema.decodeUnknown(schema as Schema.Schema) 65 | 66 | if (schema === ApiSchema.FormData) { 67 | // TODO 68 | return Effect.succeed(undefined) 69 | } 70 | 71 | return HttpServerRequest.HttpServerRequest.pipe( 72 | Effect.flatMap((request) => request.json), 73 | Effect.mapError((error) => { 74 | if (error.reason === "Transport") { 75 | return createError("body", "Unexpect request JSON body error") 76 | } 77 | 78 | return createError("body", "Invalid JSON") 79 | }), 80 | Effect.flatMap((request) => 81 | parse(request, parseOptions).pipe( 82 | Effect.mapError((error) => createError("body", error.message)) 83 | ) 84 | ) 85 | ) 86 | } 87 | 88 | /** @internal */ 89 | const parseQuery = ( 90 | endpoint: ApiEndpoint.ApiEndpoint.Any, 91 | parseOptions?: AST.ParseOptions 92 | ) => { 93 | const schema = ApiRequest.getQuerySchema(ApiEndpoint.getRequest(endpoint)) 94 | 95 | if (ApiSchema.isIgnored(schema)) { 96 | return Effect.succeed(undefined) 97 | } 98 | 99 | return Effect.mapError( 100 | HttpServerRequest.schemaSearchParams(schema, parseOptions), 101 | (error) => createError("query", error.message) 102 | ) 103 | } 104 | 105 | /** @internal */ 106 | const parseHeaders = ( 107 | endpoint: ApiEndpoint.ApiEndpoint.Any, 108 | parseOptions?: AST.ParseOptions 109 | ) => { 110 | const schema = ApiRequest.getHeadersSchema(ApiEndpoint.getRequest(endpoint)) 111 | 112 | if (ApiSchema.isIgnored(schema)) { 113 | return Effect.succeed(undefined) 114 | } 115 | 116 | const parse = Schema.decodeUnknown(schema as Schema.Schema) 117 | 118 | return HttpServerRequest.HttpServerRequest.pipe( 119 | Effect.flatMap((request) => parse(request.headers, parseOptions)), 120 | Effect.mapError((error) => createError("headers", error.message)) 121 | ) 122 | } 123 | 124 | /** @internal */ 125 | const parseSecurity = ( 126 | endpoint: ApiEndpoint.ApiEndpoint.Any 127 | ) => { 128 | const security = ApiEndpoint.getSecurity(endpoint) 129 | 130 | return Security.handleRequest(security) 131 | } 132 | 133 | /** @internal */ 134 | const parsePath = ( 135 | endpoint: ApiEndpoint.ApiEndpoint.Any, 136 | parseOptions?: AST.ParseOptions 137 | ) => { 138 | const schema = ApiRequest.getPathSchema(ApiEndpoint.getRequest(endpoint)) 139 | 140 | if (ApiSchema.isIgnored(schema)) { 141 | return Effect.succeed(undefined) 142 | } 143 | 144 | const parse = Schema.decodeUnknown(schema as Schema.Schema) 145 | 146 | return HttpRouter.RouteContext.pipe( 147 | Effect.flatMap((ctx) => parse(ctx.params, parseOptions)), 148 | Effect.mapError((error) => createError("path", error.message)) 149 | ) 150 | } 151 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/swagger-router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a router serving Swagger files. 3 | * 4 | * @since 1.0.0 5 | */ 6 | import * as Headers from "@effect/platform/Headers" 7 | import * as HttpRouter from "@effect/platform/HttpRouter" 8 | import * as HttpServerRequest from "@effect/platform/HttpServerRequest" 9 | import * as HttpServerResponse from "@effect/platform/HttpServerResponse" 10 | import * as Array from "effect/Array" 11 | import * as Context from "effect/Context" 12 | import * as Effect from "effect/Effect" 13 | import { pipe } from "effect/Function" 14 | import type * as SwaggerRouter from "../SwaggerRouter.js" 15 | 16 | /** @internal */ 17 | const createSwaggerInitializer = (path: string) => ` 18 | window.onload = function() { 19 | window.ui = SwaggerUIBundle({ 20 | url: "${path}", 21 | dom_id: '#swagger-ui', 22 | deepLinking: true, 23 | presets: [ 24 | SwaggerUIBundle.presets.apis, 25 | SwaggerUIStandalonePreset 26 | ], 27 | plugins: [ 28 | SwaggerUIBundle.plugins.DownloadUrl 29 | ], 30 | layout: "StandaloneLayout" 31 | }); 32 | }; 33 | ` 34 | 35 | /** @internal */ 36 | const createIndex = (path: string) => ` 37 | 38 | 39 | 40 | 41 | 42 | Swagger UI 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |

51 | 52 | 53 | 54 | 55 | 56 | ` 57 | 58 | /** @internal */ 59 | const SWAGGER_FILE_NAMES = [ 60 | "index.css", 61 | "swagger-ui.css", 62 | "swagger-ui-bundle.js", 63 | "swagger-ui-standalone-preset.js", 64 | "favicon-32x32.png", 65 | "favicon-16x16.png" 66 | ] 67 | 68 | /** @internal */ 69 | export const SwaggerFiles = Context.GenericTag("@services/SwaggerFiles") 70 | 71 | /** @internal */ 72 | const createHeaders = (file: string) => { 73 | if (file.endsWith(".html")) { 74 | return { "content-type": "text/html" } 75 | } else if (file.endsWith(".css")) { 76 | return { "content-type": "text/css" } 77 | } else if (file.endsWith(".js")) { 78 | return { "content-type": "application/javascript" } 79 | } else if (file.endsWith(".png")) { 80 | return { "content-type": "image/png" } 81 | } 82 | 83 | return undefined 84 | } 85 | 86 | /** @internal */ 87 | const serverStaticDocsFile = (filename: string, path?: HttpRouter.PathInput) => { 88 | const headers = createHeaders(filename) 89 | 90 | return HttpRouter.get( 91 | path ?? `/${filename}`, 92 | Effect.gen(function*(_) { 93 | const { files } = yield* _(SwaggerFiles) 94 | const content = files[filename] 95 | 96 | return HttpServerResponse.raw(content, { 97 | headers: Headers.fromInput(headers) 98 | }) 99 | }) 100 | ) 101 | } 102 | 103 | /** @internal */ 104 | const calculatePrefix = Effect.gen(function*(_) { 105 | const request = yield* _(HttpServerRequest.HttpServerRequest) 106 | 107 | const url = request.originalUrl 108 | 109 | const parts = url.split("/") 110 | 111 | if (!Array.isNonEmptyArray(parts)) { 112 | return "" 113 | } 114 | 115 | const last = parts[parts.length - 1] 116 | 117 | if (last.includes(".")) { 118 | return pipe(Array.initNonEmpty(parts), Array.join("/")) 119 | } 120 | 121 | return url 122 | }) 123 | 124 | /** @internal */ 125 | export const make = (spec: unknown) => { 126 | let router = SWAGGER_FILE_NAMES.reduce( 127 | (router, swaggerFileName) => router.pipe(serverStaticDocsFile(swaggerFileName, `/${swaggerFileName}`)), 128 | HttpRouter.empty as HttpRouter.HttpRouter 129 | ) 130 | 131 | const serveIndex = Effect.gen(function*(_) { 132 | const prefix = yield* _(calculatePrefix) 133 | const index = createIndex(prefix) 134 | return HttpServerResponse.text(index, { 135 | headers: Headers.fromInput({ 136 | "content-type": "text/html" 137 | }) 138 | }) 139 | }) 140 | 141 | const serveSwaggerInitializer = Effect.gen(function*(_) { 142 | const prefix = yield* _(calculatePrefix) 143 | const swaggerInitialiser = createSwaggerInitializer(`${prefix}/openapi.json`) 144 | return HttpServerResponse.text(swaggerInitialiser, { 145 | headers: Headers.fromInput({ 146 | "content-type": "application/javascript" 147 | }) 148 | }) 149 | }) 150 | 151 | router = router.pipe( 152 | HttpRouter.get(`/`, serveIndex), 153 | HttpRouter.get(`/index.html`, serveIndex), 154 | HttpRouter.get(`/openapi.json`, Effect.orDie(HttpServerResponse.json(spec))), 155 | HttpRouter.get(`/swagger-initializer.js`, serveSwaggerInitializer) 156 | ) 157 | 158 | return router 159 | } 160 | -------------------------------------------------------------------------------- /packages/effect-http/src/internal/utils.ts: -------------------------------------------------------------------------------- 1 | import * as Schema from "effect/Schema" 2 | import type * as Types from "effect/Types" 3 | 4 | import * as ApiEndpoint from "../ApiEndpoint.js" 5 | import * as ApiResponse from "../ApiResponse.js" 6 | import * as ApiSchema from "../ApiSchema.js" 7 | 8 | /** @internal */ 9 | export const getSchema = >( 10 | input: Schema.Schema | ApiSchema.Ignored, 11 | defaultSchema: Schema.Schema | A = Schema.Unknown 12 | ) => (ApiSchema.isIgnored(input) ? defaultSchema : input) 13 | 14 | /** @internal */ 15 | export const createResponseSchema = ( 16 | endpoint: ApiEndpoint.ApiEndpoint.Any 17 | ) => { 18 | const response = ApiEndpoint.getResponse(endpoint) 19 | const isFullResponse = ApiEndpoint.isFullResponse(endpoint) 20 | 21 | if (!isFullResponse && ApiSchema.isIgnored(ApiResponse.getBodySchema(response[0]))) { 22 | return undefined 23 | } 24 | 25 | if (!isFullResponse) { 26 | return getSchema(ApiResponse.getBodySchema(response[0])) 27 | } 28 | 29 | return Schema.Union( 30 | ...response.map( 31 | (response) => 32 | Schema.Struct({ 33 | status: Schema.Literal(ApiResponse.getStatus(response)), 34 | body: getSchema(ApiResponse.getBodySchema(response)), 35 | headers: getSchema( 36 | ApiResponse.getHeadersSchema(response), 37 | Schema.Record({ key: Schema.String, value: Schema.String }) 38 | ) 39 | }) 40 | ) 41 | ) 42 | } 43 | 44 | export type SchemaTo = S extends Schema.Schema ? A : never 45 | 46 | type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never 47 | 48 | /** 49 | * type X = IsUnion<"a" | "b" | "c"> => true 50 | * type X = IsUnion<"a" | "b"> => true 51 | * type X = IsUnion<"a"> => false 52 | */ 53 | export type IsUnion = [T] extends [UnionToIntersection] ? false : true 54 | 55 | export type RemoveIgnoredFields = Types.Simplify< 56 | { 57 | [K in keyof E as E[K] extends ApiSchema.Ignored ? never : K]: E[K] 58 | } 59 | > 60 | 61 | export type FilterNon200Responses = R extends any ? 62 | `${ApiResponse.ApiResponse.Status}` extends `2${string}` ? R : never : 63 | never 64 | 65 | export type NeedsFullResponse = ApiResponse.ApiResponse.Headers extends 66 | ApiSchema.Ignored ? false : true 67 | 68 | export type IgnoredToVoid = R extends ApiSchema.Ignored ? void : R 69 | -------------------------------------------------------------------------------- /packages/effect-http/test/api.test.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "effect" 2 | import { Api, ApiGroup } from "effect-http" 3 | import { identity, pipe } from "effect/Function" 4 | import { expect, test } from "vitest" 5 | 6 | export const simpleApi1 = pipe( 7 | Api.make(), 8 | Api.addEndpoint( 9 | Api.get("myOperation", "/get").pipe( 10 | Api.setResponseBody(Schema.String) 11 | ) 12 | ) 13 | ) 14 | 15 | test("fillDefaultSchemas", () => { 16 | expect(simpleApi1.groups).toHaveLength(1) 17 | expect(simpleApi1.groups.flatMap((group) => group.endpoints)).toHaveLength(1) 18 | }) 19 | 20 | test("Attempt to declare duplicate operation id should fail as a safe guard", () => { 21 | const api = pipe( 22 | Api.make(), 23 | Api.addEndpoint(Api.put("myOperation", "/my-operation")) 24 | ) 25 | 26 | expect(() => 27 | pipe( 28 | api, 29 | Api.addEndpoint(Api.post("myOperation", "/my-operation")) 30 | ) 31 | ).toThrowError() 32 | 33 | const apiGroup = pipe( 34 | ApiGroup.make("group"), 35 | ApiGroup.addEndpoint(Api.patch("myOperation", "/my-operation")) 36 | ) 37 | 38 | expect(() => 39 | pipe( 40 | apiGroup, 41 | ApiGroup.addEndpoint(ApiGroup.patch("myOperation", "/my-operation")) 42 | ) 43 | ).toThrowError() 44 | 45 | expect(() => pipe(api, Api.addGroup(apiGroup))).toThrowError() 46 | }) 47 | 48 | test.each( 49 | [ 50 | { 51 | expectFailure: false, 52 | path: "/hello", 53 | schema: undefined 54 | }, 55 | { 56 | expectFailure: false, 57 | path: "/hello/:input", 58 | schema: Schema.Struct({ input: Schema.String }) 59 | }, 60 | { 61 | expectFailure: true, 62 | path: "/hello/:input?", 63 | schema: Schema.Struct({ input: Schema.String }) 64 | }, 65 | { 66 | expectFailure: true, 67 | path: "/hello", 68 | schema: Schema.Struct({ input: Schema.String }) 69 | }, 70 | { 71 | expectFailure: false, 72 | path: "/hello/:id/another", 73 | schema: Schema.Struct({ id: Schema.Int }) 74 | }, 75 | { 76 | expectFailure: true, 77 | path: "/hello/:input", 78 | schema: undefined 79 | }, 80 | { 81 | expectFailure: false, 82 | path: "/hello/:input/another/:another", 83 | schema: Schema.Struct({ input: Schema.String, another: Schema.String }) 84 | }, 85 | { 86 | expectFailure: true, 87 | path: "/hello/:input/another/:another?", 88 | schema: Schema.Struct({ input: Schema.String, another: Schema.String }) 89 | }, 90 | { 91 | expectFailure: false, 92 | path: "/hello/:input/another/:another?", 93 | schema: Schema.Struct({ 94 | input: Schema.String, 95 | another: Schema.optional(Schema.String) 96 | }) 97 | } 98 | ] as const 99 | )( 100 | "Api path must match param schemas (%#)", 101 | ({ expectFailure, path, schema }) => { 102 | const createApi = () => 103 | pipe( 104 | Api.make(), 105 | Api.addEndpoint( 106 | Api.get("hello", path).pipe( 107 | schema && Api.setRequestPath(schema as Schema.Schema) || identity 108 | ) 109 | ) 110 | ) 111 | 112 | if (expectFailure) { 113 | expect(createApi).toThrowError() 114 | } else { 115 | createApi() 116 | } 117 | } 118 | ) 119 | -------------------------------------------------------------------------------- /packages/effect-http/test/client-error.test.ts: -------------------------------------------------------------------------------- 1 | import { ClientError } from "effect-http" 2 | import { expect, test } from "vitest" 3 | 4 | test("isClientSideError", () => { 5 | expect(ClientError.isClientSideError(ClientError.makeClientSide("error"))).toBe(true) 6 | expect(ClientError.isClientSideError(ClientError.makeServerSide("error", 400))).toBe(false) 7 | }) 8 | 9 | test("isServerSideError", () => { 10 | expect(ClientError.isServerSideError(ClientError.makeServerSide("error", 400))).toBe(true) 11 | expect(ClientError.isServerSideError(ClientError.makeClientSide("error"))).toBe(false) 12 | }) 13 | -------------------------------------------------------------------------------- /packages/effect-http/test/mock-client.test.ts: -------------------------------------------------------------------------------- 1 | import * as it from "@effect/vitest" 2 | import { Effect, Option } from "effect" 3 | import { MockClient } from "effect-http" 4 | import { expect } from "vitest" 5 | import { exampleApiGet, exampleApiPostNullableField } from "./examples.js" 6 | 7 | it.effect( 8 | "random example", 9 | () => 10 | Effect.gen(function*() { 11 | const client = MockClient.make(exampleApiGet) 12 | const response = yield* client.getValue({}) 13 | 14 | expect(typeof response).toEqual("number") 15 | }) 16 | ) 17 | 18 | it.effect( 19 | "custom response", 20 | () => 21 | Effect.gen(function*() { 22 | const client = MockClient.make(exampleApiGet, { 23 | responses: { getValue: 12 } 24 | }) 25 | const response = yield* client.getValue({}) 26 | 27 | expect(response).toEqual(12) 28 | }) 29 | ) 30 | 31 | it.effect( 32 | "response schema with `optionFromNullable`", 33 | () => 34 | Effect.gen(function*() { 35 | const client = MockClient.make(exampleApiPostNullableField) 36 | const response = yield* client.test({}) 37 | 38 | expect(Option.isOption(response.value)).toBe(true) 39 | }) 40 | ) 41 | -------------------------------------------------------------------------------- /packages/effect-http/test/representation.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "@effect/vitest" 2 | import { Effect } from "effect" 3 | import { Representation } from "effect-http" 4 | 5 | it.effect.each( 6 | [ 7 | { input: "string", expected: "string" }, 8 | { input: 69, expected: "69" }, 9 | { input: 69.42, expected: "69.42" }, 10 | { input: [12, "12"], expected: "[12,\"12\"]" }, 11 | { input: { a: "b" }, expected: "{\"a\":\"b\"}" } 12 | ] as const 13 | )("plain text stringify $input", ({ expected, input }) => 14 | Effect.gen(function*() { 15 | const result = yield* Representation.plainText.stringify(input) 16 | expect(result).toEqual(expected) 17 | })) 18 | -------------------------------------------------------------------------------- /packages/effect-http/test/security.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "vitest" 2 | 3 | test("wip", () => { 4 | expect(1).toBe(1) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/effect-http/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 | -------------------------------------------------------------------------------- /packages/effect-http/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 | -------------------------------------------------------------------------------- /packages/effect-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "tsconfig.src.json" }, 6 | { "path": "tsconfig.test.json" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/effect-http/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "tsBuildInfoFile": ".tsbuildinfo/src.tsbuildinfo", 6 | "rootDir": "src", 7 | "outDir": "build/src" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/effect-http/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "include": ["test"], 4 | "references": [{ "path": "tsconfig.src.json" }], 5 | "compilerOptions": { 6 | "tsBuildInfoFile": ".tsbuildinfo/test.tsbuildinfo", 7 | "rootDir": "test", 8 | "noEmit": true, 9 | "exactOptionalPropertyTypes": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/effect-http/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig, type UserConfigExport } from "vitest/config" 2 | import shared from "../../vitest.shared.js" 3 | 4 | const config: UserConfigExport = {} 5 | 6 | export default mergeConfig(shared, config) 7 | -------------------------------------------------------------------------------- /patches/@changesets__assemble-release-plan@6.0.4.patch: -------------------------------------------------------------------------------- 1 | diff --git a/dist/changesets-assemble-release-plan.cjs.js b/dist/changesets-assemble-release-plan.cjs.js 2 | index 4f7b5e5b37bb05874a5c1d8e583e29d4a9593ecf..4e9305b1a864efddec78902c4eae7977a610ab6b 100644 3 | --- a/dist/changesets-assemble-release-plan.cjs.js 4 | +++ b/dist/changesets-assemble-release-plan.cjs.js 5 | @@ -228,16 +228,6 @@ function determineDependents({ 6 | } of dependencyVersionRanges) { 7 | if (nextRelease.type === "none") { 8 | continue; 9 | - } else if (shouldBumpMajor({ 10 | - dependent, 11 | - depType, 12 | - versionRange, 13 | - releases, 14 | - nextRelease, 15 | - preInfo, 16 | - onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 17 | - })) { 18 | - type = "major"; 19 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange))) { 20 | switch (depType) { 21 | case "dependencies": 22 | diff --git a/dist/changesets-assemble-release-plan.esm.js b/dist/changesets-assemble-release-plan.esm.js 23 | index a327d9e4c709a6698f505d60d8bbf0046d4bde74..ac1d8638a13594667c76aa4fa620278841bb463a 100644 24 | --- a/dist/changesets-assemble-release-plan.esm.js 25 | +++ b/dist/changesets-assemble-release-plan.esm.js 26 | @@ -217,16 +217,6 @@ function determineDependents({ 27 | } of dependencyVersionRanges) { 28 | if (nextRelease.type === "none") { 29 | continue; 30 | - } else if (shouldBumpMajor({ 31 | - dependent, 32 | - depType, 33 | - versionRange, 34 | - releases, 35 | - nextRelease, 36 | - preInfo, 37 | - onlyUpdatePeerDependentsWhenOutOfRange: config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.onlyUpdatePeerDependentsWhenOutOfRange 38 | - })) { 39 | - type = "major"; 40 | } else if ((!releases.has(dependent) || releases.get(dependent).type === "none") && (config.___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH.updateInternalDependents === "always" || !semverSatisfies(incrementVersion(nextRelease, preInfo), versionRange))) { 41 | switch (depType) { 42 | case "dependencies": 43 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/* 3 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/clean.mjs: -------------------------------------------------------------------------------- 1 | import * as Glob from "glob" 2 | import * as Fs from "node:fs" 3 | 4 | const dirs = [".", ...Glob.sync("packages/*/")] 5 | dirs.forEach((pkg) => { 6 | const files = [".tsbuildinfo", "docs", "build", "dist", "coverage"] 7 | 8 | files.forEach((file) => { 9 | if (pkg === "." && file === "docs") { 10 | return 11 | } 12 | 13 | Fs.rmSync(`${pkg}/${file}`, { recursive: true, force: true }, () => {}) 14 | }) 15 | }) 16 | 17 | Glob.sync("docs/*/").forEach((dir) => { 18 | Fs.rmSync(dir, { recursive: true, force: true }, () => {}) 19 | }) 20 | -------------------------------------------------------------------------------- /scripts/docs.mjs: -------------------------------------------------------------------------------- 1 | import * as Fs from "node:fs" 2 | import * as Path from "node:path" 3 | 4 | function packages() { 5 | return Fs.readdirSync("packages").filter((_) => Fs.existsSync(Path.join("packages", _, "docs/modules"))) 6 | } 7 | 8 | function pkgName(pkg) { 9 | const packageJson = Fs.readFileSync( 10 | Path.join("packages", pkg, "package.json") 11 | ) 12 | return JSON.parse(packageJson).name 13 | } 14 | 15 | function copyFiles(pkg) { 16 | const name = pkgName(pkg) 17 | const docs = Path.join("packages", pkg, "docs/modules") 18 | const dest = Path.join("docs", pkg) 19 | const files = Fs.readdirSync(docs, { withFileTypes: true }) 20 | 21 | function handleFiles(root, files) { 22 | for (const file of files) { 23 | const path = Path.join(docs, root, file.name) 24 | const destPath = Path.join(dest, root, file.name) 25 | 26 | if (file.isDirectory()) { 27 | Fs.mkdirSync(destPath, { recursive: true }) 28 | handleFiles(file.name, Fs.readdirSync(path, { withFileTypes: true })) 29 | continue 30 | } 31 | 32 | const content = Fs.readFileSync(path, "utf8").replace( 33 | /^parent: Modules$/m, 34 | `parent: "${name}"` 35 | ) 36 | Fs.writeFileSync(destPath, content) 37 | } 38 | } 39 | 40 | Fs.rmSync(dest, { recursive: true, force: true }) 41 | Fs.mkdirSync(dest, { recursive: true }) 42 | handleFiles("", files) 43 | } 44 | 45 | function generateIndex(pkg, order) { 46 | const name = pkgName(pkg) 47 | const content = `--- 48 | title: "${name}" 49 | has_children: true 50 | permalink: /docs/${pkg} 51 | nav_order: ${order} 52 | --- 53 | ` 54 | 55 | Fs.writeFileSync(Path.join("docs", pkg, "index.md"), content) 56 | } 57 | 58 | packages().forEach((pkg, i) => { 59 | Fs.rmSync(Path.join("docs", pkg), { recursive: true, force: true }) 60 | Fs.mkdirSync(Path.join("docs", pkg), { recursive: true }) 61 | copyFiles(pkg) 62 | generateIndex(pkg, i + 2) 63 | }) 64 | -------------------------------------------------------------------------------- /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", "DOM.Iterable"], 16 | "types": [], 17 | "isolatedModules": true, 18 | "sourceMap": true, 19 | "declarationMap": true, 20 | "noImplicitReturns": false, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": false, 23 | "noFallthroughCasesInSwitch": true, 24 | "noEmitOnError": false, 25 | "noErrorTruncation": true, 26 | "allowJs": false, 27 | "checkJs": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "noImplicitAny": true, 30 | "noImplicitThis": true, 31 | "noUncheckedIndexedAccess": false, 32 | "strictNullChecks": true, 33 | "baseUrl": ".", 34 | "target": "ES2022", 35 | "module": "NodeNext", 36 | "incremental": true, 37 | "removeComments": false, 38 | "plugins": [{ "name": "@effect/language-service" }], 39 | "paths": { 40 | "effect-http": ["./packages/effect-http/src/index.js"], 41 | "effect-http/*": ["./packages/effect-http/src/*.js"], 42 | "effect-http-node": ["./packages/effect-http-node/src/index.js"], 43 | "effect-http-node/*": ["./packages/effect-http-node/src/*.js"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "packages/effect-http/tsconfig.build.json" }, 6 | { "path": "packages/effect-http-node/tsconfig.build.json" }, 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [], 4 | "references": [ 5 | { "path": "packages/effect-http" }, 6 | { "path": "packages/effect-http-node" } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tstyche.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://tstyche.org/schemas/config.json", 3 | "checkSourceFiles": true, 4 | "rejectAnyType": true, 5 | "testFileMatch": [ 6 | "packages/*/dtslint/**/*.tst.*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ["test/**/*.{test,spec}.?(c|m)[jt]s?(x)"], 6 | reporters: ["hanging-process", "default"], 7 | sequence: { 8 | concurrent: true 9 | }, 10 | chaiConfig: { 11 | truncateThreshold: 10000 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /vitest.shared.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path" 2 | import type { UserConfig } from "vitest/config" 3 | 4 | const alias = (pkg: string) => ({ 5 | [`${pkg}/test`]: path.join(__dirname, "packages", pkg, "test"), 6 | [pkg]: path.join(__dirname, "packages", pkg, "src") 7 | }) 8 | 9 | // This is a workaround, see https://github.com/vitest-dev/vitest/issues/4744 10 | const config: UserConfig = { 11 | test: { 12 | alias: { 13 | ...alias("effect-http"), 14 | ...alias("effect-http-node") 15 | } 16 | } 17 | } 18 | 19 | export default config 20 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from "vitest/config" 2 | 3 | export default defineWorkspace([ 4 | "packages/*" 5 | ]) 6 | --------------------------------------------------------------------------------