@tiesen/effect-tanstack-query 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: ['*']
6
+
7
+ concurrency:
8
+ group: ${{ github.workflow }}-${{ github.ref }}
9
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
10
+
11
+ env:
12
+ FORCE_COLOR: 3
13
+
14
+ jobs:
15
+ format:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v6
19
+ - uses: ./.github/workflows/setup
20
+ - name: Format
21
+ run: bun format
22
+
23
+ lint:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ - uses: ./.github/workflows/setup
28
+ - name: Lint
29
+ run: bun lint
30
+
31
+ typecheck:
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v6
35
+ - uses: ./.github/workflows/setup
36
+ - name: Typecheck
37
+ run: bun typecheck
@@ -0,0 +1,25 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ env:
9
+ FORCE_COLOR: 3
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write
16
+ id-token: write
17
+ pull-requests: write
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - uses: ./.github/workflows/setup
21
+
22
+ - name: Version & publish packages
23
+ run: bun tegami ci
24
+ env:
25
+ GITHUB_TOKEN: ${{ github.token }}
@@ -0,0 +1,15 @@
1
+ name: 'Setup Project'
2
+ description: 'Checkouts repo, sets up bun and installs dependencies'
3
+ runs:
4
+ using: 'composite'
5
+ steps:
6
+ - name: Setup Bun
7
+ uses: oven-sh/setup-bun@v2
8
+
9
+ - name: Install Dependencies
10
+ shell: bash
11
+ run: bun ci
12
+
13
+ - name: Build Packages
14
+ shell: bash
15
+ run: bun run build
@@ -0,0 +1,13 @@
1
+ core:changelogs:
2
+ - content: "---\npackages:\n npm:@tiesen/effect-tanstack-query:\n type: patch\n---\n\n## Initial Release\n\nFirst official release of `effect-tanstack-query` — a type-safe bridge between **Effect HTTP API clients** and **TanStack Query**.\n\n### Features\n\n- **TanStack Query Options Proxy**: Automatically generate `queryOptions`, `mutationOptions`, and `getQueryKey` from Effect `HttpApiClient` endpoints using ES6 Proxies.\n- **End-to-End Type Safety**:\n - Full type inference for `params`, `query`, `headers`, and `payload` schema inputs.\n - Automatic resolution of `Success` and `Error` response types.\n - Unwrap Effect `Schema` types seamlessly for TanStack Query options.\n- **Smart Arguments**: Supports optional input parameters when an endpoint requires no request parameters/payload.\n- **Effect Runtime Execution**: Seamlessly executes Effect pipelines as Promises backed by `ManagedRuntime` and supports request cancellation via `AbortSignal`.\n"
3
+ filename: 2026-07-28-ms4c4sko.md
4
+ v: 0.0.0
5
+ core:packages:
6
+ - changelogIds:
7
+ - 2026-07-28-ms4c4sko.md
8
+ id: npm:@tiesen/effect-tanstack-query
9
+ updated: true
10
+ npm:mark-latest:
11
+ - id: npm:@tiesen/effect-tanstack-query
12
+ npm:packages:
13
+ - id: npm:@tiesen/effect-tanstack-query
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ ## @tiesen/effect-tanstack-query@0.0.1
2
+
3
+ ### Initial Release
4
+
5
+ First official release of `effect-tanstack-query` — a type-safe bridge between **Effect HTTP API clients** and **TanStack Query**.
6
+
7
+ ### Features
8
+
9
+ - **TanStack Query Options Proxy**: Automatically generate `queryOptions`, `mutationOptions`, and `getQueryKey` from Effect `HttpApiClient` endpoints using ES6 Proxies.
10
+ - **End-to-End Type Safety**:
11
+ - Full type inference for `params`, `query`, `headers`, and `payload` schema inputs.
12
+ - Automatic resolution of `Success` and `Error` response types.
13
+ - Unwrap Effect `Schema` types seamlessly for TanStack Query options.
14
+ - **Smart Arguments**: Supports optional input parameters when an endpoint requires no request parameters/payload.
15
+ - **Effect Runtime Execution**: Seamlessly executes Effect pipelines as Promises backed by `ManagedRuntime` and supports request cancellation via `AbortSignal`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tiesen243
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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # effect-tanstack-query
2
+
3
+ A lightweight, type-safe bridge between **Effect HTTP API clients** and **TanStack Query** (`queryOptions`, `mutationOptions`, and `getQueryKey`).
4
+
5
+ ## Features
6
+
7
+ - **Seamless Integration**: Effortlessly map Effect `HttpApiClient` endpoints to TanStack Query config objects.
8
+ - **End-to-End Type Safety**: Automatically infers inputs (`params`, `query`, `headers`, `payload`), success types, and error types directly from your Effect schemas.
9
+ - **Zero Boilerplate**: Generates `queryKey` and execution functions under the hood using ES6 Proxies.
10
+ - **Developer Friendly**: Smart optional parameters when no input arguments are required.
11
+
12
+ ## Installation
13
+
14
+ Install using your preferred package manager:
15
+
16
+ ```bash
17
+ # Using bun
18
+ bun add effect-tanstack-query
19
+
20
+ # Using pnpm
21
+ pnpm add effect-tanstack-query
22
+
23
+ # Using npm
24
+ npm install effect-tanstack-query`
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ### 1. Define your Effect HTTP API Contract
30
+
31
+ ```ts
32
+ import { Schema } from 'effect'
33
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'
34
+
35
+ class ApiGroup extends HttpApiGroup.make('ApiGroup')
36
+ .add(
37
+ HttpApiEndpoint.get('hello', '/hello/:name', {
38
+ success: Schema.String,
39
+ params: Schema.Struct({
40
+ name: Schema.String,
41
+ }),
42
+ query: Schema.Struct({
43
+ greeting: Schema.String.pipe(Schema.optionalKey),
44
+ }),
45
+ })
46
+ )
47
+ .add(
48
+ HttpApiEndpoint.post('goodbye', '/goodbye', {
49
+ success: Schema.String,
50
+ payload: Schema.Struct({
51
+ name: Schema.String,
52
+ }),
53
+ })
54
+ ) {}
55
+
56
+ export class Api extends HttpApi.make('Api').add(ApiGroup) {}
57
+ ```
58
+
59
+ ### 2. Create an Effect HTTP API Client
60
+
61
+ ```ts
62
+ import { Context, Function, Layer } from 'effect'
63
+ import {
64
+ FetchHttpClient,
65
+ HttpClient,
66
+ HttpClientRequest,
67
+ } from 'effect/unstable/http'
68
+ import { HttpApiClient } from 'effect/unstable/httpapi'
69
+
70
+ class ApiClient extends Context.Service<
71
+ ApiClient,
72
+ HttpApiClient.ForApi<typeof Api>
73
+ >()('ApiClient') {
74
+ public static live = Layer.effect(
75
+ this,
76
+ HttpApiClient.make(Api, {
77
+ transformClient: (client) =>
78
+ client.pipe(
79
+ HttpClient.mapRequest(
80
+ Function.flow(HttpClientRequest.prependUrl('http://localhost:3000'))
81
+ )
82
+ ),
83
+ })
84
+ ).pipe(Layer.provide(FetchHttpClient.layer))
85
+ }
86
+ ```
87
+
88
+ ### 3. Use `effect-tanstack-query` to Generate Query and Mutation Options
89
+
90
+ ```ts
91
+ import { ManagedRuntime } from 'effect'
92
+ import { createTanstackQueryOptionsProxy } from 'effect-tanstack-query'
93
+
94
+ const runtime = ManagedRuntime.make(ApiClient.live)
95
+ const api = createTanstackQueryOptionsProxy(ApiClient, runtime)
96
+ ```
97
+
98
+ ### Usage
99
+
100
+ ```ts
101
+ import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'
102
+
103
+ // Query Example
104
+ const query = useQuery(
105
+ api.ApiGroup.hello.queryOptions({
106
+ params: { name: 'John' },
107
+ query: { greeting: 'Hi' },
108
+ })
109
+ )
110
+
111
+ // Mutation Example
112
+ const mutation = useMutation(api.ApiGroup.goodbye.mutationOptions())
113
+ mutation.mutate({ name: 'John' })
114
+
115
+ // Invalidate Query Example
116
+ const queryClient = useQueryClient()
117
+ void queryClient.invalidateQueries({
118
+ queryKey: api.ApiGroup.hello.getQueryKey(),
119
+ })
120
+ ```
121
+
122
+ ## License
123
+
124
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,36 @@
1
+ import { QueryClient } from '@tanstack/query-core'
2
+ import { Context, Function, Layer, ManagedRuntime } from 'effect'
3
+ import {
4
+ FetchHttpClient,
5
+ HttpClient,
6
+ HttpClientRequest,
7
+ } from 'effect/unstable/http'
8
+ import { HttpApiClient } from 'effect/unstable/httpapi'
9
+
10
+ import { createTanstackQueryOptionsProxy } from '../dist/index.mjs'
11
+ import { Api } from './contract'
12
+
13
+ class ApiClient extends Context.Service<
14
+ ApiClient,
15
+ HttpApiClient.ForApi<typeof Api>
16
+ >()('ApiClient') {
17
+ public static live = Layer.effect(
18
+ this,
19
+ HttpApiClient.make(Api, {
20
+ transformClient: (client) =>
21
+ client.pipe(
22
+ HttpClient.mapRequest(
23
+ // oxlint-disable-next-line unicorn/max-nested-calls
24
+ Function.flow(HttpClientRequest.prependUrl('http://localhost:3000'))
25
+ )
26
+ ),
27
+ })
28
+ ).pipe(Layer.provide(FetchHttpClient.layer))
29
+ }
30
+
31
+ const runtime = ManagedRuntime.make(ApiClient.live)
32
+ export const api = createTanstackQueryOptionsProxy(ApiClient, runtime)
33
+
34
+ new QueryClient().invalidateQueries({
35
+ queryKey: api.ApiGroup.hello.getQueryKey(),
36
+ })
@@ -0,0 +1,28 @@
1
+ // oxlint-disable max-classes-per-file
2
+
3
+ import { Schema } from 'effect'
4
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup } from 'effect/unstable/httpapi'
5
+
6
+ class ApiGroup extends HttpApiGroup.make('ApiGroup')
7
+ .add(
8
+ HttpApiEndpoint.get('hello', '/hello/:name', {
9
+ success: Schema.String,
10
+ params: Schema.Struct({
11
+ name: Schema.String,
12
+ }),
13
+ query: Schema.Struct({
14
+ // oxlint-disable-next-line unicorn/max-nested-calls
15
+ greeting: Schema.String.pipe(Schema.optionalKey),
16
+ }),
17
+ })
18
+ )
19
+ .add(
20
+ HttpApiEndpoint.post('goodbye', '/goodbye', {
21
+ success: Schema.String,
22
+ payload: Schema.Struct({
23
+ name: Schema.String,
24
+ }),
25
+ })
26
+ ) {}
27
+
28
+ export class Api extends HttpApi.make('Api').add(ApiGroup) {}
@@ -0,0 +1,27 @@
1
+ import { Effect, Layer } from 'effect'
2
+ import { HttpRouter, HttpServer } from 'effect/unstable/http'
3
+ import { HttpApiBuilder } from 'effect/unstable/httpapi'
4
+
5
+ import { Api } from './contract'
6
+
7
+ const ApiGroupLive = HttpApiBuilder.group(Api, 'ApiGroup', (handlers) =>
8
+ handlers
9
+ .handle('hello', ({ params, query }) =>
10
+ Effect.succeed(`${query.greeting ?? 'Hello'}, ${params.name}!`)
11
+ )
12
+ .handle('goodbye', ({ payload }) =>
13
+ Effect.succeed(`Goodbye, ${payload.name}!`)
14
+ )
15
+ )
16
+
17
+ const ApiLive = HttpApiBuilder.layer(Api, {
18
+ openapiPath: '/openapi.json',
19
+ }).pipe(
20
+ Layer.provide([ApiGroupLive]),
21
+ Layer.provide(HttpRouter.cors()),
22
+ Layer.provide(HttpServer.layerServices)
23
+ )
24
+
25
+ export default {
26
+ fetch: HttpRouter.toWebHandler(ApiLive).handler,
27
+ }
@@ -0,0 +1,38 @@
1
+ import { defineConfig } from 'oxfmt'
2
+
3
+ export default defineConfig({
4
+ printWidth: 80,
5
+ tabWidth: 2,
6
+ arrowParens: 'always',
7
+ bracketSameLine: false,
8
+ bracketSpacing: true,
9
+ endOfLine: 'lf',
10
+ quoteProps: 'as-needed',
11
+ semi: false,
12
+ singleQuote: true,
13
+ jsxSingleQuote: true,
14
+ trailingComma: 'es5',
15
+ useTabs: false,
16
+
17
+ sortImports: {
18
+ groups: [
19
+ 'side_effect_style',
20
+ 'type-import',
21
+ ['value-builtin', 'value-external'],
22
+ 'type-internal',
23
+ 'value-internal',
24
+ ['type-parent', 'type-sibling', 'type-index'],
25
+ ['value-parent', 'value-sibling', 'value-index'],
26
+ ],
27
+ newlinesBetween: true,
28
+ order: 'asc',
29
+ },
30
+
31
+ sortPackageJson: {
32
+ sortScripts: true,
33
+ },
34
+
35
+ sortTailwindcss: {
36
+ functions: ['clsx', 'cva', 'cn'],
37
+ },
38
+ })
@@ -0,0 +1,124 @@
1
+ import { defineConfig } from 'oxlint'
2
+
3
+ export default defineConfig({
4
+ plugins: [
5
+ 'eslint',
6
+ 'typescript',
7
+ 'unicorn',
8
+ 'oxc',
9
+ 'import',
10
+ 'jsdoc',
11
+ 'node',
12
+ 'promise',
13
+ ],
14
+ env: {
15
+ browser: true,
16
+ },
17
+ categories: {
18
+ correctness: 'error',
19
+ perf: 'error',
20
+ restriction: 'error',
21
+ suspicious: 'error',
22
+ pedantic: 'error',
23
+ style: 'error',
24
+ },
25
+ rules: {
26
+ 'arrow-body-style': ['error', 'as-needed'],
27
+ 'capitalized-comments': 'off',
28
+ curly: 'off',
29
+ 'func-style': 'off',
30
+ 'id-length': 'off',
31
+ 'init-declarations': 'off',
32
+ 'max-depth': 'off',
33
+ 'max-lines': 'off',
34
+ 'max-lines-per-function': 'off',
35
+ 'max-params': 'off',
36
+ 'max-statements': 'off',
37
+ 'new-cap': 'off',
38
+ 'no-await-in-loop': 'off',
39
+ 'no-console': 'off',
40
+ 'no-continue': 'off',
41
+ 'no-duplicate-imports': ['error', { allowSeparateTypeImports: true }],
42
+ 'no-implicit-coercion': 'off',
43
+ 'no-inline-comments': 'off',
44
+ 'no-magic-numbers': 'off',
45
+ 'no-use-before-define': 'off',
46
+ 'no-ternary': 'off',
47
+ 'no-undefined': 'off',
48
+ // Avoid conflict with typescript/no-floating-promises
49
+ 'no-void': ['error', { allowAsStatement: true }],
50
+ 'sort-keys': 'off',
51
+ // Rely on oxfmt `sortImports` instead
52
+ 'sort-imports': 'off',
53
+
54
+ 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
55
+ 'import/exports-last': 'off',
56
+ 'import/extensions': 'off',
57
+ 'import/group-exports': 'off',
58
+ 'import/max-dependencies': 'off',
59
+ 'import/no-anonymous-default-export': 'off',
60
+ 'import/no-commonjs': 'off',
61
+ 'import/no-default-export': 'off',
62
+ 'import/no-dynamic-require': 'off',
63
+ 'import/no-named-export': 'off',
64
+ 'import/no-namespace': 'off',
65
+ 'import/no-nodejs-modules': 'off',
66
+ 'import/no-relative-parent-imports': 'off',
67
+ 'import/no-unassigned-import': 'off',
68
+ 'import/prefer-default-export': 'off',
69
+ 'import/unambiguous': 'off',
70
+
71
+ 'jsdoc/require-param': 'off',
72
+ 'jsdoc/require-param-type': 'off',
73
+ 'jsdoc/require-returns': 'off',
74
+ 'jsdoc/require-returns-type': 'off',
75
+
76
+ 'unicorn/explicit-length-check': 'off',
77
+ 'unicorn/no-array-callback-reference': 'off',
78
+ 'unicorn/no-null': 'off',
79
+ 'unicorn/no-process-exit': 'off',
80
+ 'unicorn/number-literal-case': 'off',
81
+ 'unicorn/prefer-global-this': 'off',
82
+ 'unicorn/prefer-string-raw': 'off',
83
+ 'unicorn/prefer-top-level-await': 'off',
84
+
85
+ 'typescript/explicit-function-return-type': 'off',
86
+ 'typescript/explicit-module-boundary-types': 'off',
87
+ 'typescript/no-require-imports': 'off',
88
+ 'typescript/no-var-requires': 'off',
89
+ 'typescript/no-namespace': 'off',
90
+ 'typescript/require-await': 'off',
91
+
92
+ 'node/no-process-env': 'off',
93
+
94
+ 'oxc/no-async-await': 'off',
95
+ 'oxc/no-map-spread': 'off',
96
+ 'oxc/no-optional-chaining': 'off',
97
+ 'oxc/no-rest-spread-properties': 'off',
98
+
99
+ 'promise/always-return': 'off',
100
+ 'promise/catch-or-return': 'off',
101
+ },
102
+ overrides: [
103
+ {
104
+ // Shared test file overrides — framework-specific test rules
105
+ // are in separate jest/ and vitest/ configs to avoid conflicts.
106
+ files: [
107
+ '**/*.{test,spec}.{ts,tsx,js,jsx}',
108
+ '**/__tests__/**/*.{ts,tsx,js,jsx}',
109
+ ],
110
+ rules: {
111
+ // Disabled: mock callbacks often need empty functions
112
+ 'no-empty-function': 'off',
113
+ // Disabled: mock factories use Promise.resolve/reject (conflicts with require-await)
114
+ 'promise/prefer-await-to-then': 'off',
115
+ },
116
+ },
117
+ {
118
+ files: ['**/env.ts'],
119
+ rules: {
120
+ 'node/no-process-env': 'off',
121
+ },
122
+ },
123
+ ],
124
+ })
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@tiesen/effect-tanstack-query",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.mts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "build": "tsdown",
13
+ "format": "oxfmt --check",
14
+ "format:fix": "oxfmt",
15
+ "lint": "oxlint",
16
+ "lint:fix": "oxlint --fix",
17
+ "tegami": "bun run tegami.ts",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "devDependencies": {
21
+ "oxfmt": "^0.61.0",
22
+ "oxlint": "^1.76.0",
23
+ "tegami": "^1.2.7",
24
+ "tsdown": "^0.22.14",
25
+ "typescript": "^7.0.2"
26
+ },
27
+ "peerDependencies": {
28
+ "@tanstack/query-core": ">=5.100.0",
29
+ "effect": ">=4.0.0-beta.100 || >=4.0.0"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,137 @@
1
+ import type { MutationOptions, QueryOptions } from '@tanstack/query-core'
2
+ import type { Service } from 'effect/Context'
3
+ import type { ManagedRuntime } from 'effect/ManagedRuntime'
4
+
5
+ import * as Effect from 'effect/Effect'
6
+
7
+ import type { TanstackQueryOptionsProxy } from './types'
8
+
9
+ /**
10
+ * Creates a type-safe proxy that bridges Effect `HttpApiClient` endpoints
11
+ * with TanStack Query options (`queryOptions`, `mutationOptions`, and `getQueryKey`).
12
+ *
13
+ * @param tag - The Effect `Service` tag used to locate the client implementation within the Context.
14
+ * @param runtime - The Effect `ManagedRuntime` used to execute effects as promises.
15
+ *
16
+ * @returns A proxy object structured like the target API client, providing TanStack Query helpers for each endpoint.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * class ApiGroup extends HttpApiGroup.make('ApiGroup')
21
+ * .add(HttpApiEndpoint.get('hello', '/hello', { success: Schema.String, query: Schema.Struct({ name: Schema.String }) }))
22
+ * .add(HttpApiEndpoint.post('bye', '/bye', { success: Schema.String, payload: Schema.Struct({ name: Schema.String }) })) {}
23
+ *
24
+ * class Api extends HttpApi.make('Api').add(ApiGroup) {}
25
+ *
26
+ * class ApiClient extends Context.Service<ApiClient, HttpApiClient.ForApi<typeof Api>>()('ApiClient') {
27
+ * public static live = Layer.effect(this, HttpApiClient.make(Api)).pipe(Layer.provide(FetchHttpClient.layer))
28
+ * }
29
+ *
30
+ * const runtime = ManagedRuntime.make(ApiClient.live)
31
+ * const api = createTanstackQueryOptionsProxy(ApiClient, runtime);
32
+ *
33
+ * // Example usage in a React component with TanStack Query
34
+ * const { data } = useQuery(api.hello.queryOptions({ query: { name: 'World' } }))
35
+ * // ^? const data: string = "Hello, World!"
36
+ *
37
+ * const { mutate, data } = useMutation(api.bye.mutationOptions())
38
+ * // ^? const data: string = "Goodbye, World!"
39
+ * mutate({ name: 'World' })
40
+ * ```
41
+ */
42
+ function createTanstackQueryOptionsProxy<TServiceTag, TService>(
43
+ tag: Service<TServiceTag, TService>,
44
+ runtime: ManagedRuntime<TServiceTag, never>
45
+ ): TanstackQueryOptionsProxy<TService> {
46
+ /**
47
+ * Internal cache for memoizing Proxy instances to avoid unnecessary re-allocations.
48
+ */
49
+ const cache = new Map<string, unknown>()
50
+
51
+ /**
52
+ * Recursively creates or retrieves a cached Proxy instance for a given property path.
53
+ *
54
+ * @param path - An array of property keys representing the object trajectory (e.g., `['users', 'getById', 'queryOptions']`).
55
+ * @returns A proxy function handling property accesses and method invocations.
56
+ */
57
+ const createProxy = (path: string[]): unknown => {
58
+ const cacheKey = path.join('.')
59
+ if (cache.has(cacheKey)) return cache.get(cacheKey)
60
+
61
+ const proxy = new Proxy(
62
+ () => {
63
+ // This function is intentionally left empty. The proxy will handle method calls and property accesses.
64
+ },
65
+ {
66
+ get(_target, prop) {
67
+ if (typeof prop === 'symbol' || prop === 'then') return
68
+ return createProxy([...path, String(prop)])
69
+ },
70
+
71
+ apply(_target, _thisArg, args) {
72
+ const action = path.at(-1)
73
+ const apiPath = path.slice(0, -1)
74
+ const [input, options] = args
75
+
76
+ /**
77
+ * Executes the targeted Effect HTTP API endpoint by navigating the service instance.
78
+ *
79
+ * @param params - Parameters/payload passed into the target endpoint function.
80
+ * @param signal - Optional `AbortSignal` for canceling request execution.
81
+ * @returns A Promise resolving to the Effect result.
82
+ */
83
+ const execute = (params: unknown, signal?: AbortSignal) =>
84
+ runtime.runPromise(
85
+ Effect.gen(function* executeGen() {
86
+ // oxlint-disable-next-line typescript/no-explicit-any
87
+ const api: any = yield* tag
88
+
89
+ let fn = api
90
+ for (const p of apiPath) fn = fn[p]
91
+
92
+ return yield* fn(params)
93
+ }) as Effect.Effect<unknown, unknown, TServiceTag>,
94
+ { signal }
95
+ )
96
+
97
+ /**
98
+ * Constructs a consistent, structured array key for TanStack Query caching.
99
+ *
100
+ * @param queryType - The operation type (`'query'` or `'mutation'`).
101
+ * @param inp - The input parameters or payload associated with the endpoint request.
102
+ * @returns A read-only query key array.
103
+ */
104
+ const createKey = (queryType: string, inp: unknown) => [
105
+ { type: queryType },
106
+ ...apiPath,
107
+ ...(inp ? [inp] : []),
108
+ ]
109
+
110
+ if (action === 'queryOptions')
111
+ return {
112
+ ...options,
113
+ queryKey: createKey('query', input),
114
+ queryFn: ({ signal }) => execute(input, signal),
115
+ } satisfies QueryOptions
116
+
117
+ if (action === 'getQueryKey') return createKey('query', input)
118
+
119
+ if (action === 'mutationOptions')
120
+ return {
121
+ ...options,
122
+ mutationKey: createKey('mutation', input),
123
+ mutationFn: (payload) => execute({ payload }),
124
+ } satisfies MutationOptions
125
+ },
126
+ }
127
+ )
128
+
129
+ cache.set(cacheKey, proxy)
130
+ return proxy
131
+ }
132
+
133
+ return createProxy([]) as TanstackQueryOptionsProxy<TService>
134
+ }
135
+
136
+ export type { TanstackQueryOptionsProxy } from './types'
137
+ export { createTanstackQueryOptionsProxy }
package/src/types.ts ADDED
@@ -0,0 +1,93 @@
1
+ // oxlint-disable typescript/no-empty-object-type typescript/ban-types
2
+
3
+ import type { MutationOptions, QueryOptions } from '@tanstack/query-core'
4
+ import type { Schema } from 'effect/Schema'
5
+ import type { Client } from 'effect/unstable/httpapi/HttpApiClient'
6
+ import type { HttpApiEndpoint } from 'effect/unstable/httpapi/HttpApiEndpoint'
7
+
8
+ export type TanstackQueryOptionsProxy<T> =
9
+ T extends Client.Method<
10
+ HttpApiEndpoint<
11
+ infer _Key,
12
+ infer Method,
13
+ infer _Path,
14
+ infer Params,
15
+ infer Query,
16
+ infer Payload,
17
+ infer Headers,
18
+ infer Success,
19
+ infer Error
20
+ >,
21
+ infer _Error,
22
+ infer _Requires
23
+ >
24
+ ? {
25
+ queryOptions: Method extends 'GET'
26
+ ? <TQuery = QueryOptions<UnwrapCodec<Success>, UnwrapCodec<Error>>>(
27
+ input: MakeOptionalInput<
28
+ ([Params] extends [never]
29
+ ? {}
30
+ : { params: UnwrapCodec<Params> }) &
31
+ ([Query] extends [never]
32
+ ? {}
33
+ : { query: UnwrapCodec<Query> }) &
34
+ ([Headers] extends [never]
35
+ ? {}
36
+ : { headers: UnwrapCodec<Headers> })
37
+ >,
38
+ options?: Omit<TQuery, 'queryKey' | 'queryFn'>
39
+ ) => TQuery
40
+ : never
41
+ mutationOptions: Method extends 'GET'
42
+ ? never
43
+ : <
44
+ TMutation = MutationOptions<
45
+ UnwrapCodec<Success>,
46
+ UnwrapCodec<Error>,
47
+ UnwrapCodec<Payload>
48
+ >,
49
+ >(
50
+ input: MakeOptionalInput<
51
+ ([Params] extends [never]
52
+ ? {}
53
+ : { params: UnwrapCodec<Params> }) &
54
+ ([Headers] extends [never]
55
+ ? {}
56
+ : { headers: UnwrapCodec<Headers> })
57
+ >,
58
+ options?: Omit<TMutation, 'mutationKey' | 'mutationFn'>
59
+ ) => TMutation
60
+
61
+ getQueryKey: Method extends 'GET'
62
+ ? (
63
+ input?: Partial<
64
+ MakeOptionalInput<
65
+ ([Params] extends [never]
66
+ ? {}
67
+ : { params: UnwrapCodec<Params> }) &
68
+ ([Query] extends [never]
69
+ ? {}
70
+ : { query: UnwrapCodec<Query> }) &
71
+ ([Headers] extends [never]
72
+ ? {}
73
+ : { headers: UnwrapCodec<Headers> })
74
+ >
75
+ >
76
+ ) => readonly unknown[]
77
+ : never
78
+ }
79
+ : T extends object
80
+ ? {
81
+ readonly [K in keyof T]: TanstackQueryOptionsProxy<T[K]>
82
+ }
83
+ : T
84
+
85
+ type UnwrapCodec<T> =
86
+ T extends Schema<unknown>
87
+ ? Schema.Type<T>
88
+ : T extends object
89
+ ? { [K in keyof T]: UnwrapCodec<T[K]> }
90
+ : T
91
+
92
+ // oxlint-disable-next-line typescript/no-invalid-void-type
93
+ type MakeOptionalInput<T> = keyof T extends never ? void | undefined : T
package/tegami.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { tegami } from 'tegami'
2
+ import { runCli } from 'tegami/cli'
3
+ import { github } from 'tegami/plugins/github'
4
+
5
+ const paper = tegami({
6
+ npm: {
7
+ client: 'bun',
8
+ updateLockFile: true,
9
+ onBreakPeerDep: 'set',
10
+ trustedPublish: {
11
+ provider: 'github',
12
+ workflow: 'release.yml',
13
+ },
14
+ },
15
+
16
+ plugins: [
17
+ github({
18
+ repo: 'tiesen243/effect-tanstack-query',
19
+ versionPr: {
20
+ base: 'main',
21
+ },
22
+ }),
23
+ ],
24
+ })
25
+
26
+ // oxlint-disable-next-line node/no-top-level-await
27
+ await runCli(paper)
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["es2024", "dom"],
5
+ "target": "es2024",
6
+ "module": "preserve",
7
+ "moduleDetection": "force",
8
+ "allowJs": true,
9
+
10
+ // Bundler mode
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "verbatimModuleSyntax": true,
14
+ "noEmit": true,
15
+
16
+ // Best practices
17
+ "strict": true,
18
+ "skipLibCheck": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "noUncheckedIndexedAccess": true,
21
+ "noImplicitOverride": true,
22
+
23
+ // Some stricter flags (disabled by default)
24
+ "noUnusedLocals": false,
25
+ "noUnusedParameters": false,
26
+ "noPropertyAccessFromIndexSignature": false
27
+ },
28
+ "include": ["src/*.ts", "examples/*.ts"],
29
+ "exclude": ["node_modules", "dist"]
30
+ }
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'tsdown'
2
+
3
+ export default defineConfig({
4
+ entry: './src/index.ts',
5
+ dts: true,
6
+ minify: true,
7
+ shims: true,
8
+ })