├── .nvmrc ├── packages ├── protoc-gen-connect-query │ ├── .eslintignore │ ├── .gitignore │ ├── bin │ │ └── protoc-gen-connect-query │ ├── tsconfig.json │ ├── src │ │ ├── utils.ts │ │ ├── protoc-gen-connect-query-plugin.ts │ │ ├── generateDts.ts │ │ └── generateTs.ts │ ├── package.json │ └── README.md ├── test-utils │ ├── tsconfig.json │ ├── buf.gen.yaml │ ├── proto │ │ ├── proto2.proto │ │ ├── list.proto │ │ ├── bigint.proto │ │ ├── eliza.proto │ │ └── proto3.proto │ ├── package.json │ └── src │ │ ├── gen │ │ ├── proto2_pb.ts │ │ ├── list_pb.ts │ │ ├── bigint_pb.ts │ │ ├── eliza_pb.ts │ │ └── proto3_pb.ts │ │ └── index.tsx ├── connect-query-core │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── README.md │ ├── vite.config.ts │ ├── src │ │ ├── create-infinite-query-options.test.ts │ │ ├── index.ts │ │ ├── call-unary-method.ts │ │ ├── structural-sharing.ts │ │ ├── transport-key.ts │ │ ├── transport-key.test.ts │ │ ├── structural-sharing.test.ts │ │ ├── create-query-options.test.ts │ │ ├── utils.ts │ │ ├── create-query-options.ts │ │ ├── message-key.test.ts │ │ ├── message-key.ts │ │ ├── utils.test.ts │ │ ├── create-infinite-query-options.ts │ │ └── connect-query-key.ts │ └── package.json ├── connect-query │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── test │ │ │ └── test-wrapper.tsx │ │ ├── use-mutation.ts │ │ ├── use-transport.test.tsx │ │ ├── use-transport.tsx │ │ ├── call-unary-method.test.ts │ │ ├── use-query.ts │ │ ├── use-mutation.test.ts │ │ ├── use-infinite-query.ts │ │ └── use-query.test.ts │ ├── vite.config.ts │ └── package.json └── examples │ └── react │ └── basic │ ├── src │ ├── index.css │ ├── vite-env.d.ts │ ├── css.ts │ ├── page.tsx │ ├── gen │ │ ├── eliza-ElizaService_connectquery.ts │ │ └── eliza_pb.ts │ ├── main.test.tsx │ ├── main.tsx │ ├── example.tsx │ ├── indicator.tsx │ ├── datum.tsx │ └── assets │ │ └── react.svg │ ├── .gitignore │ ├── index.html │ ├── buf.gen.yaml │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ ├── public │ └── vite.svg │ └── eliza.proto ├── .vscode ├── extensions.json └── settings.json ├── assets ├── connect.png ├── equation.png ├── connect-query.ai ├── tanstack-query.png ├── connect-query@2x.png ├── connect-query@4x.png ├── connect-query@8x.png ├── connect-query@16x.png ├── cq-intro-thumb-github.png ├── connect-query_dependency_graph.png └── connect-query.svg ├── .gitignore ├── SECURITY.md ├── .github ├── CODE_OF_CONDUCT.md ├── workflows │ ├── add-to-project.yaml │ ├── pr-title.yaml │ ├── ci.yaml │ ├── publish-release.yml │ └── prepare-release.yml ├── dependabot.yaml ├── RELEASING.md └── CONTRIBUTING.md ├── MAINTAINERS.md ├── .gitattributes ├── scripts ├── find-workspace-version.js ├── gh-diffcheck.js ├── utils.js └── release.js ├── tsconfig.base.json ├── turbo.json ├── cspell.config.json ├── package.json └── .eslintrc.cjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v24.5.0 2 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/.eslintignore: -------------------------------------------------------------------------------- 1 | snapshots 2 | .type-dump 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["orta.vscode-twoslash-queries"] 3 | } 4 | -------------------------------------------------------------------------------- /assets/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect.png -------------------------------------------------------------------------------- /assets/equation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/equation.png -------------------------------------------------------------------------------- /assets/connect-query.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query.ai -------------------------------------------------------------------------------- /assets/tanstack-query.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/tanstack-query.png -------------------------------------------------------------------------------- /assets/connect-query@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query@2x.png -------------------------------------------------------------------------------- /assets/connect-query@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query@4x.png -------------------------------------------------------------------------------- /assets/connect-query@8x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query@8x.png -------------------------------------------------------------------------------- /packages/test-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/"], 3 | "extends": "../../tsconfig.base.json" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .turbo 2 | .wrangler 3 | node_modules 4 | /packages/*/dist 5 | /packages/*/coverage 6 | tsconfig.vitest-temp.json -------------------------------------------------------------------------------- /assets/connect-query@16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query@16x.png -------------------------------------------------------------------------------- /assets/cq-intro-thumb-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/cq-intro-thumb-github.png -------------------------------------------------------------------------------- /packages/connect-query-core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": ["src/index.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /assets/connect-query_dependency_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/connectrpc/connect-query-es/HEAD/assets/connect-query_dependency_graph.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.enableCommitSigning": true, 3 | "git.alwaysSignOff": true, 4 | "typescript.tsdk": "node_modules/typescript/lib" 5 | } 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This project follows the [Connect security policy and reporting 4 | process](https://connectrpc.com/docs/governance/security). 5 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Community Code of Conduct 2 | 3 | Connect follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 4 | -------------------------------------------------------------------------------- /packages/connect-query/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": ["src/index.ts"], 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/connect-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.test.ts", 4 | "**/*.test.tsx", 5 | "src/test/**.tsx", 6 | "vite.config.ts" 7 | ], 8 | "extends": "./tsconfig.build.json" 9 | } 10 | -------------------------------------------------------------------------------- /packages/connect-query-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*.test.ts", 4 | "**/*.test.tsx", 5 | "src/test/**.tsx", 6 | "vite.config.ts" 7 | ], 8 | "extends": "./tsconfig.build.json" 9 | } 10 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/.gitignore: -------------------------------------------------------------------------------- 1 | # A directory that exists solely to hold the result of tsc since --noEmit doesn't discover to protability issue found 2 | # by https://github.com/connectrpc/connect-query-es/issues/209 3 | .type-dump -------------------------------------------------------------------------------- /packages/examples/react/basic/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | background-color: #f5f7fa; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | display: flex; 12 | } 13 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/bin/protoc-gen-connect-query: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { runNodeJs } = require("@bufbuild/protoplugin"); 4 | const { 5 | protocGenConnectQuery, 6 | } = require("../dist/cjs/src/protoc-gen-connect-query-plugin.js"); 7 | 8 | runNodeJs(protocGenConnectQuery); 9 | -------------------------------------------------------------------------------- /packages/examples/react/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | pnpm-debug.log* 6 | 7 | node_modules 8 | dist 9 | dist-ssr 10 | *.local 11 | 12 | # Editor directories and files 13 | .vscode/* 14 | !.vscode/extensions.json 15 | .idea 16 | .DS_Store 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | ## Current 4 | 5 | - [Timo Stamm](https://github.com/timostamm), [Buf](https://buf.build) 6 | - [Steve Ayers](https://github.com/smaye81), [Buf](https://buf.build) 7 | - [Paul Sachs](https://github.com/paul-sachs), [Buf](https://buf.build) 8 | 9 | ## Former 10 | 11 | - [Dimitri Mitropoulos](https://github.com/dimitropoulos) 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # This is similar to the git option core.autocrlf but it applies to all 2 | # users of the repository and therefore doesn't depend on a developers 3 | # local configuration. 4 | * text=auto 5 | 6 | # Ignore generated files in GitHub diffs by default 7 | **/*_pb.ts linguist-generated=true 8 | **/*_connect.ts linguist-generated=true 9 | **/*_pb.js linguist-generated=true 10 | **/*_pb.d.ts linguist-generated=true 11 | -------------------------------------------------------------------------------- /packages/connect-query-core/README.md: -------------------------------------------------------------------------------- 1 | # @connectrpc/connect-query-core 2 | 3 | This package provides the core functionality for the Connect-Query API. It exposes all the necessary functions to use with the different variants of the tanstack/query packages. Documentation for these APIs can be found in the main repo readme at https://github.com/connectrpc/connect-query-es and covers any non-hook functions (anything that doesn't start with `use`). 4 | -------------------------------------------------------------------------------- /packages/test-utils/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # buf.gen.yaml defines a local generation template. 2 | # For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml 3 | version: v2 4 | inputs: 5 | - directory: proto 6 | # Deletes the directories specified in the `out` field for all plugins before running code generation. 7 | clean: true 8 | plugins: 9 | - local: protoc-gen-es 10 | out: src/gen 11 | opt: 12 | - target=ts 13 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["src/protoc-gen-connect-query-plugin.ts"], 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | // We import the plugin's version number from package.json 6 | "resolveJsonModule": true, 7 | // This package is CommonJS 8 | "verbatimModuleSyntax": false, 9 | "module": "CommonJS", 10 | "moduleResolution": "Node10" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/examples/react/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yaml: -------------------------------------------------------------------------------- 1 | name: Add issues and PRs to project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - reopened 8 | - transferred 9 | pull_request_target: 10 | types: 11 | - opened 12 | - reopened 13 | issue_comment: 14 | types: 15 | - created 16 | 17 | jobs: 18 | call-workflow-add-to-project: 19 | name: Call workflow to add issue to project 20 | uses: connectrpc/base-workflows/.github/workflows/add-to-project.yaml@main 21 | secrets: inherit 22 | -------------------------------------------------------------------------------- /packages/examples/react/basic/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # buf.gen.yaml defines a local generation template. 2 | # For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml 3 | version: v2 4 | inputs: 5 | - proto_file: eliza.proto 6 | # Deletes the directories specified in the `out` field for all plugins before running code generation. 7 | clean: true 8 | plugins: 9 | - local: protoc-gen-es 10 | out: src/gen 11 | opt: 12 | - target=ts 13 | - local: protoc-gen-connect-query 14 | out: src/gen 15 | opt: 16 | - target=ts 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yaml: -------------------------------------------------------------------------------- 1 | name: Lint PR Title 2 | # Prevent writing to the repository using the CI token. 3 | # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions 4 | permissions: 5 | pull-requests: read 6 | on: 7 | pull_request: 8 | # By default, a workflow only runs when a pull_request's activity type is opened, 9 | # synchronize, or reopened. We explicity override here so that PR titles are 10 | # re-linted when the PR text content is edited. 11 | types: 12 | - opened 13 | - edited 14 | - reopened 15 | - synchronize 16 | jobs: 17 | lint: 18 | uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main 19 | -------------------------------------------------------------------------------- /packages/connect-query/README.md: -------------------------------------------------------------------------------- 1 | # @connectrpc/connect-query 2 | 3 | This is the runtime library package for Connect-Query. You'll find its code generator at [@connectrpc/protoc-gen-connect-query](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query). 4 | 5 | Connect-Query is a wrapper around [TanStack Query](https://tanstack.com/query) (react-query), written in TypeScript and thoroughly tested. It enables effortless communication with servers that speak the [Connect Protocol](https://connectrpc.com/docs/protocol). 6 | 7 | To get started, head over to the [docs](https://github.com/connectrpc/connect-query-es) for a tutorial, or take a look at [our examples](https://github.com/connectrpc/connect-query-es/tree/main/examples). 8 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | /// 16 | -------------------------------------------------------------------------------- /scripts/find-workspace-version.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { findWorkspaceVersion } from "./utils.js"; 16 | 17 | process.stdout.write(`${findWorkspaceVersion("packages")}\n`); 18 | -------------------------------------------------------------------------------- /packages/examples/react/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": false, 6 | "exactOptionalPropertyTypes": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "jsx": "react-jsx", 10 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "noEmit": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ESNext", 18 | "useDefineForClassFields": true, 19 | "types": ["node"], 20 | "declaration": true // necessary to check if generated code can be published 21 | }, 22 | "include": ["src", "./*.config.ts", "__mocks__"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/test-utils/proto/proto2.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto2"; 16 | package test; 17 | 18 | message Proto2Message { 19 | optional string string_field = 1; 20 | optional int32 int32_field = 3; 21 | } 22 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; 16 | 17 | /** 18 | * Extracts the type of PluginInit from @bufbuild/protoplugin 19 | */ 20 | export type PluginInit = Required[0]>; 21 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": [ 5 | "ES2017", 6 | // DOM for the fetch and streams API 7 | "DOM" 8 | ], 9 | "esModuleInterop": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictBindCallApply": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitThis": true, 18 | "useUnknownInCatchVariables": true, 19 | "noUnusedLocals": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitOverride": true, 23 | 24 | // We need node's module resolution, so we do not have to skip lib checks 25 | "moduleResolution": "Node16", 26 | "module": "Node16", 27 | "verbatimModuleSyntax": true, 28 | "skipLibCheck": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/css.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export const borderRadius = 6; 16 | export const margin = 6; 17 | export const padding = 6; 18 | export const boxShadow = "0px 1px 2px rgba(15, 16, 77, 0.05)"; 19 | export const border = "1px solid #E4E9EF"; 20 | 21 | export const lightBlue = "#C4E8FC"; 22 | export const white = "#FFFFFF"; 23 | -------------------------------------------------------------------------------- /packages/test-utils/proto/list.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | service ListService { 18 | rpc List(ListRequest) returns (ListResponse); 19 | } 20 | 21 | message ListRequest { 22 | int64 page = 1; 23 | bool preview = 2; 24 | } 25 | 26 | message ListResponse { 27 | int64 page = 1; 28 | repeated string items = 2; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build", "generate"], 6 | "outputs": ["dist/**"] 7 | }, 8 | "generate": { 9 | "dependsOn": ["^build"], 10 | "outputs": ["src/gen/**"] 11 | }, 12 | "test": { 13 | "dependsOn": ["build"], 14 | "cache": false 15 | }, 16 | "format": {}, 17 | "license-header": { 18 | "dependsOn": ["generate"] 19 | }, 20 | "lint": { 21 | "dependsOn": ["format", "^build", "generate"] 22 | }, 23 | "attw": { 24 | "dependsOn": ["build"] 25 | }, 26 | "//#format": { 27 | "inputs": ["$TURBO_DEFAULT$", "!packages/**", "package-lock.json"] 28 | }, 29 | "//#license-header": { 30 | "inputs": ["$TURBO_DEFAULT$", "!packages/**"] 31 | }, 32 | "//#lint": { 33 | "dependsOn": ["format"], 34 | "inputs": ["$TURBO_DEFAULT$", "!packages/**", "package-lock.json"] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/test-utils/proto/bigint.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | import "google/protobuf/empty.proto"; 18 | 19 | service BigIntService { 20 | rpc Count(CountRequest) returns (CountResponse); 21 | rpc GetCount(google.protobuf.Empty) returns (CountResponse); 22 | } 23 | 24 | message CountRequest { 25 | int64 add = 1; 26 | } 27 | 28 | message CountResponse { 29 | int64 count = 1; 30 | } 31 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/page.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { FC, PropsWithChildren } from "react"; 16 | 17 | import { margin } from "./css"; 18 | 19 | /** 20 | * The wrapper for the whole page 21 | */ 22 | export const Page: FC = ({ children }) => ( 23 |
30 | {children} 31 |
32 | ); 33 | -------------------------------------------------------------------------------- /packages/examples/react/basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import react from "@vitejs/plugin-react"; 16 | import { defineConfig } from "vitest/config"; 17 | 18 | // https://vitejs.dev/config/ 19 | export default defineConfig({ 20 | plugins: [react()], 21 | test: { 22 | environment: "jsdom", 23 | typecheck: { 24 | enabled: true, 25 | // Modified to typecheck definition files as well as source files 26 | include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], 27 | }, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/gen/eliza-ElizaService_connectquery.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts" 16 | // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import { ElizaService } from "./eliza_pb"; 20 | 21 | /** 22 | * Say is a unary RPC. Eliza responds to the prompt with a single sentence. 23 | * 24 | * @generated from rpc connectrpc.eliza.v1.ElizaService.Say 25 | */ 26 | export const say = ElizaService.method.say; 27 | -------------------------------------------------------------------------------- /cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "words": [ 3 | "Deno", 4 | "Dimitri", 5 | "Mitropoulos", 6 | "Quickstart", 7 | "Stamm", 8 | "Timo", 9 | "Vindaloo", 10 | "Weizenbaum's", 11 | "attw", 12 | "backoffs", 13 | "bufbuild", 14 | "codegen", 15 | "connectquery", 16 | "connectrpc", 17 | "connectweb", 18 | "descriptorset", 19 | "excalidraw", 20 | "idempotence", 21 | "idempotency", 22 | "inferencing", 23 | "invalidators", 24 | "keyof", 25 | "lcov", 26 | "nocheck", 27 | "pnpm", 28 | "preconfigured", 29 | "proto", 30 | "protobuf", 31 | "protoc", 32 | "protofile", 33 | "protoplugin", 34 | "tanstack", 35 | "todos", 36 | "tsdoc", 37 | "corepack", 38 | "printables", 39 | "arethetypeswrong", 40 | "oneof", 41 | "typesafe", 42 | "setversion", 43 | "getversion", 44 | "postsetversion", 45 | "postgenerate", 46 | "npmjs" 47 | ], 48 | "ignorePaths": [ 49 | "**/*.svg", 50 | "**/*.ai", 51 | "**/pnpm-lock.yaml", 52 | "*.excalidraw", 53 | "**/gen", 54 | "**/snapshots", 55 | "**/*.css", 56 | "**/*.xml", 57 | "**/tsconfig.vitest-temp.json" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /packages/connect-query/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export * from "@connectrpc/connect-query-core"; 16 | export { useTransport, TransportProvider } from "./use-transport.js"; 17 | export { 18 | useInfiniteQuery, 19 | useSuspenseInfiniteQuery, 20 | } from "./use-infinite-query.js"; 21 | export { useQuery, useSuspenseQuery } from "./use-query.js"; 22 | export type { UseMutationOptions } from "./use-mutation.js"; 23 | export { useMutation } from "./use-mutation.js"; 24 | export type { UseInfiniteQueryOptions } from "./use-infinite-query.js"; 25 | export type { UseQueryOptions } from "./use-query.js"; 26 | -------------------------------------------------------------------------------- /packages/examples/react/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@connectrpc/connect-query-example-basic", 3 | "version": "2.2.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "generate": "buf generate", 9 | "license-header": "license-header", 10 | "test": "vitest --run", 11 | "test:watch": "vitest --watch --ui", 12 | "lint": "eslint --max-warnings 0 .", 13 | "format": "prettier --write . '!src/gen'" 14 | }, 15 | "dependencies": { 16 | "@bufbuild/buf": "1.54.0", 17 | "@bufbuild/protobuf": "^2.5.1", 18 | "@bufbuild/protoc-gen-es": "^2.5.1", 19 | "@connectrpc/connect": "^2.0.2", 20 | "@connectrpc/connect-query": "^2.2.0", 21 | "@connectrpc/connect-web": "^2.0.2", 22 | "@connectrpc/protoc-gen-connect-query": "^2.2.0", 23 | "@tanstack/react-query": "^5.79.0", 24 | "@tanstack/react-query-devtools": "^5.79.0", 25 | "@testing-library/jest-dom": "^6.6.3", 26 | "@testing-library/react": "^16.3.0", 27 | "@types/react": "^19.1.6", 28 | "@types/react-dom": "^19.1.5", 29 | "@vitejs/plugin-react": "^4.5.0", 30 | "react": "^19.1.0", 31 | "react-dom": "^19.1.0", 32 | "typescript": "^5.8.3", 33 | "vite": "^6.3.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/src/protoc-gen-connect-query-plugin.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; 16 | 17 | import { version } from "../package.json"; 18 | import { generateDts } from "./generateDts.js"; 19 | import { generateTs } from "./generateTs.js"; 20 | 21 | export const protocGenConnectQuery = createEcmaScriptPlugin({ 22 | name: "protoc-gen-connect-query", 23 | version: `v${String(version)}`, 24 | generateTs, 25 | 26 | // The generated TypeScript output is completely valid JavaScript since all the types are inferred 27 | generateJs: generateTs, 28 | generateDts, 29 | }); 30 | -------------------------------------------------------------------------------- /packages/connect-query/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { defineConfig } from "vitest/config"; 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | test: { 20 | environment: "jsdom", 21 | typecheck: { 22 | enabled: true, 23 | // Modified to typecheck definition files as well as source files 24 | include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], 25 | }, 26 | coverage: { 27 | provider: "istanbul", 28 | thresholds: { 29 | branches: 100, 30 | functions: 100, 31 | lines: 100, 32 | statements: 100, 33 | }, 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /packages/connect-query-core/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { defineConfig } from "vitest/config"; 16 | 17 | // https://vitejs.dev/config/ 18 | export default defineConfig({ 19 | test: { 20 | environment: "jsdom", 21 | typecheck: { 22 | enabled: true, 23 | // Modified to typecheck definition files as well as source files 24 | include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], 25 | }, 26 | coverage: { 27 | provider: "istanbul", 28 | thresholds: { 29 | branches: 100, 30 | functions: 100, 31 | lines: 100, 32 | statements: 100, 33 | }, 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | day: "monday" 8 | timezone: UTC 9 | time: "07:00" 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "monthly" 14 | day: "monday" 15 | timezone: UTC 16 | time: "07:00" 17 | open-pull-requests-limit: 50 18 | groups: 19 | connectRelated: 20 | patterns: 21 | - "@connectrpc/*" 22 | - "@bufbuild/*" 23 | devDependencies: 24 | patterns: 25 | - "@arethetypeswrong/*" 26 | - "@testing-library/*" 27 | - "@types/*" 28 | - "@typescript-eslint/*" 29 | - "@vitejs/*" 30 | - "cspell" 31 | - "eslint*" 32 | - "jest-mock" 33 | - "jest" 34 | - "prettier" 35 | - "react-dom" 36 | - "react" 37 | - "ts-jest" 38 | - "ts-node" 39 | - "turbo" 40 | - "typescript" 41 | - "vite" 42 | - "vitest" 43 | - "@vitest/*" 44 | reactQuery: 45 | patterns: 46 | - "@tanstack/react-query" 47 | - "@tanstack/react-query-devtools" 48 | - "@tanstack/query-core" 49 | -------------------------------------------------------------------------------- /scripts/gh-diffcheck.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { execSync } from "node:child_process"; 16 | 17 | if (gitUncommitted()) { 18 | process.stdout.write( 19 | "::error::Uncommitted changes found. Please make sure this branch is up to date, and run the command locally (for example `npx turbo format`). " + 20 | "Verify the changes are what you want and commit them.\n", 21 | ); 22 | execSync("git --no-pager diff", { 23 | stdio: "inherit", 24 | }); 25 | process.exit(1); 26 | } 27 | 28 | /** 29 | * @returns {boolean} 30 | */ 31 | function gitUncommitted() { 32 | const out = execSync("git status --porcelain", { 33 | encoding: "utf-8", 34 | }); 35 | return out.trim().length > 0; 36 | } 37 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-utils", 3 | "private": true, 4 | "version": "2.2.0", 5 | "type": "module", 6 | "scripts": { 7 | "generate": "buf generate", 8 | "postgenerate": "license-header gen", 9 | "prebuild": "rm -rf ./dist/*", 10 | "build": "npm run build:cjs && npm run build:esm", 11 | "build:cjs": "tsc --project tsconfig.json --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs --declaration --declarationDir ./dist/cjs && echo >./dist/cjs/package.json '{\"type\":\"commonjs\"}'", 12 | "build:esm": "tsc --project tsconfig.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", 13 | "format": "prettier --write --ignore-unknown '.' '!dist' '!src/gen'", 14 | "license-header": "license-header --ignore 'src/gen/**'", 15 | "lint": "eslint --max-warnings 0 ." 16 | }, 17 | "main": "./dist/cjs/index.js", 18 | "types": "./dist/cjs/index.d.ts", 19 | "exports": { 20 | ".": { 21 | "import": "./dist/esm/index.js" 22 | }, 23 | "./gen/*": { 24 | "import": "./dist/esm/gen/*" 25 | } 26 | }, 27 | "devDependencies": { 28 | "@bufbuild/buf": "^1.54.0", 29 | "@bufbuild/protobuf": "^2.5.1", 30 | "@bufbuild/protoc-gen-es": "^2.5.1", 31 | "@connectrpc/connect": "^2.0.2", 32 | "@connectrpc/connect-web": "^2.0.2", 33 | "@types/react": "^19.1.6", 34 | "react": "^19.1.0" 35 | }, 36 | "files": [ 37 | "dist/**" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /packages/examples/react/basic/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/create-infinite-query-options.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { mockEliza } from "test-utils"; 16 | import { ListService } from "test-utils/gen/list_pb.js"; 17 | import { describe, expect, expectTypeOf, it } from "vitest"; 18 | 19 | import { createInfiniteQueryOptions, skipToken } from "./index.js"; 20 | 21 | const listMethod = ListService.method.list; 22 | 23 | const mockedElizaTransport = mockEliza(); 24 | 25 | describe("createInfiniteQueryOptions", () => { 26 | it("honors skipToken", () => { 27 | const opt = createInfiniteQueryOptions(listMethod, skipToken, { 28 | transport: mockedElizaTransport, 29 | getNextPageParam: (lastPage) => lastPage.page + 1n, 30 | pageParamKey: "page", 31 | }); 32 | expect(opt.queryFn).toBe(skipToken); 33 | expectTypeOf(opt.queryFn).toEqualTypeOf(skipToken); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/test-utils/proto/eliza.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package connectrpc.eliza.v1; 18 | 19 | // ElizaService provides a way to talk to Eliza, a port of the DOCTOR script 20 | // for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at 21 | // the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the 22 | // superficiality of human-computer communication. DOCTOR simulates a 23 | // psychotherapist, and is commonly found as an Easter egg in emacs 24 | // distributions. 25 | service ElizaService { 26 | // Say is a unary RPC. Eliza responds to the prompt with a single sentence. 27 | rpc Say(SayRequest) returns (SayResponse) {} 28 | } 29 | 30 | // SayRequest is a single-sentence request. 31 | message SayRequest { 32 | string sentence = 1; 33 | } 34 | 35 | // SayResponse is a single-sentence response. 36 | message SayResponse { 37 | string sentence = 1; 38 | } 39 | -------------------------------------------------------------------------------- /packages/examples/react/basic/eliza.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package connectrpc.eliza.v1; 18 | 19 | // ElizaService provides a way to talk to Eliza, a port of the DOCTOR script 20 | // for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at 21 | // the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the 22 | // superficiality of human-computer communication. DOCTOR simulates a 23 | // psychotherapist, and is commonly found as an Easter egg in emacs 24 | // distributions. 25 | service ElizaService { 26 | // Say is a unary RPC. Eliza responds to the prompt with a single sentence. 27 | rpc Say(SayRequest) returns (SayResponse) {} 28 | } 29 | 30 | // SayRequest is a single-sentence request. 31 | message SayRequest { 32 | string sentence = 1; 33 | } 34 | 35 | // SayResponse is a single-sentence response. 36 | message SayResponse { 37 | string sentence = 1; 38 | } 39 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@connectrpc/protoc-gen-connect-query", 3 | "version": "2.2.0", 4 | "description": "Code generator for connect-query", 5 | "license": "Apache-2.0", 6 | "sideEffects": false, 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/connectrpc/connect-query-es.git", 10 | "directory": "packages/protoc-gen-connect-query" 11 | }, 12 | "files": [ 13 | "dist/**" 14 | ], 15 | "bin": { 16 | "protoc-gen-connect-query": "bin/protoc-gen-connect-query" 17 | }, 18 | "engines": { 19 | "node": ">=20" 20 | }, 21 | "scripts": { 22 | "prebuild": "rm -rf ./dist/*", 23 | "build": "tsc --project tsconfig.json --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs", 24 | "format": "prettier --write --ignore-unknown '.' '!dist'", 25 | "license-header": "license-header", 26 | "lint": "eslint --max-warnings 0 ." 27 | }, 28 | "preferUnplugged": true, 29 | "devDependencies": { 30 | "@bufbuild/buf": "1.54.0", 31 | "@bufbuild/protoc-gen-es": "^2.5.1", 32 | "@connectrpc/connect": "^2.0.2", 33 | "@connectrpc/connect-query": "^2.2.0", 34 | "@tanstack/react-query": "^5.79.0", 35 | "typescript": "^5.8.3" 36 | }, 37 | "dependencies": { 38 | "@bufbuild/protobuf": "^2.5.1", 39 | "@bufbuild/protoplugin": "^2.2.1" 40 | }, 41 | "peerDependencies": { 42 | "@bufbuild/protoc-gen-es": "2.x" 43 | }, 44 | "peerDependenciesMeta": { 45 | "@bufbuild/protoc-gen-es": { 46 | "optional": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | export type { ConnectQueryKey } from "./connect-query-key.js"; 16 | export { createConnectQueryKey } from "./connect-query-key.js"; 17 | export { createProtobufSafeUpdater } from "./utils.js"; 18 | export type { ConnectUpdater } from "./utils.js"; 19 | export { callUnaryMethod } from "./call-unary-method.js"; 20 | export { createInfiniteQueryOptions } from "./create-infinite-query-options.js"; 21 | export type { 22 | ConnectInfiniteQueryOptions, 23 | InfiniteQueryOptionsWithSkipToken, 24 | InfiniteQueryOptions, 25 | } from "./create-infinite-query-options.js"; 26 | export { createQueryOptions } from "./create-query-options.js"; 27 | export type { 28 | QueryOptions, 29 | QueryOptionsWithSkipToken, 30 | } from "./create-query-options.js"; 31 | export { addStaticKeyToTransport } from "./transport-key.js"; 32 | export type { SkipToken } from "@tanstack/query-core"; 33 | export { skipToken } from "@tanstack/query-core"; 34 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/main.test.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "@testing-library/jest-dom/vitest"; 16 | 17 | import { createRouterTransport } from "@connectrpc/connect"; 18 | import { render, screen } from "@testing-library/react"; 19 | import { describe, expect, it } from "vitest"; 20 | 21 | import * as methods from "./gen/eliza-ElizaService_connectquery"; 22 | import Main from "./main"; 23 | 24 | describe("Application", () => { 25 | it("should show success status and response data", async () => { 26 | const transport = createRouterTransport(({ rpc }) => { 27 | rpc(methods.say, () => ({ 28 | sentence: "Hello, world!", 29 | })); 30 | }); 31 | render(
); 32 | const text = await screen.findByText("Status: success"); 33 | expect(text).toBeInTheDocument(); 34 | const response = await screen.findByLabelText("data"); 35 | expect(response).toHaveTextContent( 36 | '{"$typeName":"connectrpc.eliza.v1.SayResponse","sentence":"Hello, world!"}', 37 | ); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/call-unary-method.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import { create } from "@bufbuild/protobuf"; 22 | import type { Transport } from "@connectrpc/connect"; 23 | 24 | /** 25 | * Call a unary method given its signature and input. 26 | */ 27 | // eslint-disable-next-line @typescript-eslint/max-params -- 4th param is optional 28 | export async function callUnaryMethod< 29 | I extends DescMessage, 30 | O extends DescMessage, 31 | >( 32 | transport: Transport, 33 | schema: DescMethodUnary, 34 | input: MessageInitShape | undefined, 35 | options?: { 36 | signal?: AbortSignal; 37 | headers?: HeadersInit; 38 | }, 39 | ): Promise> { 40 | const result = await transport.unary( 41 | schema, 42 | options?.signal, 43 | undefined, 44 | options?.headers, 45 | input ?? create(schema.input), 46 | undefined, 47 | ); 48 | return result.message; 49 | } 50 | -------------------------------------------------------------------------------- /packages/test-utils/proto/proto3.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | package test; 17 | 18 | // Note: We do not exhaust all field types 19 | message Proto3Message { 20 | string string_field = 1; 21 | bytes bytes_field = 2; 22 | int32 int32_field = 3; 23 | int64 int64_field = 4; 24 | double double_field = 5; 25 | bool bool_field = 6; 26 | Proto3Enum enum_field = 7; 27 | Proto3Message message_field = 8; 28 | 29 | optional string optional_string_field = 9; 30 | 31 | repeated string repeated_string_field = 17; 32 | repeated Proto3Message repeated_message_field = 18; 33 | repeated Proto3Enum repeated_enum_field = 19; 34 | 35 | oneof either { 36 | string oneof_string_field = 31; 37 | int32 oneof_int32_field = 33; 38 | } 39 | 40 | map map_string_int64_field = 39; 41 | map map_string_message_field = 40; 42 | map map_string_enum_field = 41; 43 | } 44 | 45 | enum Proto3Enum { 46 | PROTO3_ENUM_UNSPECIFIED = 0; 47 | PROTO3_ENUM_YES = 1; 48 | PROTO3_ENUM_NO = 2; 49 | } 50 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/structural-sharing.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { type DescMessage, equals, isMessage } from "@bufbuild/protobuf"; 16 | import { replaceEqualDeep } from "@tanstack/query-core"; 17 | 18 | /** 19 | * Returns a simplistic implementation for "structural sharing" for a Protobuf 20 | * message. 21 | * 22 | * To keep references intact between re-renders, we return the old version if it 23 | * equals the new version. 24 | * 25 | * See https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations#structural-sharing 26 | */ 27 | export function createStructuralSharing( 28 | schema: DescMessage, 29 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- matching the @tanstack/react-query types 30 | ): (oldData: unknown | undefined, newData: unknown) => unknown { 31 | return function (oldData, newData) { 32 | if (!isMessage(oldData) || !isMessage(newData)) { 33 | return replaceEqualDeep(oldData, newData); 34 | } 35 | if (!equals(schema, oldData, newData)) { 36 | return newData; 37 | } 38 | return oldData; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { readdirSync, readFileSync, existsSync } from "node:fs"; 16 | import { join } from "node:path"; 17 | 18 | /** 19 | * Retrieves the workspace version from the package directory. 20 | * 21 | * @param {string} packagesDir 22 | * @returns {string} 23 | */ 24 | export function findWorkspaceVersion(packagesDir) { 25 | let version = undefined; 26 | for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { 27 | if (!entry.isDirectory()) { 28 | continue; 29 | } 30 | const path = join(packagesDir, entry.name, "package.json"); 31 | if (existsSync(path)) { 32 | const pkg = JSON.parse(readFileSync(path, "utf-8")); 33 | if (pkg.private === true) { 34 | continue; 35 | } 36 | if (!pkg.version) { 37 | throw new Error(`${path} is missing "version"`); 38 | } 39 | if (version === undefined) { 40 | version = pkg.version; 41 | } else if (version !== pkg.version) { 42 | throw new Error(`${path} has unexpected version ${pkg.version}`); 43 | } 44 | } 45 | } 46 | if (version === undefined) { 47 | throw new Error(`unable to find workspace version`); 48 | } 49 | return version; 50 | } 51 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import "./index.css"; 16 | 17 | import type { Transport } from "@connectrpc/connect"; 18 | import { TransportProvider } from "@connectrpc/connect-query"; 19 | import { createConnectTransport } from "@connectrpc/connect-web"; 20 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 21 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 22 | import * as ReactDOM from "react-dom/client"; 23 | 24 | import { Example } from "./example"; 25 | 26 | const queryClient = new QueryClient(); 27 | 28 | /** 29 | * The application root 30 | */ 31 | export default function App({ transport }: { transport?: Transport }) { 32 | const finalTransport = 33 | transport ?? 34 | createConnectTransport({ 35 | baseUrl: "https://demo.connectrpc.com", 36 | }); 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | 47 | const rootElement = document.getElementById("root"); 48 | if (rootElement) { 49 | ReactDOM.createRoot(rootElement).render(); 50 | } 51 | -------------------------------------------------------------------------------- /packages/test-utils/src/gen/proto2_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file proto2.proto (package test, syntax proto2) 17 | /* eslint-disable */ 18 | 19 | import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; 20 | import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { Message } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Describes the file proto2.proto. 25 | */ 26 | export const file_proto2: GenFile = /*@__PURE__*/ 27 | fileDesc("Cgxwcm90bzIucHJvdG8SBHRlc3QiOgoNUHJvdG8yTWVzc2FnZRIUCgxzdHJpbmdfZmllbGQYASABKAkSEwoLaW50MzJfZmllbGQYAyABKAU"); 28 | 29 | /** 30 | * @generated from message test.Proto2Message 31 | */ 32 | export type Proto2Message = Message<"test.Proto2Message"> & { 33 | /** 34 | * @generated from field: optional string string_field = 1; 35 | */ 36 | stringField: string; 37 | 38 | /** 39 | * @generated from field: optional int32 int32_field = 3; 40 | */ 41 | int32Field: number; 42 | }; 43 | 44 | /** 45 | * Describes the message test.Proto2Message. 46 | * Use `create(Proto2MessageSchema)` to create a new message. 47 | */ 48 | export const Proto2MessageSchema: GenMessage = /*@__PURE__*/ 49 | messageDesc(file_proto2, 0); 50 | 51 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/example.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { useQuery } from "@connectrpc/connect-query"; 16 | import type { FC } from "react"; 17 | 18 | import { Data, Datum } from "./datum"; 19 | import { say } from "./gen/eliza-ElizaService_connectquery"; 20 | import { Indicator, Indicators } from "./indicator"; 21 | import { Page } from "./page"; 22 | 23 | /** 24 | * This example demonstrates a basic usage of Connect-Query with `useQuery` 25 | */ 26 | export const Example: FC = () => { 27 | const { status, fetchStatus, error, data } = useQuery(say, { 28 | sentence: "Hello", 29 | }); 30 | 31 | return ( 32 | 33 | Status: {status} 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/indicator.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { FC, ReactNode } from "react"; 16 | 17 | import { border, borderRadius, boxShadow, margin } from "./css"; 18 | 19 | /** 20 | * a single Indicator 21 | */ 22 | export const Indicator = ({ 23 | label, 24 | parent, 25 | }: { 26 | label: U; 27 | parent: T; 28 | }) => { 29 | const height = "50px"; 30 | const active = label === parent; 31 | 32 | return ( 33 |
46 | {label} 47 |
48 | ); 49 | }; 50 | 51 | /** 52 | * A wrapper for `Indicator`s 53 | */ 54 | export const Indicators: FC<{ 55 | children: ReactNode; 56 | label: string; 57 | }> = ({ children, label }) => { 58 | return ( 59 |
66 |
71 | {label} 72 |
73 | {children} 74 |
75 | ); 76 | }; 77 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main, "v*"] 6 | tags: ["v*"] 7 | pull_request: 8 | branches: [main, "v*"] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | env: 15 | # https://consoledonottrack.com/ 16 | DO_NOT_TRACK: 1 17 | 18 | jobs: 19 | tasks: 20 | runs-on: ubuntu-22.04 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | task: 25 | - format 26 | - license-header 27 | - lint 28 | - attw 29 | - build 30 | include: 31 | - task: format 32 | diff-check: true 33 | - task: license-header 34 | diff-check: true 35 | name: ${{ matrix.task }} 36 | steps: 37 | - uses: actions/checkout@v6 38 | - uses: actions/setup-node@v6 39 | with: 40 | node-version-file: .nvmrc 41 | cache: "npm" 42 | - uses: actions/cache@v4 43 | with: 44 | path: .turbo 45 | key: ${{ runner.os }}/${{ matrix.task }}/${{ github.sha }} 46 | restore-keys: ${{ runner.os }}/${{ matrix.task }} 47 | - run: npm ci 48 | - run: npx turbo run ${{ matrix.task }} 49 | - name: Check changed files 50 | if: ${{ matrix.diff-check }} 51 | run: node scripts/gh-diffcheck.js 52 | test: 53 | runs-on: ubuntu-22.04 54 | strategy: 55 | fail-fast: false 56 | matrix: 57 | node-version: [24.x, 22.x, 20.x] 58 | name: "test on Node.js ${{ matrix.node-version }}" 59 | steps: 60 | - uses: actions/checkout@v6 61 | - uses: actions/setup-node@v6 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | cache: "npm" 65 | - uses: actions/cache@v4 66 | with: 67 | path: .turbo 68 | key: ${{ runner.os }}/test/${{ github.sha }} 69 | restore-keys: ${{ runner.os }}/test 70 | - run: npm ci 71 | - run: npx turbo run test 72 | -------------------------------------------------------------------------------- /packages/connect-query-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@connectrpc/connect-query-core", 3 | "version": "2.2.0", 4 | "description": "Core of Connect-Query, framework agnostic helpers for type-safe queries.", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/connectrpc/connect-query-es.git", 9 | "directory": "packages/connect-query-core" 10 | }, 11 | "scripts": { 12 | "prebuild": "rm -rf ./dist/*", 13 | "build": "npm run build:cjs && npm run build:esm", 14 | "build:cjs": "tsc --project tsconfig.build.json --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs --declaration --declarationDir ./dist/cjs && echo >./dist/cjs/package.json '{\"type\":\"commonjs\"}'", 15 | "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", 16 | "test": "vitest --run", 17 | "test:watch": "vitest --watch", 18 | "format": "prettier --write --ignore-unknown '.' '!dist'", 19 | "license-header": "license-header", 20 | "lint": "eslint --max-warnings 0 .", 21 | "attw": "attw --pack" 22 | }, 23 | "type": "module", 24 | "sideEffects": false, 25 | "main": "./dist/cjs/index.js", 26 | "exports": { 27 | ".": { 28 | "import": "./dist/esm/index.js", 29 | "require": "./dist/cjs/index.js" 30 | } 31 | }, 32 | "devDependencies": { 33 | "@arethetypeswrong/cli": "^0.18.1", 34 | "@bufbuild/buf": "1.54.0", 35 | "@bufbuild/jest-environment-jsdom": "^0.1.1", 36 | "@bufbuild/protobuf": "^2.5.1", 37 | "@bufbuild/protoc-gen-es": "^2.5.1", 38 | "@connectrpc/connect": "^2.0.2", 39 | "@connectrpc/connect-web": "^2.0.2", 40 | "test-utils": "*", 41 | "typescript": "^5.8.3", 42 | "@tanstack/query-core": "^5.79.0" 43 | }, 44 | "peerDependencies": { 45 | "@bufbuild/protobuf": "2.x", 46 | "@connectrpc/connect": "^2.0.1", 47 | "@tanstack/query-core": ">=5.62.0" 48 | }, 49 | "files": [ 50 | "dist/**" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/transport-key.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { Transport } from "@connectrpc/connect"; 16 | 17 | const staticKeySymbol = Symbol("static-key"); 18 | 19 | const transportKeys = new WeakMap(); 20 | let counter = 0; 21 | 22 | interface TransportWithStaticKey extends Transport { 23 | [staticKeySymbol]?: string; 24 | } 25 | 26 | /** 27 | * For a given Transport, create a string key that is suitable for a Query Key 28 | * in TanStack Query. 29 | * 30 | * This function will return a unique string for every reference. 31 | */ 32 | export function createTransportKey(transport: TransportWithStaticKey): string { 33 | if (transport[staticKeySymbol] !== undefined) { 34 | return transport[staticKeySymbol]; 35 | } 36 | let key = transportKeys.get(transport); 37 | if (key === undefined) { 38 | key = `t${++counter}`; 39 | transportKeys.set(transport, key); 40 | } 41 | return key; 42 | } 43 | 44 | /** 45 | * Enhances a given transport with a static query key that is used in any associated queries. This may be necessary 46 | * in SSR contexts where transports are used on both client and server but they need to be considered 47 | * the same when it comes to the query cache. 48 | */ 49 | export function addStaticKeyToTransport( 50 | transport: Transport, 51 | key: string, 52 | ): TransportWithStaticKey { 53 | return { ...transport, [staticKeySymbol]: key }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/transport-key.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { createConnectTransport } from "@connectrpc/connect-web"; 16 | import { describe, expect, it } from "vitest"; 17 | 18 | import { 19 | addStaticKeyToTransport, 20 | createTransportKey, 21 | } from "./transport-key.js"; 22 | 23 | describe("transport key", () => { 24 | it("returns the same key for the same reference", () => { 25 | const transport = createConnectTransport({ 26 | baseUrl: "https://example.com", 27 | }); 28 | const key1 = createTransportKey(transport); 29 | const key2 = createTransportKey(transport); 30 | expect(key1).toBe(key2); 31 | }); 32 | it("creates a unique key for every reference", () => { 33 | const transport1 = createConnectTransport({ 34 | baseUrl: "https://example.com", 35 | }); 36 | const transport2 = createConnectTransport({ 37 | baseUrl: "https://example.com", 38 | }); 39 | const key1 = createTransportKey(transport1); 40 | const key2 = createTransportKey(transport2); 41 | expect(key1).not.toBe(key2); 42 | }); 43 | it("allows override of key transport property", () => { 44 | const transport1 = addStaticKeyToTransport( 45 | createConnectTransport({ 46 | baseUrl: "https://example.com", 47 | }), 48 | "static-key", 49 | ); 50 | const key1 = createTransportKey(transport1); 51 | expect(key1).toBe("static-key"); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/datum.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { type FC, type ReactNode, useId } from "react"; 16 | 17 | import { 18 | border, 19 | borderRadius, 20 | boxShadow, 21 | lightBlue, 22 | margin, 23 | padding, 24 | white, 25 | } from "./css"; 26 | 27 | interface DatumProps { 28 | datum: string; 29 | label: string; 30 | } 31 | 32 | /** 33 | * A single data point 34 | */ 35 | export const Datum: FC = ({ datum, label }) => { 36 | const id = useId(); 37 | return ( 38 |
48 | 57 | 58 |
65 | {datum} 66 |
67 |
68 | ); 69 | }; 70 | 71 | /** 72 | * Wrapper Datum children 73 | */ 74 | export const Data: FC<{ 75 | children: ReactNode[]; 76 | }> = ({ children }) => ( 77 |
84 | {children} 85 |
86 | ); 87 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/src/generateDts.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { DescFile, DescService } from "@bufbuild/protobuf"; 16 | import type { Schema } from "@bufbuild/protoplugin"; 17 | import { safeIdentifier } from "@bufbuild/protoplugin"; 18 | 19 | import type { PluginInit } from "./utils.js"; 20 | 21 | // prettier-ignore 22 | /** 23 | * Handles generating a TypeScript Declaration file for a given Schema, DescFile (protobuf definition) and protobuf Service. 24 | */ 25 | const generateServiceFile = 26 | (schema: Schema, protoFile: DescFile) => (service: DescService) => { 27 | 28 | const f = schema.generateFile( 29 | `${protoFile.name}-${service.name}_connectquery.d.ts`, 30 | ); 31 | 32 | f.preamble(protoFile); 33 | 34 | service.methods.forEach((method) => { 35 | switch (method.methodKind) { 36 | case "unary": 37 | { 38 | f.print(f.jsDoc(method)); 39 | f.print(f.export("const", safeIdentifier(method.localName)), ": typeof ", f.importSchema(service), '["method"]["', method.localName, '"];'); 40 | } 41 | break; 42 | 43 | default: 44 | return; 45 | } 46 | }); 47 | }; 48 | 49 | /** 50 | * This function generates the TypeScript Definition output files 51 | */ 52 | export const generateDts: PluginInit["generateDts"] = (schema) => { 53 | schema.files.forEach((protoFile) => { 54 | protoFile.services.forEach(generateServiceFile(schema, protoFile)); 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/connect-query/src/test/test-wrapper.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { Transport } from "@connectrpc/connect"; 16 | import { createConnectTransport } from "@connectrpc/connect-web"; 17 | import type { QueryClientConfig } from "@tanstack/react-query"; 18 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 19 | import type { JSXElementConstructor, PropsWithChildren } from "react"; 20 | 21 | import { TransportProvider } from "../use-transport.js"; 22 | 23 | /** 24 | * A utils wrapper that supplies Tanstack Query's `QueryClientProvider` as well as Connect-Query's `TransportProvider`. 25 | */ 26 | export const wrapper = ( 27 | config?: QueryClientConfig, 28 | transport = createConnectTransport({ 29 | baseUrl: "https://demo.connectrpc.com", 30 | }), 31 | ): { 32 | wrapper: JSXElementConstructor; 33 | queryClient: QueryClient; 34 | transport: Transport; 35 | queryClientWrapper: JSXElementConstructor; 36 | } => { 37 | const queryClient = new QueryClient(config); 38 | return { 39 | wrapper: ({ children }) => ( 40 | 41 | 42 | {children} 43 | 44 | 45 | ), 46 | queryClient, 47 | transport, 48 | queryClientWrapper: ({ children }) => ( 49 | {children} 50 | ), 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "root", 4 | "type": "module", 5 | "workspaces": [ 6 | "packages/connect-query-core", 7 | "packages/connect-query", 8 | "packages/examples/react/basic", 9 | "packages/protoc-gen-connect-query", 10 | "packages/test-utils" 11 | ], 12 | "scripts": { 13 | "all": "turbo run --ui tui build format test lint attw license-header", 14 | "clean": "git clean -Xdf", 15 | "setversion": "node scripts/set-workspace-version.js", 16 | "getversion": "node scripts/find-workspace-version.js", 17 | "postsetversion": "npm run all", 18 | "release": "node scripts/release.js", 19 | "prerelease": "npm run all", 20 | "format": "prettier --write --ignore-unknown '.' '!packages' '!.turbo' '!node_modules'", 21 | "license-header": "license-header --ignore 'packages/**'", 22 | "lint": "eslint --max-warnings 0 . --ignore-pattern 'packages/**' && npm run check:spelling", 23 | "check:spelling": "cspell \"**\" --gitignore" 24 | }, 25 | "packageManager": "npm@10.1.0", 26 | "licenseHeader": { 27 | "licenseType": "apache", 28 | "yearRange": "2021-2023", 29 | "copyrightHolder": "The Connect Authors" 30 | }, 31 | "devDependencies": { 32 | "@bufbuild/license-header": "^0.0.4", 33 | "@types/node": "^22.15.29", 34 | "@typescript-eslint/eslint-plugin": "8.33.0", 35 | "@typescript-eslint/parser": "8.33.0", 36 | "@typescript-eslint/utils": "8.33.0", 37 | "@vitest/ui": "^3.2.4", 38 | "cspell": "9.0.2", 39 | "eslint": "8.57.0", 40 | "eslint-config-prettier": "10.1.5", 41 | "eslint-import-resolver-typescript": "^4.4.2", 42 | "eslint-plugin-eslint-comments": "3.2.0", 43 | "eslint-plugin-import": "^2.29.1", 44 | "eslint-plugin-n": "^17.18.0", 45 | "eslint-plugin-react-hooks": "^5.2.0", 46 | "eslint-plugin-simple-import-sort": "^12.1.1", 47 | "eslint-plugin-vitest": "0.5.4", 48 | "prettier": "3.5.3", 49 | "turbo": "^2.5.4", 50 | "typescript": "5.8.3", 51 | "vitest": "^3.2.4" 52 | }, 53 | "engineStrict": true, 54 | "engines": { 55 | "node": ">=20", 56 | "npm": ">=10.8" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - main 8 | 9 | jobs: 10 | publish-release: 11 | runs-on: ubuntu-latest 12 | # Only run if PR was merged and branch name starts with release/prep-release- 13 | if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/prep-release-') 14 | permissions: 15 | id-token: write # Required for OIDC 16 | contents: write 17 | pull-requests: write 18 | issues: write 19 | 20 | steps: 21 | - name: Checkout base branch 22 | uses: actions/checkout@v4 23 | with: 24 | ref: ${{ github.event.pull_request.base.ref }} 25 | fetch-depth: 0 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v6 30 | with: 31 | node-version-file: ".nvmrc" 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Get current workspace version 37 | id: workspace_version 38 | run: | 39 | VERSION=$(npm run getversion --silent) 40 | echo "version=$VERSION" >> $GITHUB_OUTPUT 41 | 42 | - name: Get updated release notes from PR 43 | id: pr_notes 44 | run: | 45 | RELEASE_NOTES=$(gh pr view ${{ github.event.pull_request.number }} --json body | jq -r ".body") 46 | echo "notes<> $GITHUB_OUTPUT 47 | echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT 48 | echo "EOF" >> $GITHUB_OUTPUT 49 | env: 50 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | - name: Publish to npm 53 | run: npm run release 54 | 55 | - name: Publish GitHub release 56 | run: | 57 | gh release create v${{ steps.workspace_version.outputs.version }} \ 58 | --title "Release v${{ steps.workspace_version.outputs.version }}" \ 59 | --notes "${{ steps.pr_notes.outputs.notes }}" 60 | # --discussion-category "Announcements" ## Enable if discussions are enabled 61 | env: 62 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/src/generateTs.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { DescFile, DescService } from "@bufbuild/protobuf"; 16 | import type { Schema } from "@bufbuild/protoplugin"; 17 | import { safeIdentifier } from "@bufbuild/protoplugin"; 18 | 19 | import type { PluginInit } from "./utils.js"; 20 | 21 | // prettier-ignore 22 | /** 23 | * Handles generating a source code file for a given Schema, DescFile (protobuf definition) and protobuf Service. 24 | * 25 | * By pure luck, this file happens to be completely valid JavaScript since all the types are inferred. 26 | */ 27 | const generateServiceFile = 28 | (schema: Schema, protoFile: DescFile, extension: 'js' | 'ts') => 29 | (service: DescService) => { 30 | const f = schema.generateFile( 31 | `${protoFile.name}-${service.name}_connectquery.${extension}`, 32 | ); 33 | f.preamble(protoFile); 34 | 35 | service.methods 36 | .filter((method) => method.methodKind === "unary") 37 | .forEach((method, index, filteredMethods) => { 38 | f.print(f.jsDoc(method)); 39 | f.print(f.export("const", safeIdentifier(method.localName)), " = ", f.importSchema(service), ".method.", method.localName, ";"); 40 | const lastIndex = index === filteredMethods.length - 1; 41 | if (!lastIndex) { 42 | f.print(); 43 | } 44 | }); 45 | }; 46 | 47 | /** 48 | * This function generates the TypeScript output files 49 | */ 50 | export const generateTs: PluginInit["generateJs"] & PluginInit["generateTs"] = ( 51 | schema, 52 | extension, 53 | ) => { 54 | schema.files.forEach((protoFile) => { 55 | protoFile.services.forEach( 56 | generateServiceFile(schema, protoFile, extension), 57 | ); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/connect-query/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@connectrpc/connect-query", 3 | "version": "2.2.0", 4 | "description": "TypeScript-first expansion pack for TanStack Query that gives you Protobuf superpowers.", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/connectrpc/connect-query-es.git", 9 | "directory": "packages/connect-query" 10 | }, 11 | "scripts": { 12 | "prebuild": "rm -rf ./dist/*", 13 | "build": "npm run build:cjs && npm run build:esm", 14 | "build:cjs": "tsc --project tsconfig.build.json --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs --declaration --declarationDir ./dist/cjs && echo >./dist/cjs/package.json '{\"type\":\"commonjs\"}'", 15 | "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", 16 | "test": "vitest --run", 17 | "test:watch": "vitest --watch", 18 | "format": "prettier --write --ignore-unknown '.' '!dist'", 19 | "license-header": "license-header", 20 | "lint": "eslint --max-warnings 0 .", 21 | "attw": "attw --pack" 22 | }, 23 | "type": "module", 24 | "sideEffects": false, 25 | "main": "./dist/cjs/index.js", 26 | "exports": { 27 | ".": { 28 | "import": "./dist/esm/index.js", 29 | "require": "./dist/cjs/index.js" 30 | } 31 | }, 32 | "dependencies": { 33 | "@connectrpc/connect-query-core": "^2.2.0" 34 | }, 35 | "devDependencies": { 36 | "@arethetypeswrong/cli": "^0.18.1", 37 | "@bufbuild/buf": "1.54.0", 38 | "@bufbuild/jest-environment-jsdom": "^0.1.1", 39 | "@bufbuild/protobuf": "^2.5.1", 40 | "@bufbuild/protoc-gen-es": "^2.5.1", 41 | "@connectrpc/connect": "^2.0.2", 42 | "@connectrpc/connect-web": "^2.0.2", 43 | "@tanstack/react-query": "^5.79.0", 44 | "@testing-library/react": "^16.3.0", 45 | "@types/react": "^19.1.6", 46 | "@types/react-dom": "^19.1.5", 47 | "react": "^19.1.0", 48 | "react-dom": "^19.1.0", 49 | "test-utils": "*", 50 | "typescript": "^5.8.3" 51 | }, 52 | "peerDependencies": { 53 | "@bufbuild/protobuf": "2.x", 54 | "@connectrpc/connect": "^2.0.1", 55 | "@tanstack/react-query": ">=5.62.0", 56 | "react": "^18 || ^19", 57 | "react-dom": "^18 || ^19" 58 | }, 59 | "files": [ 60 | "dist/**" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { execSync } from "node:child_process"; 16 | import { findWorkspaceVersion } from "./utils.js"; 17 | 18 | /* 19 | * Publish connect-query 20 | * 21 | * Recommended procedure: 22 | * 1. Trigger the prepare-release workflow with the version you want to release. 23 | * 2. Reviews release notes in the created PR, wait for approval. 24 | * 3. Merge the PR. 25 | */ 26 | 27 | const tag = determinePublishTag(findWorkspaceVersion("packages")); 28 | const uncommitted = gitUncommitted(); 29 | if (uncommitted.length > 0) { 30 | throw new Error("Uncommitted changes found: \n" + uncommitted); 31 | } 32 | npmPublish(); 33 | 34 | /** 35 | * 36 | */ 37 | function npmPublish() { 38 | const command = 39 | `npm publish --tag ${tag}` + 40 | " --workspace packages/connect-query" + 41 | " --workspace packages/connect-query-core" + 42 | " --workspace packages/protoc-gen-connect-query"; 43 | execSync(command, { 44 | stdio: "inherit", 45 | }); 46 | } 47 | 48 | /** 49 | * @returns {string} 50 | */ 51 | function gitUncommitted() { 52 | const out = execSync("git status --short", { 53 | encoding: "utf-8", 54 | }); 55 | if (out.trim().length === 0) { 56 | return ""; 57 | } 58 | return out; 59 | } 60 | 61 | /** 62 | * @param {string} version 63 | * @returns {string} 64 | */ 65 | function determinePublishTag(version) { 66 | if (/^\d+\.\d+\.\d+$/.test(version)) { 67 | return "latest"; 68 | } else if (/^\d+\.\d+\.\d+-alpha.*$/.test(version)) { 69 | return "alpha"; 70 | } else if (/^\d+\.\d+\.\d+-beta.*$/.test(version)) { 71 | return "beta"; 72 | } else if (/^\d+\.\d+\.\d+-rc.*$/.test(version)) { 73 | return "rc"; 74 | } else { 75 | throw new Error(`Unable to determine publish tag from version ${version}`); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-mutation.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import type { ConnectError, Transport } from "@connectrpc/connect"; 22 | import { callUnaryMethod } from "@connectrpc/connect-query-core"; 23 | import type { 24 | UseMutationOptions as TSUseMutationOptions, 25 | UseMutationResult, 26 | } from "@tanstack/react-query"; 27 | import { useMutation as tsUseMutation } from "@tanstack/react-query"; 28 | import { useCallback } from "react"; 29 | 30 | import { useTransport } from "./use-transport.js"; 31 | 32 | /** 33 | * Options for useMutation 34 | */ 35 | export type UseMutationOptions< 36 | I extends DescMessage, 37 | O extends DescMessage, 38 | Ctx = unknown, 39 | > = TSUseMutationOptions< 40 | MessageShape, 41 | ConnectError, 42 | MessageInitShape, 43 | Ctx 44 | > & { 45 | /** The transport to be used for the fetching. */ 46 | transport?: Transport; 47 | }; 48 | 49 | /** 50 | * Query the method provided. Maps to useMutation on tanstack/react-query 51 | */ 52 | export function useMutation< 53 | I extends DescMessage, 54 | O extends DescMessage, 55 | Ctx = unknown, 56 | >( 57 | schema: DescMethodUnary, 58 | { transport, ...queryOptions }: UseMutationOptions = {}, 59 | ): UseMutationResult, ConnectError, MessageInitShape, Ctx> { 60 | const transportFromCtx = useTransport(); 61 | const transportToUse = transport ?? transportFromCtx; 62 | const mutationFn = useCallback( 63 | async (input: MessageInitShape) => 64 | callUnaryMethod(transportToUse, schema, input), 65 | [transportToUse, schema], 66 | ); 67 | return tsUseMutation({ 68 | ...queryOptions, 69 | mutationFn, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /.github/RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | ## Prerequisites 4 | 5 | - See the setup and tools required in CONTRIBUTING.md 6 | - A granular access token for npmjs.com with read and write permissions, scoped 7 | to the `connectrpc` organization. 8 | - Make sure that the repository is in a good state, without PRs close to merge 9 | that would ideally be part of the release. 10 | 11 | ## Steps 12 | 13 | 1. Choose a new version (e.g. 1.2.3), making sure to follow semver. Note that all 14 | packages in this repository use the same version number. 15 | 2. Trigger the prepare-release workflow that will create a release PR. 16 | 17 | - Note: If releasing for a hotfix of a major version that is behind the current main branch, make sure to create an appropriate branch (e.g. release/v1.x) before running the workflow with the branch name set as the base_branch. 18 | 19 | 3. Edit the PR description with release notes. See the section below for details. 20 | 4. Make sure CI passed on your PR and ask a maintainer for review. 21 | 5. After approval, merge your PR. 22 | 23 | ## Release notes 24 | 25 | - We generate release notes with the GitHub feature, see 26 | https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 27 | - Only changes that impact users should be listed. No need to list things like 28 | doc changes (unless it’s something major), dependency version bumps, or similar. 29 | Remove them from the generated release notes. 30 | - If the release introduces a major new feature or change, add a section at the 31 | top that explains it for users. A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.10.0 32 | It lists a major new feature and a major change with dedicated sections, and 33 | moves the changelist with PR links to a separate "Enhancement" section below. 34 | - If the release includes a very long list of changes, consider breaking the 35 | changelist up with the sections "Enhancements", "Bugfixes", "Breaking changes". 36 | A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.9.0 37 | - If the release includes changes specific to a npm package, group and explain 38 | the changelist in according separate sections. A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.8.0 39 | Note that we are not using full package names with scope - a more user-friendly 40 | name like "Connect for Node.js" or "Connect for Fastify" is preferable. 41 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-transport.test.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { ConnectError } from "@connectrpc/connect"; 16 | import { renderHook, waitFor } from "@testing-library/react"; 17 | import { mockBigInt } from "test-utils"; 18 | import { ElizaService } from "test-utils/gen/eliza_pb.js"; 19 | import { describe, expect, it } from "vitest"; 20 | 21 | import { wrapper } from "./test/test-wrapper.js"; 22 | import { useQuery } from "./use-query.js"; 23 | import { TransportProvider, useTransport } from "./use-transport.js"; 24 | 25 | const sayMethodDescriptor = ElizaService.method.say; 26 | 27 | const error = new ConnectError( 28 | "To use Connect, you must provide a `Transport`: a simple object that handles `unary` and `stream` requests. `Transport` objects can easily be created by using `@connectrpc/connect-web`'s exports `createConnectTransport` and `createGrpcWebTransport`. see: https://connectrpc.com/docs/web/getting-started for more info.", 29 | ); 30 | 31 | describe("useTransport", () => { 32 | it("throws the fallback error", async () => { 33 | const { result, rerender } = renderHook( 34 | () => useQuery(sayMethodDescriptor, undefined, { retry: false }), 35 | { 36 | wrapper: wrapper().queryClientWrapper, 37 | }, 38 | ); 39 | rerender(); 40 | 41 | expect(result.current.error).toStrictEqual(null); 42 | expect(result.current.isError).toStrictEqual(false); 43 | 44 | await waitFor(() => { 45 | expect(result.current.isError).toStrictEqual(true); 46 | }); 47 | 48 | expect(result.current.error).toEqual(error); 49 | }); 50 | }); 51 | 52 | describe("TransportProvider", () => { 53 | it("provides a custom transport to the useTransport hook", () => { 54 | const transport = mockBigInt(); 55 | const { result } = renderHook(() => useTransport(), { 56 | wrapper: ({ children }) => ( 57 | {children} 58 | ), 59 | }); 60 | expect(result.current).toBe(transport); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/structural-sharing.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create } from "@bufbuild/protobuf"; 16 | import { 17 | SayRequestSchema, 18 | SayResponseSchema, 19 | } from "test-utils/gen/eliza_pb.js"; 20 | import { describe, expect, it } from "vitest"; 21 | 22 | import { createStructuralSharing } from "./structural-sharing.js"; 23 | 24 | describe("structural sharing", () => { 25 | const schema = SayResponseSchema; 26 | const fn = createStructuralSharing(schema); 27 | it("returns old data if new data is equal", () => { 28 | const oldData = create(schema, { sentence: "hi" }); 29 | const newData = create(schema, { sentence: "hi" }); 30 | const result = fn(oldData, newData); 31 | expect(result).toBe(oldData); 32 | }); 33 | it("returns new data if not equal to old data", () => { 34 | const oldData = create(schema, { sentence: "hi" }); 35 | const newData = create(schema, { sentence: "hello" }); 36 | const result = fn(oldData, newData); 37 | expect(result).toBe(newData); 38 | }); 39 | it("returns new data if old data is undefined", () => { 40 | const oldData = undefined; 41 | const newData = create(schema, { sentence: "hello" }); 42 | const result = fn(oldData, newData); 43 | expect(result).toBe(newData); 44 | }); 45 | it.each([123, null, create(SayRequestSchema, { sentence: "hi" })])( 46 | "returns new data for unexpected old data $#", 47 | (oldData) => { 48 | const newData = create(schema, { sentence: "hi" }); 49 | const result = fn(oldData, newData); 50 | expect(result).toBe(newData); 51 | }, 52 | ); 53 | it.each([123, null, create(SayRequestSchema, { sentence: "hi" })])( 54 | "returns new data for unexpected new data $#", 55 | (newData) => { 56 | const oldData = create(schema, { sentence: "hi" }); 57 | const result = fn(oldData, newData); 58 | expect(result).toBe(newData); 59 | }, 60 | ); 61 | 62 | it("allows returning old data if new data is equal", () => { 63 | const oldData = { count: 2 }; 64 | const newData = { count: 2 }; 65 | const result = fn(oldData, newData); 66 | expect(result).toBe(oldData); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /.github/workflows/prepare-release.yml: -------------------------------------------------------------------------------- 1 | name: Prepare Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version to release (e.g. 1.2.3)" 8 | required: true 9 | type: string 10 | 11 | jobs: 12 | prepare-release: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | ref: main 23 | fetch-depth: 0 24 | token: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Setup Node.js 27 | uses: actions/setup-node@v6 28 | with: 29 | node-version-file: ".nvmrc" 30 | 31 | - name: Install dependencies 32 | run: npm ci 33 | 34 | - name: Create release branch 35 | run: | 36 | git config --global user.name 'github-actions[bot]' 37 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 38 | git checkout -b "release/prep-release-${{ inputs.version }}" 39 | 40 | - name: Get current workspace version 41 | id: workspace_version 42 | run: | 43 | VERSION=$(npm run getversion --silent) 44 | echo "version=$VERSION" >> $GITHUB_OUTPUT 45 | 46 | - name: Set version and run build 47 | run: | 48 | npm run setversion ${{ inputs.version }} 49 | 50 | - name: Commit version changes 51 | run: | 52 | git add . 53 | git commit -s -m "Release ${{ inputs.version }}" 54 | git push --set-upstream origin "release/prep-release-${{ inputs.version }}" 55 | 56 | - name: Get release notes 57 | id: release_notes 58 | run: | 59 | RELEASE_NOTES=$( 60 | gh api \ 61 | --method POST \ 62 | -H "Accept: application/vnd.github+json" \ 63 | -H "X-GitHub-Api-Version: 2022-11-28" \ 64 | /repos/${{ github.repository }}/releases/generate-notes \ 65 | -f 'tag_name=v${{ inputs.version }}' -f 'target_commitish=${{ inputs.base_branch }}' -f 'previous_tag_name=v${{ steps.workspace_version.outputs.version }}' \ 66 | --jq ".body" \ 67 | ) 68 | echo "notes<> $GITHUB_OUTPUT 69 | echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT 70 | echo "EOF" >> $GITHUB_OUTPUT 71 | env: 72 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - name: Create pull request 75 | run: | 76 | gh pr create \ 77 | --title "Release ${{ inputs.version }}" \ 78 | --body "${{ steps.release_notes.outputs.notes }}" \ 79 | --base "${{ inputs.base_branch }}" 80 | env: 81 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We'd love your help making `connect-query-es` better! 4 | 5 | If you'd like to add new exported APIs, please [open an issue][open-issue] 6 | describing your proposal — discussing API changes ahead of time makes 7 | pull request review much smoother. In your issue, pull request, and any other 8 | communications, please remember to treat your fellow contributors with 9 | respect! 10 | 11 | Note that for a contribution to be accepted, you must sign off on all commits 12 | in order to affirm that they comply with the [Developer Certificate of Origin][dco]. 13 | Make sure to configure `git` with the same name and E-Mail as your GitHub account, 14 | and run `git commit` with the `-s` flag to sign. If necessary, a bot will remind 15 | you to sign your commits when you open your pull request, and provide helpful tips. 16 | 17 | ## Setup 18 | 19 | [Fork][fork], then clone the repository: 20 | 21 | ``` 22 | git clone git@github.com:your_github_username/connect-query-es.git 23 | cd connect-query-es 24 | git remote add upstream https://github.com/connectrpc/connect-query-es.git 25 | git fetch upstream 26 | ``` 27 | 28 | Install dependencies (you'll need Node.js in the version specified in `.nvmrc`, 29 | and `npm` in the version specified in `package.json`): 30 | 31 | ```bash 32 | npm ci 33 | ``` 34 | 35 | Make sure that the tests, linters, and other checks pass: 36 | 37 | ```bash 38 | npm run all 39 | ``` 40 | 41 | We're using `turborepo` to run tasks. If you haven't used it yet, take a look at 42 | [filtering and package scoping](https://turbo.build/repo/docs/crafting-your-repository/running-tasks). 43 | 44 | ## Making Changes 45 | 46 | Start by creating a new branch for your changes: 47 | 48 | ``` 49 | git checkout main 50 | git fetch upstream 51 | git rebase upstream/main 52 | git checkout -b cool_new_feature 53 | ``` 54 | 55 | Make your changes, then ensure that `npm run all` still passes. 56 | When you're satisfied with your changes, push them to your fork. 57 | 58 | ``` 59 | git commit -a 60 | git push origin cool_new_feature 61 | ``` 62 | 63 | Then use the GitHub UI to open a pull request. 64 | 65 | At this point, you're waiting on us to review your changes. We _try_ to respond 66 | to issues and pull requests within a few business days, and we may suggest some 67 | improvements or alternatives. Once your changes are approved, one of the 68 | project maintainers will merge them. 69 | 70 | We're much more likely to approve your changes if you: 71 | 72 | - Add tests for new functionality. 73 | - Write a [good commit message][commit-message]. 74 | - Maintain backward compatibility. 75 | 76 | [fork]: https://github.com/connectrpc/connect-query-es/fork 77 | [open-issue]: https://github.com/connectrpc/connect-query-es/issues/new 78 | [dco]: https://developercertificate.org 79 | [commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 80 | -------------------------------------------------------------------------------- /packages/test-utils/src/gen/list_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file list.proto (syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; 20 | import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { Message } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Describes the file list.proto. 25 | */ 26 | export const file_list: GenFile = /*@__PURE__*/ 27 | fileDesc("CgpsaXN0LnByb3RvIiwKC0xpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAMSDwoHcHJldmlldxgCIAEoCCIrCgxMaXN0UmVzcG9uc2USDAoEcGFnZRgBIAEoAxINCgVpdGVtcxgCIAMoCTIyCgtMaXN0U2VydmljZRIjCgRMaXN0EgwuTGlzdFJlcXVlc3QaDS5MaXN0UmVzcG9uc2ViBnByb3RvMw"); 28 | 29 | /** 30 | * @generated from message ListRequest 31 | */ 32 | export type ListRequest = Message<"ListRequest"> & { 33 | /** 34 | * @generated from field: int64 page = 1; 35 | */ 36 | page: bigint; 37 | 38 | /** 39 | * @generated from field: bool preview = 2; 40 | */ 41 | preview: boolean; 42 | }; 43 | 44 | /** 45 | * Describes the message ListRequest. 46 | * Use `create(ListRequestSchema)` to create a new message. 47 | */ 48 | export const ListRequestSchema: GenMessage = /*@__PURE__*/ 49 | messageDesc(file_list, 0); 50 | 51 | /** 52 | * @generated from message ListResponse 53 | */ 54 | export type ListResponse = Message<"ListResponse"> & { 55 | /** 56 | * @generated from field: int64 page = 1; 57 | */ 58 | page: bigint; 59 | 60 | /** 61 | * @generated from field: repeated string items = 2; 62 | */ 63 | items: string[]; 64 | }; 65 | 66 | /** 67 | * Describes the message ListResponse. 68 | * Use `create(ListResponseSchema)` to create a new message. 69 | */ 70 | export const ListResponseSchema: GenMessage = /*@__PURE__*/ 71 | messageDesc(file_list, 1); 72 | 73 | /** 74 | * @generated from service ListService 75 | */ 76 | export const ListService: GenService<{ 77 | /** 78 | * @generated from rpc ListService.List 79 | */ 80 | list: { 81 | methodKind: "unary"; 82 | input: typeof ListRequestSchema; 83 | output: typeof ListResponseSchema; 84 | }, 85 | }> = /*@__PURE__*/ 86 | serviceDesc(file_list, 0); 87 | 88 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/create-query-options.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { skipToken as tanstackSkipToken } from "@tanstack/query-core"; 16 | import { mockEliza } from "test-utils"; 17 | import { ElizaService } from "test-utils/gen/eliza_pb.js"; 18 | import { describe, expect, expectTypeOf, it } from "vitest"; 19 | 20 | import { createConnectQueryKey } from "./connect-query-key.js"; 21 | import { createQueryOptions } from "./create-query-options.js"; 22 | import { skipToken } from "./index.js"; 23 | 24 | // TODO: maybe create a helper to take a service and method and generate this. 25 | const sayMethodDescriptor = ElizaService.method.say; 26 | 27 | const mockedElizaTransport = mockEliza(); 28 | 29 | describe("createQueryOptions", () => { 30 | it("honors skipToken", () => { 31 | const opt = createQueryOptions(sayMethodDescriptor, skipToken, { 32 | transport: mockedElizaTransport, 33 | }); 34 | expect(opt.queryFn).toBe(skipToken); 35 | expectTypeOf(opt.queryFn).toEqualTypeOf(skipToken); 36 | }); 37 | 38 | it("honors skipToken directly from tanstack", () => { 39 | const opt = createQueryOptions(sayMethodDescriptor, tanstackSkipToken, { 40 | transport: mockedElizaTransport, 41 | }); 42 | expect(opt.queryFn).toBe(tanstackSkipToken); 43 | }); 44 | 45 | it("sets queryKey", () => { 46 | const want = createConnectQueryKey({ 47 | schema: sayMethodDescriptor, 48 | input: { sentence: "hi" }, 49 | transport: mockedElizaTransport, 50 | cardinality: "finite", 51 | headers: { 52 | "x-custom-header": "custom-value", 53 | }, 54 | }); 55 | const opt = createQueryOptions( 56 | sayMethodDescriptor, 57 | { sentence: "hi" }, 58 | { 59 | transport: mockedElizaTransport, 60 | headers: { 61 | "x-custom-header": "custom-value", 62 | }, 63 | }, 64 | ); 65 | expect(opt.queryKey).toStrictEqual(want); 66 | }); 67 | 68 | it("ensures type safety of parameters", () => { 69 | // @ts-expect-error(2322) cannot provide invalid parameters 70 | createQueryOptions( 71 | sayMethodDescriptor, 72 | { 73 | sentence: 1, 74 | }, 75 | { 76 | transport: mockedElizaTransport, 77 | }, 78 | ); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import { create, isMessage } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Throws an error with the provided message when the condition is `false` 25 | */ 26 | export function assert(condition: boolean, message: string): asserts condition { 27 | if (!condition) { 28 | throw new Error(`Invalid assertion: ${message}`); 29 | } 30 | } 31 | 32 | /** 33 | * Verifies that the provided input is a valid AbortController 34 | */ 35 | export const isAbortController = (input: unknown): input is AbortController => { 36 | if ( 37 | typeof input === "object" && 38 | input !== null && 39 | "signal" in input && 40 | typeof input.signal === "object" && 41 | input.signal !== null && 42 | "aborted" in input.signal && 43 | typeof input.signal.aborted === "boolean" && 44 | "abort" in input && 45 | typeof input.abort === "function" 46 | // note, there are more things in this interface, but I stop the check here at `context.signal.aborted` and `context.abort` because (as off November 2022) that's all that connect-web is using (in `callback-client.ts`). 47 | ) { 48 | return true; 49 | } 50 | return false; 51 | }; 52 | 53 | /** 54 | * @see `Updater` from `@tanstack/react-query` 55 | */ 56 | export type ConnectUpdater = 57 | | MessageInitShape 58 | | undefined 59 | | ((prev?: MessageShape) => MessageShape | undefined); 60 | 61 | /** 62 | * This helper makes sure that the type for the original response message is returned. 63 | * 64 | * @deprecated the ConnectQueryKey type now links to the return data type so `setQueryData` can be called safely without this helper. 65 | */ 66 | export const createProtobufSafeUpdater = 67 | ( 68 | schema: Pick, "output">, 69 | updater: ConnectUpdater, 70 | ) => 71 | (prev?: MessageShape): MessageShape | undefined => { 72 | if (typeof updater !== "function") { 73 | if (updater === undefined) { 74 | return undefined; 75 | } 76 | if (isMessage(updater, schema.output)) { 77 | return updater; 78 | } 79 | return create(schema.output, updater); 80 | } 81 | return updater(prev); 82 | }; 83 | -------------------------------------------------------------------------------- /packages/test-utils/src/gen/bigint_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file bigint.proto (syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; 20 | import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { EmptySchema } from "@bufbuild/protobuf/wkt"; 22 | import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; 23 | import type { Message } from "@bufbuild/protobuf"; 24 | 25 | /** 26 | * Describes the file bigint.proto. 27 | */ 28 | export const file_bigint: GenFile = /*@__PURE__*/ 29 | fileDesc("CgxiaWdpbnQucHJvdG8iGwoMQ291bnRSZXF1ZXN0EgsKA2FkZBgBIAEoAyIeCg1Db3VudFJlc3BvbnNlEg0KBWNvdW50GAEgASgDMmsKDUJpZ0ludFNlcnZpY2USJgoFQ291bnQSDS5Db3VudFJlcXVlc3QaDi5Db3VudFJlc3BvbnNlEjIKCEdldENvdW50EhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Gg4uQ291bnRSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_empty]); 30 | 31 | /** 32 | * @generated from message CountRequest 33 | */ 34 | export type CountRequest = Message<"CountRequest"> & { 35 | /** 36 | * @generated from field: int64 add = 1; 37 | */ 38 | add: bigint; 39 | }; 40 | 41 | /** 42 | * Describes the message CountRequest. 43 | * Use `create(CountRequestSchema)` to create a new message. 44 | */ 45 | export const CountRequestSchema: GenMessage = /*@__PURE__*/ 46 | messageDesc(file_bigint, 0); 47 | 48 | /** 49 | * @generated from message CountResponse 50 | */ 51 | export type CountResponse = Message<"CountResponse"> & { 52 | /** 53 | * @generated from field: int64 count = 1; 54 | */ 55 | count: bigint; 56 | }; 57 | 58 | /** 59 | * Describes the message CountResponse. 60 | * Use `create(CountResponseSchema)` to create a new message. 61 | */ 62 | export const CountResponseSchema: GenMessage = /*@__PURE__*/ 63 | messageDesc(file_bigint, 1); 64 | 65 | /** 66 | * @generated from service BigIntService 67 | */ 68 | export const BigIntService: GenService<{ 69 | /** 70 | * @generated from rpc BigIntService.Count 71 | */ 72 | count: { 73 | methodKind: "unary"; 74 | input: typeof CountRequestSchema; 75 | output: typeof CountResponseSchema; 76 | }, 77 | /** 78 | * @generated from rpc BigIntService.GetCount 79 | */ 80 | getCount: { 81 | methodKind: "unary"; 82 | input: typeof EmptySchema; 83 | output: typeof CountResponseSchema; 84 | }, 85 | }> = /*@__PURE__*/ 86 | serviceDesc(file_bigint, 0); 87 | 88 | -------------------------------------------------------------------------------- /packages/test-utils/src/gen/eliza_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; 20 | import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { Message } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Describes the file eliza.proto. 25 | */ 26 | export const file_eliza: GenFile = /*@__PURE__*/ 27 | fileDesc("CgtlbGl6YS5wcm90bxITY29ubmVjdHJwYy5lbGl6YS52MSIeCgpTYXlSZXF1ZXN0EhAKCHNlbnRlbmNlGAEgASgJIh8KC1NheVJlc3BvbnNlEhAKCHNlbnRlbmNlGAEgASgJMloKDEVsaXphU2VydmljZRJKCgNTYXkSHy5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlcXVlc3QaIC5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlc3BvbnNlIgBiBnByb3RvMw"); 28 | 29 | /** 30 | * SayRequest is a single-sentence request. 31 | * 32 | * @generated from message connectrpc.eliza.v1.SayRequest 33 | */ 34 | export type SayRequest = Message<"connectrpc.eliza.v1.SayRequest"> & { 35 | /** 36 | * @generated from field: string sentence = 1; 37 | */ 38 | sentence: string; 39 | }; 40 | 41 | /** 42 | * Describes the message connectrpc.eliza.v1.SayRequest. 43 | * Use `create(SayRequestSchema)` to create a new message. 44 | */ 45 | export const SayRequestSchema: GenMessage = /*@__PURE__*/ 46 | messageDesc(file_eliza, 0); 47 | 48 | /** 49 | * SayResponse is a single-sentence response. 50 | * 51 | * @generated from message connectrpc.eliza.v1.SayResponse 52 | */ 53 | export type SayResponse = Message<"connectrpc.eliza.v1.SayResponse"> & { 54 | /** 55 | * @generated from field: string sentence = 1; 56 | */ 57 | sentence: string; 58 | }; 59 | 60 | /** 61 | * Describes the message connectrpc.eliza.v1.SayResponse. 62 | * Use `create(SayResponseSchema)` to create a new message. 63 | */ 64 | export const SayResponseSchema: GenMessage = /*@__PURE__*/ 65 | messageDesc(file_eliza, 1); 66 | 67 | /** 68 | * ElizaService provides a way to talk to Eliza, a port of the DOCTOR script 69 | * for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at 70 | * the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the 71 | * superficiality of human-computer communication. DOCTOR simulates a 72 | * psychotherapist, and is commonly found as an Easter egg in emacs 73 | * distributions. 74 | * 75 | * @generated from service connectrpc.eliza.v1.ElizaService 76 | */ 77 | export const ElizaService: GenService<{ 78 | /** 79 | * Say is a unary RPC. Eliza responds to the prompt with a single sentence. 80 | * 81 | * @generated from rpc connectrpc.eliza.v1.ElizaService.Say 82 | */ 83 | say: { 84 | methodKind: "unary"; 85 | input: typeof SayRequestSchema; 86 | output: typeof SayResponseSchema; 87 | }, 88 | }> = /*@__PURE__*/ 89 | serviceDesc(file_eliza, 0); 90 | 91 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/gen/eliza_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; 20 | import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { Message } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Describes the file eliza.proto. 25 | */ 26 | export const file_eliza: GenFile = /*@__PURE__*/ 27 | fileDesc("CgtlbGl6YS5wcm90bxITY29ubmVjdHJwYy5lbGl6YS52MSIeCgpTYXlSZXF1ZXN0EhAKCHNlbnRlbmNlGAEgASgJIh8KC1NheVJlc3BvbnNlEhAKCHNlbnRlbmNlGAEgASgJMloKDEVsaXphU2VydmljZRJKCgNTYXkSHy5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlcXVlc3QaIC5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlc3BvbnNlIgBiBnByb3RvMw"); 28 | 29 | /** 30 | * SayRequest is a single-sentence request. 31 | * 32 | * @generated from message connectrpc.eliza.v1.SayRequest 33 | */ 34 | export type SayRequest = Message<"connectrpc.eliza.v1.SayRequest"> & { 35 | /** 36 | * @generated from field: string sentence = 1; 37 | */ 38 | sentence: string; 39 | }; 40 | 41 | /** 42 | * Describes the message connectrpc.eliza.v1.SayRequest. 43 | * Use `create(SayRequestSchema)` to create a new message. 44 | */ 45 | export const SayRequestSchema: GenMessage = /*@__PURE__*/ 46 | messageDesc(file_eliza, 0); 47 | 48 | /** 49 | * SayResponse is a single-sentence response. 50 | * 51 | * @generated from message connectrpc.eliza.v1.SayResponse 52 | */ 53 | export type SayResponse = Message<"connectrpc.eliza.v1.SayResponse"> & { 54 | /** 55 | * @generated from field: string sentence = 1; 56 | */ 57 | sentence: string; 58 | }; 59 | 60 | /** 61 | * Describes the message connectrpc.eliza.v1.SayResponse. 62 | * Use `create(SayResponseSchema)` to create a new message. 63 | */ 64 | export const SayResponseSchema: GenMessage = /*@__PURE__*/ 65 | messageDesc(file_eliza, 1); 66 | 67 | /** 68 | * ElizaService provides a way to talk to Eliza, a port of the DOCTOR script 69 | * for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at 70 | * the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the 71 | * superficiality of human-computer communication. DOCTOR simulates a 72 | * psychotherapist, and is commonly found as an Easter egg in emacs 73 | * distributions. 74 | * 75 | * @generated from service connectrpc.eliza.v1.ElizaService 76 | */ 77 | export const ElizaService: GenService<{ 78 | /** 79 | * Say is a unary RPC. Eliza responds to the prompt with a single sentence. 80 | * 81 | * @generated from rpc connectrpc.eliza.v1.ElizaService.Say 82 | */ 83 | say: { 84 | methodKind: "unary"; 85 | input: typeof SayRequestSchema; 86 | output: typeof SayResponseSchema; 87 | }, 88 | }> = /*@__PURE__*/ 89 | serviceDesc(file_eliza, 0); 90 | 91 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | module.exports = { 16 | env: { 17 | browser: true, 18 | es2021: true, 19 | node: true, 20 | }, 21 | root: true, 22 | ignorePatterns: ["packages/*/dist/**"], 23 | plugins: ["@typescript-eslint", "n", "import", "vitest"], 24 | // Rules and settings that do not require a non-default parser 25 | extends: ["eslint:recommended"], 26 | rules: { 27 | "no-console": "error", 28 | "import/no-cycle": "error", 29 | "import/no-duplicates": "error", 30 | }, 31 | overrides: [ 32 | { 33 | files: ["**/*.{ts,tsx,cts,mts}"], 34 | parser: "@typescript-eslint/parser", 35 | parserOptions: { 36 | project: true, 37 | }, 38 | settings: { 39 | "import/resolver": { 40 | typescript: { 41 | project: "tsconfig.json", 42 | }, 43 | }, 44 | }, 45 | extends: [ 46 | "plugin:@typescript-eslint/recommended", 47 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 48 | "plugin:import/recommended", 49 | "plugin:import/typescript", 50 | ], 51 | rules: { 52 | "@typescript-eslint/strict-boolean-expressions": "error", 53 | "@typescript-eslint/no-unnecessary-condition": "error", 54 | "@typescript-eslint/array-type": "off", // we use complex typings, where Array is actually more readable than T[] 55 | "@typescript-eslint/switch-exhaustiveness-check": [ 56 | "error", 57 | { 58 | considerDefaultExhaustiveForUnions: true, 59 | }, 60 | ], 61 | "@typescript-eslint/prefer-nullish-coalescing": "error", 62 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", 63 | "@typescript-eslint/no-invalid-void-type": "error", 64 | "@typescript-eslint/no-base-to-string": "error", 65 | "import/no-cycle": "error", 66 | "import/no-duplicates": "error", 67 | }, 68 | }, 69 | // For scripts and configurations, use Node.js rules 70 | { 71 | files: ["**/*.{js,mjs,cjs}"], 72 | parserOptions: { 73 | ecmaVersion: 13, // ES2022 - https://eslint.org/docs/latest/use/configure/language-options#specifying-environments 74 | }, 75 | extends: ["eslint:recommended", "plugin:n/recommended"], 76 | rules: { 77 | "n/hashbang": "off", // this rule reports _any_ hashbang outside of an npm binary as an error 78 | "n/prefer-global/process": "off", 79 | "n/no-process-exit": "off", 80 | "n/exports-style": ["error", "module.exports"], 81 | "n/file-extension-in-import": ["error", "always"], 82 | "n/prefer-global/buffer": ["error", "always"], 83 | "n/prefer-global/console": ["error", "always"], 84 | "n/prefer-global/url-search-params": ["error", "always"], 85 | "n/prefer-global/url": ["error", "always"], 86 | "n/prefer-promises/dns": "error", 87 | "n/prefer-promises/fs": "error", 88 | "n/no-unsupported-features/node-builtins": "error", 89 | "n/no-unsupported-features/es-syntax": "error", 90 | }, 91 | }, 92 | ], 93 | }; 94 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-transport.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { Transport } from "@connectrpc/connect"; 16 | import { ConnectError } from "@connectrpc/connect"; 17 | import type { FC, PropsWithChildren } from "react"; 18 | import { createContext, useContext } from "react"; 19 | 20 | const fallbackTransportError = new ConnectError( 21 | "To use Connect, you must provide a `Transport`: a simple object that handles `unary` and `stream` requests. `Transport` objects can easily be created by using `@connectrpc/connect-web`'s exports `createConnectTransport` and `createGrpcWebTransport`. see: https://connectrpc.com/docs/web/getting-started for more info.", 22 | ); 23 | 24 | // istanbul ignore next 25 | export const fallbackTransport: Transport = { 26 | unary: () => { 27 | throw fallbackTransportError; 28 | }, 29 | stream: () => { 30 | throw fallbackTransportError; 31 | }, 32 | }; 33 | 34 | const transportContext = createContext(fallbackTransport); 35 | 36 | /** 37 | * Use this helper to get the default transport that's currently attached to the React context for the calling component. 38 | */ 39 | export const useTransport = () => useContext(transportContext); 40 | 41 | /** 42 | * `TransportProvider` is the main mechanism by which Connect-Query keeps track of the `Transport` used by your application. 43 | * 44 | * Broadly speaking, "transport" joins two concepts: 45 | * 46 | * 1. The protocol of communication. For this there are two options: the {@link https://connectrpc.com/docs/protocol/ Connect Protocol}, or the {@link https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md gRPC-Web Protocol}. 47 | * 1. The protocol options. The primary important piece of information here is the `baseUrl`, but there are also other potentially critical options like request credentials and binary wire format encoding options. 48 | * 49 | * With these two pieces of information in hand, the transport provides the critical mechanism by which your app can make network requests. 50 | * 51 | * To learn more about the two modes of transport, take a look at the npm package `@connectrpc/connect-web`. 52 | * 53 | * To get started with Connect-Query, simply import a transport (either `createConnectTransport` or `createGrpcWebTransport` from `@connectrpc/connect-web`) and pass it to the provider. 54 | * 55 | * @example 56 | * import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 57 | * import { TransportProvider } from "@connectrpc/connect-query"; 58 | * 59 | * const queryClient = new QueryClient(); 60 | * 61 | * export const App() { 62 | * const transport = createConnectTransport({ 63 | * baseUrl: "", 64 | * }); 65 | * return ( 66 | * 67 | * 68 | * 69 | * 70 | * 71 | * ); 72 | * } 73 | */ 74 | export const TransportProvider: FC< 75 | PropsWithChildren<{ 76 | transport: Transport; 77 | }> 78 | > = ({ children, transport }) => ( 79 | 80 | {children} 81 | 82 | ); 83 | -------------------------------------------------------------------------------- /packages/connect-query/src/call-unary-method.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create } from "@bufbuild/protobuf"; 16 | import type { ConnectQueryKey } from "@connectrpc/connect-query-core"; 17 | import { 18 | callUnaryMethod, 19 | createConnectQueryKey, 20 | } from "@connectrpc/connect-query-core"; 21 | import type { QueryFunctionContext } from "@tanstack/react-query"; 22 | import { useQueries } from "@tanstack/react-query"; 23 | import { renderHook, waitFor } from "@testing-library/react"; 24 | import { mockEliza } from "test-utils"; 25 | import type { SayRequest } from "test-utils/gen/eliza_pb.js"; 26 | import { ElizaService, SayRequestSchema } from "test-utils/gen/eliza_pb.js"; 27 | import { describe, expect, it } from "vitest"; 28 | 29 | import { wrapper } from "./test/test-wrapper.js"; 30 | 31 | describe("callUnaryMethod", () => { 32 | it("can be used with useQueries", async () => { 33 | const transport = mockEliza({ 34 | sentence: "Response 1", 35 | }); 36 | const { result } = renderHook(() => { 37 | const input: SayRequest = create(SayRequestSchema, { 38 | sentence: "query 1", 39 | }); 40 | const [query1] = useQueries({ 41 | queries: [ 42 | { 43 | queryKey: createConnectQueryKey({ 44 | schema: ElizaService.method.say, 45 | input, 46 | transport, 47 | cardinality: "finite", 48 | }), 49 | queryFn: async ({ 50 | signal, 51 | }: QueryFunctionContext) => { 52 | const res = await callUnaryMethod( 53 | transport, 54 | ElizaService.method.say, 55 | input, 56 | { 57 | signal, 58 | }, 59 | ); 60 | return res; 61 | }, 62 | }, 63 | ], 64 | }); 65 | return { 66 | query1, 67 | }; 68 | }, wrapper()); 69 | 70 | await waitFor(() => { 71 | expect(result.current.query1.isSuccess).toBeTruthy(); 72 | }); 73 | expect(result.current.query1.data?.sentence).toEqual("Response 1"); 74 | }); 75 | it("can pass headers through", async () => { 76 | let resolve: () => void; 77 | const promise = new Promise((res) => { 78 | resolve = res; 79 | }); 80 | const transport = mockEliza( 81 | { 82 | sentence: "Response 1", 83 | }, 84 | false, 85 | { 86 | router: { 87 | interceptors: [ 88 | (next) => (req) => { 89 | expect(req.header.get("x-custom-header")).toEqual("custom-value"); 90 | resolve(); 91 | return next(req); 92 | }, 93 | ], 94 | }, 95 | }, 96 | ); 97 | const input: SayRequest = create(SayRequestSchema, { 98 | sentence: "query 1", 99 | }); 100 | const res = await callUnaryMethod( 101 | transport, 102 | ElizaService.method.say, 103 | input, 104 | { 105 | headers: { 106 | "x-custom-header": "custom-value", 107 | }, 108 | }, 109 | ); 110 | await promise; 111 | expect(res.sentence).toEqual("Response 1"); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-query.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import type { ConnectError, Transport } from "@connectrpc/connect"; 22 | import type { 23 | ConnectQueryKey, 24 | SkipToken, 25 | } from "@connectrpc/connect-query-core"; 26 | import { createQueryOptions } from "@connectrpc/connect-query-core"; 27 | import type { 28 | UseQueryOptions as TanStackUseQueryOptions, 29 | UseQueryResult, 30 | UseSuspenseQueryOptions as TanStackUseSuspenseQueryOptions, 31 | UseSuspenseQueryResult, 32 | } from "@tanstack/react-query"; 33 | import { 34 | useQuery as tsUseQuery, 35 | useSuspenseQuery as tsUseSuspenseQuery, 36 | } from "@tanstack/react-query"; 37 | 38 | import { useTransport } from "./use-transport.js"; 39 | 40 | /** 41 | * Options for useQuery 42 | */ 43 | export type UseQueryOptions< 44 | O extends DescMessage, 45 | SelectOutData = MessageShape, 46 | > = Omit< 47 | TanStackUseQueryOptions< 48 | MessageShape, 49 | ConnectError, 50 | SelectOutData, 51 | ConnectQueryKey 52 | >, 53 | "queryFn" | "queryKey" 54 | > & { 55 | /** The transport to be used for the fetching. */ 56 | transport?: Transport; 57 | }; 58 | 59 | /** 60 | * Query the method provided. Maps to useQuery on tanstack/react-query 61 | */ 62 | export function useQuery< 63 | I extends DescMessage, 64 | O extends DescMessage, 65 | SelectOutData = MessageShape, 66 | >( 67 | schema: DescMethodUnary, 68 | input?: SkipToken | MessageInitShape, 69 | { transport, ...queryOptions }: UseQueryOptions = {}, 70 | ): UseQueryResult { 71 | const transportFromCtx = useTransport(); 72 | const baseOptions = createQueryOptions(schema, input, { 73 | transport: transport ?? transportFromCtx, 74 | }); 75 | return tsUseQuery({ 76 | ...baseOptions, 77 | ...queryOptions, 78 | }); 79 | } 80 | 81 | /** 82 | * Options for useSuspenseQuery 83 | */ 84 | export type UseSuspenseQueryOptions< 85 | O extends DescMessage, 86 | SelectOutData = 0, 87 | > = Omit< 88 | TanStackUseSuspenseQueryOptions< 89 | MessageShape, 90 | ConnectError, 91 | SelectOutData, 92 | ConnectQueryKey 93 | >, 94 | "queryFn" | "queryKey" 95 | > & { 96 | /** The transport to be used for the fetching. */ 97 | transport?: Transport; 98 | headers?: HeadersInit; 99 | }; 100 | 101 | /** 102 | * Query the method provided. Maps to useSuspenseQuery on tanstack/react-query 103 | */ 104 | export function useSuspenseQuery< 105 | I extends DescMessage, 106 | O extends DescMessage, 107 | SelectOutData = MessageShape, 108 | >( 109 | schema: DescMethodUnary, 110 | input?: MessageInitShape, 111 | { 112 | transport, 113 | headers, 114 | ...queryOptions 115 | }: UseSuspenseQueryOptions = {}, 116 | ): UseSuspenseQueryResult { 117 | const transportFromCtx = useTransport(); 118 | const baseOptions = createQueryOptions(schema, input, { 119 | transport: transport ?? transportFromCtx, 120 | headers, 121 | }); 122 | return tsUseSuspenseQuery({ 123 | ...baseOptions, 124 | ...queryOptions, 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /packages/examples/react/basic/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-mutation.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create } from "@bufbuild/protobuf"; 16 | import { renderHook, waitFor } from "@testing-library/react"; 17 | import { mockPaginatedTransport } from "test-utils"; 18 | import { ListResponseSchema, ListService } from "test-utils/gen/list_pb.js"; 19 | import { describe, expect, it, vi } from "vitest"; 20 | 21 | import { wrapper } from "./test/test-wrapper.js"; 22 | import { useMutation } from "./use-mutation.js"; 23 | 24 | // TODO: maybe create a helper to take a service and method and generate this. 25 | const methodDescriptor = ListService.method.list; 26 | 27 | const mockedPaginatedTransport = mockPaginatedTransport(); 28 | 29 | describe("useMutation", () => { 30 | it("performs a mutation", async () => { 31 | const onSuccess = vi.fn(); 32 | const { result } = renderHook( 33 | () => { 34 | return useMutation(methodDescriptor, { 35 | onSuccess, 36 | }); 37 | }, 38 | wrapper({}, mockedPaginatedTransport), 39 | ); 40 | 41 | result.current.mutate({ 42 | page: 0n, 43 | }); 44 | 45 | await waitFor(() => { 46 | expect(result.current.isSuccess).toBeTruthy(); 47 | }); 48 | 49 | expect(onSuccess).toHaveBeenCalledWith( 50 | create(ListResponseSchema, { 51 | items: ["-2 Item", "-1 Item", "0 Item"], 52 | page: 0n, 53 | }), 54 | { 55 | page: 0n, 56 | }, 57 | undefined, 58 | ); 59 | }); 60 | 61 | it("can be provided a custom transport", async () => { 62 | const { result } = renderHook( 63 | () => { 64 | return useMutation(methodDescriptor, { 65 | transport: mockPaginatedTransport({ 66 | page: 1n, 67 | items: ["Intercepted!"], 68 | }), 69 | }); 70 | }, 71 | wrapper({}, mockedPaginatedTransport), 72 | ); 73 | 74 | result.current.mutate({ 75 | page: 0n, 76 | }); 77 | 78 | await waitFor(() => { 79 | expect(result.current.isSuccess).toBeTruthy(); 80 | }); 81 | 82 | expect(result.current.data?.items[0]).toBe("Intercepted!"); 83 | }); 84 | 85 | it("can forward onMutate params", async () => { 86 | const onSuccess = vi.fn(); 87 | const { result } = renderHook( 88 | () => { 89 | return useMutation(methodDescriptor, { 90 | onMutate: (variables) => { 91 | return { 92 | somethingElse: `Some additional context: ${(variables.page ?? 0n) + 2n}`, 93 | }; 94 | }, 95 | onSuccess: (data, variables, context) => { 96 | onSuccess(data, variables, context); 97 | // Customizing on success so we can test the types 98 | expect(context.somethingElse).toBe("Some additional context: 2"); 99 | }, 100 | }); 101 | }, 102 | wrapper({}, mockedPaginatedTransport), 103 | ); 104 | 105 | result.current.mutate({ 106 | page: 0n, 107 | }); 108 | 109 | await waitFor(() => { 110 | expect(result.current.isSuccess).toBeTruthy(); 111 | }); 112 | 113 | expect(onSuccess).toHaveBeenCalledWith( 114 | create(ListResponseSchema, { 115 | items: ["-2 Item", "-1 Item", "0 Item"], 116 | page: 0n, 117 | }), 118 | { 119 | page: 0n, 120 | }, 121 | { somethingElse: "Some additional context: 2" }, 122 | ); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /packages/test-utils/src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { MessageInitShape } from "@bufbuild/protobuf"; 16 | import { create } from "@bufbuild/protobuf"; 17 | import { 18 | createRouterTransport, 19 | type ConnectRouterOptions, 20 | } from "@connectrpc/connect"; 21 | 22 | import { 23 | BigIntService, 24 | type CountRequest, 25 | CountResponseSchema, 26 | } from "./gen/bigint_pb.js"; 27 | import { 28 | ElizaService, 29 | type SayRequest, 30 | SayResponseSchema, 31 | } from "./gen/eliza_pb.js"; 32 | import { type ListResponseSchema, ListService } from "./gen/list_pb.js"; 33 | 34 | /** 35 | * A test-only helper to increase time (necessary for testing react-query) 36 | */ 37 | export const sleep = async (timeout: number) => 38 | new Promise((resolve) => { 39 | setTimeout(resolve, timeout); 40 | }); 41 | 42 | /** 43 | * a stateless mock for ElizaService 44 | */ 45 | export const mockEliza = ( 46 | override?: MessageInitShape, 47 | addDelay = false, 48 | options?: { 49 | router?: ConnectRouterOptions; 50 | }, 51 | ) => 52 | createRouterTransport( 53 | ({ service }) => { 54 | service(ElizaService, { 55 | say: async (input: SayRequest) => { 56 | if (addDelay) { 57 | await sleep(1000); 58 | } 59 | return create( 60 | SayResponseSchema, 61 | override ?? { sentence: `Hello ${input.sentence}` }, 62 | ); 63 | }, 64 | }); 65 | }, 66 | { 67 | router: options?.router, 68 | }, 69 | ); 70 | 71 | /** 72 | * a stateless mock for BigIntService 73 | */ 74 | export const mockBigInt = () => 75 | createRouterTransport(({ service }) => { 76 | service(BigIntService, { 77 | count: () => create(CountResponseSchema, { count: 1n }), 78 | }); 79 | }); 80 | 81 | /** 82 | * a mock for BigIntService that acts as an impromptu database 83 | */ 84 | export const mockStatefulBigIntTransport = (addDelay = false) => 85 | createRouterTransport(({ service }) => { 86 | let count = 0n; 87 | service(BigIntService, { 88 | count: async (request?: CountRequest) => { 89 | if (addDelay) { 90 | await sleep(1000); 91 | } 92 | if (request) { 93 | count += request.add; 94 | } 95 | return create(CountResponseSchema, { count }); 96 | }, 97 | getCount: () => create(CountResponseSchema, { count }), 98 | }); 99 | }); 100 | 101 | /** 102 | * a mock for PaginatedService that acts as an impromptu database 103 | */ 104 | export const mockPaginatedTransport = ( 105 | override?: MessageInitShape, 106 | addDelay = false, 107 | options?: { 108 | router?: ConnectRouterOptions; 109 | }, 110 | ) => 111 | createRouterTransport( 112 | ({ service }) => { 113 | service(ListService, { 114 | list: async (request) => { 115 | if (addDelay) { 116 | await sleep(1000); 117 | } 118 | if (override !== undefined) { 119 | return override; 120 | } 121 | const base = (request.page - 1n) * 3n; 122 | const result = { 123 | page: request.page, 124 | items: [ 125 | `${base + 1n} Item`, 126 | `${base + 2n} Item`, 127 | `${base + 3n} Item`, 128 | ], 129 | }; 130 | return result; 131 | }, 132 | }); 133 | }, 134 | { 135 | router: options?.router, 136 | }, 137 | ); 138 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/create-query-options.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import { create } from "@bufbuild/protobuf"; 22 | import type { Transport } from "@connectrpc/connect"; 23 | import type { QueryFunction, SkipToken } from "@tanstack/query-core"; 24 | import { skipToken } from "@tanstack/query-core"; 25 | 26 | import { callUnaryMethod } from "./call-unary-method.js"; 27 | import type { ConnectQueryKey } from "./connect-query-key.js"; 28 | import { createConnectQueryKey } from "./connect-query-key.js"; 29 | import { createStructuralSharing } from "./structural-sharing.js"; 30 | 31 | /** 32 | * Return type of createQueryOptions 33 | */ 34 | export interface QueryOptions { 35 | queryKey: ConnectQueryKey; 36 | queryFn: QueryFunction, ConnectQueryKey>; 37 | structuralSharing: (oldData: unknown, newData: unknown) => unknown; 38 | } 39 | 40 | export interface QueryOptionsWithSkipToken 41 | extends Omit, "queryFn"> { 42 | queryFn: SkipToken; 43 | } 44 | 45 | function createUnaryQueryFn( 46 | transport: Transport, 47 | schema: DescMethodUnary, 48 | input: MessageInitShape | undefined, 49 | ): QueryFunction, ConnectQueryKey> { 50 | return async (context) => { 51 | return callUnaryMethod(transport, schema, input, { 52 | signal: context.signal, 53 | headers: context.queryKey[1].headers, 54 | }); 55 | }; 56 | } 57 | 58 | /** 59 | * Creates all options required to make a query. Useful in combination with `useQueries` from tanstack/react-query. 60 | */ 61 | export function createQueryOptions< 62 | I extends DescMessage, 63 | O extends DescMessage, 64 | >( 65 | schema: DescMethodUnary, 66 | input: MessageInitShape | undefined, 67 | { 68 | transport, 69 | headers, 70 | }: { 71 | transport: Transport; 72 | headers?: HeadersInit; 73 | }, 74 | ): QueryOptions; 75 | export function createQueryOptions< 76 | I extends DescMessage, 77 | O extends DescMessage, 78 | >( 79 | schema: DescMethodUnary, 80 | input: SkipToken, 81 | { 82 | transport, 83 | headers, 84 | }: { 85 | transport: Transport; 86 | headers?: HeadersInit; 87 | }, 88 | ): QueryOptionsWithSkipToken; 89 | export function createQueryOptions< 90 | I extends DescMessage, 91 | O extends DescMessage, 92 | >( 93 | schema: DescMethodUnary, 94 | input: SkipToken | MessageInitShape | undefined, 95 | { 96 | transport, 97 | headers, 98 | }: { 99 | transport: Transport; 100 | headers?: HeadersInit; 101 | }, 102 | ): QueryOptions | QueryOptionsWithSkipToken; 103 | export function createQueryOptions< 104 | I extends DescMessage, 105 | O extends DescMessage, 106 | >( 107 | schema: DescMethodUnary, 108 | input: SkipToken | MessageInitShape | undefined, 109 | { 110 | transport, 111 | headers, 112 | }: { 113 | transport: Transport; 114 | headers?: HeadersInit; 115 | }, 116 | ): QueryOptions | QueryOptionsWithSkipToken { 117 | const queryKey = createConnectQueryKey({ 118 | schema, 119 | input: input ?? create(schema.input), 120 | transport, 121 | cardinality: "finite", 122 | headers, 123 | }); 124 | const structuralSharing = createStructuralSharing(schema.output); 125 | const queryFn = 126 | input === skipToken 127 | ? skipToken 128 | : createUnaryQueryFn(transport, schema, input); 129 | return { 130 | queryKey, 131 | queryFn, 132 | structuralSharing, 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/message-key.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create } from "@bufbuild/protobuf"; 16 | import { Proto2MessageSchema } from "test-utils/gen/proto2_pb.js"; 17 | import { Proto3Enum, Proto3MessageSchema } from "test-utils/gen/proto3_pb.js"; 18 | import { describe, expect, it } from "vitest"; 19 | 20 | import { createMessageKey } from "./message-key.js"; 21 | 22 | describe("message key", () => { 23 | it("omits proto3 default values", () => { 24 | const schema = Proto3MessageSchema; 25 | const message = create(schema); 26 | const key = createMessageKey(schema, message); 27 | expect(key).toStrictEqual({}); 28 | }); 29 | it("omits proto2 default values", () => { 30 | const schema = Proto2MessageSchema; 31 | const message = create(schema); 32 | const key = createMessageKey(schema, message); 33 | expect(key).toStrictEqual({}); 34 | }); 35 | it("omits the pageParamKey", () => { 36 | const schema = Proto3MessageSchema; 37 | const message = create(schema, { 38 | int32Field: 123, 39 | stringField: "abc", 40 | }); 41 | const key = createMessageKey(schema, message, "int32Field"); 42 | expect(key).toStrictEqual({ 43 | stringField: "abc", 44 | }); 45 | }); 46 | it("converts as expected", () => { 47 | const key = createMessageKey(Proto3MessageSchema, { 48 | int64Field: 123n, 49 | bytesField: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), 50 | doubleField: Number.NaN, 51 | messageField: { 52 | doubleField: Infinity, 53 | messageField: { 54 | doubleField: -Infinity, 55 | }, 56 | }, 57 | boolField: true, 58 | enumField: Proto3Enum.YES, 59 | repeatedStringField: ["a", "b"], 60 | repeatedMessageField: [{ int64Field: 456n }], 61 | repeatedEnumField: [Proto3Enum.YES, Proto3Enum.NO], 62 | either: { 63 | case: "oneofInt32Field", 64 | value: 123, 65 | }, 66 | mapStringInt64Field: { 67 | foo: 123n, 68 | }, 69 | mapStringMessageField: { 70 | foo: { 71 | int64Field: 123n, 72 | }, 73 | }, 74 | mapStringEnumField: { 75 | foo: Proto3Enum.YES, 76 | }, 77 | }); 78 | expect(key).toStrictEqual({ 79 | int64Field: "123", 80 | bytesField: "3q2+7w", 81 | doubleField: "NaN", 82 | messageField: { 83 | doubleField: "Infinity", 84 | messageField: { 85 | doubleField: "-Infinity", 86 | }, 87 | }, 88 | boolField: true, 89 | enumField: 1, 90 | repeatedStringField: ["a", "b"], 91 | repeatedMessageField: [{ int64Field: "456" }], 92 | repeatedEnumField: [1, 2], 93 | oneofInt32Field: 123, 94 | mapStringInt64Field: { 95 | foo: "123", 96 | }, 97 | mapStringMessageField: { 98 | foo: { 99 | int64Field: "123", 100 | }, 101 | }, 102 | mapStringEnumField: { 103 | foo: 1, 104 | }, 105 | }); 106 | }); 107 | it("sorts map keys", () => { 108 | const key = createMessageKey(Proto3MessageSchema, { 109 | mapStringInt64Field: { 110 | b: 2n, 111 | a: 1n, 112 | }, 113 | }); 114 | const mapKeys = 115 | typeof key.mapStringInt64Field == "object" && 116 | key.mapStringInt64Field !== null 117 | ? Object.keys(key.mapStringInt64Field) 118 | : []; 119 | expect(mapKeys).toStrictEqual(["a", "b"]); 120 | }); 121 | it("sorts properties by protobuf source order", () => { 122 | const key = createMessageKey(Proto3MessageSchema, { 123 | boolField: true, 124 | stringField: "a", 125 | }); 126 | expect(Object.keys(key)).toStrictEqual(["stringField", "boolField"]); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/message-key.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { DescMessage, MessageInitShape } from "@bufbuild/protobuf"; 16 | import { create } from "@bufbuild/protobuf"; 17 | import type { 18 | ReflectList, 19 | ReflectMap, 20 | ReflectMessage, 21 | } from "@bufbuild/protobuf/reflect"; 22 | import { reflect } from "@bufbuild/protobuf/reflect"; 23 | import { base64Encode } from "@bufbuild/protobuf/wire"; 24 | 25 | /** 26 | * For any given message, create an object that is suitable for a Query Key in 27 | * TanStack Query: 28 | * 29 | * - Default values are omitted (both implicit and explicit field presence). 30 | * - NaN, Infinity, and -Infinity are converted to a string. 31 | * - Uint8Array is encoded to a string with Base64. 32 | * - BigInt values are converted to a string. 33 | * - Properties are sorted by Protobuf source order. 34 | * - Map keys are sorted with Array.sort. 35 | * 36 | * If pageParamKey is provided, omit the field with this name from the key. 37 | */ 38 | export function createMessageKey< 39 | Desc extends DescMessage, 40 | PageParamKey extends keyof MessageInitShape, 41 | >( 42 | schema: Desc, 43 | value: MessageInitShape, 44 | pageParamKey?: PageParamKey, 45 | ): Record { 46 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- circular reference 47 | return messageKey( 48 | reflect(schema, create(schema, value)), 49 | pageParamKey?.toString(), 50 | ); 51 | } 52 | 53 | function scalarKey(value: unknown): unknown { 54 | if (typeof value == "bigint") { 55 | return String(value); 56 | } 57 | if (typeof value == "number" && !isFinite(value)) { 58 | return String(value); 59 | } 60 | if (value instanceof Uint8Array) { 61 | return base64Encode(value, "std_raw"); 62 | } 63 | return value; 64 | } 65 | 66 | function listKey(list: ReflectList): unknown[] { 67 | const arr = Array.from(list); 68 | const { listKind } = list.field(); 69 | if (listKind == "scalar") { 70 | return arr.map(scalarKey); 71 | } 72 | if (listKind == "message") { 73 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- circular reference 74 | return (arr as ReflectMessage[]).map((m) => messageKey(m)); 75 | } 76 | return arr; 77 | } 78 | 79 | function mapKey(map: ReflectMap): Record { 80 | // eslint-disable-next-line @typescript-eslint/require-array-sort-compare -- we want the standard behavior 81 | return Array.from(map.keys()) 82 | .sort() 83 | .reduce>((result, k) => { 84 | switch (map.field().mapKind) { 85 | case "message": 86 | // eslint-disable-next-line @typescript-eslint/no-use-before-define -- circular reference 87 | result[k as string] = messageKey(map.get(k) as ReflectMessage); 88 | break; 89 | case "scalar": 90 | result[k as string] = scalarKey(map.get(k)); 91 | break; 92 | case "enum": 93 | result[k as string] = map.get(k); 94 | break; 95 | } 96 | return result; 97 | }, {}); 98 | } 99 | 100 | function messageKey( 101 | message: ReflectMessage, 102 | pageParamKey?: string, 103 | ): Record { 104 | const result: Record = {}; 105 | for (const f of message.sortedFields) { 106 | if (!message.isSet(f)) { 107 | continue; 108 | } 109 | if (f.localName === pageParamKey) { 110 | continue; 111 | } 112 | switch (f.fieldKind) { 113 | case "scalar": 114 | result[f.localName] = scalarKey(message.get(f)); 115 | break; 116 | case "enum": 117 | result[f.localName] = message.get(f); 118 | break; 119 | case "list": 120 | result[f.localName] = listKey(message.get(f)); 121 | break; 122 | case "map": 123 | result[f.localName] = mapKey(message.get(f)); 124 | break; 125 | case "message": 126 | result[f.localName] = messageKey(message.get(f)); 127 | break; 128 | } 129 | } 130 | return result; 131 | } 132 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-infinite-query.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import type { ConnectError, Transport } from "@connectrpc/connect"; 22 | import type { 23 | ConnectInfiniteQueryOptions, 24 | ConnectQueryKey, 25 | } from "@connectrpc/connect-query-core"; 26 | import { createInfiniteQueryOptions } from "@connectrpc/connect-query-core"; 27 | import type { 28 | InfiniteData, 29 | SkipToken, 30 | UseInfiniteQueryOptions as TanStackUseInfiniteQueryOptions, 31 | UseInfiniteQueryResult, 32 | UseSuspenseInfiniteQueryOptions as TanStackUseSuspenseInfiniteQueryOptions, 33 | UseSuspenseInfiniteQueryResult, 34 | } from "@tanstack/react-query"; 35 | import { 36 | useInfiniteQuery as tsUseInfiniteQuery, 37 | useSuspenseInfiniteQuery as tsUseSuspenseInfiniteQuery, 38 | } from "@tanstack/react-query"; 39 | 40 | import { useTransport } from "./use-transport.js"; 41 | 42 | /** 43 | * Options for useInfiniteQuery 44 | */ 45 | export type UseInfiniteQueryOptions< 46 | I extends DescMessage, 47 | O extends DescMessage, 48 | ParamKey extends keyof MessageInitShape, 49 | > = Omit< 50 | TanStackUseInfiniteQueryOptions< 51 | MessageShape, 52 | ConnectError, 53 | InfiniteData>, 54 | ConnectQueryKey, 55 | MessageInitShape[ParamKey] 56 | >, 57 | "getNextPageParam" | "initialPageParam" | "queryFn" | "queryKey" 58 | > & 59 | ConnectInfiniteQueryOptions & { 60 | /** The transport to be used for the fetching. */ 61 | transport?: Transport; 62 | }; 63 | 64 | /** 65 | * Query the method provided. Maps to useInfiniteQuery on tanstack/react-query 66 | */ 67 | export function useInfiniteQuery< 68 | I extends DescMessage, 69 | O extends DescMessage, 70 | ParamKey extends keyof MessageInitShape, 71 | >( 72 | schema: DescMethodUnary, 73 | input: 74 | | SkipToken 75 | | (MessageInitShape & Required, ParamKey>>), 76 | { 77 | transport, 78 | pageParamKey, 79 | getNextPageParam, 80 | ...queryOptions 81 | }: UseInfiniteQueryOptions, 82 | ): UseInfiniteQueryResult>, ConnectError> { 83 | const transportFromCtx = useTransport(); 84 | const baseOptions = createInfiniteQueryOptions(schema, input, { 85 | transport: transport ?? transportFromCtx, 86 | getNextPageParam, 87 | pageParamKey, 88 | }); 89 | return tsUseInfiniteQuery({ 90 | ...baseOptions, 91 | ...queryOptions, 92 | }); 93 | } 94 | 95 | /** 96 | * Options for useSuspenseInfiniteQuery 97 | */ 98 | export type UseSuspenseInfiniteQueryOptions< 99 | I extends DescMessage, 100 | O extends DescMessage, 101 | ParamKey extends keyof MessageInitShape, 102 | > = Omit< 103 | TanStackUseSuspenseInfiniteQueryOptions< 104 | MessageShape, 105 | ConnectError, 106 | InfiniteData>, 107 | ConnectQueryKey, 108 | MessageInitShape[ParamKey] 109 | >, 110 | "getNextPageParam" | "initialPageParam" | "queryFn" | "queryKey" 111 | > & 112 | ConnectInfiniteQueryOptions & { 113 | /** The transport to be used for the fetching. */ 114 | transport?: Transport; 115 | }; 116 | 117 | /** 118 | * Query the method provided. Maps to useSuspenseInfiniteQuery on tanstack/react-query 119 | */ 120 | export function useSuspenseInfiniteQuery< 121 | I extends DescMessage, 122 | O extends DescMessage, 123 | ParamKey extends keyof MessageInitShape, 124 | >( 125 | schema: DescMethodUnary, 126 | input: MessageInitShape & Required, ParamKey>>, 127 | { 128 | transport, 129 | pageParamKey, 130 | getNextPageParam, 131 | headers, 132 | ...queryOptions 133 | }: UseSuspenseInfiniteQueryOptions, 134 | ): UseSuspenseInfiniteQueryResult>, ConnectError> { 135 | const transportFromCtx = useTransport(); 136 | const baseOptions = createInfiniteQueryOptions(schema, input, { 137 | transport: transport ?? transportFromCtx, 138 | getNextPageParam, 139 | pageParamKey, 140 | headers, 141 | }); 142 | return tsUseSuspenseInfiniteQuery({ 143 | ...baseOptions, 144 | ...queryOptions, 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /assets/connect-query.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/test-utils/src/gen/proto3_pb.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" 16 | // @generated from file proto3.proto (package test, syntax proto3) 17 | /* eslint-disable */ 18 | 19 | import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; 20 | import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; 21 | import type { Message } from "@bufbuild/protobuf"; 22 | 23 | /** 24 | * Describes the file proto3.proto. 25 | */ 26 | export const file_proto3: GenFile = /*@__PURE__*/ 27 | fileDesc("Cgxwcm90bzMucHJvdG8SBHRlc3QirgcKDVByb3RvM01lc3NhZ2USFAoMc3RyaW5nX2ZpZWxkGAEgASgJEhMKC2J5dGVzX2ZpZWxkGAIgASgMEhMKC2ludDMyX2ZpZWxkGAMgASgFEhMKC2ludDY0X2ZpZWxkGAQgASgDEhQKDGRvdWJsZV9maWVsZBgFIAEoARISCgpib29sX2ZpZWxkGAYgASgIEiQKCmVudW1fZmllbGQYByABKA4yEC50ZXN0LlByb3RvM0VudW0SKgoNbWVzc2FnZV9maWVsZBgIIAEoCzITLnRlc3QuUHJvdG8zTWVzc2FnZRIiChVvcHRpb25hbF9zdHJpbmdfZmllbGQYCSABKAlIAYgBARIdChVyZXBlYXRlZF9zdHJpbmdfZmllbGQYESADKAkSMwoWcmVwZWF0ZWRfbWVzc2FnZV9maWVsZBgSIAMoCzITLnRlc3QuUHJvdG8zTWVzc2FnZRItChNyZXBlYXRlZF9lbnVtX2ZpZWxkGBMgAygOMhAudGVzdC5Qcm90bzNFbnVtEhwKEm9uZW9mX3N0cmluZ19maWVsZBgfIAEoCUgAEhsKEW9uZW9mX2ludDMyX2ZpZWxkGCEgASgFSAASTAoWbWFwX3N0cmluZ19pbnQ2NF9maWVsZBgnIAMoCzIsLnRlc3QuUHJvdG8zTWVzc2FnZS5NYXBTdHJpbmdJbnQ2NEZpZWxkRW50cnkSUAoYbWFwX3N0cmluZ19tZXNzYWdlX2ZpZWxkGCggAygLMi4udGVzdC5Qcm90bzNNZXNzYWdlLk1hcFN0cmluZ01lc3NhZ2VGaWVsZEVudHJ5EkoKFW1hcF9zdHJpbmdfZW51bV9maWVsZBgpIAMoCzIrLnRlc3QuUHJvdG8zTWVzc2FnZS5NYXBTdHJpbmdFbnVtRmllbGRFbnRyeRo6ChhNYXBTdHJpbmdJbnQ2NEZpZWxkRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgDOgI4ARpRChpNYXBTdHJpbmdNZXNzYWdlRmllbGRFbnRyeRILCgNrZXkYASABKAkSIgoFdmFsdWUYAiABKAsyEy50ZXN0LlByb3RvM01lc3NhZ2U6AjgBGksKF01hcFN0cmluZ0VudW1GaWVsZEVudHJ5EgsKA2tleRgBIAEoCRIfCgV2YWx1ZRgCIAEoDjIQLnRlc3QuUHJvdG8zRW51bToCOAFCCAoGZWl0aGVyQhgKFl9vcHRpb25hbF9zdHJpbmdfZmllbGQqUgoKUHJvdG8zRW51bRIbChdQUk9UTzNfRU5VTV9VTlNQRUNJRklFRBAAEhMKD1BST1RPM19FTlVNX1lFUxABEhIKDlBST1RPM19FTlVNX05PEAJiBnByb3RvMw"); 28 | 29 | /** 30 | * Note: We do not exhaust all field types 31 | * 32 | * @generated from message test.Proto3Message 33 | */ 34 | export type Proto3Message = Message<"test.Proto3Message"> & { 35 | /** 36 | * @generated from field: string string_field = 1; 37 | */ 38 | stringField: string; 39 | 40 | /** 41 | * @generated from field: bytes bytes_field = 2; 42 | */ 43 | bytesField: Uint8Array; 44 | 45 | /** 46 | * @generated from field: int32 int32_field = 3; 47 | */ 48 | int32Field: number; 49 | 50 | /** 51 | * @generated from field: int64 int64_field = 4; 52 | */ 53 | int64Field: bigint; 54 | 55 | /** 56 | * @generated from field: double double_field = 5; 57 | */ 58 | doubleField: number; 59 | 60 | /** 61 | * @generated from field: bool bool_field = 6; 62 | */ 63 | boolField: boolean; 64 | 65 | /** 66 | * @generated from field: test.Proto3Enum enum_field = 7; 67 | */ 68 | enumField: Proto3Enum; 69 | 70 | /** 71 | * @generated from field: test.Proto3Message message_field = 8; 72 | */ 73 | messageField?: Proto3Message; 74 | 75 | /** 76 | * @generated from field: optional string optional_string_field = 9; 77 | */ 78 | optionalStringField?: string; 79 | 80 | /** 81 | * @generated from field: repeated string repeated_string_field = 17; 82 | */ 83 | repeatedStringField: string[]; 84 | 85 | /** 86 | * @generated from field: repeated test.Proto3Message repeated_message_field = 18; 87 | */ 88 | repeatedMessageField: Proto3Message[]; 89 | 90 | /** 91 | * @generated from field: repeated test.Proto3Enum repeated_enum_field = 19; 92 | */ 93 | repeatedEnumField: Proto3Enum[]; 94 | 95 | /** 96 | * @generated from oneof test.Proto3Message.either 97 | */ 98 | either: { 99 | /** 100 | * @generated from field: string oneof_string_field = 31; 101 | */ 102 | value: string; 103 | case: "oneofStringField"; 104 | } | { 105 | /** 106 | * @generated from field: int32 oneof_int32_field = 33; 107 | */ 108 | value: number; 109 | case: "oneofInt32Field"; 110 | } | { case: undefined; value?: undefined }; 111 | 112 | /** 113 | * @generated from field: map map_string_int64_field = 39; 114 | */ 115 | mapStringInt64Field: { [key: string]: bigint }; 116 | 117 | /** 118 | * @generated from field: map map_string_message_field = 40; 119 | */ 120 | mapStringMessageField: { [key: string]: Proto3Message }; 121 | 122 | /** 123 | * @generated from field: map map_string_enum_field = 41; 124 | */ 125 | mapStringEnumField: { [key: string]: Proto3Enum }; 126 | }; 127 | 128 | /** 129 | * Describes the message test.Proto3Message. 130 | * Use `create(Proto3MessageSchema)` to create a new message. 131 | */ 132 | export const Proto3MessageSchema: GenMessage = /*@__PURE__*/ 133 | messageDesc(file_proto3, 0); 134 | 135 | /** 136 | * @generated from enum test.Proto3Enum 137 | */ 138 | export enum Proto3Enum { 139 | /** 140 | * @generated from enum value: PROTO3_ENUM_UNSPECIFIED = 0; 141 | */ 142 | UNSPECIFIED = 0, 143 | 144 | /** 145 | * @generated from enum value: PROTO3_ENUM_YES = 1; 146 | */ 147 | YES = 1, 148 | 149 | /** 150 | * @generated from enum value: PROTO3_ENUM_NO = 2; 151 | */ 152 | NO = 2, 153 | } 154 | 155 | /** 156 | * Describes the enum test.Proto3Enum. 157 | */ 158 | export const Proto3EnumSchema: GenEnum = /*@__PURE__*/ 159 | enumDesc(file_proto3, 0); 160 | 161 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create, isFieldSet, isMessage } from "@bufbuild/protobuf"; 16 | import { Proto2MessageSchema } from "test-utils/gen/proto2_pb.js"; 17 | import { describe, expect, it } from "vitest"; 18 | 19 | import { 20 | assert, 21 | createProtobufSafeUpdater, 22 | isAbortController, 23 | } from "./utils.js"; 24 | 25 | describe("assert", () => { 26 | const message = "assertion message"; 27 | it("throws on a false condition", () => { 28 | expect(() => { 29 | assert(false, message); 30 | }).toThrow(`Invalid assertion: ${message}`); 31 | }); 32 | 33 | it("does not throw on a true condition", () => { 34 | expect(() => { 35 | assert(true, message); 36 | }).not.toThrow(); 37 | }); 38 | }); 39 | 40 | describe("isAbortController", () => { 41 | it("returns false for non-objects", () => { 42 | expect(isAbortController(true)).toBeFalsy(); 43 | expect(isAbortController(false)).toBeFalsy(); 44 | expect(isAbortController(0)).toBeFalsy(); 45 | expect(isAbortController(1)).toBeFalsy(); 46 | expect(isAbortController("a")).toBeFalsy(); 47 | expect(isAbortController(undefined)).toBeFalsy(); 48 | expect(isAbortController([])).toBeFalsy(); 49 | expect(isAbortController(null)).toBeFalsy(); 50 | }); 51 | 52 | it("returns false for objects missing the AbortController properties", () => { 53 | expect(isAbortController({})).toBeFalsy(); 54 | expect(isAbortController({ signal: undefined })).toBeFalsy(); 55 | expect(isAbortController({ signal: null })).toBeFalsy(); 56 | expect(isAbortController({ signal: {} })).toBeFalsy(); 57 | expect(isAbortController({ signal: { aborted: undefined } })).toBeFalsy(); 58 | expect(isAbortController({ signal: { aborted: true } })).toBeFalsy(); 59 | expect( 60 | isAbortController({ signal: { aborted: true }, abort: undefined }), 61 | ).toBeFalsy(); 62 | }); 63 | 64 | it("returns true for the two necessary AbortController properties", () => { 65 | expect( 66 | isAbortController({ 67 | signal: { 68 | aborted: false, 69 | }, 70 | abort: () => {}, 71 | }), 72 | ).toBeTruthy(); 73 | 74 | expect(isAbortController(new AbortController())).toBeTruthy(); 75 | }); 76 | }); 77 | 78 | describe("createProtobufSafeUpdater", () => { 79 | describe("with update message", () => { 80 | const schema = { output: Proto2MessageSchema }; 81 | const update = create(Proto2MessageSchema, { 82 | int32Field: 999, 83 | }); 84 | const safeUpdater = createProtobufSafeUpdater(schema, update); 85 | it("returns update message for previous value undefined", () => { 86 | const next = safeUpdater(undefined); 87 | expect(next).toBe(update); 88 | }); 89 | it("returns update message for previous value", () => { 90 | const prev = create(Proto2MessageSchema, { 91 | int32Field: 123, 92 | }); 93 | const next = safeUpdater(prev); 94 | expect(next).toBe(update); 95 | }); 96 | }); 97 | 98 | describe("with update message init", () => { 99 | const schema = { output: Proto2MessageSchema }; 100 | const update = { 101 | int32Field: 999, 102 | }; 103 | const safeUpdater = createProtobufSafeUpdater(schema, update); 104 | it("returns update message for previous value undefined", () => { 105 | const next = safeUpdater(undefined); 106 | expect(next?.int32Field).toBe(999); 107 | }); 108 | it("returns update message for previous value", () => { 109 | const prev = create(Proto2MessageSchema, { 110 | int32Field: 123, 111 | }); 112 | const next = safeUpdater(prev); 113 | expect(next?.$typeName).toBe(Proto2MessageSchema.typeName); 114 | expect(next?.int32Field).toBe(999); 115 | }); 116 | }); 117 | 118 | describe("with updater function", () => { 119 | const schema = { output: Proto2MessageSchema }; 120 | const safeUpdater = createProtobufSafeUpdater(schema, (prev) => { 121 | if (prev === undefined) { 122 | return undefined; 123 | } 124 | return { 125 | ...prev, 126 | int32Field: 999, 127 | }; 128 | }); 129 | it("accepts undefined", () => { 130 | const next = safeUpdater(undefined); 131 | expect(next).toBeUndefined(); 132 | }); 133 | it("accepts previous message", () => { 134 | const prev = create(Proto2MessageSchema, { 135 | int32Field: 123, 136 | }); 137 | const next = safeUpdater(prev); 138 | expect(next).toBeDefined(); 139 | }); 140 | it("returns message", () => { 141 | const prev = create(Proto2MessageSchema); 142 | const next = safeUpdater(prev); 143 | expect(isMessage(next, Proto2MessageSchema)).toBe(true); 144 | }); 145 | it("updates field", () => { 146 | const prev = create(Proto2MessageSchema); 147 | const next = safeUpdater(prev); 148 | expect(next?.int32Field).toBe(999); 149 | }); 150 | it("keeps existing fields", () => { 151 | const prev = create(Proto2MessageSchema, { 152 | stringField: "abc", 153 | }); 154 | const next = safeUpdater(prev); 155 | expect(next?.stringField).toBe("abc"); 156 | }); 157 | describe("keeps field presence", () => { 158 | it("for unset field", () => { 159 | const prev = create(Proto2MessageSchema); 160 | expect(isFieldSet(prev, Proto2MessageSchema.field.stringField)).toBe( 161 | false, 162 | ); 163 | const next = safeUpdater(prev); 164 | const hasStringField = 165 | next === undefined 166 | ? undefined 167 | : isFieldSet(next, Proto2MessageSchema.field.stringField); 168 | expect(hasStringField).toBe(false); 169 | }); 170 | it("for set field", () => { 171 | const prev = create(Proto2MessageSchema, { 172 | stringField: "abc", 173 | }); 174 | expect(isFieldSet(prev, Proto2MessageSchema.field.stringField)).toBe( 175 | true, 176 | ); 177 | const next = safeUpdater(prev); 178 | const hasStringField = 179 | next === undefined 180 | ? undefined 181 | : isFieldSet(next, Proto2MessageSchema.field.stringField); 182 | expect(hasStringField).toBe(true); 183 | }); 184 | }); 185 | }); 186 | }); 187 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/create-infinite-query-options.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethodUnary, 18 | MessageInitShape, 19 | MessageShape, 20 | } from "@bufbuild/protobuf"; 21 | import type { Transport } from "@connectrpc/connect"; 22 | import type { 23 | GetNextPageParamFunction, 24 | QueryFunction, 25 | SkipToken, 26 | } from "@tanstack/query-core"; 27 | import { skipToken } from "@tanstack/query-core"; 28 | 29 | import { callUnaryMethod } from "./call-unary-method.js"; 30 | import { 31 | type ConnectQueryKey, 32 | createConnectQueryKey, 33 | } from "./connect-query-key.js"; 34 | import { createStructuralSharing } from "./structural-sharing.js"; 35 | import { assert } from "./utils.js"; 36 | 37 | /** 38 | * Return type of createInfiniteQueryOptions assuming SkipToken was not provided. 39 | */ 40 | export interface InfiniteQueryOptions< 41 | I extends DescMessage, 42 | O extends DescMessage, 43 | ParamKey extends keyof MessageInitShape, 44 | > { 45 | getNextPageParam: ConnectInfiniteQueryOptions< 46 | I, 47 | O, 48 | ParamKey 49 | >["getNextPageParam"]; 50 | queryKey: ConnectQueryKey; 51 | queryFn: QueryFunction< 52 | MessageShape, 53 | ConnectQueryKey, 54 | MessageInitShape[ParamKey] 55 | >; 56 | structuralSharing: (oldData: unknown, newData: unknown) => unknown; 57 | initialPageParam: MessageInitShape[ParamKey]; 58 | } 59 | 60 | /** 61 | * Return type of createInfiniteQueryOptions when SkipToken is provided 62 | */ 63 | export interface InfiniteQueryOptionsWithSkipToken< 64 | I extends DescMessage, 65 | O extends DescMessage, 66 | ParamKey extends keyof MessageInitShape, 67 | > extends Omit, "queryFn"> { 68 | queryFn: SkipToken; 69 | } 70 | 71 | /** 72 | * Options specific to connect-query 73 | */ 74 | export interface ConnectInfiniteQueryOptions< 75 | I extends DescMessage, 76 | O extends DescMessage, 77 | ParamKey extends keyof MessageInitShape, 78 | > { 79 | /** Defines which part of the input should be considered the page param */ 80 | pageParamKey: ParamKey; 81 | /** Determines the next page. */ 82 | getNextPageParam: GetNextPageParamFunction< 83 | MessageInitShape[ParamKey], 84 | MessageShape 85 | >; 86 | headers?: HeadersInit; 87 | } 88 | 89 | // eslint-disable-next-line @typescript-eslint/max-params -- we have 4 required arguments 90 | function createUnaryInfiniteQueryFn< 91 | I extends DescMessage, 92 | O extends DescMessage, 93 | ParamKey extends keyof MessageInitShape, 94 | >( 95 | transport: Transport, 96 | schema: DescMethodUnary, 97 | input: MessageInitShape, 98 | { 99 | pageParamKey, 100 | }: { 101 | pageParamKey: ParamKey; 102 | }, 103 | ): QueryFunction< 104 | MessageShape, 105 | ConnectQueryKey, 106 | MessageInitShape[ParamKey] 107 | > { 108 | return async (context) => { 109 | assert("pageParam" in context, "pageParam must be part of context"); 110 | 111 | const inputCombinedWithPageParam = { 112 | ...input, 113 | [pageParamKey]: context.pageParam, 114 | }; 115 | return callUnaryMethod(transport, schema, inputCombinedWithPageParam, { 116 | signal: context.signal, 117 | headers: context.queryKey[1].headers, 118 | }); 119 | }; 120 | } 121 | 122 | /** 123 | * Query the method provided. Maps to useInfiniteQuery on tanstack/react-query 124 | */ 125 | export function createInfiniteQueryOptions< 126 | I extends DescMessage, 127 | O extends DescMessage, 128 | ParamKey extends keyof MessageInitShape, 129 | >( 130 | schema: DescMethodUnary, 131 | input: MessageInitShape & Required, ParamKey>>, 132 | { 133 | transport, 134 | getNextPageParam, 135 | pageParamKey, 136 | headers, 137 | }: ConnectInfiniteQueryOptions & { transport: Transport }, 138 | ): InfiniteQueryOptions; 139 | export function createInfiniteQueryOptions< 140 | I extends DescMessage, 141 | O extends DescMessage, 142 | ParamKey extends keyof MessageInitShape, 143 | >( 144 | schema: DescMethodUnary, 145 | input: SkipToken, 146 | { 147 | transport, 148 | getNextPageParam, 149 | pageParamKey, 150 | headers, 151 | }: ConnectInfiniteQueryOptions & { transport: Transport }, 152 | ): InfiniteQueryOptionsWithSkipToken; 153 | export function createInfiniteQueryOptions< 154 | I extends DescMessage, 155 | O extends DescMessage, 156 | ParamKey extends keyof MessageInitShape, 157 | >( 158 | schema: DescMethodUnary, 159 | input: 160 | | SkipToken 161 | | (MessageInitShape & Required, ParamKey>>), 162 | { 163 | transport, 164 | getNextPageParam, 165 | pageParamKey, 166 | headers, 167 | }: ConnectInfiniteQueryOptions & { transport: Transport }, 168 | ): 169 | | InfiniteQueryOptions 170 | | InfiniteQueryOptionsWithSkipToken; 171 | export function createInfiniteQueryOptions< 172 | I extends DescMessage, 173 | O extends DescMessage, 174 | ParamKey extends keyof MessageInitShape, 175 | >( 176 | schema: DescMethodUnary, 177 | input: 178 | | SkipToken 179 | | (MessageInitShape & Required, ParamKey>>), 180 | { 181 | transport, 182 | getNextPageParam, 183 | pageParamKey, 184 | headers, 185 | }: ConnectInfiniteQueryOptions & { transport: Transport }, 186 | ): 187 | | InfiniteQueryOptions 188 | | InfiniteQueryOptionsWithSkipToken { 189 | const queryKey = createConnectQueryKey({ 190 | cardinality: "infinite", 191 | schema, 192 | transport, 193 | input, 194 | pageParamKey, 195 | headers, 196 | }); 197 | const structuralSharing = createStructuralSharing(schema.output); 198 | const queryFn = 199 | input === skipToken 200 | ? skipToken 201 | : createUnaryInfiniteQueryFn(transport, schema, input, { 202 | pageParamKey, 203 | }); 204 | return { 205 | getNextPageParam, 206 | initialPageParam: 207 | input === skipToken 208 | ? (undefined as MessageInitShape[ParamKey]) 209 | : (input[pageParamKey] as MessageInitShape[ParamKey]), 210 | queryKey, 211 | queryFn, 212 | structuralSharing, 213 | }; 214 | } 215 | -------------------------------------------------------------------------------- /packages/connect-query-core/src/connect-query-key.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import type { 16 | DescMessage, 17 | DescMethod, 18 | DescMethodUnary, 19 | DescService, 20 | MessageInitShape, 21 | MessageShape, 22 | } from "@bufbuild/protobuf"; 23 | import type { ConnectError, Transport } from "@connectrpc/connect"; 24 | import type { DataTag, InfiniteData, SkipToken } from "@tanstack/query-core"; 25 | 26 | import { createMessageKey } from "./message-key.js"; 27 | import { createTransportKey } from "./transport-key.js"; 28 | 29 | type SharedConnectQueryOptions = { 30 | /** 31 | * A key for a Transport reference, created with createTransportKey(). 32 | */ 33 | transport?: string; 34 | /** 35 | * The name of the service, e.g. connectrpc.eliza.v1.ElizaService 36 | */ 37 | serviceName: string; 38 | /** 39 | * The name of the method, e.g. Say. 40 | */ 41 | methodName?: string; 42 | /** 43 | * A key for the request message, created with createMessageKey(), 44 | * or "skipped". 45 | */ 46 | input?: Record | "skipped"; 47 | /** 48 | * Headers to be sent with the request. 49 | * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. 50 | */ 51 | headers?: Record; 52 | }; 53 | 54 | type InfiniteConnectQueryKey = 55 | DataTag< 56 | [ 57 | "connect-query", 58 | SharedConnectQueryOptions & { 59 | /** This data represents a infinite, paged result */ 60 | cardinality: "infinite"; 61 | }, 62 | ], 63 | InfiniteData>, 64 | ConnectError 65 | >; 66 | 67 | type FiniteConnectQueryKey = 68 | DataTag< 69 | [ 70 | "connect-query", 71 | SharedConnectQueryOptions & { 72 | /** This data represents a finite result */ 73 | cardinality: "finite"; 74 | }, 75 | ], 76 | MessageShape, 77 | ConnectError 78 | >; 79 | 80 | /** 81 | * TanStack Query manages query caching for you based on query keys. `QueryKey`s in TanStack Query are arrays with arbitrary JSON-serializable data - typically handwritten for each endpoint. 82 | * 83 | * In Connect Query, query keys are more structured, since queries are always tied to a service, RPC, input message, and transport. For example, for a query key might look like this: 84 | * 85 | * @example 86 | * [ 87 | * "connect-query", 88 | * { 89 | * transport: "t1", 90 | * serviceName: "connectrpc.eliza.v1.ElizaService", 91 | * methodName: "Say", 92 | * input: { 93 | * sentence: "hello there", 94 | * }, 95 | * cardinality: "finite", 96 | * } 97 | * ] 98 | */ 99 | export type ConnectQueryKey = 100 | | InfiniteConnectQueryKey 101 | | FiniteConnectQueryKey 102 | | [ 103 | "connect-query", 104 | SharedConnectQueryOptions & { 105 | cardinality: undefined; 106 | }, 107 | ]; 108 | 109 | type KeyParamsForMethod = { 110 | /** 111 | * Set `serviceName` and `methodName` in the key. 112 | */ 113 | schema: Desc; 114 | /** 115 | * Set `input` in the key: 116 | * - If a SkipToken is provided, `input` is "skipped". 117 | * - If an init shape is provided, `input` is set to a message key. 118 | * - If omitted or undefined, `input` is not set in the key. 119 | */ 120 | input?: MessageInitShape | SkipToken | undefined; 121 | /** 122 | * Set `transport` in the key. 123 | */ 124 | transport?: Transport; 125 | /** 126 | * Set `cardinality` in the key - undefined is used for filters to match both finite and infinite queries. 127 | */ 128 | cardinality: "finite" | "infinite" | undefined; 129 | /** 130 | * If omit the field with this name from the key for infinite queries. 131 | */ 132 | pageParamKey?: keyof MessageInitShape; 133 | /** 134 | * Set `headers` in the key. 135 | * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. 136 | */ 137 | headers?: HeadersInit; 138 | }; 139 | 140 | type KeyParamsForService = { 141 | /** 142 | * Set `serviceName` in the key, and omit `methodName`. 143 | */ 144 | schema: Desc; 145 | /** 146 | * Set `transport` in the key. 147 | */ 148 | transport?: Transport; 149 | /** 150 | * Set `cardinality` in the key - undefined is used for filters to match both finite and infinite queries. 151 | */ 152 | cardinality: "finite" | "infinite" | undefined; 153 | }; 154 | 155 | /** 156 | * TanStack Query manages query caching for you based on query keys. In Connect Query, keys are structured, and can easily be created using this factory function. 157 | * 158 | * When you make a query, a unique key is automatically created from the schema, input message, and transport. For example: 159 | * 160 | * ```ts 161 | * import { useQuery } from "@connectrpc/connect-query"; 162 | * 163 | * useQuery(ElizaService.method.say, { sentence: "hello" }); 164 | * 165 | * // creates the key: 166 | * [ 167 | * "connect-query", 168 | * { 169 | * transport: "t1", 170 | * serviceName: "connectrpc.eliza.v1.ElizaService", 171 | * methodName: "Say", 172 | * input: { sentence: "hello" }, 173 | * cardinality: "finite", 174 | * } 175 | * ] 176 | * ``` 177 | * 178 | * The same key can be created manually with this factory: 179 | * 180 | * ```ts 181 | * createConnectQueryKey({ 182 | * transport: myTransportReference, 183 | * schema: ElizaService.method.say, 184 | * input: { sentence: "hello" } 185 | * }); 186 | * ``` 187 | * 188 | * Note that the factory allows to create partial keys that can be used to filter queries. For example, you can create a key without a transport, any cardinality, any input message, or with a partial input message. 189 | * 190 | * @see ConnectQueryKey for information on the components of Connect-Query's keys. 191 | */ 192 | export function createConnectQueryKey< 193 | I extends DescMessage, 194 | O extends DescMessage, 195 | >( 196 | params: KeyParamsForMethod> & { 197 | cardinality: "finite"; 198 | }, 199 | ): FiniteConnectQueryKey; 200 | export function createConnectQueryKey< 201 | I extends DescMessage, 202 | O extends DescMessage, 203 | >( 204 | params: KeyParamsForMethod> & { 205 | cardinality: "infinite"; 206 | }, 207 | ): InfiniteConnectQueryKey; 208 | export function createConnectQueryKey< 209 | I extends DescMessage, 210 | O extends DescMessage, 211 | >( 212 | params: KeyParamsForMethod> & { 213 | cardinality: undefined; 214 | }, 215 | ): ConnectQueryKey; 216 | export function createConnectQueryKey< 217 | O extends DescMessage, 218 | Desc extends DescService, 219 | >(params: KeyParamsForService): ConnectQueryKey; 220 | export function createConnectQueryKey< 221 | I extends DescMessage, 222 | O extends DescMessage, 223 | Desc extends DescService, 224 | >( 225 | params: KeyParamsForMethod> | KeyParamsForService, 226 | ): ConnectQueryKey { 227 | const props: { 228 | serviceName: string; 229 | methodName?: string; 230 | transport?: string; 231 | cardinality?: "finite" | "infinite"; 232 | input?: "skipped" | Record; 233 | headers?: Record; 234 | } = 235 | params.schema.kind == "rpc" 236 | ? { 237 | serviceName: params.schema.parent.typeName, 238 | methodName: params.schema.name, 239 | } 240 | : { 241 | serviceName: params.schema.typeName, 242 | }; 243 | if (params.transport !== undefined) { 244 | props.transport = createTransportKey(params.transport); 245 | } 246 | if (params.cardinality !== undefined) { 247 | props.cardinality = params.cardinality; 248 | } 249 | if (params.schema.kind == "rpc" && "input" in params) { 250 | if (typeof params.input == "symbol") { 251 | props.input = "skipped"; 252 | } else if (params.input !== undefined) { 253 | props.input = createMessageKey( 254 | params.schema.input, 255 | params.input, 256 | params.pageParamKey, 257 | ); 258 | } 259 | } 260 | if ( 261 | params.schema.kind === "rpc" && 262 | "headers" in params && 263 | params.headers !== undefined 264 | ) { 265 | props.headers = createHeadersKey(params.headers); 266 | } 267 | return ["connect-query", props] as ConnectQueryKey; 268 | } 269 | 270 | /** 271 | * Creates a record of headers from a HeadersInit object. 272 | * 273 | */ 274 | function createHeadersKey(headers: HeadersInit): Record { 275 | const result: Record = {}; 276 | 277 | for (const [key, value] of new Headers(headers)) { 278 | result[key] = value; 279 | } 280 | return result; 281 | } 282 | -------------------------------------------------------------------------------- /packages/connect-query/src/use-query.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021-2023 The Connect Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import { create } from "@bufbuild/protobuf"; 16 | import { 17 | createConnectQueryKey, 18 | skipToken, 19 | } from "@connectrpc/connect-query-core"; 20 | import { renderHook, waitFor } from "@testing-library/react"; 21 | import { mockBigInt, mockEliza } from "test-utils"; 22 | import { BigIntService } from "test-utils/gen/bigint_pb.js"; 23 | import { ElizaService } from "test-utils/gen/eliza_pb.js"; 24 | import { describe, expect, it } from "vitest"; 25 | 26 | import { wrapper } from "./test/test-wrapper.js"; 27 | import { useQuery, useSuspenseQuery } from "./use-query.js"; 28 | 29 | // TODO: maybe create a helper to take a service and method and generate this. 30 | const sayMethodDescriptor = ElizaService.method.say; 31 | 32 | const mockedElizaTransport = mockEliza(); 33 | 34 | const bigintTransport = mockBigInt(); 35 | 36 | const elizaWithDelayTransport = mockEliza(undefined, true); 37 | 38 | describe("useQuery", () => { 39 | it("can query data", async () => { 40 | const { result } = renderHook( 41 | () => { 42 | return useQuery(sayMethodDescriptor, { 43 | sentence: "hello", 44 | }); 45 | }, 46 | wrapper({}, mockedElizaTransport), 47 | ); 48 | 49 | await waitFor(() => { 50 | expect(result.current.isSuccess).toBeTruthy(); 51 | }); 52 | 53 | expect(typeof result.current.data?.sentence).toBe("string"); 54 | }); 55 | 56 | it("can be disabled", () => { 57 | const { result } = renderHook( 58 | () => { 59 | return useQuery(sayMethodDescriptor, skipToken); 60 | }, 61 | wrapper(undefined, mockedElizaTransport), 62 | ); 63 | expect(result.current.isPending).toBeTruthy(); 64 | expect(result.current.isFetching).toBeFalsy(); 65 | }); 66 | 67 | it("can be provided a custom transport", async () => { 68 | const transport = mockEliza({ 69 | sentence: "Intercepted!", 70 | }); 71 | const { result } = renderHook( 72 | () => { 73 | return useQuery( 74 | sayMethodDescriptor, 75 | {}, 76 | { 77 | transport, 78 | }, 79 | ); 80 | }, 81 | wrapper(undefined, mockedElizaTransport), 82 | ); 83 | await waitFor(() => { 84 | expect(result.current.isSuccess).toBeTruthy(); 85 | }); 86 | 87 | expect(result.current.data?.sentence).toBe("Intercepted!"); 88 | }); 89 | 90 | it("can be provided other props for react-query", () => { 91 | const { result } = renderHook( 92 | () => { 93 | return useQuery( 94 | sayMethodDescriptor, 95 | {}, 96 | { 97 | transport: elizaWithDelayTransport, 98 | placeholderData: create(sayMethodDescriptor.output, { 99 | sentence: "placeholder!", 100 | }), 101 | }, 102 | ); 103 | }, 104 | wrapper(undefined, mockedElizaTransport), 105 | ); 106 | expect(result.current.data?.sentence).toBe("placeholder!"); 107 | }); 108 | 109 | it("can be used along with the select", async () => { 110 | const { result } = renderHook( 111 | () => { 112 | return useQuery( 113 | sayMethodDescriptor, 114 | {}, 115 | { 116 | select: (data) => data.sentence.length, 117 | }, 118 | ); 119 | }, 120 | wrapper(undefined, mockedElizaTransport), 121 | ); 122 | 123 | await waitFor(() => { 124 | expect(result.current.isSuccess).toBeTruthy(); 125 | }); 126 | 127 | expect(result.current.data).toBe(6); 128 | }); 129 | 130 | it("can be disabled with enabled: false", () => { 131 | const { result } = renderHook( 132 | () => { 133 | return useQuery( 134 | sayMethodDescriptor, 135 | { 136 | sentence: "hello", 137 | }, 138 | { 139 | enabled: false, 140 | }, 141 | ); 142 | }, 143 | wrapper({}, mockedElizaTransport), 144 | ); 145 | 146 | expect(result.current.data).toBeUndefined(); 147 | expect(result.current.isPending).toBeTruthy(); 148 | expect(result.current.isFetching).toBeFalsy(); 149 | }); 150 | 151 | it("can be disabled with enabled: false in QueryClient default options", () => { 152 | const { result } = renderHook( 153 | () => { 154 | return useQuery(sayMethodDescriptor, { 155 | sentence: "hello", 156 | }); 157 | }, 158 | wrapper( 159 | { 160 | defaultOptions: { 161 | queries: { 162 | enabled: false, 163 | }, 164 | }, 165 | }, 166 | mockedElizaTransport, 167 | ), 168 | ); 169 | 170 | expect(result.current.data).toBeUndefined(); 171 | expect(result.current.isPending).toBeTruthy(); 172 | expect(result.current.isFetching).toBeFalsy(); 173 | }); 174 | 175 | it("can be disabled with skipToken", () => { 176 | const { result } = renderHook( 177 | () => { 178 | return useQuery(sayMethodDescriptor, skipToken); 179 | }, 180 | wrapper({}, mockedElizaTransport), 181 | ); 182 | 183 | expect(result.current.data).toBeUndefined(); 184 | expect(result.current.isPending).toBeTruthy(); 185 | expect(result.current.isFetching).toBeFalsy(); 186 | }); 187 | 188 | it("supports schemas with bigint keys", async () => { 189 | const { result } = renderHook( 190 | () => { 191 | return useQuery(BigIntService.method.count, { 192 | add: 2n, 193 | }); 194 | }, 195 | wrapper({}, bigintTransport), 196 | ); 197 | 198 | await waitFor(() => { 199 | expect(result.current.isSuccess).toBeTruthy(); 200 | }); 201 | 202 | expect(result.current.data?.count).toBe(1n); 203 | }); 204 | 205 | it("data can be fetched from cache", async () => { 206 | const { queryClient, ...rest } = wrapper({}, bigintTransport); 207 | const { result } = renderHook(() => { 208 | return useQuery(BigIntService.method.count, {}); 209 | }, rest); 210 | 211 | await waitFor(() => { 212 | expect(result.current.isSuccess).toBeTruthy(); 213 | }); 214 | 215 | expect( 216 | queryClient.getQueryData( 217 | createConnectQueryKey({ 218 | schema: BigIntService.method.count, 219 | input: {}, 220 | transport: bigintTransport, 221 | cardinality: "finite", 222 | }), 223 | ), 224 | ).toBe(result.current.data); 225 | }); 226 | }); 227 | 228 | describe("useSuspenseQuery", () => { 229 | it("can query data", async () => { 230 | const { result } = renderHook( 231 | () => { 232 | return useSuspenseQuery(sayMethodDescriptor, { 233 | sentence: "hello", 234 | }); 235 | }, 236 | wrapper({}, mockedElizaTransport), 237 | ); 238 | 239 | await waitFor(() => { 240 | expect(result.current.isSuccess).toBeTruthy(); 241 | }); 242 | 243 | expect(typeof result.current.data.sentence).toBe("string"); 244 | }); 245 | 246 | it("can be used along with the select", async () => { 247 | const { result } = renderHook( 248 | () => { 249 | return useSuspenseQuery( 250 | sayMethodDescriptor, 251 | { 252 | sentence: "hello", 253 | }, 254 | { 255 | select: (data) => data.sentence.length, 256 | }, 257 | ); 258 | }, 259 | wrapper({}, mockedElizaTransport), 260 | ); 261 | 262 | await waitFor(() => { 263 | expect(result.current.isSuccess).toBeTruthy(); 264 | }); 265 | 266 | expect(result.current.data).toBe(11); 267 | }); 268 | 269 | it("can pass headers through", async () => { 270 | let resolve: () => void; 271 | const promise = new Promise((res) => { 272 | resolve = res; 273 | }); 274 | const transport = mockEliza( 275 | { 276 | sentence: "Response 1", 277 | }, 278 | false, 279 | { 280 | router: { 281 | interceptors: [ 282 | (next) => (req) => { 283 | expect(req.header.get("x-custom-header")).toEqual("custom-value"); 284 | resolve(); 285 | return next(req); 286 | }, 287 | ], 288 | }, 289 | }, 290 | ); 291 | const { result } = renderHook( 292 | () => { 293 | return useSuspenseQuery( 294 | sayMethodDescriptor, 295 | { 296 | sentence: "hello", 297 | }, 298 | { 299 | transport, 300 | headers: { 301 | "x-custom-header": "custom-value", 302 | }, 303 | }, 304 | ); 305 | }, 306 | wrapper({}, mockedElizaTransport), 307 | ); 308 | 309 | await waitFor(() => { 310 | expect(result.current.isSuccess).toBeTruthy(); 311 | }); 312 | 313 | await promise; 314 | 315 | expect(result.current.data.sentence).toBe("Response 1"); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /packages/protoc-gen-connect-query/README.md: -------------------------------------------------------------------------------- 1 | # @connectrpc/protoc-gen-connect-query 2 | 3 | - [@connectrpc/protoc-gen-connect-query](#connectrpcprotoc-gen-connect-query) 4 | - [Installation](#installation) 5 | - [Generating Code](#generating-code) 6 | - [`example.proto`](#exampleproto) 7 | - [`buf.gen.yaml`](#bufgenyaml) 8 | - [With the `buf` CLI](#with-the-buf-cli) 9 | - [With `protoc`](#with-protoc) 10 | - [With Node](#with-node) 11 | - [Generated Output](#generated-output) 12 | - [Plugin options](#plugin-options) 13 | - [`target`](#target) 14 | - [`import_extension`](#import_extension) 15 | - [`keep_empty_files=true`](#keep_empty_filestrue) 16 | - [`js_import_style`](#js_import_style) 17 | - [`ts_nocheck=true`](#ts_nochecktrue) 18 | - [Example Generated Code](#example-generated-code) 19 | 20 | The code generator for Connect-Query, a expansion pack for [TanStack Query](https://tanstack.com/query) (react-query), that enables effortless communication with servers that speak the [Connect Protocol](https://connectrpc.com/docs/protocol). 21 | 22 | Learn more about Connect-Query at [github.com/connectrpc/connect-query-es](https://github.com/connectrpc/connect-query-es). 23 | 24 | ## Installation 25 | 26 | `protoc-gen-connect-query` is a code generator plugin for Protocol Buffer compilers like [buf](https://github.com/bufbuild/buf) and [protoc](https://github.com/protocolbuffers/protobuf/releases). It generates clients from your Protocol Buffer schema, and works in tandem with 27 | [@bufbuild/protoc-gen-es](https://www.npmjs.com/package/@bufbuild/protoc-gen-es), the code generator plugin for all Protocol Buffer base types. The code those two plugins generate requires the runtime libraries [@connectrpc/connect-query](https://www.npmjs.com/package/@connectrpc/connect-query), and [@bufbuild/protobuf](https://www.npmjs.com/package/@bufbuild/protobuf). 28 | 29 | To install the plugins and their runtime libraries, run: 30 | 31 | ```shell 32 | npm install --save-dev @connectrpc/protoc-gen-connect-query @bufbuild/protoc-gen-es 33 | npm install @connectrpc/connect-query @bufbuild/protobuf 34 | ``` 35 | 36 | We use peer dependencies to ensure that code generator and runtime library are compatible with each other. Note that yarn and pnpm only emit a warning in this case. 37 | 38 | ## Generating Code 39 | 40 | ### `example.proto` 41 | 42 | For these examples, consider the following example proto file `example.proto`: 43 | 44 | ```protobuf 45 | syntax = "proto3"; 46 | 47 | package example.v1; 48 | 49 | message Nothing {} 50 | 51 | message Todo { 52 | string id = 1; 53 | string name = 2; 54 | bool completed = 3; 55 | } 56 | 57 | message Todos { 58 | repeated Todo todos = 1; 59 | } 60 | 61 | service TodoService { 62 | rpc GetTodos(Nothing) returns (Todos); 63 | rpc AddTodo(Todo) returns (Nothing); 64 | } 65 | ``` 66 | 67 | This file creates an RPC service with the following: 68 | 69 | - `GetTodos` takes no inputs and returns an array of `Todo`s. 70 | - `AddTodo` adds a new `Todo` and returns nothing. 71 | 72 | ### `buf.gen.yaml` 73 | 74 | Add a new configuration file `buf.gen.yaml` 75 | 76 | ```yaml 77 | version: v2 78 | plugins: 79 | # This will invoke protoc-gen-es and write output to src/gen 80 | - local: protoc-gen-es 81 | out: src/gen 82 | opt: target=ts 83 | # This will invoke protoc-gen-connect-query 84 | - local: protoc-gen-connect-query 85 | out: src/gen 86 | opt: target=ts 87 | ``` 88 | 89 | ### With the `buf` CLI 90 | 91 | To use the [buf CLI](https://docs.buf.build/generate/usage#generate-code-using-local-plugins) to generate code for all protobuf files within your project, simply run: 92 | 93 | ```bash 94 | npx @bufbuild/buf generate 95 | ``` 96 | 97 | > Note that `buf` can generate from various [inputs](https://docs.buf.build/reference/inputs), not just local protobuf files. For example, `npm run generate buf.build/connectrpc/eliza` generates code for the module [connectrpc/eliza](https://buf.build/connectrpc/eliza) on the Buf Schema Registry. 98 | 99 | ### With `protoc` 100 | 101 | ```bash 102 | PATH=$PATH:$(pwd)/node_modules/.bin \ 103 | protoc -I . \ 104 | --es_out src/gen \ 105 | --es_opt target=ts \ 106 | --connect-query_out src/gen \ 107 | --connect-query_opt target=ts \ 108 | example.proto 109 | ``` 110 | 111 | Note that we are adding `node_modules/.bin` to the `$PATH`, so that the protocol buffer compiler can find them. This happens automatically with npm scripts. 112 | 113 | > Note: Since yarn v2 and above does not use a `node_modules` directory, you need to change the variable a bit: 114 | > 115 | > ```bash 116 | > PATH=$(dirname $(yarn bin protoc-gen-es)):$(dirname $(yarn bin protoc-gen-connect-es)):$PATH 117 | > ``` 118 | 119 | ### With Node 120 | 121 | Add a line to the `scripts` section of your `package.json` to run `buf generate`. 122 | 123 | ```json 124 | "scripts": { 125 | ... 126 | "buf:generate": "buf generate" 127 | }, 128 | ``` 129 | 130 | Finally, tell Buf to generate code by running your command: 131 | 132 | ```bash 133 | npm run buf:generate 134 | ``` 135 | 136 | Now you should see your generated code: 137 | 138 | ```tree 139 | . 140 | └── gen/ 141 | ├── example_pb.ts 142 | └── example-TodoService_connectquery.ts 143 | ``` 144 | 145 | ## Generated Output 146 | 147 | Connect-Query will create one output file for every service in every protofile. Say you have the following file structure: 148 | 149 | ```tree 150 | . 151 | └── proto/ 152 | ├── pizza.proto 153 | └── curry.proto 154 | ``` 155 | 156 | Where `pizza.proto` contains `DetroitStyleService` and `ChicagoStyleService`, and where `curry.proto` contains `VindalooService`. Your generated output will look like this: 157 | 158 | ```tree 159 | . 160 | └── gen/ 161 | ├── pizza_pb.ts 162 | ├── pizza-DetroitStyleService_connectquery.ts 163 | ├── pizza-ChicagoStyleService_connectquery.ts 164 | ├── curry_pb.ts 165 | └── curry-VindalooService_connectquery.ts 166 | ``` 167 | 168 | The reason each service gets a separate file is to facilitate intellisense and [language server protocol imports](https://github.com/typescript-language-server/typescript-language-server#organize-imports). Notice that one file per input proto is generated by `protoc-gen-es` (`pizza_pb.ts` and `curry_pb.ts`), and that one file per service is created by `protoc-gen-connect-query` (making up the remainder). The Protobuf-ES generated files (`*_pb.ts`) are important because those files are referenced from the `*_connectquery.ts` files. 169 | 170 | ## Plugin options 171 | 172 | ### `target` 173 | 174 | This option controls whether the plugin generates JavaScript, TypeScript, or TypeScript declaration files. 175 | 176 | Say, for example, you used [`example.proto`](#exampleproto): 177 | 178 | | Target | Generated output | 179 | | ------------ | --------------------------------------- | 180 | | `target=js` | `example-TodoService_connectquery.js` | 181 | | `target=ts` | `example-TodoService_connectquery.ts` | 182 | | `target=dts` | `example-TodoService_connectquery.d.ts` | 183 | 184 | Multiple values can be given by separating them with `+`, for example `target=js+dts`. 185 | 186 | By default, we generate JavaScript and TypeScript declaration files, which produces the smallest code size and is the most compatible with various bundler configurations. If you prefer to generate TypeScript, use `target=ts`. 187 | 188 | ### `import_extension` 189 | 190 | By default, [protoc-gen-connect-query](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) (and all other plugins based on [@bufbuild/protoplugin](https://www.npmjs.com/package/@bufbuild/protoplugin)) doesn't add file extensions to import paths. However, some environments require an import extension. For example, using ECMAScript modules in Node.js requires the `.js` extension, and Deno requires `.ts`. With this plugin option, you can add `.js`/`.ts` extensions in import paths with the given value. Possible values: 191 | 192 | - `import_extension=none`: Doesn't add an extension. (Default) 193 | - `import_extension=js`: Adds the `.js` extension. 194 | - `import_extension=ts`. Adds the `.ts` extension. 195 | 196 | ### `js_import_style` 197 | 198 | By default, [protoc-gen-connect-query](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) 199 | (and all other plugins based on [@bufbuild/protoplugin](https://www.npmjs.com/package/@bufbuild/protoplugin)) 200 | generate ECMAScript `import` and `export` statements. For use cases where 201 | CommonJS is difficult to avoid, this option can be used to generate CommonJS 202 | `require()` calls. 203 | 204 | Possible values: 205 | 206 | - `js_import_style=module` generate ECMAScript `import` / `export` statements - 207 | the default behavior. 208 | - `js_import_style=legacy_commonjs` generate CommonJS `require()` calls. 209 | 210 | ### `keep_empty_files=true` 211 | 212 | This option exists for other plugins but is not applicable to `protoc-gen-connect-query` because, unlike most other plugins, it does not generate a maximum of one output file for every input proto file. Instead, it generates one output file per service. If you provide a valid proto file that contains no services, `protoc-gen-connect-query` will have no output. 213 | 214 | ### `ts_nocheck=true` 215 | 216 | [protoc-gen-connect-query](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) generates valid TypeScript for current versions of the TypeScript compiler with standard settings. If you use compiler settings that yield an error for generated code, setting this option generates an annotation at the top of each file to skip type checks: `// @ts-nocheck`. 217 | 218 | ## Example Generated Code 219 | 220 | See [`eliza.proto`](../examples/react/basic/eliza.proto) for example inputs, and look [here](../examples/react/basic/src/gen) to see the outputs those files generate. 221 | --------------------------------------------------------------------------------