Repository: connectrpc/connect-query-es Branch: main Commit: ef9eb4ec7611 Files: 111 Total size: 353.8 KB Directory structure: gitextract_bg1etjgz/ ├── .eslintrc.cjs ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── RELEASING.md │ ├── dependabot.yaml │ ├── release.yaml │ └── workflows/ │ ├── add-to-project.yaml │ ├── ci.yaml │ ├── pr-title.yaml │ ├── prepare-release.yml │ └── publish-release.yml ├── .gitignore ├── .nvmrc ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── SECURITY.md ├── assets/ │ ├── connect-query.ai │ └── connect-query_dependency_graph.excalidraw ├── cspell.config.json ├── package.json ├── packages/ │ ├── connect-query/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── call-unary-method.test.ts │ │ │ ├── index.ts │ │ │ ├── test/ │ │ │ │ └── test-wrapper.tsx │ │ │ ├── use-infinite-query.test.ts │ │ │ ├── use-infinite-query.ts │ │ │ ├── use-mutation.test.ts │ │ │ ├── use-mutation.ts │ │ │ ├── use-query.test.ts │ │ │ ├── use-query.ts │ │ │ ├── use-transport.test.tsx │ │ │ └── use-transport.tsx │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── connect-query-core/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── call-unary-method.ts │ │ │ ├── connect-query-key.test.ts │ │ │ ├── connect-query-key.ts │ │ │ ├── create-infinite-query-options.test.ts │ │ │ ├── create-infinite-query-options.ts │ │ │ ├── create-query-options.test.ts │ │ │ ├── create-query-options.ts │ │ │ ├── index.ts │ │ │ ├── message-key.test.ts │ │ │ ├── message-key.ts │ │ │ ├── page-param-key.ts │ │ │ ├── structural-sharing.test.ts │ │ │ ├── structural-sharing.ts │ │ │ ├── transport-key.test.ts │ │ │ ├── transport-key.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── tsconfig.build.json │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── examples/ │ │ └── react/ │ │ └── basic/ │ │ ├── .gitignore │ │ ├── buf.gen.yaml │ │ ├── eliza.proto │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── css.ts │ │ │ ├── datum.tsx │ │ │ ├── example.tsx │ │ │ ├── gen/ │ │ │ │ ├── eliza-ElizaService_connectquery.ts │ │ │ │ └── eliza_pb.ts │ │ │ ├── index.css │ │ │ ├── indicator.tsx │ │ │ ├── main.test.tsx │ │ │ ├── main.tsx │ │ │ ├── page.tsx │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── protoc-gen-connect-query/ │ │ ├── .eslintignore │ │ ├── .gitignore │ │ ├── README.md │ │ ├── bin/ │ │ │ └── protoc-gen-connect-query │ │ ├── package.json │ │ ├── src/ │ │ │ ├── generateDts.ts │ │ │ ├── generateTs.ts │ │ │ ├── protoc-gen-connect-query-plugin.ts │ │ │ └── utils.ts │ │ └── tsconfig.json │ └── test-utils/ │ ├── buf.gen.yaml │ ├── package.json │ ├── proto/ │ │ ├── bigint.proto │ │ ├── eliza.proto │ │ ├── list.proto │ │ ├── proto2.proto │ │ └── proto3.proto │ ├── src/ │ │ ├── gen/ │ │ │ ├── bigint_pb.ts │ │ │ ├── eliza_pb.ts │ │ │ ├── list_pb.ts │ │ │ ├── proto2_pb.ts │ │ │ └── proto3_pb.ts │ │ └── index.tsx │ └── tsconfig.json ├── scripts/ │ ├── find-workspace-version.js │ ├── gh-diffcheck.js │ ├── release.js │ ├── set-workspace-version.js │ └── utils.js ├── tsconfig.base.json └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.cjs ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. module.exports = { env: { browser: true, es2021: true, node: true, }, root: true, ignorePatterns: ["packages/*/dist/**"], plugins: ["@typescript-eslint", "n", "import", "vitest"], // Rules and settings that do not require a non-default parser extends: ["eslint:recommended"], rules: { "no-console": "error", "import/no-cycle": "error", "import/no-duplicates": "error", }, overrides: [ { files: ["**/*.{ts,tsx,cts,mts}"], parser: "@typescript-eslint/parser", parserOptions: { project: true, }, settings: { "import/resolver": { typescript: { project: "tsconfig.json", }, }, }, extends: [ "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:import/recommended", "plugin:import/typescript", ], rules: { "@typescript-eslint/strict-boolean-expressions": "error", "@typescript-eslint/no-unnecessary-condition": "error", "@typescript-eslint/array-type": "off", // we use complex typings, where Array is actually more readable than T[] "@typescript-eslint/switch-exhaustiveness-check": [ "error", { considerDefaultExhaustiveForUnions: true, }, ], "@typescript-eslint/prefer-nullish-coalescing": "error", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", "@typescript-eslint/no-invalid-void-type": "error", "@typescript-eslint/no-base-to-string": "error", "import/no-cycle": "error", "import/no-duplicates": "error", }, }, // For scripts and configurations, use Node.js rules { files: ["**/*.{js,mjs,cjs}"], parserOptions: { ecmaVersion: 13, // ES2022 - https://eslint.org/docs/latest/use/configure/language-options#specifying-environments }, extends: ["eslint:recommended", "plugin:n/recommended"], rules: { "n/hashbang": "off", // this rule reports _any_ hashbang outside of an npm binary as an error "n/prefer-global/process": "off", "n/no-process-exit": "off", "n/exports-style": ["error", "module.exports"], "n/file-extension-in-import": ["error", "always"], "n/prefer-global/buffer": ["error", "always"], "n/prefer-global/console": ["error", "always"], "n/prefer-global/url-search-params": ["error", "always"], "n/prefer-global/url": ["error", "always"], "n/prefer-promises/dns": "error", "n/prefer-promises/fs": "error", "n/no-unsupported-features/node-builtins": "error", "n/no-unsupported-features/es-syntax": "error", }, }, ], }; ================================================ FILE: .gitattributes ================================================ # This is similar to the git option core.autocrlf but it applies to all # users of the repository and therefore doesn't depend on a developers # local configuration. * text=auto # Ignore generated files in GitHub diffs by default **/*_pb.ts linguist-generated=true **/*_connect.ts linguist-generated=true **/*_pb.js linguist-generated=true **/*_pb.d.ts linguist-generated=true ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ ## Community Code of Conduct Connect follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing We'd love your help making `connect-query-es` better! If you'd like to add new exported APIs, please [open an issue][open-issue] describing your proposal — discussing API changes ahead of time makes pull request review much smoother. In your issue, pull request, and any other communications, please remember to treat your fellow contributors with respect! Note that for a contribution to be accepted, you must sign off on all commits in order to affirm that they comply with the [Developer Certificate of Origin][dco]. Make sure to configure `git` with the same name and E-Mail as your GitHub account, and run `git commit` with the `-s` flag to sign. If necessary, a bot will remind you to sign your commits when you open your pull request, and provide helpful tips. ## Setup [Fork][fork], then clone the repository: ``` git clone git@github.com:your_github_username/connect-query-es.git cd connect-query-es git remote add upstream https://github.com/connectrpc/connect-query-es.git git fetch upstream ``` Install dependencies (you'll need Node.js in the version specified in `.nvmrc`, and `npm` in the version specified in `package.json`): ```bash npm ci ``` Make sure that the tests, linters, and other checks pass: ```bash npm run all ``` We're using `turborepo` to run tasks. If you haven't used it yet, take a look at [filtering and package scoping](https://turbo.build/repo/docs/crafting-your-repository/running-tasks). ## Making Changes Start by creating a new branch for your changes: ``` git checkout main git fetch upstream git rebase upstream/main git checkout -b cool_new_feature ``` Make your changes, then ensure that `npm run all` still passes. When you're satisfied with your changes, push them to your fork. ``` git commit -a git push origin cool_new_feature ``` Then use the GitHub UI to open a pull request. At this point, you're waiting on us to review your changes. We _try_ to respond to issues and pull requests within a few business days, and we may suggest some improvements or alternatives. Once your changes are approved, one of the project maintainers will merge them. We're much more likely to approve your changes if you: - Add tests for new functionality. - Write a [good commit message][commit-message]. - Maintain backward compatibility. [fork]: https://github.com/connectrpc/connect-query-es/fork [open-issue]: https://github.com/connectrpc/connect-query-es/issues/new [dco]: https://developercertificate.org [commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html ================================================ FILE: .github/RELEASING.md ================================================ # Releasing ## Prerequisites - See the setup and tools required in CONTRIBUTING.md - A granular access token for npmjs.com with read and write permissions, scoped to the `connectrpc` organization. - Make sure that the repository is in a good state, without PRs close to merge that would ideally be part of the release. ## Steps 1. Choose a new version (e.g. 1.2.3), making sure to follow semver. Note that all packages in this repository use the same version number. 2. Trigger the prepare-release workflow that will create a release PR. - 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. 3. Edit the PR description with release notes. See the section below for details. 4. Make sure CI passed on your PR and ask a maintainer for review. 5. After approval, merge your PR. ## Release notes - We generate release notes with the GitHub feature, see https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes - Only changes that impact users should be listed. No need to list things like doc changes (unless it’s something major), dependency version bumps, or similar. Remove them from the generated release notes. - If the release introduces a major new feature or change, add a section at the top that explains it for users. A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.10.0 It lists a major new feature and a major change with dedicated sections, and moves the changelist with PR links to a separate "Enhancement" section below. - If the release includes a very long list of changes, consider breaking the changelist up with the sections "Enhancements", "Bugfixes", "Breaking changes". A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.9.0 - If the release includes changes specific to a npm package, group and explain the changelist in according separate sections. A good example is https://github.com/connectrpc/connect-es/releases/tag/v0.8.0 Note that we are not using full package names with scope - a more user-friendly name like "Connect for Node.js" or "Connect for Fastify" is preferable. ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" day: "monday" timezone: UTC time: "07:00" - package-ecosystem: "npm" directory: "/" schedule: interval: "monthly" day: "monday" timezone: UTC time: "07:00" open-pull-requests-limit: 50 groups: connectRelated: patterns: - "@connectrpc/*" - "@bufbuild/*" devDependencies: patterns: - "@arethetypeswrong/*" - "@testing-library/*" - "@types/*" - "@typescript-eslint/*" - "@vitejs/*" - "cspell" - "eslint*" - "jest-mock" - "jest" - "prettier" - "react-dom" - "react" - "ts-jest" - "ts-node" - "turbo" - "typescript" - "vite" - "vitest" - "@vitest/*" reactQuery: patterns: - "@tanstack/react-query" - "@tanstack/react-query-devtools" - "@tanstack/query-core" ================================================ FILE: .github/release.yaml ================================================ changelog: exclude: labels: - ignore-for-release authors: - dependabot[bot] ================================================ FILE: .github/workflows/add-to-project.yaml ================================================ name: Add issues and PRs to project on: issues: types: - opened - reopened - transferred pull_request_target: types: - opened - reopened issue_comment: types: - created jobs: call-workflow-add-to-project: name: Call workflow to add issue to project uses: connectrpc/base-workflows/.github/workflows/add-to-project.yaml@main secrets: inherit ================================================ FILE: .github/workflows/ci.yaml ================================================ name: ci on: push: branches: [main, "v*"] tags: ["v*"] pull_request: branches: [main, "v*"] workflow_dispatch: permissions: contents: read env: # https://consoledonottrack.com/ DO_NOT_TRACK: 1 jobs: tasks: runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: task: - format - license-header - lint - attw - build include: - task: format diff-check: true - task: license-header diff-check: true name: ${{ matrix.task }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version-file: .nvmrc cache: "npm" - uses: actions/cache@v5 with: path: .turbo key: ${{ runner.os }}/${{ matrix.task }}/${{ github.sha }} restore-keys: ${{ runner.os }}/${{ matrix.task }} - run: npm ci - run: npx turbo run ${{ matrix.task }} - name: Check changed files if: ${{ matrix.diff-check }} run: node scripts/gh-diffcheck.js test: runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: node-version: [24.x, 22.x, 20.x] name: "test on Node.js ${{ matrix.node-version }}" steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} cache: "npm" - uses: actions/cache@v5 with: path: .turbo key: ${{ runner.os }}/test/${{ github.sha }} restore-keys: ${{ runner.os }}/test - run: npm ci - run: npx turbo run test ================================================ FILE: .github/workflows/pr-title.yaml ================================================ name: Lint PR Title # Prevent writing to the repository using the CI token. # Ref: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#permissions permissions: pull-requests: read on: pull_request: # By default, a workflow only runs when a pull_request's activity type is opened, # synchronize, or reopened. We explicity override here so that PR titles are # re-linted when the PR text content is edited. types: - opened - edited - reopened - synchronize jobs: lint: uses: bufbuild/base-workflows/.github/workflows/pr-title.yaml@main ================================================ FILE: .github/workflows/prepare-release.yml ================================================ name: Prepare Release on: workflow_dispatch: inputs: version: description: "Version to release (e.g. 1.2.3)" required: true type: string jobs: prepare-release: runs-on: ubuntu-latest permissions: contents: write pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v6 with: ref: main fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" - name: Install dependencies run: npm ci - name: Create release branch run: | git config --global user.name 'github-actions[bot]' git config --global user.email 'github-actions[bot]@users.noreply.github.com' git checkout -b "release/prep-release-${{ inputs.version }}" - name: Get current workspace version id: workspace_version run: | VERSION=$(npm run getversion --silent) echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Set version and run build run: | npm run setversion ${{ inputs.version }} - name: Commit version changes run: | git add . git commit -s -m "Release ${{ inputs.version }}" git push --set-upstream origin "release/prep-release-${{ inputs.version }}" - name: Get release notes id: release_notes run: | RELEASE_NOTES=$( gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ /repos/${{ github.repository }}/releases/generate-notes \ -f 'tag_name=v${{ inputs.version }}' -f 'target_commitish=${{ inputs.base_branch }}' -f 'previous_tag_name=v${{ steps.workspace_version.outputs.version }}' \ --jq ".body" \ ) echo "notes<> $GITHUB_OUTPUT echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create pull request run: | gh pr create \ --title "Release ${{ inputs.version }}" \ --body "${{ steps.release_notes.outputs.notes }}" \ --base "${{ inputs.base_branch }}" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/publish-release.yml ================================================ name: Publish Release on: pull_request: types: [closed] branches: - main jobs: publish-release: runs-on: ubuntu-latest # Only run if PR was merged and branch name starts with release/prep-release- if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/prep-release-') permissions: id-token: write # Required for OIDC contents: write pull-requests: write issues: write steps: - name: Checkout base branch uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.base.ref }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" - name: Install dependencies run: npm ci - name: Get current workspace version id: workspace_version run: | VERSION=$(npm run getversion --silent) echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Get updated release notes from PR id: pr_notes run: | RELEASE_NOTES=$(gh pr view ${{ github.event.pull_request.number }} --json body | jq -r ".body") echo "notes<> $GITHUB_OUTPUT echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Publish to npm run: npm run release - name: Publish GitHub release run: | gh release create v${{ steps.workspace_version.outputs.version }} \ --title "Release v${{ steps.workspace_version.outputs.version }}" \ --notes "${{ steps.pr_notes.outputs.notes }}" # --discussion-category "Announcements" ## Enable if discussions are enabled env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ .turbo .wrangler node_modules /packages/*/dist /packages/*/coverage tsconfig.vitest-temp.json ================================================ FILE: .nvmrc ================================================ v24.5.0 ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["orta.vscode-twoslash-queries"] } ================================================ FILE: .vscode/settings.json ================================================ { "git.enableCommitSigning": true, "git.alwaysSignOff": true, "typescript.tsdk": "node_modules/typescript/lib" } ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2021-2023 The Connect Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: MAINTAINERS.md ================================================ # Maintainers ## Current - [Timo Stamm](https://github.com/timostamm), [Buf](https://buf.build) - [Steve Ayers](https://github.com/smaye81), [Buf](https://buf.build) - [Paul Sachs](https://github.com/paul-sachs), [Buf](https://buf.build) ## Former - [Dimitri Mitropoulos](https://github.com/dimitropoulos) ================================================ FILE: README.md ================================================ # Connect-Query [![License](https://img.shields.io/github/license/connectrpc/connect-query-es?color=blue)](./LICENSE) [![Build](https://github.com/connectrpc/connect-query-es/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/connect-query-es/actions/workflows/ci.yaml) [![NPM Version](https://img.shields.io/npm/v/@connectrpc/connect-query/latest?color=green&label=%40connectrpc%2Fconnect-query)](https://www.npmjs.com/package/@connectrpc/connect-query) [![NPM Version](https://img.shields.io/npm/v/@connectrpc/protoc-gen-connect-query/latest?color=green&label=%40connectrpc%2Fprotoc-gen-connect-query)](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) Connect-Query is an 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). - [Quickstart](#quickstart) - [Install](#install) - [Usage](#usage) - [Generated Code](#generated-code) - [Connect-Query API](#connect-query-api) - [`TransportProvider`](#transportprovider) - [`useTransport`](#usetransport) - [`useQuery`](#usequery) - [`useSuspenseQuery`](#usesuspensequery) - [`useInfiniteQuery`](#useinfinitequery) - [`useSuspenseInfiniteQuery`](#usesuspenseinfinitequery) - [`useMutation`](#usemutation) - [`createConnectQueryKey`](#createconnectquerykey) - [`callUnaryMethod`](#callunarymethod) - [`createProtobufSafeUpdater`](#createprotobufsafeupdater) - [`createQueryOptions`](#createqueryoptions) - [`createInfiniteQueryOptions`](#createinfinitequeryoptions) - [`addStaticKeyToTransport`](#addstatickeytotransport) - [`ConnectQueryKey`](#connectquerykey) - [`QueryOptions`](#queryoptions) - [`QueryOptionsWithSkipToken`](#queryoptionswithskiptoken) - [`InfiniteQueryOptions`](#infinitequeryoptions) - [`InfiniteQueryOptionsWithSkipToken`](#infinitequeryoptionswithskiptoken) ## Quickstart ### Install ```sh npm install @connectrpc/connect-query @connectrpc/connect-web ``` > [!TIP] > > If you are using something that doesn't automatically install peerDependencies (npm older than v7), you'll want to make sure you also have `@bufbuild/protobuf`, `@connectrpc/connect`, and `@tanstack/react-query` installed. `@connectrpc/connect-web` is required for defining > the transport to be used by the client. ### Usage Connect-Query will immediately feel familiar to you if you've used TanStack Query. It provides a similar API, but instead takes a definition for your endpoint and returns a typesafe API for that endpoint. First, make sure you've configured your provider and query client: ```tsx import { createConnectTransport } from "@connectrpc/connect-web"; import { TransportProvider } from "@connectrpc/connect-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const finalTransport = createConnectTransport({ baseUrl: "https://demo.connectrpc.com", }); const queryClient = new QueryClient(); function App() { return ( ); } ``` With configuration completed, you can now use the `useQuery` hook to make a request: ```ts import { useQuery } from '@connectrpc/connect-query'; import { say } from 'your-generated-code/eliza-ElizaService_connectquery'; export const Example: FC = () => { const { data } = useQuery(say, { sentence: "Hello" }); return
{data}
; }; ``` **_That's it!_** The code generator does all the work of turning your Protobuf file into something you can easily import. TypeScript types all populate out-of-the-box. Your documentation is also converted to [TSDoc](https://tsdoc.org/). One of the best features of this library is that once you write your schema in Protobuf form, the TypeScript types are generated and then inferred. You never again need to specify the types of your data since the library does it automatically. ### Generated Code To make a query, you need a schema for a remote procedure call (RPC). A typed schema can be generated with [`protoc-gen-es`](https://www.npmjs.com/package/@bufbuild/protoc-gen-es). It generates an export for every service: ```ts /** * @generated from service connectrpc.eliza.v1.ElizaService */ export declare const ElizaService: GenService<{ /** * Say is a unary RPC. Eliza responds to the prompt with a single sentence. * * @generated from rpc connectrpc.eliza.v1.ElizaService.Say */ say: { methodKind: "unary"; input: typeof SayRequestSchema; output: typeof SayResponseSchema; }; }>; ``` [`protoc-gen-connect-query`](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) is an optional additional plugin that exports every RPC individually for convenience: ```ts import { ElizaService } from "./eliza_pb"; /** * Say is a unary RPC. Eliza responds to the prompt with a single sentence. * * @generated from rpc connectrpc.eliza.v1.ElizaService.Say */ export const say: (typeof ElizaService)["method"]["say"]; ``` For more information on code generation, see the [documentation for `protoc-gen-connect-query`](https://www.npmjs.com/package/@connectrpc/protoc-gen-connect-query) and the [documentation for `protoc-gen-es`](https://www.npmjs.com/package/@bufbuild/protoc-gen-es). ## Connect-Query API ### `TransportProvider` ```ts const TransportProvider: FC< PropsWithChildren<{ transport: Transport; }> >; ``` `TransportProvider` is the main mechanism by which Connect-Query keeps track of the `Transport` used by your application. Broadly speaking, "transport" joins two concepts: 1. The protocol of communication. For this there are two options: the [Connect Protocol](https://connectrpc.com/docs/protocol/), or the [gRPC-Web Protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md). 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, wire serialization options, or protocol-specific options like Connect's support for [HTTP GET](https://connectrpc.com/docs/web/get-requests-and-caching). With these two pieces of information in hand, the transport provides the critical mechanism by which your app can make network requests. To learn more about the two modes of transport, take a look at the Connect-Web documentation on [choosing a protocol](https://connectrpc.com/docs/web/choosing-a-protocol/). To get started with Connect-Query, simply import a transport (either [`createConnectTransport`](https://github.com/connectrpc/connect-es/blob/main/packages/connect-web/src/connect-transport.ts) or [`createGrpcWebTransport`](https://github.com/connectrpc/connect-es/blob/main/packages/connect-web/src/grpc-web-transport.ts) from [`@connectrpc/connect-web`](https://www.npmjs.com/package/@connectrpc/connect-web)) and pass it to the provider. A common use case for the transport is to add headers to requests (like auth tokens, etc). You can do this with a custom [interceptor](https://connectrpc.com/docs/web/interceptors). ```tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { TransportProvider } from "@connectrpc/connect-query"; const queryClient = new QueryClient(); export const App = () => { const transport = createConnectTransport({ baseUrl: "", interceptors: [ (next) => (request) => { request.header.append("some-new-header", "some-value"); // Add your headers here return next(request); }, ], }); return ( ); }; ``` For more details about what you can do with the transport, see the [Connect-Web documentation](https://connectrpc.com/docs/web/). ### `useTransport` ```ts const useTransport: () => Transport; ``` Use this helper to get the default transport that's currently attached to the React context for the calling component. > [!TIP] > > All hooks accept a `transport` in the options. You can use the Transport from the context, or create one dynamically. If you create a Transport dynamically, make sure to memoize it, because it is taken into consideration when building query keys. ### `useQuery` ```ts function useQuery< I extends DescMessage, O extends DescMessage, SelectOutData = MessageShape, >( schema: DescMethodUnary, input?: SkipToken | MessageInitShape, { transport, ...queryOptions }: UseQueryOptions = {}, ): UseQueryResult; ``` The `useQuery` hook is the primary way to make a unary request. It's a wrapper around TanStack Query's [`useQuery`](https://tanstack.com/query/v5/docs/react/reference/useQuery) hook, but it's preconfigured with the correct `queryKey` and `queryFn` for the given method. Any additional `options` you pass to `useQuery` will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports. ### `useSuspenseQuery` Identical to useQuery but mapping to the `useSuspenseQuery` hook from [TanStack Query](https://tanstack.com/query/v5/docs/react/reference/useSuspenseQuery). This includes the benefits of narrowing the resulting data type (data will never be undefined). ### `useInfiniteQuery` ```ts function useInfiniteQuery< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: SkipToken | MessageInitWithPageParam, ParamKey>, { transport, pageParamKey, getNextPageParam, ...queryOptions }: UseInfiniteQueryOptions, ): UseInfiniteQueryResult>, ConnectError>; ``` The `useInfiniteQuery` is a wrapper around TanStack Query's [`useInfiniteQuery`](https://tanstack.com/query/v5/docs/react/reference/useInfiniteQuery) hook, but it's preconfigured with the correct `queryKey` and `queryFn` for the given method. There are some required options for `useInfiniteQuery`, primarily `pageParamKey` and `getNextPageParam`. These are required because Connect-Query doesn't know how to paginate your data. You must provide a mapping from the output of the previous page and getting the next page. `pageParamKey` supports root keys as strings (`"page"`) and nested keys as dot-separated strings (`"query.page"`). All other options passed to `useInfiniteQuery` will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports. ### `useSuspenseInfiniteQuery` Identical to useInfiniteQuery but mapping to the `useSuspenseInfiniteQuery` hook from [TanStack Query](https://tanstack.com/query/v5/docs/react/reference/useSuspenseInfiniteQuery). This includes the benefits of narrowing the resulting data type (data will never be undefined). ### `useMutation` ```ts function useMutation( schema: DescMethodUnary, { transport, ...queryOptions }: UseMutationOptions = {}, ): UseMutationResult, ConnectError, PartialMessage>; ``` The `useMutation` is a wrapper around TanStack Query's [`useMutation`](https://tanstack.com/query/v5/docs/react/reference/useMutation) hook, but it's preconfigured with the correct `mutationFn` for the given method. Any additional `options` you pass to `useMutation` will be merged with the options that Connect-Query provides to @tanstack/react-query. This means that you can pass any additional options that TanStack Query supports. ### `createConnectQueryKey` This function is used under the hood of `useQuery` and other hooks to compute a [`queryKey`](https://tanstack.com/query/v4/docs/react/guides/query-keys) for TanStack Query. You can use it to create keys yourself to filter queries. `useQuery` creates a query key with the following parameters: 1. The qualified name of the RPC. 2. The transport being used. 3. The request message. 4. The cardinality of the RPC (either "finite" or "infinite"). 5. Adds a DataTag which brands the key with the associated data type of the response. The DataTag type allows @tanstack/react-query functions to properly infer the type of the data returned by the query. This is useful for things like `QueryClient.setQueryData` and `QueryClient.getQueryData`. To create the same key manually, you simply provide the same parameters: ```ts import { createConnectQueryKey, useTransport } from "@connectrpc/connect-query"; import { ElizaService } from "./gen/eliza_pb"; const myTransport = useTransport(); const queryKey = createConnectQueryKey({ schema: ElizaService.method.say, transport: myTransport, // You can provide a partial message here. input: { sentence: "hello" }, // This defines what kind of request it is (either for an infinite or finite query). cardinality: "finite", }); // queryKey: [ "connect-query", { transport: "t1", serviceName: "connectrpc.eliza.v1.ElizaService", methodName: "Say", input: { sentence: "hello" }, cardinality: "finite", }, ]; ``` You can create a partial key that matches all RPCs of a service: ```ts import { createConnectQueryKey } from "@connectrpc/connect-query"; import { ElizaService } from "./gen/eliza_pb"; const queryKey = createConnectQueryKey({ schema: ElizaService, cardinality: "finite", }); // queryKey: [ "connect-query", { serviceName: "connectrpc.eliza.v1.ElizaService", cardinality: "finite", }, ]; ``` Infinite queries have distinct keys. To create a key for an infinite query, use the parameter `cardinality`: ```ts import { createConnectQueryKey } from "@connectrpc/connect-query"; import { ListService } from "./gen/list_pb"; // The hook useInfiniteQuery() creates a query key with cardinality: "infinite", // and passes on the pageParamKey. const queryKey = createConnectQueryKey({ schema: ListService.method.list, cardinality: "infinite", pageParamKey: "page", input: { preview: true }, }); ``` ### `callUnaryMethod` ```ts function callUnaryMethod( transport: Transport, schema: DescMethodUnary, input: MessageInitShape | undefined, options?: { signal?: AbortSignal; }, ): Promise; ``` This API allows you to directly call the method using the provided transport. Use this if you need to manually call a method outside of the context of a React component, or need to call it where you can't use hooks. ### `createProtobufSafeUpdater` (deprecated) Creates a typesafe updater that can be used to update data in a query cache. Used in combination with a queryClient. ```ts import { createProtobufSafeUpdater, useTransport } from '@connectrpc/connect-query'; import { useQueryClient } from "@tanstack/react-query"; ... const queryClient = useQueryClient(); const transport = useTransport(); queryClient.setQueryData( createConnectQueryKey({ schema: example, transport, input: {}, cardinality: "finite", }), createProtobufSafeUpdater(example, (prev) => { if (prev === undefined) { return undefined; } return { ...prev, completed: true, }; }) ); ``` ** Note: This API is deprecated and will be removed in a future version. `ConnectQueryKey` now contains type information to make it safer to use `setQueryData` directly. ** ### `createQueryOptions` ```ts function createQueryOptions( schema: DescMethodUnary, input: SkipToken | PartialMessage | undefined, { transport, }: { transport: Transport; }, ): { queryKey: ConnectQueryKey; queryFn: QueryFunction, ConnectQueryKey> | SkipToken; structuralSharing: (oldData: unknown, newData: unknown) => unknown; }; ``` A functional version of the options that can be passed to the `useQuery` hook from `@tanstack/react-query`. When called, it will return the appropriate `queryKey`, `queryFn`, and `structuralSharing` flag. This is useful when interacting with `useQueries` API or queryClient methods (like [ensureQueryData](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientensurequerydata), etc). An example of how to use this function with `useQueries`: ```ts import { useQueries } from "@tanstack/react-query"; import { createQueryOptions, useTransport } from "@connectrpc/connect-query"; import { example } from "your-generated-code/example-ExampleService_connectquery"; const MyComponent = () => { const transport = useTransport(); const [query1, query2] = useQueries([ createQueryOptions(example, { sentence: "First query" }, { transport }), createQueryOptions(example, { sentence: "Second query" }, { transport }), ]); ... }; ``` ### `createInfiniteQueryOptions` ```ts function createInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: SkipToken | MessageInitWithPageParam, ParamKey>, { transport, getNextPageParam, pageParamKey, }: ConnectInfiniteQueryOptions, ): { getNextPageParam: ConnectInfiniteQueryOptions< I, O, ParamKey >["getNextPageParam"]; queryKey: ConnectInfiniteQueryKey; queryFn: | QueryFunction< MessageShape, ConnectInfiniteQueryKey, MessageInitShape[ParamKey] > | SkipToken; structuralSharing: (oldData: unknown, newData: unknown) => unknown; initialPageParam: PartialMessage[ParamKey]; }; ``` A functional version of the options that can be passed to the `useInfiniteQuery` hook from `@tanstack/react-query`.When called, it will return the appropriate `queryKey`, `queryFn`, and `structuralSharing` flags, as well as a few other parameters required for `useInfiniteQuery`. This is useful when interacting with some queryClient methods (like [ensureQueryData](https://tanstack.com/query/latest/docs/reference/QueryClient#queryclientensurequerydata), etc). ### `addStaticKeyToTransport` Transports are taken into consideration when building query keys for associated queries. This can cause issues with SSR since the transport on the server is not the same transport that gets executed on the client (cannot be tracked by reference). To bypass this, you can use this method to add an explicit key to the transport that will be used in the query key. For example: ```ts import { addStaticKeyToTransport } from "@connectrpc/connect-query"; import { createConnectTransport } from "@connectrpc/connect-web"; const transport = addStaticKeyToTransport( createConnectTransport({ baseUrl: "https://demo.connectrpc.com", }), "demo", ); ``` ### `ConnectQueryKey` ```ts type ConnectQueryKey = [ /** * To distinguish Connect query keys from other query keys, they always start with the string "connect-query". */ "connect-query", { /** * A key for a Transport reference, created with createTransportKey(). */ transport?: string; /** * The name of the service, e.g. connectrpc.eliza.v1.ElizaService */ serviceName: string; /** * The name of the method, e.g. Say. */ methodName?: string; /** * A key for the request message, created with createMessageKey(), * or "skipped". */ input?: Record | "skipped"; /** * Whether this is an infinite query, or a regular one. */ cardinality?: "infinite" | "finite"; }, ]; ``` TanStack Query manages query caching for you based on query keys. [`QueryKey`s](https://tanstack.com/query/v4/docs/react/guides/query-keys) in TanStack Query are arrays with arbitrary JSON-serializable data - typically handwritten for each endpoint. In Connect-Query, query keys are more structured, since queries are always tied to a service, RPC, input message, and transport. For example, a query key might look like this: ```ts [ "connect-query", { transport: "t1", serviceName: "connectrpc.eliza.v1.ElizaService", methodName: "Say", input: { sentence: "hello there", }, cardinality: "finite", }, ]; ``` The factory [`createConnectQueryKey`](#createconnectquerykey) makes it easy to create a `ConnectQueryKey`, including partial keys for query filters. ### `QueryOptions` Return type of `createQueryOptions` assuming SkipToken was not provided. ```ts interface QueryOptions { queryKey: ConnectQueryKey; queryFn: QueryFunction, ConnectQueryKey, MessageShape>; structuralSharing: (oldData: unknown, newData: unknown) => unknown; } ``` ### `QueryOptionsWithSkipToken` Return type of `createQueryOptions` when SkipToken is provided. ```ts interface QueryOptionsWithSkipToken< I extends DescMessage, O extends DescMessage, > extends Omit, "queryFn"> { queryFn: SkipToken; } ``` ### `InfiniteQueryOptions` Return type of `createInfiniteQueryOptions` assuming SkipToken was not provided. ```ts interface InfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends keyof MessageInitShape, > { getNextPageParam: ConnectInfiniteQueryOptions< I, O, ParamKey >["getNextPageParam"]; queryKey: ConnectQueryKey; queryFn: QueryFunction< MessageShape, ConnectQueryKey, MessageInitShape[ParamKey] >; structuralSharing: (oldData: unknown, newData: unknown) => unknown; initialPageParam: MessageInitShape[ParamKey]; } ``` ### `InfiniteQueryOptionsWithSkipToken` Return type of `createInfiniteQueryOptions` when SkipToken is provided. ```ts interface InfiniteQueryOptionsWithSkipToken< I extends DescMessage, O extends DescMessage, ParamKey extends keyof MessageInitShape, > extends Omit, "queryFn"> { queryFn: SkipToken; } ``` ## Testing Connect-query (along with all other javascript based connect packages) can be tested with the `createRouterTransport` function from `@connectrpc/connect`. This function allows you to create a transport that can be used to test your application without needing to make any network requests. We also have a dedicated package, [@connectrpc/connect-playwright](https://github.com/connectrpc/connect-playwright-es) for testing within [playwright](https://playwright.dev/). For playwright, you can see a sample test [here](https://github.com/connectrpc/connect-playwright-es/blob/main/packages/connect-playwright-example/tests/simple.spec.ts). ## Frequently Asked Questions ### How do I pass other TanStack Query options? Each function that interacts with TanStack Query also provides for options that can be passed through. ```ts import { useQuery } from '@connectrpc/connect-query'; import { example } from 'your-generated-code/example-ExampleService_connectquery'; export const Example: FC = () => { const { data } = useQuery(example, undefined, { // These are typesafe options that are passed to underlying TanStack Query. refetchInterval: 1000, }); return
{data}
; }; ``` ### What is Connect-Query's relationship to Connect-Web and Protobuf-ES? Here is a high-level overview of how Connect-Query fits in with Connect-Web and Protobuf-ES:
Expand to see a detailed dependency graph connect-query_dependency_graph
Your Protobuf files serve as the primary input to the code generators `protoc-gen-connect-query` and `protoc-gen-es`. Both of these code generators also rely on primitives provided by Protobuf-ES. The Buf CLI produces the generated output. The final generated code uses `Transport` from Connect-Web and generates a final Connect-Query API. ### What is `Transport` `Transport` is a regular JavaScript object with two methods, `unary` and `stream`. See the definition in the Connect-Web codebase [here](https://github.com/connectrpc/connect-es/blob/main/packages/connect/src/transport.ts). `Transport` defines the mechanism by which the browser can call a gRPC-web or Connect backend. Read more about Transport on the [connect docs](https://connectrpc.com/docs/web/choosing-a-protocol). ### What if I already use Connect-Web? You can use Connect-Web and Connect-Query together if you like! ### What if I use gRPC-web? Connect-Query also supports gRPC-web! All you need to do is make sure you call `createGrpcWebTransport` instead of `createConnectTransport`. That said, we encourage you to check out the [Connect protocol](https://connectrpc.com/docs/protocol/), a simple, POST-only protocol that works over HTTP/1.1 or HTTP/2. It supports server-streaming methods just like gRPC-Web, but is easy to debug in the network inspector. ### What if I have a custom `Transport`? If the `Transport` attached to React Context via the `TransportProvider` isn't working for you, then you can override transport at every level. For example, you can pass a custom transport directly to the lowest-level API like `useQuery` or `callUnaryMethod`. ### Does this only work with React? Connect-Query does require React, but the core (`createConnectQueryKey` and `callUnaryMethod`) is not React specific so splitting off a `connect-solid-query` is possible. ### How do I do Prefetching? When you might not have access to React context, you can use `createQueryOptions` and provide a transport directly. For example: ```ts import { say } from "./gen/eliza-ElizaService_connectquery"; function prefetch() { return queryClient.prefetchQuery( createQueryOptions(say, { sentence: "Hello" }, { transport: myTransport }), ); } ``` > [!TIP] > > Transports are taken into consideration when building query keys. If you want to prefetch queries on the server, and hydrate them in the client, make sure to use the same transport key on both sides with [`addStaticKeyToTransport`](#addstatickeytotransport). ### What about Streaming? Connect-Query currently only supports Unary RPC methods, which use a simple request/response style of communication similar to GET or POST requests in REST. This is because it aligns most closely with TanStack Query's paradigms. However, we understand that there may be use cases for Server Streaming, Client Streaming, and Bidirectional Streaming, and we're eager to hear about them. At Buf, we strive to build software that solves real-world problems, so we'd love to learn more about your specific use case. If you can provide a small, reproducible example, it will help us shape the development of a future API for streaming with Connect-Query. To get started, we invite you to open a pull request with an example project in the examples directory of the Connect-Query repository. If you're not quite sure how to implement your idea, don't worry - we want to see how you envision it working. If you already have an isolated example, you may also provide a simple CodeSandbox or Git repository. If you're not yet at the point of creating an example project, feel free to open an issue in the repository and describe your use case. We'll follow up with questions to better understand your needs. Your input and ideas are crucial in shaping the future development of Connect-Query. We appreciate your input and look forward to hearing from you. ## Legal Offered under the [Apache 2 license](/LICENSE). ================================================ FILE: SECURITY.md ================================================ # Security Policy This project follows the [Connect security policy and reporting process](https://connectrpc.com/docs/governance/security). ================================================ FILE: assets/connect-query.ai ================================================ %PDF-1.6 % 1 0 obj <>/OCGs[18 0 R]>>/Pages 3 0 R/Type/Catalog>> endobj 2 0 obj <>stream Adobe Illustrator 26.5 (Windows) 2022-11-15T17:46:08-04:00 2022-11-15T17:46:08-05:00 2022-11-15T17:46:08-05:00 256 232 JPEG /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgA6AEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq8 4/Nv87NB/L+3W1CDUNfnTlb6crcQinYSTsK8V8B1b8cuxYTP3NGbOIe98rebfzm/MfzRK5v9Zmgt XJpY2TG2gAP7PGMhnH+uWOZ8MMY9HXzzSlzLCmZnYsxLMxqzHcknuctak60Tzt5w0J1bR9avLEJ0 jhnkWMgdjHXgw9iMjKETzDOM5DkXrnkv/nLDzTp7R2/mmzj1i16Ndwhbe6Ar1IUek9B24r88x56U Hls5ENWRz3fRPkn8yfJ/nS09fQb9ZpUFZ7KT93cxf68R3p/lCq+BzCnjlHm5sMsZcmTZBsdirsVd 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 1 False False 190.000000 190.000000 Pixels Cyan Magenta Yellow Black Default Swatch Group 0 application/pdf connect-query xmp.did:ef71b652-b2a1-4a4e-86d6-63c0de777b3d uuid:fe6fd78e-c88a-4c95-8b8a-014f7e1940d4 xmp.did:7c4f2302-202b-7840-b399-95443e775df7 proof:pdf xmp.iid:41fc62d4-bcde-a748-bf99-2213076a569f xmp.did:9f50c985-9253-b841-ae5b-ac499933c02f xmp.did:7c4f2302-202b-7840-b399-95443e775df7 saved xmp.iid:7c4f2302-202b-7840-b399-95443e775df7 2022-11-15T17:39:29-05:00 Adobe Illustrator 26.5 (Windows) / saved xmp.iid:ef71b652-b2a1-4a4e-86d6-63c0de777b3d 2022-11-15T17:42:05-05:00 Adobe Illustrator 26.5 (Windows) / Document AIRobin Adobe PDF library 16.07 endstream endobj 3 0 obj <> endobj 5 0 obj <>/Resources<>/Properties<>>>/Thumb 22 0 R/TrimBox[0.0 0.0 190.0 190.0]/Type/Page>> endobj 20 0 obj <>stream HdɎ$DyP9/W AAАC7RyT75:LDfd/O߯ǯtۿ~i-]>~K9\=hۇ>>N^{޳Ma1HwHj^_Oj㞭vͻdon={ar5n[y*{Lf5G˄W΃2;[YmQ@S;Uʸȫ&{7 |)~H Uq *Aƺc2VD:j4E5cz'T81Nėכ9@侸T4Mx~$WSRDWx {÷/ I"~9 ϩcjwXT#'J=5! m &O}1B#D\5/G菽J|P4OЊzqceSYmI䥋*$CV躙Na.':R4$!Fa5G mT/ ,BQS" 8.)]4>#͔k&5\> :S}eM0UmCBjh;PY@ZcHT['  MTu2~!5JGR^'e65{ʀ"W'5OLBEnjHh}^AcGW 2ǍhSH6{ >~)i5U+!|=vΡ\"W.)-ɒy Acݕj Jf{^G{WZL+ev EYXЊ*#E A/ęuƞ%]XoVd"v K|:Ա>SlUg:=rxfc1-:yRߏi1E;:!o ȕ(U"v`&oQ#+B]j!Vzfz!p` =258a!$j8"o?f9>DEJE(Pg'B8`$tR )\⥽QF6Mr !B#YAv,׏_kkuk?AfU!qRḘ|㱱5찪JLi>:9s /v(|=gnOd(.qN$xjmSW)̎5jTKI:<3^v^U!2w7f]}ݰνA~M4쒭 NZ{e3>b@Nݳg w)3lu@23Fj!~ >stream 8;V_Vd1+#:#_lhU!caSg'i,jiJb5p7\B]X,dIci`%;l(Aq118L01&:,0,cSE[\l?L jE*gpfUHdLGgQ!=8c'!DB7&!>;TsN(6o9+Oaos7rTuAUD6q\+!>[5tSd9RG7R94@0 Dp*Ss#!u#Oh+h*d,RE*e_klX+RG'@fRT7%'-E0NM?+nIoR.`#)1ZPG)h35SoV!kZl ]M2*;a\S?O`JC@'jKi1ZB)iJ=4'q~> endstream endobj 23 0 obj [/Indexed/DeviceRGB 255 24 0 R] endobj 24 0 obj <>stream 8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn 6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 18 0 obj <> endobj 25 0 obj [/View/Design] endobj 26 0 obj <>>> endobj 21 0 obj <> endobj 7 0 obj <> endobj 14 0 obj <> endobj 15 0 obj <>stream %!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 24.0 %%AI8_CreatorVersion: 26.5.1 %%For: (KIRKLAND) () %%Title: (connect-query.ai) %%CreationDate: 11/15/2022 5:46 PM %%Canvassize: 16383 %%BoundingBox: 0 10 191 180 %%HiResBoundingBox: 0.000004398789315 10.0000007744293 190.00000001085 179.99999970249 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 14.0 %AI12_BuildNumber: 228 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_Cropmarks: 0 0 190 190 %AI3_TemplateBox: 95.5 94.5 95.5 94.5 %AI3_TileBox: -211 -301 401 491 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI24_LargeCanvasScale: 1 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 1 %AI17_Begin_Content_if_version_gt:24 4 %AI10_OpenToVie: -184.931677018638 417 1.11805555555556 0 8220.98136645962 8147.60869565217 2117 1873 18 0 0 227 219 0 0 0 1 1 0 1 1 0 1 %AI17_Alternate_Content %AI9_OpenToView: -184.931677018638 417 1.11805555555556 2117 1873 18 0 0 227 219 0 0 0 1 1 0 1 1 0 1 %AI17_End_Versioned_Content %AI5_OpenViewLayers: 7 %AI17_Begin_Content_if_version_gt:24 4 %AI17_Alternate_Content %AI17_End_Versioned_Content %%PageOrigin:-864 -446 %AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 16 0 obj <>stream %AI24_ZStandard_Data(/X,=$á(DDc:,@AQFRk<A@ & - 1䷃p o M^X> կ÷W~woYj`C1ʬY-[=m~53T1E>k`=k@+[mzin{`=ֿexZ=mzukk7չ ?GbptcƏiE,y]c|౦iVۖa]#k.kn}:vOZ9FktzcXǷ3nf]Ǭ57ytխ/X,ztխ׏k0~G\0d^e3c\r5Z7~mz7 ~~tokH 8H h6 ;b+Ѯ]5I`woB;pYc8F_r;R~kc&4|5,j ;]ǽEu7dznAZcZ-eg&~[s5[? ,_?S$k 2w|΢Df38~gڎz.F\vW;ZY-"`pn ^2Ubj5Xeƴ wԻj[7ӭ ZkQe`OY5g񧔵ǟn-my`8xg0^?cv_g\~8 tP-㷃o]/=)ڑgNh8nugߏ}s{Ueu5lo2ߵ?_s݉ɶMf]XTYa[o{-mxp5L .Rvít}g,\ƂIo8'=|flw.uZ_ږ?fu~نcלg~Ow)}ÛuwmfYz`k]9[~g]ây. An8Y ܲp̆wTsr6Cv;ĀoY]ϴ[ v+݂6YMp1l\qo^%*u)usuEi:޻tt|:qj]_8#8X8Ln6Lsqv7X;a2f }:qoo?g08wCv_ {tr!7tתs;}:qp\2jjs#w՜W~5Bqmۙp|><:]_~߱btGFJFw}\E/F_8Ns({1r'o>ͮQ.3^:Fã9$La3E%ð?Q1]ofV4ZGѝӳRqGe;gȻt&A,*FʆݣYmDE1ܪNmƓ.9{o^M'(îxou֚891:;sGJ7M쥣h8Jsh\i;z{ǻya\'߷qe\@~5na>{ɳw<ԿFSܭ/s ?mF`> =9.|gخHհ߷q|4}|muz_So:>Ϛ]hG~%:~%HLΆ*i\k;Ʉ R`Ö<7B~l)j  W@2Yiî w`ի9g"]d2gOKo:OߪC 9PZLGv` ځĐZ&STZplEXU(vctP! yh,<, uR-$n7bf9QSu1R^ P,f(fJOH!G1؜Ť&&z2j4aa$4]tQ;.ӷqǍN2~G aaXD_Ͽ} W` #"f@ml\E __@Ir  K ({ ]ic!M#;񬣰Ja)oj"bOxw1xD mEG_(Y* I\J%gB#zң":5*|yiЉ{>Ԗ5pMCڰ_s7n/il_d'Ϊ5á07< C.F #S3DxiZzI%63BOzumgF1Y<9"Uu%}A҈~BHYy` ςYIM&_ 6Vơ+,'VK O;6j gy& ]N4TB*JR 7 A,㬨Xi,+>AFW/.5oDYs5~o_y)/^=wo<w 5pEEfHLMdqC+GQh(*ON!r8mCf̵տu_lʛLXͬX 稉7!TdNxʇG&$9IBD5z6, H%GqݢBbK x[ wrkܾ;ݷ<@];`}}G0-2{gY5fsw溵):֚Y-˚zeq3U.A4\쿎r,k ۵.~UtZ5k1M]qo1w_jzDvN;6-v i F*J«wqD5ḰTxaQ=‚6ըmD >l1Bn_װ|k-SzfV 6&M謨MЄĂzt}:B+PF-y8 `@B:S !4I1U=(CX m A~m'_o㾍,#D 5ud%Vu*%JUᡥ@=c#0&zxP薓QpK ݐl…V@8sğȥo 4,᡾ZeA ao ɷq,9ԫϸb'HP<’kx VS+x}\dm)Vٰķq7%v254EH&TR/  @Bs+&2v霞X AMiRe]; b@!&g' ,ecR"TTWC'_m\PCÁ#S5Nz6<*w*GvuZ8DWT$/6p }̧BcļHՎ6&BjlY *b4$ZPt/aT;3t!C*ʳ?5f`PUmX^Vfy!aUA]L+f݄G9NDVT)8Z$8ZC,'u6<6j2XTtM`$(n#6.4jA&!IgaƧI'\JYOϓXw@F(~U=&=UKrM#4t1I6T:TlXqZ<-_=?,Aj"TRPЀ\3Yn3ZMe'$Gc@m\=yIFP7 &L&D*FK.MEz2G )x'R%fL Fq[~J0VYJˤ>$Faiuer=PAsM( fZ5Z"Ob"O)*EB!/5诒vr)////vx&1PfC9P%~/:ά㢠RdUpsU9d|"Iy $yH0Ю%xIʻ4G]2suQ(1]r X›@Q[ NUi+FoK$w i}O&h7s23/>8tD7tp%:'Sj/|U4lE}P*K;=тp}Y#RUP-v`L& عŐz@]WvOM b*R0e \8؜4@O@6GPDSTPC /R(5=g$EG b3+oqjCPth)/~#MXRND*XS(XOlP/N\D!A;PfQ!&d<*瑋J4:O$)K'`h!x ME.&Q(ڥ_f2> %KKyhh]!;x;-hĦM„IMl| QݦtDtdLS=?BXqN)HV+ ei|*]\in̯#iauL 60?; 'PF  %N&(oyjq%@+dӰu=:n(Yj@d]>a%] P8U$UMQ;2^/FD{4PFI fZCjދ(NX 3EhlYnIYwi)ć6M& Si/j֪ZvN. GRI;hëM@OlNdٟ ev2]ܫ^A R J,b)`6+(1UIW&_yfܟ:KG"?r>O)9!; T<},6)N&6E8ΦX6̪V3taPFU Q L!Cba~W诏:'XHA[:3  FW;6rgV[2!1 'L.6Ӹ(Q.>2TH)xg1u),RK!:D2VLUaշt>˾>.9R|'y 2&aUG[FFX :ZVf y hupxIɪȾD m\ɢv`=CCe Gڭ[YCf VdZaUĞySbR5^+^#?ٕS.#XhLU!ͨzqBKkCR-DA^AD< O bwR0bQ3ݐc.G[^LƥN@EV[! aIq!5TN ytyxV7UT*o㾍;(1&SNp>R5+BҬ&)0/qL8meb 4 I#HQ ut7D ZpF/6 K e UЖi)zW8Ƶ;fJVbT-VTM!Buo>7b frb1h^TF}O?Vs !ChLrL/\VInvv"Ɖ-qh/>M,xԬ83LqAlb 2GD-s"̈b;@ ⣩4񠉒܀lu#$"FDDCQfVN4U/KT]Q^E>9@\Z нp2Oj5ňmE$}w&9vK7F<ᴋHiV^UzoJ HȒ䊏t/䳷8PbxEX"aU 5_A#|۸^2eYCni\QOu&+Њ2axU1ʘH> 0ta)iȒC&D$C3x{#m'ڎV`|x*M|B4hqEKD%y0H$-+(J! Gq*'*+h/^90 @99d( &v4wNTjID̼x*A0gfR*EES5;ZکŹ6.Fѯ*\/O)Y)$)J'AR0&,EQ\U8G"JDuĪQm\XUxb |S`I͒WeF JfERHܟk*k!A^Ή #rb@`WsQ>f]>ơUV٧ KGCI).# ‘V;y>o9J49@J E=@ Ӑ>vH^Dv(UيR1АܜhH>h+6'*Ɣۿ$"nsPYr -v̉ %99x0E"f%s ẢCVN4SniBVN4ğJ8IG{(&PV;!4rvRYdکc䢙ozLpBF`[w~uMg 8]7+$у4#QJ Bծ HXU$o4l"J1V4aQ'$rԉ0#D?ˆC!(@FrMb u忍+_y>@T'.f$h8$,I3 (rI:ѐxV ՛2e"yh;.@*1rsH#V~BI O>T  4,EDÆEDQD#F%zN4CsaTzߜhXJZen7,P؜hM܎9FH)00Qbʉ g[)'k9IN[o?V`$aŷqAG&" dG1b[ttTB  F:>4(TOlhĭ)԰YGL6.ٟMt5ȥ 3*qR%4M\%!Ȥ-|292C!Bj{V}^ٲ-"eօ (xbBf:>]ֹfJ`AªYW=!V\5XE߄!?ȉ']T\/^dx` J|5V=hh2n-ۼرR `{>[PO8v&0F? 1k^<$)ɱZ+NtaRlpʢ@Wt/^Ļ/(_n#oqsI_HXlp,tM0 ueKDWum$C7]c3FaF="Ùh?}SԸ$RhT& ^poqhT *j|gaRɚyUAmI:LK%eJ`b9r0X+4Z6&OoX:%PJm,֪E"%Q<,[ԽDž#2C" 4:x8M.|H;Ţ^7+!c-nR8{Q#%Q|sЬ6T୦('!kjVjj6k`O`\ D{~3VWf}vy#&PUA_gZݴTxzԶ!bmp._B2S%HKī~dcr,o:Y'eZdV"qK^ ڎ{~:c~YW4m!5WYH_cKa6I4{a.=m- jFm-t].^^!,c:l^o1& %YlHV[?.…n5'̴RnZ `,;iD:.hv#nhЍUgbhUdV}'g,ߌQ ~;Vk;fM*-{ZM ]$}hHL8ϳCc?a(yU\O]4!}˅ "lkvՠ'pйtR'NܺQnS 4ݦCJ67hD A ) vr7a@n>{Xx"nc'zBѬtӎXr0B u!/7NzO/;}с--C/4 nՖ<]dN1 czQH`QAL5MЀ 1{5ČJBBKhf#n] RX(X~@WVF/|o`ۛ Pp%WAX0Rt-uyO^Lffg܆Aqv.MR64RNLq̆գ xBY̎ ICG teH%1EHXƽlmV٪xhr2jK4YHdpzdb'4@Mb|bqN'D祹&ЦT@'nch3CeyVw ܯ7;VTISiJЈ^Ή}o6hGV",] ƐE+{Rr#7uCU)ew7(TO3M7)vSHg\Dń oh'L*\ܟ8H9/RҩWϠWe~dl)Yͼ^-Dko*=GL^IU y/ӟlE6ZSr`ncdXt媸6!()OJm $m.2A,`_9* ļ67'}^-4ȖR@ :q#7Q2r,m^y?jf0֚Y;ߢ4iBCr$Bi48tH$9{Cu6g65AN%9S$4Lz\>u7ϵB?IӨ$_L?O7<ˁ$7<'UҰ/3x =OBާY$BJv (֍]`pIgz2 IObOy*uVk%e{C@UZ7}PQnǣM"oȈ 4xWaOX`*$ t1Y㨈 d,1 L5"f_֖.3萄/` 2!]2C=RaJw WaUnsvV͈a*.B @="RW!h#ZOpx*`nd+%r`S!yjUH$?mĀ{FYr"~ \r}L]NXɕnm% $K,]T&ײݭ@W0T)W T! gUI95I(~!od;5?`>'!%sV0h ~}ΑT6nOL}ʒT!i$tIb128zLG=je]~^GAQ'>=sBb≺[4Dxys~` {p ,+&qL&+na1Onz؝?%ˑ2b|5&$~K; :G}fKBXLQFi@6D,YK@'(MW Տ?Nڱx`7]:l5DΜ8~5LA(s^ABںbO o ]@e\> R@-I~e7~S7D%-h!P@̷?6n$u١R0+@kk#||!<8 /\2Synyy|0 :Ei]~KrX?vTE+nR|N7Bm wi+1/L8 8l\@A X4]P@T-} WjpⒾ w>\7D#'ᏹ%>2A [ӿKކc"$Y I6us}1yFhghOHاS$!Y/ Ɠ#GrjMo VRn`ܺ$w<#IGEF9h"ZPvMUCgq d D{\]8ڜ׺麶)(Lm= `'m mN[NiN{uAoYK %fZM̭df;[l$7ȈjuUJa5ELǧr(~vﶣɝC>wd>>4rn+Rz<1$+PNftMJ<$1;@UJ,7`thId_Ri( Zk[O $xFOǛxDk` ;tݘ3:TX(cƕu II8Krv拄ٸ>M|L!0VI^TE^魣nG&L@Ljd"g=yӤbߣ:&e-XȖȳФٖn(%AfD:O/@#ag4RiRnGF+1)n<๞qJN.,)'MտSUCtqI}MΰwTo÷$]j2rZ<2UWzM<`nlҾٱUBNApiB.J2Ӈ{ow$ZaR 6O|5a)` `L70qaGWjjiʇjsXgprXpo-x-X DS#u>FGfnABhVK;<ݬQ,TXuDhnSĝ]1|H <#^m5CcDEHjt~˕AU]*6ny9ry~ LS-IԬo-J/<|dq%b@uGwxt 'Ajh3C= MryMk݄.EieO0֕ =(R*0!xs[@Zc"HClׅDKdjPKX@)2:zPߝC/}4 v؋ 3m'A1aGT2 v%_ .藌Cd0OU L}MlM,W<:{`规o+ӗ"#dK]PhӗހǮXZJnCmYf<áMSY4Qx#q(m4 Pvǎڃ Ο?N!آ Z`teԺܭQ)TYݹѶ2!_ k ]DSdFs6_-kGEX)lk58ޛ!AATL$׫.Yppeetdfљ`m\Ԯ;]Pbɮ!d #a]ćxhkmIcFOIg@ܶGV$"DOrڭ@І@lG&AC,4K /$&6caD ,>AAyߊG^.o0s҈q#V| x0l02X Y5o9G@BlʃDŽ7f "Q%c?+{5KϊOѽbvlpƖ% 7b(M *VjXKkpFsE(Y#\tԮ3 qrl(ZP^L܀rI/t@BB, D͟$f;$(ͳEOrDbMpa(qla )uAv֛~P$'>JV%o3b 9݇wDWO./ %qv 㟉ŨDԒvg1і<G3}~Z}(묲/7b>M v鳴,[- Q>;Z%{|]0s#/lBH8T,D^ak=i "\ev8Z6jt ^RVt@C4Sb(6S*eZq4CB睧VԧQ<'l}ܬ&p+"1{2rGJ!O0#WwJ{qj K~1mƁ_Udp>3^pupBh[OND Wul2rI6<_ը9 zt׺[\ J.HDqk+޼o,&G_: Kk׈N2 bӆwp(D-SD"xP>(r79S|y/Oɨ:&s20f,oO~T٢.'w=\BjşΩ#w/ϐzFty*A5Tͨ2w *g- D:JV$"HV&$IL~H#RHfK5p\&S렬B36~qbR+fGosޭlT387e{jo#wĿn0M PuWr^X1ÿӳۤ;<}Q`ߘȾO 'beCe pȪuŋ/%X!=EuC]ߟO2俾Ga%4 oC%4 ^e3Kě=Mע5 M/1ujoH/~Q GX .NB$8g8 #}̉ËL*P=ܛц=Kw|1`b&li.5ʶ){/TȎL`)֡N1T@0;ϵiu;]V&.1Mdݖwy!}\2/1EU4p(³V%_pd.Q!hQx0k'\bSf(wh=Th8]*i0Csx8&es}̇ha)xJS.JBCDRhsV/§ !2@K(Xi;oBHKW} av|FPb2ގ= .h26j Z`CLnXGʳ6܂V>)7Q0˿ h꘼Ê;;l'Xp@5?Iqi}ۢj\t\.9{8!J֞Γ{By ߅i8 '/oYDwYt%Kf@%ﯞ67;ǟRMB*چmdѦ27B qdLmh1u#WW݇AQQbt,ςv[ evZ$/%yz#W AMN5MK>S$G4xP>!+L,2^ .w {T!纨mhM){nnIx{ #%U$7,Y' *Bt9aHЮ,CɍNj6m 6&0yAH8/*$rIn_ GPEon8xNm Vc,j":XX=L̥E\x8t|WQ`A/_!.6m_L &V\J3We:N[r`Lpc{gm/vS' XsuZuKDQߘ) '_aw}.)),.oհ-\c(7Դq lx׳Ј)]p3a`MChXDwvKa){P\f}:.㫔;'Id eƣ~` J ?$i5 D6m:rQTDvΫTC4Y8Ok(;כb հm9~@J8xPVc!Y$|y{@\V6 (5L=gbWEh𬉰` }* M'4.uU?K?P> T!>Lֆdj,jٿ?NRz䰈YZVh?!-|t}pDZg@UkEt AV*#!Θ!igJ Pr qbG] fs|Nt\V fxI<6:*_6=g̦C<^My-Ihr#̀>K]8D46v/aTDp㍍%{ZW3Hvߦ%"򩏢#ht#"YvV6\ 2SvـeZoXӨS}oTġK` u\(-5 K"!}| ma_sV3&er{&>9\0(_sLÞ qUZHoK+/{׿& -Z2,nJ5Jpq.:/]ߋ'͇<ާV:s: g*pܠjpZEK%eF)߁sXK= 9zy(A$6f9(|j;NΕ0P sFOQNӨ8~:rM?4R50, _W'@3<,iL! "RS?J3W^1QꚐOnM܋E5N#,\ȩ:c{zfކ[ (N* *T's^^|Tz 6F<1, hʵQ;\dؘn\$:l~򾉮v1<r ZJn};+-F)=y#4 ?R_ȸ*TQ3rrZn:'%0I0YgDYJn rmUBD盝(2km@j'( (YfUlc ot2L 'dGO_+@dXĦ ߨsAflԂFydn|5o0r >fuRU>2!FsMEJ -,9v}(f 1e&$! ǐ׮ѐciE(CWo\rWX78Ȳ25̶¦/غGBCBSHNI0y@Mhdن?N;2@;ɁZ*%S+(0'>}i>I+|%{:xZ2Asv*AY.|Yd n!"b5t1fHҊ\vO,#~??-N*1x N9y¦pwqڵ! 9k#ƮᑴXc;-1j3x08oL2v72>qꅥwVآ]M ZGb:"ӝhs@  Q&r:LĝDRYIѨ\Zr'I[[ Q uB L|Hp^{jd6ETaznЮ( gUs{sT _~u_vt8ݐ `yiIuq?3Q!oKb6'cfi/yS@k{٩9 x8u QF4Ò$@9 ΋0HŨso([\)%Vzf$6(k!,j~N,0Կ-ژ]qMU uF(-eȎؖGOsjkUEële_|y0;#,C[`.:@I5C+7kʒsfѨEh[LLrL}Oh"2 P2{J kx ;uNi]I[r hzb$Ԟ.95V" p[ ƌ׆\[I }^bJ^3>ACݛbѢvo;Z<=19*s7g,\6I2 6|LxR32R)^ rnDV^_5{(1 jIj%`;eC 9Q%E37jO_f8wNC{Kё.ŐzIT?> 8FܹB_\:hޤ\>LVt>8n*UVVk"Er6 ).pgueܾdT]F,Bh~x5XVɸFK@P<6+kōcPSz©?L4 ǂ NP:dӀ(@WՖ/{P~Qi{Fu\EH;g(csT@r!JǔS-?@(W+}!FH:z5I3$Uٛ}1\c/,@k~*oɢő C yAB"G!*MA_,[VY*|AivI;UuxKw cԛ ,݀>{Q)BvЯ"t9Dra#dOqP7jr 7MRkp Km{p3J KX(CZ`tC/>sZ-i[W#g"@jaً ʺTl>(n'1ڱ.Ȍ?Qp&m2Lta''w82 Xč^1!I!HTT.js52tflEq~o & -V85j0$KY8.$TY7T5TI8$c?=(p|yJ%.xdk!Ő`eAӤR2MC #D}cH[VLN~OmՎNAz88zx挽"G+}L .~Io)|Șp 1VT"OZtsZ?^D`Rl-= ,tP$2b(hx7ӊ "1lӃe:2L+DŽe?7249,;z[ oF37Y#εmSc1Fޅ d[3;SXY Ex4^yOFC /O#<;l3ALX`԰8~MpDU s9ˇ,\/D`H)ARsTv8MNr&"+$= mb۵$bԙtγ=h25jh a>&ցI6iz &bƃυǑh;erE12ǣ֥\W2,uNc"8U]C!9UEvx1Jp`cqEXTQ(_\n:,$WmX]^k1I#!=bm)Yźú|&u5('}_kv&^ MBqf\\FRqbs$DUe6EhBOg26sAvR9#sLlZ$̍m[D&WFFmX.;5]u9T< qSxK+׭6tbtL7 AK#}d^s'NQa1|ӌy٣QΞ2+( ujwۉ%EGwp>=Irȇ]~JJ2NRTX2z^xtٌUHf'3: IN-k/'JJQaFPtY6ꨓ0{i79vЯT/u#ʳsY/=לv^f:"թL*]JQabP3,`@"DH! Jp!8`) X R3 pn0/> F$Bp 08(PB7PA\x.S054b&x0 @ '1`(*6A F!P .X !y ƒ 48aBLPAXd0T%L@%@ C!C1< c  6A #EF !8aBp8h6P Bb .@< & . 'İH lpB8+/`NÇ.7  D h,lB6 xBpPL]؂ D%(pAbR@9%:B@@Pa:`䁅B"Ph"HA,9L" < h 'L@f a l `D A#JA1P# 0 Ip@k@DLBD0„^ >0MF( 6A4X`@ˆ <(T!^0PЀ" >H *`_ku2qT* [s)kem27^(*L hŒ̇MR,uTivg,̕(*,茕R474|Io$uv^\";[]ga!;W ~򏣴` J[kn+azOcդǹRTXԔf2ro Y}^ASʛU |(Þw|?kg*oXXqV3.6KýbGQтNX$Ž/v%3۩ly2YӖp(h&rNU=ɤ0QxCS0+ o<;qRT\PG N^)֙JMIlwx4)4f)SDz|1)Ϥ9^2}rĚK MQMSUΕ~+߷K"J-M QYسG[Z/ _gzS3s'FdtKgfMMvx{wtRU3/cNh>I<4[򼰰lo\#:o>x Zs$bsŌrFja^hXUE5Ct3"9:mJJmUv Jff#ju+Z"GhE(َSVt|MSS/(*,dLQqaJdRSTX:1R<,7#E8{h~)4EŽgB3qtL-Df)Y,ՍT{QfI[1"cմdNQJY\]X(me<(JQ_I8#C180 Ejk\sudj32V:5)\&w%=79id̘u+bhvZUyKP;Z }Mh׬;YH(3>NYҖ|;de;&b5Ksh.%aX%AIf!߁3WG1{\>)7YBǪ]1yu]xإjF#knhl~.qHxi^K/B|dY3vT.iVH7ISo Q +B7q9wBf7[ m<uH\YBĢ̇~"tɎjNʒ:N#hmm u!e˽RM=e:*_#dm_U犒#_$di͹\iUQl,Fw$X95%;ʮzYp5TG%H#KEQUojZ IkMi#j^Zd}ˁ67]kOdd=H]ӞY| RIi\uB7euz[ <9GQaS~Ojyl5dAVt#&rAcFtx,&Ʈ=_](r ԝGRot&?ф>/,֓rq`5}“{ܲس[T7ՓCM { ZVsp:w*̈_I:;L(eN퐪SSuZ"&܃Ҩ3?Es6-_X,& ЍQξ3Rkմqٚb%gKݪl:͝Qk ږdNvAe2.=̉M=GT+m˰tIɡ>-_fkbMD4N 4f '6LJ_}Wˢ;;E=#aoV{:hb94.LG2hKkaߕR)Qp:~Ds\; -qΓb1tM"9ΌMG2A=(g?LM]ƒy\D,?;zg;XcUILulȂ6wbEEF:~cCK\#9yr_f:AvX}'ZUݔ?D$c"4ޜ :䫒{nVfIYӮ͚,d7#56W$hP^OgrR=&}$a݂wq7r%f]Ӳ?Ƀ~7Wn*a%bQ?5zɮ#2㵔YȞ+g v^8fy3vYd5pfIiƆcrYoKʕ/-WKޢtDo%[%b TfͤkE{h%:URX3YN:;rGɟ{Pk8tt9r8FX=+}YSɜ+~~XD5K ɜ{ .;ڹJ*0OYR毸1A +$NRoL Q 4߱S^V|͘->ߔdEo"$8+hUn{{G?uVRgY4 L` _ǵR-~P o3 yFCy8sgnEX g~[MoJqt8a>w=̚啲7fcwds [wsx6%"U߹CHU1.k=uQpФE.(7'=MXͩ/g0gfxr,/CGKHʩnaz|t pPmT*<>z6M;ļhixe=‘UWZWX&.ݙmbS (Ghr,XߩG7x!fY ZU Ӓ+A䏌A;?Μs*L=m5EYil7EϽjnj9q1g+O (m\Ԭ34s MjXfTϟG)e=4ù)܎Vʣs%HN*|ٝ݇GwvótU N3_K6$tdɒ4#n'!O*˓mSs]fc1;YYO8whwއM3n8_Yչ2YIHUv%VEm6)՜ѯGIOK$9WOK4 +Ҳ̜y*: ~QEvNpPo;csMeY(K1R8k;C31)t.^EVh>%N%Zdc "^T{Yre4묪dҷ4[i;4D Nϓ9͢(/6L3Şƴ?iy$-,BSTcKFy@$ E!r m30GEQȖHBCA㐚B@ȀQ§?<r9nEʟꆼθaZnx8Qr闖QF$BGZUȝ8&@CˤHi$ A}-j8֨ɸt(U{DF|Š06_Ӭv2RnS'70ʿOyWjrR+LlCRˣȽBDVt}+[O:צ ^mK!5 NKaJӜz=Zgz#u!^mQdu"A]bT Zgvuͷl/DB% FvSD3n8y2&!/%%nh-|˓jȱnmހ(MG!Qa=>J"6&H;9EkA)aPW8E;`]hQEExaQ=!KAtR6}Ma|IpCZ މ0ˉZGY;asU+|0. .#?d@KNuçS]mŐEKq8)Mޡj $܉~(Rgb;id&zb^*t,GUzDNKy63>2*|24A!I+[?á]ƌnVQ/"8n-ʖJftJrzUB3BA+$u29Fr( ~FNju5yw#5 Z* r0JW-Vx|ޢ5iuqq%bDMOT.kJ:;!.R+1X.~qF9t( cӇ`=ZD] &&L.gӓ3&˝D!{rGsZ+hʞm8f uH77zMWhe^oYVUzcu$%Oꔃо5.ϧԀY\`;yfʏc%8yck*wvH $S;>yOĹaFd(AF/9 qg(x&x {jiFx'۶hF}"k#_tRb{:QsDdħ=/17}>)Z p\CE,[ 'Kʠn{٨2c4*t<U݅9Yw_dٛ5afeOQW=X & CgܗT?76rKt"2*=u4hPgifM[Te-]u:  zn|Oɚ8:ێf 8Yp-,  iuiZъT!S5Q;rK瘴Z+zH Ih V0P|uJA,zW,lpC9R@tҩ" sU\29]5|Mة\xЈR[^tVUBP;`*h6pXB^?Zx~,gz"Z+KI_zX ~dD~,t5?\w KFDg ?u-+7Ы"(A)aN?k(pԝRRwE^;]FUp}ߠeT2kE#@9bxN[Eo4;,,dqDa=,ns{XBhJ s=I$sAϝV /;]Cn؝~'7<(7taSi[Q||+Ui[a( Οw0:QU& OrCX5_4ڃ.ac7x~n*:=UĎΝƪ)Z8b$ƙ {r_?GzE wп1_}`~DVPc4}E7u\ujh9zXݮ_-ófp#@U Dt <J(|].j(CfW/.K(Te4P 9_? 5ާϚM+*400]5w `K-K$3rUX8Sp8}aYH9煄k|)u}ohqo+bFMUl62]%@vmfB3diQ y"ؤCO ng X*ee(O~5x)@Yٚάn&aH-:Owv*w7N6||PSNW6P'8["?P]|7]{ǧYrJJؗl7dZަGjy0W-u\6!ZA84+6W.zpOxgAD%%)dvljpkWCrUD#DX(.^PȬ_.><>{FC8^_#mC ą cSg=E]m@VmΕNfLJn .x"cw~dSUHA of4 ;++y}85$ɮEefK!Bpz˰P6ps㈏諵18ɆIuA C 8)Ģ\\VҭjNizR~hCMޮD&dP*;z;71:dzhҀ9kʚ4IV-ڦ;?n|C4jZY/~W*$om>%HB ?ȵ,D 7t,'-sLXњz6i/YQ D u}k:/&όIԤLo= + _BA@t '+f0p~Fu 3T'bF -]ia],ǜ851%>Ec--|3/eƲ+,î2(+jƓe :8 /DD㌊ķT(,jR3U&xU'cλ DwzzdPv.) Q K WQUz E sz N]֔[[Ӎ教q߳הddϱ n- )d /eM-Hq+5'KtB)fD=xd@ c0eߵd1⫣h-OnjB@='*|QX%Ě@S[2M^ +*dGpzy\⪕*We[ie> endobj xref 0 28 0000000004 65535 f 0000000016 00000 n 0000000147 00000 n 0000031392 00000 n 0000000000 00000 f 0000031443 00000 n 0000000000 00000 f 0000036465 00000 n 0000000000 00000 f 0000000000 00000 f 0000000000 00000 f 0000000000 00000 f 0000000000 00000 f 0000000000 00000 f 0000036538 00000 n 0000036679 00000 n 0000038208 00000 n 0000000000 00000 f 0000036165 00000 n 0000077176 00000 n 0000031797 00000 n 0000036352 00000 n 0000035233 00000 n 0000035603 00000 n 0000035651 00000 n 0000036236 00000 n 0000036267 00000 n 0000077201 00000 n trailer <<1C85417C8E6E7F45A66543808DE86DA0>]>> startxref 77392 %%EOF ================================================ FILE: assets/connect-query_dependency_graph.excalidraw ================================================ { "type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", "version": 338, "versionNonce": 2062451677, "isDeleted": false, "id": "XeSFIfHiUS6jxoqpDlWJf", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 460.8332824707031, "y": 499.1667175292969, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 922502867, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "nCL0iXpxT4jwwx2rTwPB8" }, { "id": "KmuiK-MBdqr2GTx-Jkug_", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 281, "versionNonce": 922683517, "isDeleted": false, "id": "nCL0iXpxT4jwwx2rTwPB8", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 524.8332824707031, "y": 539.5000457763672, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 130, "height": 24, "seed": 543194013, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673457516212, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Connect-Web", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "XeSFIfHiUS6jxoqpDlWJf", "originalText": "Connect-Web" }, { "type": "rectangle", "version": 653, "versionNonce": 1196429683, "isDeleted": false, "id": "HdkWX-Z70zh5w9kpusrjy", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 900, "y": 780, "strokeColor": "#000000", "backgroundColor": "#228be6", "width": 258, "height": 104.66665649414062, "seed": 1886852829, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "v4h5RDwi8qngLrgWkXvyY" }, { "id": "gvjIhXtSYcnwYBZ2oDDpS", "type": "arrow" }, { "id": "7wHm2jFQS6ITLYsjqcOU2", "type": "arrow" }, { "id": "KmuiK-MBdqr2GTx-Jkug_", "type": "arrow" }, { "id": "a_FVwdNlnqR-gZIKraQ74", "type": "arrow" }, { "id": "FvGaYurosrW130jE07qnK", "type": "arrow" }, { "id": "PAWB2iARdE5gRjLXwnX02", "type": "arrow" } ], "updated": 1673457529506, "link": null, "locked": false }, { "type": "text", "version": 629, "versionNonce": 1087894771, "isDeleted": false, "id": "v4h5RDwi8qngLrgWkXvyY", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 917, "y": 820.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 224, "height": 24, "seed": 224714451, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334543, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Your Generated Code", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "HdkWX-Z70zh5w9kpusrjy", "originalText": "Your Generated Code" }, { "type": "rectangle", "version": 539, "versionNonce": 1221815059, "isDeleted": false, "id": "1QmxoknneXyizwjmTBfsW", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 900, "y": 1060, "strokeColor": "#000000", "backgroundColor": "#228be6", "width": 258, "height": 104.66665649414062, "seed": 579482429, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "TqMtSvn1VfRhyRcNsGcbp" }, { "id": "I_CLj4w8V5pobtEXl2Buj", "type": "arrow" }, { "id": "7wHm2jFQS6ITLYsjqcOU2", "type": "arrow" } ], "updated": 1673457529506, "link": null, "locked": false }, { "type": "text", "version": 524, "versionNonce": 512941715, "isDeleted": false, "id": "TqMtSvn1VfRhyRcNsGcbp", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 923, "y": 1100.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 212, "height": 24, "seed": 1530453501, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334543, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Your Frontend Code", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "1QmxoknneXyizwjmTBfsW", "originalText": "Your Frontend Code" }, { "type": "rectangle", "version": 453, "versionNonce": 629932093, "isDeleted": false, "id": "70r-ffCEpPw4jZ47fTXY6", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 1340, "y": 220, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 1410462835, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "zgjnIw46XBD2g0GOjB_qZ" }, { "id": "pq6WADDyw4BOp77bAngzy", "type": "arrow" }, { "id": "EUUJPfgFQLQKC9QmFn2Hj", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 401, "versionNonce": 75469875, "isDeleted": false, "id": "zgjnIw46XBD2g0GOjB_qZ", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1404, "y": 260.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 130, "height": 24, "seed": 825000477, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334543, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Protobuf-ES", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "70r-ffCEpPw4jZ47fTXY6", "originalText": "Protobuf-ES" }, { "type": "rectangle", "version": 552, "versionNonce": 737865885, "isDeleted": false, "id": "lN_Z7SPGJvUJ1_qQgp_my", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 900.8332824707031, "y": 499.1667175292969, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 1231089171, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "6HsLOBS0-yHGej94R9uAN" }, { "id": "gvjIhXtSYcnwYBZ2oDDpS", "type": "arrow" }, { "id": "pq6WADDyw4BOp77bAngzy", "type": "arrow" }, { "id": "FvGaYurosrW130jE07qnK", "type": "arrow" }, { "id": "a_FVwdNlnqR-gZIKraQ74", "type": "arrow" }, { "id": "EUUJPfgFQLQKC9QmFn2Hj", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 541, "versionNonce": 1575669053, "isDeleted": false, "id": "6HsLOBS0-yHGej94R9uAN", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 953.3332824707031, "y": 527.5000457763672, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 153, "height": 48, "seed": 543020243, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673457516213, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "protoc-gen-\nconnect-query", "baseline": 43, "textAlign": "center", "verticalAlign": "middle", "containerId": "lN_Z7SPGJvUJ1_qQgp_my", "originalText": "protoc-gen-\nconnect-query" }, { "type": "rectangle", "version": 537, "versionNonce": 2079118589, "isDeleted": false, "id": "IJST3a9MULbY3nKP5O-vj", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 1340, "y": 780, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 1507832755, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "pmTReKQQCE0eqd4B1ZO4t" }, { "id": "I_CLj4w8V5pobtEXl2Buj", "type": "arrow" }, { "id": "7wHm2jFQS6ITLYsjqcOU2", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 525, "versionNonce": 476151059, "isDeleted": false, "id": "pmTReKQQCE0eqd4B1ZO4t", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1386.5, "y": 820.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 165, "height": 24, "seed": 433861949, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334543, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "TanStack Query", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "IJST3a9MULbY3nKP5O-vj", "originalText": "TanStack Query" }, { "type": "arrow", "version": 404, "versionNonce": 1650651357, "isDeleted": false, "id": "KmuiK-MBdqr2GTx-Jkug_", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 737.6617093328613, "y": 609.239365369029, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 152.24842287378317, "height": 159.87055354799793, "seed": 458946877, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "LwfPx1xIn8ViH6r4EKaCf" } ], "updated": 1673457516212, "link": null, "locked": false, "startBinding": { "elementId": "XeSFIfHiUS6jxoqpDlWJf", "focus": -0.5197748421647741, "gap": 19.589139861980925 }, "endBinding": { "elementId": "HdkWX-Z70zh5w9kpusrjy", "focus": -0.4410732002860223, "gap": 14.845851207698502 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ 152.24842287378317, 159.87055354799793 ] ] }, { "id": "LwfPx1xIn8ViH6r4EKaCf", "type": "text", "x": 758.6592881134363, "y": 675.748149562487, "width": 106, "height": 24, "angle": 0, "strokeColor": "#1864ab", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": null, "seed": 1474013971, "version": 34, "versionNonce": 589494973, "isDeleted": false, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "text": "Transport", "fontSize": 20, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "baseline": 19, "containerId": "KmuiK-MBdqr2GTx-Jkug_", "originalText": "Transport" }, { "type": "arrow", "version": 123, "versionNonce": 1761090973, "isDeleted": false, "id": "gvjIhXtSYcnwYBZ2oDDpS", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1021.2470991923436, "y": 618.9294060114771, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 0.37181829465941973, "height": 140.83328247070312, "seed": 633861949, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1673457516213, "link": null, "locked": false, "startBinding": { "elementId": "lN_Z7SPGJvUJ1_qQgp_my", "focus": 0.06439858402412948, "gap": 15.096031988039613 }, "endBinding": { "elementId": "HdkWX-Z70zh5w9kpusrjy", "focus": -0.06439858402412948, "gap": 20.237311517819762 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ -0.37181829465941973, 140.83328247070312 ] ] }, { "type": "arrow", "version": 426, "versionNonce": 1592209277, "isDeleted": false, "id": "I_CLj4w8V5pobtEXl2Buj", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1323.7183536001407, "y": 896.2816463998595, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 149.19417890388058, "height": 149.19417890388058, "seed": 699791827, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "CUlnnQd_zqWGo4jP9YXIQ" } ], "updated": 1673456334544, "link": null, "locked": false, "startBinding": { "elementId": "IJST3a9MULbY3nKP5O-vj", "focus": 0.44852939629637206, "gap": 20 }, "endBinding": { "elementId": "1QmxoknneXyizwjmTBfsW", "focus": 0.4338235696294328, "gap": 22 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ -149.19417890388058, 149.19417890388058 ] ] }, { "id": "CUlnnQd_zqWGo4jP9YXIQ", "type": "text", "x": 1184.1212641482005, "y": 958.8787358517998, "width": 130, "height": 24, "angle": 0, "strokeColor": "#1864ab", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": null, "seed": 1784188211, "version": 35, "versionNonce": 1241731795, "isDeleted": false, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "text": "Query Logic", "fontSize": 20, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "baseline": 19, "containerId": "I_CLj4w8V5pobtEXl2Buj", "originalText": "Query Logic" }, { "type": "arrow", "version": 347, "versionNonce": 173506675, "isDeleted": false, "id": "7wHm2jFQS6ITLYsjqcOU2", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1020, "y": 900, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 0, "height": 140, "seed": 84359645, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "LHppZw4mGtiNgeBRWCQIc" } ], "updated": 1673456334544, "link": null, "locked": false, "startBinding": { "elementId": "HdkWX-Z70zh5w9kpusrjy", "focus": 0.06976744186046512, "gap": 15.333343505859375 }, "endBinding": { "elementId": "1QmxoknneXyizwjmTBfsW", "focus": -0.06976744186046512, "gap": 20 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ 0, 140 ] ] }, { "id": "LHppZw4mGtiNgeBRWCQIc", "type": "text", "x": 943.5, "y": 958, "width": 153, "height": 24, "angle": 0, "strokeColor": "#1864ab", "backgroundColor": "transparent", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": null, "seed": 687336477, "version": 37, "versionNonce": 154481725, "isDeleted": false, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "text": "Query Helpers", "fontSize": 20, "fontFamily": 3, "textAlign": "center", "verticalAlign": "middle", "baseline": 19, "containerId": "7wHm2jFQS6ITLYsjqcOU2", "originalText": "Query Helpers" }, { "type": "arrow", "version": 1349, "versionNonce": 1522295293, "isDeleted": false, "id": "pq6WADDyw4BOp77bAngzy", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1323.6385511190695, "y": 336.1689601989077, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 148.31503045010072, "height": 148.4350637552135, "seed": 1060741971, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1673457516213, "link": null, "locked": false, "startBinding": { "elementId": "70r-ffCEpPw4jZ47fTXY6", "focus": 0.44852939629637206, "gap": 20 }, "endBinding": { "elementId": "lN_Z7SPGJvUJ1_qQgp_my", "focus": 0.4338235696294328, "gap": 22 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ -148.31503045010072, 148.4350637552135 ] ] }, { "type": "rectangle", "version": 774, "versionNonce": 1357065565, "isDeleted": false, "id": "wmmX8X0PhUxG4APAQUfdA", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 1340, "y": 500, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 632603987, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "o8v54ziV3hNiqpchzZcem" }, { "id": "FvGaYurosrW130jE07qnK", "type": "arrow" }, { "id": "-w3ngGHZOH7S7mmdmHwqP", "type": "arrow" }, { "id": "EUUJPfgFQLQKC9QmFn2Hj", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 773, "versionNonce": 1171851517, "isDeleted": false, "id": "o8v54ziV3hNiqpchzZcem", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1404, "y": 528.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 130, "height": 48, "seed": 1027074227, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "protoc-gen-\nes", "baseline": 43, "textAlign": "center", "verticalAlign": "middle", "containerId": "wmmX8X0PhUxG4APAQUfdA", "originalText": "protoc-gen-\nes" }, { "type": "arrow", "version": 1343, "versionNonce": 1725288787, "isDeleted": false, "id": "FvGaYurosrW130jE07qnK", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1320, "y": 620, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 140, "height": 140, "seed": 814557597, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1673456334544, "link": null, "locked": false, "startBinding": { "elementId": "wmmX8X0PhUxG4APAQUfdA", "focus": 0.44852939629637206, "gap": 20 }, "endBinding": { "elementId": "HdkWX-Z70zh5w9kpusrjy", "focus": 0.4338235696294328, "gap": 22 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ -140, 140 ] ] }, { "type": "rectangle", "version": 735, "versionNonce": 2049931709, "isDeleted": false, "id": "NLMbxwdPMz1O8gu8ku7cT", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 460, "y": 640, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 258, "height": 104.66665649414062, "seed": 70689523, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "FKPW0XXW-xf9J70NAgjEI" }, { "id": "PAWB2iARdE5gRjLXwnX02", "type": "arrow" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 700, "versionNonce": 899591613, "isDeleted": false, "id": "FKPW0XXW-xf9J70NAgjEI", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 512.5, "y": 680.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 153, "height": 24, "seed": 808891261, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Connect-Query", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "NLMbxwdPMz1O8gu8ku7cT", "originalText": "Connect-Query" }, { "type": "rectangle", "version": 840, "versionNonce": 536514739, "isDeleted": false, "id": "58DCEeIe5AGFChCK8Lm0R", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, "x": 900, "y": 220, "strokeColor": "#000000", "backgroundColor": "#228be6", "width": 258, "height": 104.66665649414062, "seed": 1891287965, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "vIDwM655KULSinXrVbpHD" }, { "id": "a_FVwdNlnqR-gZIKraQ74", "type": "arrow" }, { "id": "-w3ngGHZOH7S7mmdmHwqP", "type": "arrow" } ], "updated": 1673457529506, "link": null, "locked": false }, { "type": "text", "version": 827, "versionNonce": 1348202013, "isDeleted": false, "id": "vIDwM655KULSinXrVbpHD", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 946.5, "y": 260.3333282470703, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 165, "height": 24, "seed": 1728515901, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "Your Protofile", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "58DCEeIe5AGFChCK8Lm0R", "originalText": "Your Protofile" }, { "id": "a_FVwdNlnqR-gZIKraQ74", "type": "arrow", "x": 1020, "y": 339.1666793823242, "width": 0.5369143190519026, "height": 139.16671752929688, "angle": 0, "strokeColor": "#1864ab", "backgroundColor": "#ced4da", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "groupIds": [], "roundness": { "type": 2 }, "seed": 1769855859, "version": 697, "versionNonce": 674830941, "isDeleted": false, "boundElements": [], "updated": 1673457516213, "link": null, "locked": false, "points": [ [ 0, 0 ], [ 0.5369143190519026, 139.16671752929688 ] ], "lastCommittedPoint": null, "startBinding": { "elementId": "58DCEeIe5AGFChCK8Lm0R", "focus": 0.06976744186046512, "gap": 14.500022888183594 }, "endBinding": { "elementId": "lN_Z7SPGJvUJ1_qQgp_my", "focus": -0.06976744186046512, "gap": 20.83332061767578 }, "startArrowhead": null, "endArrowhead": "arrow" }, { "type": "rectangle", "version": 387, "versionNonce": 1626552861, "isDeleted": false, "id": "8A9Hj-N3wkhESFIv5cuO9", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "dotted", "roughness": 0, "opacity": 100, "angle": 0, "x": 460, "y": 1060, "strokeColor": "#000000", "backgroundColor": "#868e96", "width": 160, "height": 40, "seed": 501992595, "groupIds": [], "roundness": { "type": 3 }, "boundElements": [ { "type": "text", "id": "xtKa4RSXNkP5YhJ4M_VYx" } ], "updated": 1673457524342, "link": null, "locked": false }, { "type": "text", "version": 339, "versionNonce": 582969149, "isDeleted": false, "id": "xtKa4RSXNkP5YhJ4M_VYx", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 475, "y": 1068, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 130, "height": 24, "seed": 1601738291, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "npm package", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "8A9Hj-N3wkhESFIv5cuO9", "originalText": "npm package" }, { "type": "rectangle", "version": 692, "versionNonce": 2025551635, "isDeleted": false, "id": "kl6tzqBN_O2oPLMieESou", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 460, "y": 1120, "strokeColor": "#000000", "backgroundColor": "#228be6", "width": 160, "height": 40, "seed": 314591827, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "uJxuk7GZFFnxpY8yy_bUC" } ], "updated": 1673456334544, "link": null, "locked": false }, { "type": "text", "version": 683, "versionNonce": 472479645, "isDeleted": false, "id": "uJxuk7GZFFnxpY8yy_bUC", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 475, "y": 1128, "strokeColor": "#000000", "backgroundColor": "transparent", "width": 130, "height": 24, "seed": 805206109, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334544, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "source code", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "kl6tzqBN_O2oPLMieESou", "originalText": "source code" }, { "type": "arrow", "version": 550, "versionNonce": 788602035, "isDeleted": false, "id": "PAWB2iARdE5gRjLXwnX02", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 731.2059132037748, "y": 750.780397087813, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 148.7940867962252, "height": 89.21960291218704, "seed": 890429011, "groupIds": [], "roundness": null, "boundElements": [ { "type": "text", "id": "WRx0CxczbENMaHeXeBTVU" } ], "updated": 1673456334545, "link": null, "locked": false, "startBinding": { "elementId": "NLMbxwdPMz1O8gu8ku7cT", "focus": -0.20682688999780358, "gap": 14.552455723772255 }, "endBinding": { "elementId": "HdkWX-Z70zh5w9kpusrjy", "focus": -0.7480468885848, "gap": 20 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ 148.7940867962252, 89.21960291218704 ] ] }, { "type": "text", "version": 40, "versionNonce": 313687037, "isDeleted": false, "id": "WRx0CxczbENMaHeXeBTVU", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 775.6029566018874, "y": 783.3901985439065, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 60, "height": 24, "seed": 13575261, "groupIds": [], "roundness": null, "boundElements": null, "updated": 1673456334545, "link": null, "locked": false, "fontSize": 20, "fontFamily": 3, "text": "hooks", "baseline": 19, "textAlign": "center", "verticalAlign": "middle", "containerId": "PAWB2iARdE5gRjLXwnX02", "originalText": "hooks" }, { "type": "arrow", "version": 1336, "versionNonce": 79760979, "isDeleted": false, "id": "-w3ngGHZOH7S7mmdmHwqP", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1180, "y": 339.1666793823242, "strokeColor": "#1864ab", "backgroundColor": "#ced4da", "width": 140, "height": 140, "seed": 417848861, "groupIds": [], "roundness": { "type": 2 }, "boundElements": null, "updated": 1673456334545, "link": null, "locked": false, "startBinding": { "elementId": "58DCEeIe5AGFChCK8Lm0R", "focus": -0.46415432661152806, "gap": 22 }, "endBinding": { "elementId": "wmmX8X0PhUxG4APAQUfdA", "focus": -0.41819863931427675, "gap": 20.83332061767578 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ 140, 140 ] ] }, { "type": "arrow", "version": 1996, "versionNonce": 993645971, "isDeleted": false, "id": "EUUJPfgFQLQKC9QmFn2Hj", "fillStyle": "hachure", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, "x": 1460, "y": 340, "strokeColor": "#1864ab", "backgroundColor": "transparent", "width": 0, "height": 140, "seed": 795421171, "groupIds": [], "roundness": null, "boundElements": [], "updated": 1673456334545, "link": null, "locked": false, "startBinding": { "elementId": "70r-ffCEpPw4jZ47fTXY6", "focus": 0.06976744186046512, "gap": 15.333343505859375 }, "endBinding": { "elementId": "wmmX8X0PhUxG4APAQUfdA", "focus": -0.06976744186046512, "gap": 20 }, "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ 0, 140 ] ] } ], "appState": { "gridSize": null, "viewBackgroundColor": "#ffffff" }, "files": {} } ================================================ FILE: cspell.config.json ================================================ { "words": [ "Deno", "Dimitri", "Mitropoulos", "Quickstart", "Stamm", "Timo", "Vindaloo", "Weizenbaum's", "attw", "backoffs", "bufbuild", "codegen", "connectquery", "connectrpc", "connectweb", "descriptorset", "excalidraw", "idempotence", "idempotency", "inferencing", "invalidators", "keyof", "lcov", "nocheck", "pnpm", "preconfigured", "proto", "protobuf", "protoc", "protofile", "protoplugin", "tanstack", "todos", "tsdoc", "corepack", "printables", "arethetypeswrong", "oneof", "typesafe", "setversion", "getversion", "postsetversion", "postgenerate", "npmjs" ], "ignorePaths": [ "**/*.svg", "**/*.ai", "**/pnpm-lock.yaml", "*.excalidraw", "**/gen", "**/snapshots", "**/*.css", "**/*.xml", "**/tsconfig.vitest-temp.json" ] } ================================================ FILE: package.json ================================================ { "private": true, "name": "root", "type": "module", "workspaces": [ "packages/connect-query-core", "packages/connect-query", "packages/examples/react/basic", "packages/protoc-gen-connect-query", "packages/test-utils" ], "scripts": { "all": "turbo run --ui tui build format test lint attw license-header", "clean": "git clean -Xdf", "setversion": "node scripts/set-workspace-version.js", "getversion": "node scripts/find-workspace-version.js", "postsetversion": "npm run all", "release": "node scripts/release.js", "prerelease": "npm run all", "format": "prettier --write --ignore-unknown '.' '!packages' '!.turbo' '!node_modules'", "license-header": "license-header --ignore 'packages/**'", "lint": "eslint --max-warnings 0 . --ignore-pattern 'packages/**' && npm run check:spelling", "check:spelling": "cspell \"**\" --gitignore" }, "packageManager": "npm@10.1.0", "licenseHeader": { "licenseType": "apache", "yearRange": "2021-2023", "copyrightHolder": "The Connect Authors" }, "devDependencies": { "@bufbuild/license-header": "^0.0.4", "@types/node": "^22.15.29", "@typescript-eslint/eslint-plugin": "8.33.0", "@typescript-eslint/parser": "8.33.0", "@typescript-eslint/utils": "8.33.0", "@vitest/ui": "^3.2.4", "cspell": "9.0.2", "eslint": "8.57.0", "eslint-config-prettier": "10.1.5", "eslint-import-resolver-typescript": "^4.4.2", "eslint-plugin-eslint-comments": "3.2.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-n": "^17.18.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-vitest": "0.5.4", "prettier": "3.5.3", "turbo": "^2.5.4", "typescript": "5.8.3", "vitest": "^3.2.4" }, "engineStrict": true, "engines": { "node": ">=20", "npm": ">=10.8" } } ================================================ FILE: packages/connect-query/README.md ================================================ # @connectrpc/connect-query 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). 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). 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). ================================================ FILE: packages/connect-query/package.json ================================================ { "name": "@connectrpc/connect-query", "version": "2.2.0", "description": "TypeScript-first expansion pack for TanStack Query that gives you Protobuf superpowers.", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/connectrpc/connect-query-es.git", "directory": "packages/connect-query" }, "scripts": { "prebuild": "rm -rf ./dist/*", "build": "npm run build:cjs && npm run build:esm", "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\"}'", "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", "test": "vitest --run", "test:watch": "vitest --watch", "format": "prettier --write --ignore-unknown '.' '!dist'", "license-header": "license-header", "lint": "eslint --max-warnings 0 .", "attw": "attw --pack" }, "type": "module", "sideEffects": false, "main": "./dist/cjs/index.js", "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } }, "dependencies": { "@connectrpc/connect-query-core": "^2.2.0" }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.1", "@bufbuild/buf": "1.54.0", "@bufbuild/jest-environment-jsdom": "^0.1.1", "@bufbuild/protobuf": "^2.5.1", "@bufbuild/protoc-gen-es": "^2.5.1", "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "@tanstack/react-query": "^5.79.0", "@testing-library/react": "^16.3.0", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "react": "^19.1.0", "react-dom": "^19.1.0", "test-utils": "*", "typescript": "^5.8.3" }, "peerDependencies": { "@bufbuild/protobuf": "2.x", "@connectrpc/connect": "^2.0.1", "@tanstack/react-query": ">=5.62.0", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "files": [ "dist/**" ] } ================================================ FILE: packages/connect-query/src/call-unary-method.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import type { ConnectQueryKey } from "@connectrpc/connect-query-core"; import { callUnaryMethod, createConnectQueryKey, } from "@connectrpc/connect-query-core"; import type { QueryFunctionContext } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import { mockEliza } from "test-utils"; import type { SayRequest } from "test-utils/gen/eliza_pb.js"; import { ElizaService, SayRequestSchema } from "test-utils/gen/eliza_pb.js"; import { describe, expect, it } from "vitest"; import { wrapper } from "./test/test-wrapper.js"; describe("callUnaryMethod", () => { it("can be used with useQueries", async () => { const transport = mockEliza({ sentence: "Response 1", }); const { result } = renderHook(() => { const input: SayRequest = create(SayRequestSchema, { sentence: "query 1", }); const [query1] = useQueries({ queries: [ { queryKey: createConnectQueryKey({ schema: ElizaService.method.say, input, transport, cardinality: "finite", }), queryFn: async ({ signal, }: QueryFunctionContext) => { const res = await callUnaryMethod( transport, ElizaService.method.say, input, { signal, }, ); return res; }, }, ], }); return { query1, }; }, wrapper()); await waitFor(() => { expect(result.current.query1.isSuccess).toBeTruthy(); }); expect(result.current.query1.data?.sentence).toEqual("Response 1"); }); it("can pass headers through", async () => { let resolve: () => void; const promise = new Promise((res) => { resolve = res; }); const transport = mockEliza( { sentence: "Response 1", }, false, { router: { interceptors: [ (next) => (req) => { expect(req.header.get("x-custom-header")).toEqual("custom-value"); resolve(); return next(req); }, ], }, }, ); const input: SayRequest = create(SayRequestSchema, { sentence: "query 1", }); const res = await callUnaryMethod( transport, ElizaService.method.say, input, { headers: { "x-custom-header": "custom-value", }, }, ); await promise; expect(res.sentence).toEqual("Response 1"); }); }); ================================================ FILE: packages/connect-query/src/index.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export * from "@connectrpc/connect-query-core"; export { useTransport, TransportProvider } from "./use-transport.js"; export { useInfiniteQuery, useSuspenseInfiniteQuery, } from "./use-infinite-query.js"; export { useQuery, useSuspenseQuery } from "./use-query.js"; export type { UseMutationOptions } from "./use-mutation.js"; export { useMutation } from "./use-mutation.js"; export type { UseInfiniteQueryOptions } from "./use-infinite-query.js"; export type { UseQueryOptions } from "./use-query.js"; ================================================ FILE: packages/connect-query/src/test/test-wrapper.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { Transport } from "@connectrpc/connect"; import { createConnectTransport } from "@connectrpc/connect-web"; import type { QueryClientConfig } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { JSXElementConstructor, PropsWithChildren } from "react"; import { TransportProvider } from "../use-transport.js"; /** * A utils wrapper that supplies Tanstack Query's `QueryClientProvider` as well as Connect-Query's `TransportProvider`. */ export const wrapper = ( config?: QueryClientConfig, transport = createConnectTransport({ baseUrl: "https://demo.connectrpc.com", }), ): { wrapper: JSXElementConstructor; queryClient: QueryClient; transport: Transport; queryClientWrapper: JSXElementConstructor; } => { const queryClient = new QueryClient(config); return { wrapper: ({ children }) => ( {children} ), queryClient, transport, queryClientWrapper: ({ children }) => ( {children} ), }; }; ================================================ FILE: packages/connect-query/src/use-infinite-query.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import { createConnectQueryKey } from "@connectrpc/connect-query-core"; import { QueryCache, skipToken } from "@tanstack/react-query"; import { renderHook, waitFor } from "@testing-library/react"; import { mockNestedPaginatedTransport, mockPaginatedTransport, } from "test-utils"; import { ListRequestSchema, ListResponseSchema, ListService, } from "test-utils/gen/list_pb.js"; import { describe, expect, it, vi } from "vitest"; import { wrapper } from "./test/test-wrapper.js"; import { useInfiniteQuery, useSuspenseInfiniteQuery, } from "./use-infinite-query.js"; import { useQuery } from "./use-query.js"; // TODO: maybe create a helper to take a service and method and generate this. const methodDescriptor = ListService.method.list; const nestedMethodDescriptor = ListService.method.nestedList; const mockedPaginatedTransport = mockPaginatedTransport(); const mockedNestedPaginatedTransport = mockNestedPaginatedTransport(); describe("useInfiniteQuery", () => { it("can query paginated data", async () => { const { result } = renderHook( () => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapper({}, mockedPaginatedTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data).toEqual({ pageParams: [0n], pages: [ create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), ], }); await result.current.fetchNextPage(); await waitFor(() => { expect(result.current.isFetching).toBeFalsy(); }); expect(result.current.data).toEqual({ pageParams: [0n, 1n], pages: [ create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), create(ListResponseSchema, { items: ["1 Item", "2 Item", "3 Item"], page: 1n, }), ], }); }); it("can be disabled with skipToken", () => { const { result } = renderHook( () => { return useInfiniteQuery(methodDescriptor, skipToken, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }); }, wrapper(undefined, mockedPaginatedTransport), ); expect(result.current.isPending).toBeTruthy(); expect(result.current.isFetching).toBeFalsy(); }); it("can be provided a custom transport", async () => { const customTransport = mockPaginatedTransport({ items: ["Intercepted!"], page: 0n, }); const { result } = renderHook( () => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", transport: customTransport, }, ); }, wrapper({}, mockedPaginatedTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.pages[0].items).toEqual(["Intercepted!"]); }); it("can be provided other props for react-query", () => { const { result } = renderHook( () => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", transport: mockPaginatedTransport(undefined, true), placeholderData: { pageParams: [-1n], pages: [ create(methodDescriptor.output, { page: -1n, items: [], }), ], }, }, ); }, wrapper({}, mockedPaginatedTransport), ); expect(result.current.data?.pages[0].page).toEqual(-1n); }); it("can be used along with the select", async () => { const { result } = renderHook( () => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { select: ({ pages, pageParams }) => ({ pages: pages.map((p) => p.items.join(",")), pageParams: pageParams.map((p) => p?.toString()), }), getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapper({}, mockedPaginatedTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data).toEqual({ pageParams: ["0"], pages: ["-2 Item,-1 Item,0 Item"], }); await result.current.fetchNextPage(); await waitFor(() => { expect(result.current.isFetching).toBeFalsy(); }); expect(result.current.data).toEqual({ pageParams: ["0", "1"], pages: ["-2 Item,-1 Item,0 Item", "1 Item,2 Item,3 Item"], }); }); it("page param doesn't persist to the query cache", async () => { const { queryClient, ...remainingWrapper } = wrapper( {}, mockedPaginatedTransport, ); const { result } = renderHook(() => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, remainingWrapper); const cache = queryClient.getQueryCache().getAll(); expect(cache).toHaveLength(1); expect(cache[0].queryKey).toEqual( createConnectQueryKey({ schema: methodDescriptor, transport: mockedPaginatedTransport, cardinality: "infinite", pageParamKey: "page", input: {}, }), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.pageParams[0]).toEqual(0n); }); it("doesn't share data with a similar non-infinite query", async () => { const remainingWrapper = wrapper({}, mockedPaginatedTransport); const { result } = renderHook(() => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, remainingWrapper); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.pages[0].items).toHaveLength(3); const { result: useQueryResult } = renderHook(() => { return useQuery(methodDescriptor); }, remainingWrapper); await waitFor(() => { expect(useQueryResult.current.isSuccess).toBeTruthy(); }); expect(useQueryResult.current.data?.items).toHaveLength(3); }); it("cache can be invalidated with the shared, non-infinite key", async () => { const onSuccessSpy = vi.fn(); const spiedQueryCache = new QueryCache({ onSuccess: onSuccessSpy, }); const { queryClient, ...remainingWrapper } = wrapper( { queryCache: spiedQueryCache }, mockedPaginatedTransport, ); const { result } = renderHook(() => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, remainingWrapper); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(onSuccessSpy).toHaveBeenCalledTimes(1); await queryClient.invalidateQueries({ queryKey: createConnectQueryKey({ schema: methodDescriptor, transport: mockedPaginatedTransport, cardinality: undefined, pageParamKey: "page", input: { page: 0n, }, }), }); expect(onSuccessSpy).toHaveBeenCalledTimes(2); }); it("cache can be invalidated with a non-exact key", async () => { const onSuccessSpy = vi.fn(); const spiedQueryCache = new QueryCache({ onSuccess: onSuccessSpy, }); const { queryClient, ...remainingWrapper } = wrapper( { queryCache: spiedQueryCache }, mockedPaginatedTransport, ); const { result } = renderHook(() => { return useInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, remainingWrapper); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(onSuccessSpy).toHaveBeenCalledTimes(1); await queryClient.invalidateQueries({ exact: false, queryKey: createConnectQueryKey({ schema: methodDescriptor, cardinality: "infinite", }), }); expect(onSuccessSpy).toHaveBeenCalledTimes(2); }); it("can query paginated data with a non-zero page param", async () => { const wrapperOpts = wrapper({}, mockedPaginatedTransport); const { result } = renderHook(() => { return useInfiniteQuery( methodDescriptor, { page: 1n, preview: true, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapperOpts); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.pages[0].items).toEqual([ "1 Item", "2 Item", "3 Item", ]); const manuallyCreatedQueryKey = createConnectQueryKey({ schema: methodDescriptor, transport: mockedPaginatedTransport, cardinality: "infinite", pageParamKey: "page", input: create(ListRequestSchema, { preview: true, }), }); expect( wrapperOpts.queryClient.getQueryData(manuallyCreatedQueryKey), ).toEqual(result.current.data); }); it("builds nested page input for successive pages", async () => { const wrapperOpts = wrapper({}, mockedNestedPaginatedTransport); const { result } = renderHook(() => { return useInfiniteQuery( nestedMethodDescriptor, { nested: { page: 1n, preview: true, }, }, { getNextPageParam: (lastPage) => (lastPage.nested?.page ?? 0n) + 1n, pageParamKey: "nested.page", }, ); }, wrapperOpts); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.pageParams).toEqual([1n]); expect(result.current.data?.pages.map((page) => page.nested?.page)).toEqual( [1n], ); await result.current.fetchNextPage(); await waitFor(() => { expect(result.current.isFetching).toBeFalsy(); }); await result.current.fetchNextPage(); await waitFor(() => { expect(result.current.isFetching).toBeFalsy(); }); expect(result.current.data?.pageParams).toEqual([1n, 2n, 3n]); expect(result.current.data?.pages.map((page) => page.nested?.page)).toEqual( [1n, 2n, 3n], ); const manuallyCreatedQueryKey = createConnectQueryKey({ schema: nestedMethodDescriptor, transport: mockedNestedPaginatedTransport, cardinality: "infinite", pageParamKey: "nested.page", input: { nested: { page: 1n, preview: true, }, }, }); expect( wrapperOpts.queryClient.getQueryData(manuallyCreatedQueryKey), ).toEqual(result.current.data); }); }); describe("useSuspenseInfiniteQuery", () => { it("can query paginated data", async () => { const { result } = renderHook( () => { return useSuspenseInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapper({}, mockedPaginatedTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data).toEqual({ pageParams: [0n], pages: [ create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), ], }); await result.current.fetchNextPage(); await waitFor(() => { expect(result.current.isFetching).toBeFalsy(); }); expect(result.current.data).toEqual({ pageParams: [0n, 1n], pages: [ create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), create(ListResponseSchema, { items: ["1 Item", "2 Item", "3 Item"], page: 1n, }), ], }); }); // eslint-disable-next-line vitest/expect-expect -- We are asserting via @ts-expect-error it("can not be disabled with skipToken", () => { renderHook( () => { return useSuspenseInfiniteQuery( methodDescriptor, // @ts-expect-error(2345) skipToken is not allowed skipToken, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapper({}, mockedPaginatedTransport), ); }); // eslint-disable-next-line vitest/expect-expect -- We are asserting via @ts-expect-error it("does not allow excess properties", () => { renderHook( () => { return useInfiniteQuery( methodDescriptor, { page: 0n, // @ts-expect-error(2345) extra fields should not be allowed extraField: "extra", }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }, ); }, wrapper({}, mockedPaginatedTransport), ); }); it("can pass headers through", async () => { let resolve: () => void; const promise = new Promise((res) => { resolve = res; }); const transport = mockPaginatedTransport( { items: ["Intercepted!"], page: 0n, }, false, { router: { interceptors: [ (next) => (req) => { expect(req.header.get("x-custom-header")).toEqual("custom-value"); resolve(); return next(req); }, ], }, }, ); const { result } = renderHook(() => { return useSuspenseInfiniteQuery( methodDescriptor, { page: 0n, }, { getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", transport, headers: { "x-custom-header": "custom-value", }, }, ); }, wrapper({})); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); await promise; expect(result.current.data.pages[0].items).toEqual(["Intercepted!"]); }); }); ================================================ FILE: packages/connect-query/src/use-infinite-query.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import type { ConnectError, Transport } from "@connectrpc/connect"; import type { ConnectInfiniteQueryOptions, ConnectQueryKey, MessageInitWithPageParam, MessagePageParamKey, MessagePageParamValue, } from "@connectrpc/connect-query-core"; import { createInfiniteQueryOptions } from "@connectrpc/connect-query-core"; import type { InfiniteData, SkipToken, UseInfiniteQueryOptions as TanStackUseInfiniteQueryOptions, UseInfiniteQueryResult, UseSuspenseInfiniteQueryOptions as TanStackUseSuspenseInfiniteQueryOptions, UseSuspenseInfiniteQueryResult, } from "@tanstack/react-query"; import { useInfiniteQuery as tsUseInfiniteQuery, useSuspenseInfiniteQuery as tsUseSuspenseInfiniteQuery, } from "@tanstack/react-query"; import { useTransport } from "./use-transport.js"; /** * Options for useInfiniteQuery */ export type UseInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, SelectOutData = MessageShape, SelectOutPageParam = unknown, > = Omit< TanStackUseInfiniteQueryOptions< MessageShape, ConnectError, InfiniteData, ConnectQueryKey, MessagePageParamValue, ParamKey> >, "getNextPageParam" | "initialPageParam" | "queryFn" | "queryKey" > & ConnectInfiniteQueryOptions & { /** The transport to be used for the fetching. */ transport?: Transport; }; /** * Query the method provided. Maps to useInfiniteQuery on tanstack/react-query */ export function useInfiniteQuery< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, SelectOutData = MessageShape, SelectOutPageParam = unknown, >( schema: DescMethodUnary, input: SkipToken | MessageInitWithPageParam, ParamKey>, { transport, pageParamKey, getNextPageParam, ...queryOptions }: UseInfiniteQueryOptions, ): UseInfiniteQueryResult< InfiniteData, ConnectError > { const transportFromCtx = useTransport(); const baseOptions = createInfiniteQueryOptions(schema, input, { transport: transport ?? transportFromCtx, getNextPageParam, pageParamKey, }); return tsUseInfiniteQuery({ ...baseOptions, ...queryOptions, }); } /** * Options for useSuspenseInfiniteQuery */ export type UseSuspenseInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, SelectOutData = MessageShape, SelectOutPageParam = unknown, > = Omit< TanStackUseSuspenseInfiniteQueryOptions< MessageShape, ConnectError, InfiniteData, ConnectQueryKey, MessagePageParamValue, ParamKey> >, "getNextPageParam" | "initialPageParam" | "queryFn" | "queryKey" > & ConnectInfiniteQueryOptions & { /** The transport to be used for the fetching. */ transport?: Transport; }; /** * Query the method provided. Maps to useSuspenseInfiniteQuery on tanstack/react-query */ export function useSuspenseInfiniteQuery< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, SelectOutData = MessageShape, SelectOutPageParam = unknown, >( schema: DescMethodUnary, input: MessageInitWithPageParam, ParamKey>, { transport, pageParamKey, getNextPageParam, headers, ...queryOptions }: UseSuspenseInfiniteQueryOptions< I, O, ParamKey, SelectOutData, SelectOutPageParam >, ): UseSuspenseInfiniteQueryResult< InfiniteData, ConnectError > { const transportFromCtx = useTransport(); const baseOptions = createInfiniteQueryOptions(schema, input, { transport: transport ?? transportFromCtx, getNextPageParam, pageParamKey, headers, }); return tsUseSuspenseInfiniteQuery({ ...baseOptions, ...queryOptions, }); } ================================================ FILE: packages/connect-query/src/use-mutation.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import { renderHook, waitFor } from "@testing-library/react"; import { mockPaginatedTransport } from "test-utils"; import { ListResponseSchema, ListService } from "test-utils/gen/list_pb.js"; import { describe, expect, it, vi } from "vitest"; import { wrapper } from "./test/test-wrapper.js"; import { useMutation } from "./use-mutation.js"; // TODO: maybe create a helper to take a service and method and generate this. const methodDescriptor = ListService.method.list; const mockedPaginatedTransport = mockPaginatedTransport(); describe("useMutation", () => { it("performs a mutation", async () => { const onSuccess = vi.fn(); const { result } = renderHook( () => { return useMutation(methodDescriptor, { onSuccess, }); }, wrapper({}, mockedPaginatedTransport), ); result.current.mutate({ page: 0n, }); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(onSuccess).toHaveBeenCalledWith( create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), { page: 0n, }, undefined, ); }); it("can be provided a custom transport", async () => { const { result } = renderHook( () => { return useMutation(methodDescriptor, { transport: mockPaginatedTransport({ page: 1n, items: ["Intercepted!"], }), }); }, wrapper({}, mockedPaginatedTransport), ); result.current.mutate({ page: 0n, }); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.items[0]).toBe("Intercepted!"); }); it("can forward onMutate params", async () => { const onSuccess = vi.fn(); const { result } = renderHook( () => { return useMutation(methodDescriptor, { onMutate: (variables) => { return { somethingElse: `Some additional context: ${(variables.page ?? 0n) + 2n}`, }; }, onSuccess: (data, variables, context) => { onSuccess(data, variables, context); // Customizing on success so we can test the types expect(context.somethingElse).toBe("Some additional context: 2"); }, }); }, wrapper({}, mockedPaginatedTransport), ); result.current.mutate({ page: 0n, }); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(onSuccess).toHaveBeenCalledWith( create(ListResponseSchema, { items: ["-2 Item", "-1 Item", "0 Item"], page: 0n, }), { page: 0n, }, { somethingElse: "Some additional context: 2" }, ); }); }); ================================================ FILE: packages/connect-query/src/use-mutation.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import type { ConnectError, Transport } from "@connectrpc/connect"; import { callUnaryMethod } from "@connectrpc/connect-query-core"; import type { UseMutationOptions as TSUseMutationOptions, UseMutationResult, } from "@tanstack/react-query"; import { useMutation as tsUseMutation } from "@tanstack/react-query"; import { useCallback } from "react"; import { useTransport } from "./use-transport.js"; /** * Options for useMutation */ export type UseMutationOptions< I extends DescMessage, O extends DescMessage, Ctx = unknown, > = TSUseMutationOptions< MessageShape, ConnectError, MessageInitShape, Ctx > & { /** The transport to be used for the fetching. */ transport?: Transport; }; /** * Query the method provided. Maps to useMutation on tanstack/react-query */ export function useMutation< I extends DescMessage, O extends DescMessage, Ctx = unknown, >( schema: DescMethodUnary, { transport, ...queryOptions }: UseMutationOptions = {}, ): UseMutationResult, ConnectError, MessageInitShape, Ctx> { const transportFromCtx = useTransport(); const transportToUse = transport ?? transportFromCtx; const mutationFn = useCallback( async (input: MessageInitShape) => callUnaryMethod(transportToUse, schema, input), [transportToUse, schema], ); return tsUseMutation({ ...queryOptions, mutationFn, }); } ================================================ FILE: packages/connect-query/src/use-query.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import { createConnectQueryKey, skipToken, } from "@connectrpc/connect-query-core"; import { renderHook, waitFor } from "@testing-library/react"; import { mockBigInt, mockEliza } from "test-utils"; import { BigIntService } from "test-utils/gen/bigint_pb.js"; import { ElizaService } from "test-utils/gen/eliza_pb.js"; import { describe, expect, it } from "vitest"; import { wrapper } from "./test/test-wrapper.js"; import { useQuery, useSuspenseQuery } from "./use-query.js"; // TODO: maybe create a helper to take a service and method and generate this. const sayMethodDescriptor = ElizaService.method.say; const mockedElizaTransport = mockEliza(); const bigintTransport = mockBigInt(); const elizaWithDelayTransport = mockEliza(undefined, true); describe("useQuery", () => { it("can query data", async () => { const { result } = renderHook( () => { return useQuery(sayMethodDescriptor, { sentence: "hello", }); }, wrapper({}, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(typeof result.current.data?.sentence).toBe("string"); }); it("can be disabled", () => { const { result } = renderHook( () => { return useQuery(sayMethodDescriptor, skipToken); }, wrapper(undefined, mockedElizaTransport), ); expect(result.current.isPending).toBeTruthy(); expect(result.current.isFetching).toBeFalsy(); }); it("can be provided a custom transport", async () => { const transport = mockEliza({ sentence: "Intercepted!", }); const { result } = renderHook( () => { return useQuery( sayMethodDescriptor, {}, { transport, }, ); }, wrapper(undefined, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.sentence).toBe("Intercepted!"); }); it("can be provided other props for react-query", () => { const { result } = renderHook( () => { return useQuery( sayMethodDescriptor, {}, { transport: elizaWithDelayTransport, placeholderData: create(sayMethodDescriptor.output, { sentence: "placeholder!", }), }, ); }, wrapper(undefined, mockedElizaTransport), ); expect(result.current.data?.sentence).toBe("placeholder!"); }); it("can be used along with the select", async () => { const { result } = renderHook( () => { return useQuery( sayMethodDescriptor, {}, { select: (data) => data.sentence.length, }, ); }, wrapper(undefined, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data).toBe(6); }); it("can be disabled with enabled: false", () => { const { result } = renderHook( () => { return useQuery( sayMethodDescriptor, { sentence: "hello", }, { enabled: false, }, ); }, wrapper({}, mockedElizaTransport), ); expect(result.current.data).toBeUndefined(); expect(result.current.isPending).toBeTruthy(); expect(result.current.isFetching).toBeFalsy(); }); it("can be disabled with enabled: false in QueryClient default options", () => { const { result } = renderHook( () => { return useQuery(sayMethodDescriptor, { sentence: "hello", }); }, wrapper( { defaultOptions: { queries: { enabled: false, }, }, }, mockedElizaTransport, ), ); expect(result.current.data).toBeUndefined(); expect(result.current.isPending).toBeTruthy(); expect(result.current.isFetching).toBeFalsy(); }); it("can be disabled with skipToken", () => { const { result } = renderHook( () => { return useQuery(sayMethodDescriptor, skipToken); }, wrapper({}, mockedElizaTransport), ); expect(result.current.data).toBeUndefined(); expect(result.current.isPending).toBeTruthy(); expect(result.current.isFetching).toBeFalsy(); }); it("supports schemas with bigint keys", async () => { const { result } = renderHook( () => { return useQuery(BigIntService.method.count, { add: 2n, }); }, wrapper({}, bigintTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data?.count).toBe(1n); }); it("data can be fetched from cache", async () => { const { queryClient, ...rest } = wrapper({}, bigintTransport); const { result } = renderHook(() => { return useQuery(BigIntService.method.count, {}); }, rest); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect( queryClient.getQueryData( createConnectQueryKey({ schema: BigIntService.method.count, input: {}, transport: bigintTransport, cardinality: "finite", }), ), ).toBe(result.current.data); }); }); describe("useSuspenseQuery", () => { it("can query data", async () => { const { result } = renderHook( () => { return useSuspenseQuery(sayMethodDescriptor, { sentence: "hello", }); }, wrapper({}, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(typeof result.current.data.sentence).toBe("string"); }); it("can be used along with the select", async () => { const { result } = renderHook( () => { return useSuspenseQuery( sayMethodDescriptor, { sentence: "hello", }, { select: (data) => data.sentence.length, }, ); }, wrapper({}, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); expect(result.current.data).toBe(11); }); it("can pass headers through", async () => { let resolve: () => void; const promise = new Promise((res) => { resolve = res; }); const transport = mockEliza( { sentence: "Response 1", }, false, { router: { interceptors: [ (next) => (req) => { expect(req.header.get("x-custom-header")).toEqual("custom-value"); resolve(); return next(req); }, ], }, }, ); const { result } = renderHook( () => { return useSuspenseQuery( sayMethodDescriptor, { sentence: "hello", }, { transport, headers: { "x-custom-header": "custom-value", }, }, ); }, wrapper({}, mockedElizaTransport), ); await waitFor(() => { expect(result.current.isSuccess).toBeTruthy(); }); await promise; expect(result.current.data.sentence).toBe("Response 1"); }); }); ================================================ FILE: packages/connect-query/src/use-query.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import type { ConnectError, Transport } from "@connectrpc/connect"; import type { ConnectQueryKey, SkipToken, } from "@connectrpc/connect-query-core"; import { createQueryOptions } from "@connectrpc/connect-query-core"; import type { UseQueryOptions as TanStackUseQueryOptions, UseQueryResult, UseSuspenseQueryOptions as TanStackUseSuspenseQueryOptions, UseSuspenseQueryResult, } from "@tanstack/react-query"; import { useQuery as tsUseQuery, useSuspenseQuery as tsUseSuspenseQuery, } from "@tanstack/react-query"; import { useTransport } from "./use-transport.js"; /** * Options for useQuery */ export type UseQueryOptions< O extends DescMessage, SelectOutData = MessageShape, > = Omit< TanStackUseQueryOptions< MessageShape, ConnectError, SelectOutData, ConnectQueryKey >, "queryFn" | "queryKey" > & { /** The transport to be used for the fetching. */ transport?: Transport; }; /** * Query the method provided. Maps to useQuery on tanstack/react-query */ export function useQuery< I extends DescMessage, O extends DescMessage, SelectOutData = MessageShape, >( schema: DescMethodUnary, input?: SkipToken | MessageInitShape, { transport, ...queryOptions }: UseQueryOptions = {}, ): UseQueryResult { const transportFromCtx = useTransport(); const baseOptions = createQueryOptions(schema, input, { transport: transport ?? transportFromCtx, }); return tsUseQuery({ ...baseOptions, ...queryOptions, }); } /** * Options for useSuspenseQuery */ export type UseSuspenseQueryOptions< O extends DescMessage, SelectOutData = 0, > = Omit< TanStackUseSuspenseQueryOptions< MessageShape, ConnectError, SelectOutData, ConnectQueryKey >, "queryFn" | "queryKey" > & { /** The transport to be used for the fetching. */ transport?: Transport; headers?: HeadersInit; }; /** * Query the method provided. Maps to useSuspenseQuery on tanstack/react-query */ export function useSuspenseQuery< I extends DescMessage, O extends DescMessage, SelectOutData = MessageShape, >( schema: DescMethodUnary, input?: MessageInitShape, { transport, headers, ...queryOptions }: UseSuspenseQueryOptions = {}, ): UseSuspenseQueryResult { const transportFromCtx = useTransport(); const baseOptions = createQueryOptions(schema, input, { transport: transport ?? transportFromCtx, headers, }); return tsUseSuspenseQuery({ ...baseOptions, ...queryOptions, }); } ================================================ FILE: packages/connect-query/src/use-transport.test.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { ConnectError } from "@connectrpc/connect"; import { renderHook, waitFor } from "@testing-library/react"; import { mockBigInt } from "test-utils"; import { ElizaService } from "test-utils/gen/eliza_pb.js"; import { describe, expect, it } from "vitest"; import { wrapper } from "./test/test-wrapper.js"; import { useQuery } from "./use-query.js"; import { TransportProvider, useTransport } from "./use-transport.js"; const sayMethodDescriptor = ElizaService.method.say; const error = new ConnectError( "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.", ); describe("useTransport", () => { it("throws the fallback error", async () => { const { result, rerender } = renderHook( () => useQuery(sayMethodDescriptor, undefined, { retry: false }), { wrapper: wrapper().queryClientWrapper, }, ); rerender(); expect(result.current.error).toStrictEqual(null); expect(result.current.isError).toStrictEqual(false); await waitFor(() => { expect(result.current.isError).toStrictEqual(true); }); expect(result.current.error).toEqual(error); }); }); describe("TransportProvider", () => { it("provides a custom transport to the useTransport hook", () => { const transport = mockBigInt(); const { result } = renderHook(() => useTransport(), { wrapper: ({ children }) => ( {children} ), }); expect(result.current).toBe(transport); }); }); ================================================ FILE: packages/connect-query/src/use-transport.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { Transport } from "@connectrpc/connect"; import { ConnectError } from "@connectrpc/connect"; import type { FC, PropsWithChildren } from "react"; import { createContext, useContext } from "react"; const fallbackTransportError = new ConnectError( "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.", ); // istanbul ignore next export const fallbackTransport: Transport = { unary: () => { throw fallbackTransportError; }, stream: () => { throw fallbackTransportError; }, }; const transportContext = createContext(fallbackTransport); /** * Use this helper to get the default transport that's currently attached to the React context for the calling component. */ export const useTransport = () => useContext(transportContext); /** * `TransportProvider` is the main mechanism by which Connect-Query keeps track of the `Transport` used by your application. * * Broadly speaking, "transport" joins two concepts: * * 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}. * 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. * * With these two pieces of information in hand, the transport provides the critical mechanism by which your app can make network requests. * * To learn more about the two modes of transport, take a look at the npm package `@connectrpc/connect-web`. * * To get started with Connect-Query, simply import a transport (either `createConnectTransport` or `createGrpcWebTransport` from `@connectrpc/connect-web`) and pass it to the provider. * * @example * import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; * import { TransportProvider } from "@connectrpc/connect-query"; * * const queryClient = new QueryClient(); * * export const App() { * const transport = createConnectTransport({ * baseUrl: "", * }); * return ( * * * * * * ); * } */ export const TransportProvider: FC< PropsWithChildren<{ transport: Transport; }> > = ({ children, transport }) => ( {children} ); ================================================ FILE: packages/connect-query/tsconfig.build.json ================================================ { "extends": "../../tsconfig.base.json", "files": ["src/index.ts"], "compilerOptions": { "jsx": "react-jsx" } } ================================================ FILE: packages/connect-query/tsconfig.json ================================================ { "include": [ "**/*.test.ts", "**/*.test.tsx", "src/test/**.tsx", "vite.config.ts" ], "extends": "./tsconfig.build.json" } ================================================ FILE: packages/connect-query/vite.config.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: "jsdom", typecheck: { enabled: true, // Modified to typecheck definition files as well as source files include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], }, coverage: { provider: "istanbul", thresholds: { branches: 100, functions: 100, lines: 100, statements: 100, }, }, }, }); ================================================ FILE: packages/connect-query-core/README.md ================================================ # @connectrpc/connect-query-core 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`). ================================================ FILE: packages/connect-query-core/package.json ================================================ { "name": "@connectrpc/connect-query-core", "version": "2.2.0", "description": "Core of Connect-Query, framework agnostic helpers for type-safe queries.", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/connectrpc/connect-query-es.git", "directory": "packages/connect-query-core" }, "scripts": { "prebuild": "rm -rf ./dist/*", "build": "npm run build:cjs && npm run build:esm", "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\"}'", "build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", "test": "vitest --run", "test:watch": "vitest --watch", "format": "prettier --write --ignore-unknown '.' '!dist'", "license-header": "license-header", "lint": "eslint --max-warnings 0 .", "attw": "attw --pack" }, "type": "module", "sideEffects": false, "main": "./dist/cjs/index.js", "exports": { ".": { "import": "./dist/esm/index.js", "require": "./dist/cjs/index.js" } }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.1", "@bufbuild/buf": "1.54.0", "@bufbuild/jest-environment-jsdom": "^0.1.1", "@bufbuild/protobuf": "^2.5.1", "@bufbuild/protoc-gen-es": "^2.5.1", "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "test-utils": "*", "typescript": "^5.8.3", "@tanstack/query-core": "^5.79.0" }, "peerDependencies": { "@bufbuild/protobuf": "2.x", "@connectrpc/connect": "^2.0.1", "@tanstack/query-core": ">=5.62.0" }, "files": [ "dist/**" ] } ================================================ FILE: packages/connect-query-core/src/call-unary-method.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; import type { Transport } from "@connectrpc/connect"; /** * Call a unary method given its signature and input. */ // eslint-disable-next-line @typescript-eslint/max-params -- 4th param is optional export async function callUnaryMethod< I extends DescMessage, O extends DescMessage, >( transport: Transport, schema: DescMethodUnary, input: MessageInitShape | undefined, options?: { signal?: AbortSignal; headers?: HeadersInit; }, ): Promise> { const result = await transport.unary( schema, options?.signal, undefined, options?.headers, input ?? create(schema.input), undefined, ); return result.message; } ================================================ FILE: packages/connect-query-core/src/connect-query-key.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import type { MessageInitShape } from "@bufbuild/protobuf"; import type { Transport } from "@connectrpc/connect"; import { ElizaService, SayRequestSchema, SayResponseSchema, type SayResponse, } from "test-utils/gen/eliza_pb.js"; import { NestedListRequestSchema, ListRequestSchema, ListResponseSchema, ListService, type ListResponse, } from "test-utils/gen/list_pb.js"; import { describe, expect, expectTypeOf, it } from "vitest"; import { createConnectQueryKey } from "./connect-query-key.js"; import { skipToken } from "./index.js"; import type { MessagePageParamKey } from "./index.js"; import { createMessageKey } from "./message-key.js"; import { createTransportKey } from "./transport-key.js"; import { type InfiniteData, QueryClient } from "@tanstack/query-core"; describe("createConnectQueryKey", () => { const fakeTransport: Transport = { async stream() { return Promise.reject(new Error("unexpected")); }, async unary() { return Promise.reject(new Error("unexpected")); }, }; it("creates a full key", () => { const key = createConnectQueryKey({ transport: fakeTransport, schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", }); expect(key).toStrictEqual([ "connect-query", { transport: createTransportKey(fakeTransport), serviceName: "connectrpc.eliza.v1.ElizaService", methodName: "Say", cardinality: "finite", input: createMessageKey(SayRequestSchema, { sentence: "hi" }), }, ]); }); it("creates a full infinite key", () => { const key = createConnectQueryKey({ transport: fakeTransport, schema: ListService.method.list, input: create(ListRequestSchema, { page: 0n }), pageParamKey: "page", cardinality: "infinite", }); expect(key).toStrictEqual([ "connect-query", { transport: createTransportKey(fakeTransport), serviceName: "ListService", methodName: "List", cardinality: "infinite", input: createMessageKey(ListRequestSchema, {}), }, ]); }); it("creates a full infinite key with nested pageParamKey", () => { const key = createConnectQueryKey({ transport: fakeTransport, schema: ListService.method.nestedList, input: create(NestedListRequestSchema, { nested: { page: 0n } }), pageParamKey: "nested.page", cardinality: "infinite", }); expect(key).toStrictEqual([ "connect-query", { transport: createTransportKey(fakeTransport), serviceName: "ListService", methodName: "NestedList", cardinality: "infinite", input: createMessageKey(NestedListRequestSchema, { nested: {} }), }, ]); }); it("rejects an invalid nested pageParamKey segment", () => { type Key = MessagePageParamKey< MessageInitShape >; // @ts-expect-error(2322) nested segment must be a valid key const invalidPageParamKey: Key = "nested.p@ge"; expect(invalidPageParamKey).toBeDefined(); }); it("rejects array prototype key paths", () => { type Key = MessagePageParamKey<{ nested: { page: bigint; }; items: string[]; }>; // @ts-expect-error(2322) array prototype keys are not valid path segments const invalidArrayPrototypePath: Key = "items.pop"; expect(invalidArrayPrototypePath).toBeDefined(); }); it("rejects protobuf internal keys", () => { type Key = MessagePageParamKey>; // @ts-expect-error(2322) protobuf internal key is not supported const invalidTypeNameKey: Key = "$typeName"; // @ts-expect-error(2322) protobuf internal key is not supported const invalidUnknownKey: Key = "$unknown"; expect(invalidTypeNameKey).toBeDefined(); expect(invalidUnknownKey).toBeDefined(); }); it("allows input: undefined", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, input: undefined, cardinality: "finite", }); expect(key[1].input).toBeUndefined(); }); it("allows to omit input", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, cardinality: "finite", }); expect(key[1].input).toBeUndefined(); }); it("allows input: skipToken", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, input: skipToken, cardinality: "finite", }); expect(key[1].input).toBe("skipped"); }); it("allows to set cardinality: finite", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, cardinality: "finite", }); expect(key[1].cardinality).toBe("finite"); }); it("allows to set cardinality: undefined", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, cardinality: undefined, }); expect(key[1].cardinality).toBeUndefined(); }); it("allows to set a service schema", () => { const key = createConnectQueryKey({ schema: ElizaService, cardinality: "finite", }); expect(key[1].serviceName).toBe(ElizaService.typeName); expect(key[1].methodName).toBeUndefined(); }); it("cannot except invalid input", () => { createConnectQueryKey({ // @ts-expect-error(2322) cannot create a key with invalid input schema: ElizaService.method.say, input: { sentence: 1, }, cardinality: undefined, }); }); it("contains type hints to indicate the output type", () => { const sampleQueryClient = new QueryClient(); const key = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", }); const data = sampleQueryClient.getQueryData(key); expectTypeOf(data).toEqualTypeOf(); }); it("supports typesafe data updaters", () => { const sampleQueryClient = new QueryClient(); const key = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", }); // @ts-expect-error(2345) this is a test to check if the type is correct sampleQueryClient.setQueryData(key, { sentence: 1 }); // @ts-expect-error(2345) $typename is required sampleQueryClient.setQueryData(key, { sentence: "a proper value but missing $typename", }); sampleQueryClient.setQueryData( key, create(SayResponseSchema, { sentence: "a proper value" }), ); sampleQueryClient.setQueryData(key, (prev) => { expectTypeOf(prev).toEqualTypeOf(); return create(SayResponseSchema, { sentence: "a proper value", }); }); }); describe("headers", () => { it("allows headers to be passed as an object", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: { "x-custom-header": "custom-value", }, }); expect(key[1].headers).toEqual({ "x-custom-header": "custom-value", }); }); it("allows headers to be passed as a tuple", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: [["x-custom-header", "custom-value"]], }); expect(key[1].headers).toEqual({ "x-custom-header": "custom-value", }); }); it("allows headers to be passed as a HeadersInit", () => { const key = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: new Headers({ "x-custom-header": "custom-value", }), }); expect(key[1].headers).toEqual({ "x-custom-header": "custom-value", }); }); it("normalizes header values", () => { const keyA = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: { foo: "a", Foo: "b", }, }); const keyB = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: { foo: "a, b", }, }); const keyC = createConnectQueryKey({ schema: ElizaService.method.say, input: create(SayRequestSchema, { sentence: "hi" }), cardinality: "finite", headers: [ ["foo", "a"], ["foo", "b"], ], }); expect(keyA[1].headers).toEqual(keyB[1].headers); expect(keyA[1].headers).toEqual(keyC[1].headers); }); }); describe("infinite queries", () => { it("contains type hints to indicate the output type", () => { const sampleQueryClient = new QueryClient(); const key = createConnectQueryKey({ schema: ListService.method.list, input: create(ListRequestSchema, { page: 0n }), cardinality: "infinite", pageParamKey: "page", }); const data = sampleQueryClient.getQueryData(key); expectTypeOf(data).toEqualTypeOf< InfiniteData | undefined >(); }); it("supports typesafe data updaters", () => { const sampleQueryClient = new QueryClient(); const key = createConnectQueryKey({ schema: ListService.method.list, input: create(ListRequestSchema, { page: 0n }), cardinality: "infinite", pageParamKey: "page", }); sampleQueryClient.setQueryData(key, { pages: [ // @ts-expect-error(2345) make sure the shape is as expected { sentence: 1 }, ], }); sampleQueryClient.setQueryData(key, { // @ts-expect-error(2345) $typename is required pages: [{ sentence: "a proper value but missing $typename" }], }); sampleQueryClient.setQueryData(key, { pageParams: [0], pages: [create(ListResponseSchema, { items: ["a proper value"] })], }); sampleQueryClient.setQueryData(key, (prev) => { expectTypeOf(prev).toEqualTypeOf< InfiniteData | undefined >(); return { pageParams: [0], pages: [create(ListResponseSchema, { items: ["a proper value"] })], }; }); }); }); }); ================================================ FILE: packages/connect-query-core/src/connect-query-key.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethod, DescMethodUnary, DescService, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import type { ConnectError, Transport } from "@connectrpc/connect"; import type { DataTag, InfiniteData, SkipToken } from "@tanstack/query-core"; import { createMessageKey } from "./message-key.js"; import type { MessagePageParamKey } from "./page-param-key.js"; import { createTransportKey } from "./transport-key.js"; type SharedConnectQueryOptions = { /** * A key for a Transport reference, created with createTransportKey(). */ transport?: string; /** * The name of the service, e.g. connectrpc.eliza.v1.ElizaService */ serviceName: string; /** * The name of the method, e.g. Say. */ methodName?: string; /** * A key for the request message, created with createMessageKey(), * or "skipped". */ input?: Record | "skipped"; /** * Headers to be sent with the request. * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. */ headers?: Record; }; type InfiniteConnectQueryKey = DataTag< [ "connect-query", SharedConnectQueryOptions & { /** This data represents a infinite, paged result */ cardinality: "infinite"; }, ], InfiniteData>, ConnectError >; type FiniteConnectQueryKey = DataTag< [ "connect-query", SharedConnectQueryOptions & { /** This data represents a finite result */ cardinality: "finite"; }, ], MessageShape, ConnectError >; /** * 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. * * 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: * * @example * [ * "connect-query", * { * transport: "t1", * serviceName: "connectrpc.eliza.v1.ElizaService", * methodName: "Say", * input: { * sentence: "hello there", * }, * cardinality: "finite", * } * ] */ export type ConnectQueryKey = | InfiniteConnectQueryKey | FiniteConnectQueryKey | [ "connect-query", SharedConnectQueryOptions & { cardinality: undefined; }, ]; type KeyParamsForMethod = { /** * Set `serviceName` and `methodName` in the key. */ schema: Desc; /** * Set `input` in the key: * - If a SkipToken is provided, `input` is "skipped". * - If an init shape is provided, `input` is set to a message key. * - If omitted or undefined, `input` is not set in the key. */ input?: MessageInitShape | SkipToken | undefined; /** * Set `transport` in the key. */ transport?: Transport; /** * Set `cardinality` in the key - undefined is used for filters to match both finite and infinite queries. */ cardinality: "finite" | "infinite" | undefined; /** * If omit the field with this name from the key for infinite queries. */ pageParamKey?: MessagePageParamKey>; /** * Set `headers` in the key. * Note that invalid HTTP header names will raise a TypeError, and that the Set-Cookie header is not supported. */ headers?: HeadersInit; }; type KeyParamsForService = { /** * Set `serviceName` in the key, and omit `methodName`. */ schema: Desc; /** * Set `transport` in the key. */ transport?: Transport; /** * Set `cardinality` in the key - undefined is used for filters to match both finite and infinite queries. */ cardinality: "finite" | "infinite" | undefined; }; /** * 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. * * When you make a query, a unique key is automatically created from the schema, input message, and transport. For example: * * ```ts * import { useQuery } from "@connectrpc/connect-query"; * * useQuery(ElizaService.method.say, { sentence: "hello" }); * * // creates the key: * [ * "connect-query", * { * transport: "t1", * serviceName: "connectrpc.eliza.v1.ElizaService", * methodName: "Say", * input: { sentence: "hello" }, * cardinality: "finite", * } * ] * ``` * * The same key can be created manually with this factory: * * ```ts * createConnectQueryKey({ * transport: myTransportReference, * schema: ElizaService.method.say, * input: { sentence: "hello" } * }); * ``` * * 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. * * @see ConnectQueryKey for information on the components of Connect-Query's keys. */ export function createConnectQueryKey< I extends DescMessage, O extends DescMessage, >( params: KeyParamsForMethod> & { cardinality: "finite"; }, ): FiniteConnectQueryKey; export function createConnectQueryKey< I extends DescMessage, O extends DescMessage, >( params: KeyParamsForMethod> & { cardinality: "infinite"; }, ): InfiniteConnectQueryKey; export function createConnectQueryKey< I extends DescMessage, O extends DescMessage, >( params: KeyParamsForMethod> & { cardinality: undefined; }, ): ConnectQueryKey; export function createConnectQueryKey< O extends DescMessage, Desc extends DescService, >(params: KeyParamsForService): ConnectQueryKey; export function createConnectQueryKey< I extends DescMessage, O extends DescMessage, Desc extends DescService, >( params: KeyParamsForMethod> | KeyParamsForService, ): ConnectQueryKey { const props: { serviceName: string; methodName?: string; transport?: string; cardinality?: "finite" | "infinite"; input?: "skipped" | Record; headers?: Record; } = params.schema.kind == "rpc" ? { serviceName: params.schema.parent.typeName, methodName: params.schema.name, } : { serviceName: params.schema.typeName, }; if (params.transport !== undefined) { props.transport = createTransportKey(params.transport); } if (params.cardinality !== undefined) { props.cardinality = params.cardinality; } if (params.schema.kind == "rpc" && "input" in params) { if (typeof params.input == "symbol") { props.input = "skipped"; } else if (params.input !== undefined) { props.input = createMessageKey( params.schema.input, params.input, params.pageParamKey, ); } } if ( params.schema.kind === "rpc" && "headers" in params && params.headers !== undefined ) { props.headers = createHeadersKey(params.headers); } return ["connect-query", props] as ConnectQueryKey; } /** * Creates a record of headers from a HeadersInit object. * */ function createHeadersKey(headers: HeadersInit): Record { const result: Record = {}; for (const [key, value] of new Headers(headers)) { result[key] = value; } return result; } ================================================ FILE: packages/connect-query-core/src/create-infinite-query-options.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { mockEliza } from "test-utils"; import { ListService } from "test-utils/gen/list_pb.js"; import { describe, expect, expectTypeOf, it } from "vitest"; import { createInfiniteQueryOptions, skipToken } from "./index.js"; const listMethod = ListService.method.list; const nestedListMethod = ListService.method.nestedList; const mockedElizaTransport = mockEliza(); describe("createInfiniteQueryOptions", () => { it("honors skipToken", () => { const opt = createInfiniteQueryOptions(listMethod, skipToken, { transport: mockedElizaTransport, getNextPageParam: (lastPage) => lastPage.page + 1n, pageParamKey: "page", }); expect(opt.queryFn).toBe(skipToken); expectTypeOf(opt.queryFn).toEqualTypeOf(skipToken); }); it("allows nested pageParamKey as key path array", () => { const initialPage = 10n; const opt = createInfiniteQueryOptions( nestedListMethod, { nested: { page: initialPage, }, }, { transport: mockedElizaTransport, getNextPageParam: (lastPage) => (lastPage.nested?.page ?? 0n) + 1n, pageParamKey: "nested.page", }, ); expect(opt.initialPageParam).toBe(initialPage); expect(opt.queryKey[1].input).toStrictEqual({ nested: {} }); expectTypeOf(opt.initialPageParam).toEqualTypeOf(); }); }); ================================================ FILE: packages/connect-query-core/src/create-infinite-query-options.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import type { Transport } from "@connectrpc/connect"; import type { GetNextPageParamFunction, QueryFunction, SkipToken, } from "@tanstack/query-core"; import { skipToken } from "@tanstack/query-core"; import { callUnaryMethod } from "./call-unary-method.js"; import { type ConnectQueryKey, createConnectQueryKey, } from "./connect-query-key.js"; import { getValueAtPath, type MessageInitWithPageParam, type MessagePageParamKey, type MessagePageParamValue, setValueAtPath, } from "./page-param-key.js"; import { createStructuralSharing } from "./structural-sharing.js"; import { assert } from "./utils.js"; /** * Return type of createInfiniteQueryOptions assuming SkipToken was not provided. */ export interface InfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, > { getNextPageParam: ConnectInfiniteQueryOptions< I, O, ParamKey >["getNextPageParam"]; queryKey: ConnectQueryKey; queryFn: QueryFunction< MessageShape, ConnectQueryKey, MessagePageParamValue, ParamKey> >; structuralSharing: (oldData: unknown, newData: unknown) => unknown; initialPageParam: MessagePageParamValue, ParamKey>; } /** * Return type of createInfiniteQueryOptions when SkipToken is provided */ export interface InfiniteQueryOptionsWithSkipToken< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, > extends Omit, "queryFn"> { queryFn: SkipToken; } /** * Options specific to connect-query */ export interface ConnectInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, > { /** Defines which part of the input should be considered the page param */ pageParamKey: ParamKey; /** Determines the next page. */ getNextPageParam: GetNextPageParamFunction< MessagePageParamValue, ParamKey>, MessageShape >; headers?: HeadersInit; } // eslint-disable-next-line @typescript-eslint/max-params -- we have 4 required arguments function createUnaryInfiniteQueryFn< I extends DescMessage, O extends DescMessage, ParamKey extends MessagePageParamKey>, >( transport: Transport, schema: DescMethodUnary, input: MessageInitShape, { pageParamKey, }: { pageParamKey: ParamKey; }, ): QueryFunction< MessageShape, ConnectQueryKey, MessagePageParamValue, ParamKey> > { return async (context) => { assert("pageParam" in context, "pageParam must be part of context"); const inputCombinedWithPageParam = setValueAtPath( input, pageParamKey as MessagePageParamKey>, context.pageParam, ) as MessageInitShape; return callUnaryMethod(transport, schema, inputCombinedWithPageParam, { signal: context.signal, headers: context.queryKey[1].headers, }); }; } /** * Query the method provided. Maps to useInfiniteQuery on tanstack/react-query */ export function createInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: MessageInitWithPageParam, ParamKey>, { transport, getNextPageParam, pageParamKey, headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): InfiniteQueryOptions; export function createInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: SkipToken, { transport, getNextPageParam, pageParamKey, headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): InfiniteQueryOptionsWithSkipToken; export function createInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: SkipToken | MessageInitWithPageParam, ParamKey>, { transport, getNextPageParam, pageParamKey, headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): | InfiniteQueryOptions | InfiniteQueryOptionsWithSkipToken; export function createInfiniteQueryOptions< I extends DescMessage, O extends DescMessage, const ParamKey extends MessagePageParamKey>, >( schema: DescMethodUnary, input: SkipToken | MessageInitWithPageParam, ParamKey>, { transport, getNextPageParam, pageParamKey, headers, }: ConnectInfiniteQueryOptions & { transport: Transport }, ): | InfiniteQueryOptions | InfiniteQueryOptionsWithSkipToken { const queryKey = createConnectQueryKey({ cardinality: "infinite", schema, transport, input, pageParamKey, headers, }); const structuralSharing = createStructuralSharing(schema.output); const queryFn = input === skipToken ? skipToken : createUnaryInfiniteQueryFn(transport, schema, input, { pageParamKey, }); return { getNextPageParam, initialPageParam: input === skipToken ? (undefined as MessagePageParamValue, ParamKey>) : (getValueAtPath( input, pageParamKey as MessagePageParamKey>, ) as MessagePageParamValue, ParamKey>), queryKey, queryFn, structuralSharing, }; } ================================================ FILE: packages/connect-query-core/src/create-query-options.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { skipToken as tanstackSkipToken } from "@tanstack/query-core"; import { mockEliza } from "test-utils"; import { ElizaService } from "test-utils/gen/eliza_pb.js"; import { describe, expect, expectTypeOf, it } from "vitest"; import { createConnectQueryKey } from "./connect-query-key.js"; import { createQueryOptions } from "./create-query-options.js"; import { skipToken } from "./index.js"; // TODO: maybe create a helper to take a service and method and generate this. const sayMethodDescriptor = ElizaService.method.say; const mockedElizaTransport = mockEliza(); describe("createQueryOptions", () => { it("honors skipToken", () => { const opt = createQueryOptions(sayMethodDescriptor, skipToken, { transport: mockedElizaTransport, }); expect(opt.queryFn).toBe(skipToken); expectTypeOf(opt.queryFn).toEqualTypeOf(skipToken); }); it("honors skipToken directly from tanstack", () => { const opt = createQueryOptions(sayMethodDescriptor, tanstackSkipToken, { transport: mockedElizaTransport, }); expect(opt.queryFn).toBe(tanstackSkipToken); }); it("sets queryKey", () => { const want = createConnectQueryKey({ schema: sayMethodDescriptor, input: { sentence: "hi" }, transport: mockedElizaTransport, cardinality: "finite", headers: { "x-custom-header": "custom-value", }, }); const opt = createQueryOptions( sayMethodDescriptor, { sentence: "hi" }, { transport: mockedElizaTransport, headers: { "x-custom-header": "custom-value", }, }, ); expect(opt.queryKey).toStrictEqual(want); }); it("ensures type safety of parameters", () => { // @ts-expect-error(2322) cannot provide invalid parameters createQueryOptions( sayMethodDescriptor, { sentence: 1, }, { transport: mockedElizaTransport, }, ); }); }); ================================================ FILE: packages/connect-query-core/src/create-query-options.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; import type { Transport } from "@connectrpc/connect"; import type { QueryFunction, SkipToken } from "@tanstack/query-core"; import { skipToken } from "@tanstack/query-core"; import { callUnaryMethod } from "./call-unary-method.js"; import type { ConnectQueryKey } from "./connect-query-key.js"; import { createConnectQueryKey } from "./connect-query-key.js"; import { createStructuralSharing } from "./structural-sharing.js"; /** * Return type of createQueryOptions */ export interface QueryOptions { queryKey: ConnectQueryKey; queryFn: QueryFunction, ConnectQueryKey>; structuralSharing: (oldData: unknown, newData: unknown) => unknown; } export interface QueryOptionsWithSkipToken extends Omit, "queryFn"> { queryFn: SkipToken; } function createUnaryQueryFn( transport: Transport, schema: DescMethodUnary, input: MessageInitShape | undefined, ): QueryFunction, ConnectQueryKey> { return async (context) => { return callUnaryMethod(transport, schema, input, { signal: context.signal, headers: context.queryKey[1].headers, }); }; } /** * Creates all options required to make a query. Useful in combination with `useQueries` from tanstack/react-query. */ export function createQueryOptions< I extends DescMessage, O extends DescMessage, >( schema: DescMethodUnary, input: MessageInitShape | undefined, { transport, headers, }: { transport: Transport; headers?: HeadersInit; }, ): QueryOptions; export function createQueryOptions< I extends DescMessage, O extends DescMessage, >( schema: DescMethodUnary, input: SkipToken, { transport, headers, }: { transport: Transport; headers?: HeadersInit; }, ): QueryOptionsWithSkipToken; export function createQueryOptions< I extends DescMessage, O extends DescMessage, >( schema: DescMethodUnary, input: SkipToken | MessageInitShape | undefined, { transport, headers, }: { transport: Transport; headers?: HeadersInit; }, ): QueryOptions | QueryOptionsWithSkipToken; export function createQueryOptions< I extends DescMessage, O extends DescMessage, >( schema: DescMethodUnary, input: SkipToken | MessageInitShape | undefined, { transport, headers, }: { transport: Transport; headers?: HeadersInit; }, ): QueryOptions | QueryOptionsWithSkipToken { const queryKey = createConnectQueryKey({ schema, input: input ?? create(schema.input), transport, cardinality: "finite", headers, }); const structuralSharing = createStructuralSharing(schema.output); const queryFn = input === skipToken ? skipToken : createUnaryQueryFn(transport, schema, input); return { queryKey, queryFn, structuralSharing, }; } ================================================ FILE: packages/connect-query-core/src/index.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export type { ConnectQueryKey } from "./connect-query-key.js"; export { createConnectQueryKey } from "./connect-query-key.js"; export { createProtobufSafeUpdater } from "./utils.js"; export type { ConnectUpdater } from "./utils.js"; export { callUnaryMethod } from "./call-unary-method.js"; export { createInfiniteQueryOptions } from "./create-infinite-query-options.js"; export type { ConnectInfiniteQueryOptions, InfiniteQueryOptionsWithSkipToken, InfiniteQueryOptions, } from "./create-infinite-query-options.js"; export type { MessageInitWithPageParam, MessagePageParamKey, MessagePageParamValue, } from "./page-param-key.js"; export { createQueryOptions } from "./create-query-options.js"; export type { QueryOptions, QueryOptionsWithSkipToken, } from "./create-query-options.js"; export { addStaticKeyToTransport } from "./transport-key.js"; export type { SkipToken } from "@tanstack/query-core"; export { skipToken } from "@tanstack/query-core"; ================================================ FILE: packages/connect-query-core/src/message-key.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import { Proto2MessageSchema } from "test-utils/gen/proto2_pb.js"; import { Proto3Enum, Proto3MessageSchema } from "test-utils/gen/proto3_pb.js"; import { describe, expect, it } from "vitest"; import { createMessageKey } from "./message-key.js"; describe("message key", () => { it("omits proto3 default values", () => { const schema = Proto3MessageSchema; const message = create(schema); const key = createMessageKey(schema, message); expect(key).toStrictEqual({}); }); it("omits proto2 default values", () => { const schema = Proto2MessageSchema; const message = create(schema); const key = createMessageKey(schema, message); expect(key).toStrictEqual({}); }); it("omits the pageParamKey", () => { const schema = Proto3MessageSchema; const message = create(schema, { int32Field: 123, stringField: "abc", }); const key = createMessageKey(schema, message, "int32Field"); expect(key).toStrictEqual({ stringField: "abc", }); }); it("omits nested pageParamKey", () => { const key = createMessageKey( Proto3MessageSchema, { messageField: { int32Field: 1, stringField: "abc", }, }, "messageField.int32Field", ); expect(key).toStrictEqual({ messageField: { stringField: "abc", }, }); }); it("converts as expected", () => { const key = createMessageKey(Proto3MessageSchema, { int64Field: 123n, bytesField: new Uint8Array([0xde, 0xad, 0xbe, 0xef]), doubleField: Number.NaN, messageField: { doubleField: Infinity, messageField: { doubleField: -Infinity, }, }, boolField: true, enumField: Proto3Enum.YES, repeatedStringField: ["a", "b"], repeatedMessageField: [{ int64Field: 456n }], repeatedEnumField: [Proto3Enum.YES, Proto3Enum.NO], either: { case: "oneofInt32Field", value: 123, }, mapStringInt64Field: { foo: 123n, }, mapStringMessageField: { foo: { int64Field: 123n, }, }, mapStringEnumField: { foo: Proto3Enum.YES, }, }); expect(key).toStrictEqual({ int64Field: "123", bytesField: "3q2+7w", doubleField: "NaN", messageField: { doubleField: "Infinity", messageField: { doubleField: "-Infinity", }, }, boolField: true, enumField: 1, repeatedStringField: ["a", "b"], repeatedMessageField: [{ int64Field: "456" }], repeatedEnumField: [1, 2], oneofInt32Field: 123, mapStringInt64Field: { foo: "123", }, mapStringMessageField: { foo: { int64Field: "123", }, }, mapStringEnumField: { foo: 1, }, }); }); it("sorts map keys", () => { const key = createMessageKey(Proto3MessageSchema, { mapStringInt64Field: { b: 2n, a: 1n, }, }); const mapKeys = typeof key.mapStringInt64Field == "object" && key.mapStringInt64Field !== null ? Object.keys(key.mapStringInt64Field) : []; expect(mapKeys).toStrictEqual(["a", "b"]); }); it("sorts properties by protobuf source order", () => { const key = createMessageKey(Proto3MessageSchema, { boolField: true, stringField: "a", }); expect(Object.keys(key)).toStrictEqual(["stringField", "boolField"]); }); }); ================================================ FILE: packages/connect-query-core/src/message-key.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, MessageInitShape } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; import type { ReflectList, ReflectMap, ReflectMessage, } from "@bufbuild/protobuf/reflect"; import { reflect } from "@bufbuild/protobuf/reflect"; import { base64Encode } from "@bufbuild/protobuf/wire"; import { pageParamPathSegments, type MessagePageParamKey, } from "./page-param-key.js"; /** * For any given message, create an object that is suitable for a Query Key in * TanStack Query: * * - Default values are omitted (both implicit and explicit field presence). * - NaN, Infinity, and -Infinity are converted to a string. * - Uint8Array is encoded to a string with Base64. * - BigInt values are converted to a string. * - Properties are sorted by Protobuf source order. * - Map keys are sorted with Array.sort. * * If pageParamKey is provided, omit the field with this name from the key. */ export function createMessageKey< Desc extends DescMessage, PageParamKey extends MessagePageParamKey>, >( schema: Desc, value: MessageInitShape, pageParamKey?: PageParamKey, ): Record { const pageParamPath = pageParamKey === undefined ? undefined : pageParamPathSegments( pageParamKey as MessagePageParamKey>, ); return messageKey(reflect(schema, create(schema, value)), pageParamPath); } function scalarKey(value: unknown): unknown { if (typeof value == "bigint") { return String(value); } if (typeof value == "number" && !isFinite(value)) { return String(value); } if (value instanceof Uint8Array) { return base64Encode(value, "std_raw"); } return value; } function listKey(list: ReflectList): unknown[] { const arr = Array.from(list); const { listKind } = list.field(); if (listKind == "scalar") { return arr.map(scalarKey); } if (listKind == "message") { // eslint-disable-next-line @typescript-eslint/no-use-before-define -- circular reference return (arr as ReflectMessage[]).map((m) => messageKey(m)); } return arr; } function mapKey(map: ReflectMap): Record { // eslint-disable-next-line @typescript-eslint/require-array-sort-compare -- we want the standard behavior return Array.from(map.keys()) .sort() .reduce>((result, k) => { switch (map.field().mapKind) { case "message": // eslint-disable-next-line @typescript-eslint/no-use-before-define -- circular reference result[k as string] = messageKey(map.get(k) as ReflectMessage); break; case "scalar": result[k as string] = scalarKey(map.get(k)); break; case "enum": result[k as string] = map.get(k); break; } return result; }, {}); } function messageKey( message: ReflectMessage, pageParamPath?: string[], ): Record { const result: Record = {}; for (const f of message.sortedFields) { if (!message.isSet(f)) { continue; } const includesPageParam = pageParamPath !== undefined && f.localName === pageParamPath[0]; if (includesPageParam && pageParamPath.length === 1) { continue; } switch (f.fieldKind) { case "scalar": result[f.localName] = scalarKey(message.get(f)); break; case "enum": result[f.localName] = message.get(f); break; case "list": result[f.localName] = listKey(message.get(f)); break; case "map": result[f.localName] = mapKey(message.get(f)); break; case "message": result[f.localName] = messageKey( message.get(f), includesPageParam ? pageParamPath.slice(1) : undefined, ); break; } } return result; } ================================================ FILE: packages/connect-query-core/src/page-param-key.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { Message } from "@bufbuild/protobuf"; type StringKeyOf = Exclude, keyof Message>; type PrevDepth = [never, 0, 1, 2, 3, 4, 5]; type CanDescend = T extends readonly unknown[] ? false : T extends object ? true : false; type MessagePageParamPathString = Depth extends 0 ? never : T extends object ? { [K in StringKeyOf]: CanDescend> extends true ? | K | `${K}.${MessagePageParamPathString< NonNullable, PrevDepth[Depth] >}` : K; }[StringKeyOf] : never; /** * A page param key can be a root key, * or a dot-separated key path. */ export type MessagePageParamKey = MessagePageParamPathString; type DotPathValue = P extends `${infer Head}.${infer Tail}` ? Head extends StringKeyOf ? DotPathValue, Tail> : never : P extends StringKeyOf ? T[P] : never; /** * Resolves the value type at a page param key path. */ export type MessagePageParamValue< T, K extends MessagePageParamKey, > = K extends string ? DotPathValue : never; type RootKey = K extends `${infer Head}.${string}` ? Head : K extends StringKeyOf> ? K : never; /** * Requires the root object key for a page param path. */ export type MessageInitWithPageParam> = T & Required, StringKeyOf>>>; export function pageParamPathSegments( pageParamKey: MessagePageParamKey>, ): string[] { return pageParamKey.split("."); } export function getValueAtPath( value: Record, pageParamKey: MessagePageParamKey>, ): unknown { const path = pageParamPathSegments(pageParamKey); let current: unknown = value; for (const segment of path) { if ( typeof current !== "object" || current === null || !(segment in current) ) { return undefined; } current = (current as Record)[segment]; } return current; } export function setValueAtPath( value: Record, pageParamKey: MessagePageParamKey>, pageParam: unknown, ): Record { const path = pageParamPathSegments(pageParamKey); const result: Record = { ...value }; let source: Record = value; let target = result; for (let i = 0; i < path.length - 1; i++) { const key = path[i]; const sourceNext = source[key]; const targetNext = typeof sourceNext === "object" && sourceNext !== null ? { ...(sourceNext as Record) } : {}; target[key] = targetNext; target = targetNext; source = typeof sourceNext === "object" && sourceNext !== null ? (sourceNext as Record) : {}; } target[path[path.length - 1]] = pageParam; return result; } ================================================ FILE: packages/connect-query-core/src/structural-sharing.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create } from "@bufbuild/protobuf"; import { SayRequestSchema, SayResponseSchema, } from "test-utils/gen/eliza_pb.js"; import { describe, expect, it } from "vitest"; import { createStructuralSharing } from "./structural-sharing.js"; describe("structural sharing", () => { const schema = SayResponseSchema; const fn = createStructuralSharing(schema); it("returns old data if new data is equal", () => { const oldData = create(schema, { sentence: "hi" }); const newData = create(schema, { sentence: "hi" }); const result = fn(oldData, newData); expect(result).toBe(oldData); }); it("returns new data if not equal to old data", () => { const oldData = create(schema, { sentence: "hi" }); const newData = create(schema, { sentence: "hello" }); const result = fn(oldData, newData); expect(result).toBe(newData); }); it("returns new data if old data is undefined", () => { const oldData = undefined; const newData = create(schema, { sentence: "hello" }); const result = fn(oldData, newData); expect(result).toBe(newData); }); it.each([123, null, create(SayRequestSchema, { sentence: "hi" })])( "returns new data for unexpected old data $#", (oldData) => { const newData = create(schema, { sentence: "hi" }); const result = fn(oldData, newData); expect(result).toBe(newData); }, ); it.each([123, null, create(SayRequestSchema, { sentence: "hi" })])( "returns new data for unexpected new data $#", (newData) => { const oldData = create(schema, { sentence: "hi" }); const result = fn(oldData, newData); expect(result).toBe(newData); }, ); it("allows returning old data if new data is equal", () => { const oldData = { count: 2 }; const newData = { count: 2 }; const result = fn(oldData, newData); expect(result).toBe(oldData); }); }); ================================================ FILE: packages/connect-query-core/src/structural-sharing.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { type DescMessage, equals, isMessage } from "@bufbuild/protobuf"; import { replaceEqualDeep } from "@tanstack/query-core"; /** * Returns a simplistic implementation for "structural sharing" for a Protobuf * message. * * To keep references intact between re-renders, we return the old version if it * equals the new version. * * See https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations#structural-sharing */ export function createStructuralSharing( schema: DescMessage, // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- matching the @tanstack/react-query types ): (oldData: unknown | undefined, newData: unknown) => unknown { return function (oldData, newData) { if (!isMessage(oldData) || !isMessage(newData)) { return replaceEqualDeep(oldData, newData); } if (!equals(schema, oldData, newData)) { return newData; } return oldData; }; } ================================================ FILE: packages/connect-query-core/src/transport-key.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { createConnectTransport } from "@connectrpc/connect-web"; import { describe, expect, it } from "vitest"; import { addStaticKeyToTransport, createTransportKey, } from "./transport-key.js"; describe("transport key", () => { it("returns the same key for the same reference", () => { const transport = createConnectTransport({ baseUrl: "https://example.com", }); const key1 = createTransportKey(transport); const key2 = createTransportKey(transport); expect(key1).toBe(key2); }); it("creates a unique key for every reference", () => { const transport1 = createConnectTransport({ baseUrl: "https://example.com", }); const transport2 = createConnectTransport({ baseUrl: "https://example.com", }); const key1 = createTransportKey(transport1); const key2 = createTransportKey(transport2); expect(key1).not.toBe(key2); }); it("allows override of key transport property", () => { const transport1 = addStaticKeyToTransport( createConnectTransport({ baseUrl: "https://example.com", }), "static-key", ); const key1 = createTransportKey(transport1); expect(key1).toBe("static-key"); }); }); ================================================ FILE: packages/connect-query-core/src/transport-key.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { Transport } from "@connectrpc/connect"; const staticKeySymbol = Symbol("static-key"); const transportKeys = new WeakMap(); let counter = 0; interface TransportWithStaticKey extends Transport { [staticKeySymbol]?: string; } /** * For a given Transport, create a string key that is suitable for a Query Key * in TanStack Query. * * This function will return a unique string for every reference. */ export function createTransportKey(transport: TransportWithStaticKey): string { if (transport[staticKeySymbol] !== undefined) { return transport[staticKeySymbol]; } let key = transportKeys.get(transport); if (key === undefined) { key = `t${++counter}`; transportKeys.set(transport, key); } return key; } /** * Enhances a given transport with a static query key that is used in any associated queries. This may be necessary * in SSR contexts where transports are used on both client and server but they need to be considered * the same when it comes to the query cache. */ export function addStaticKeyToTransport( transport: Transport, key: string, ): TransportWithStaticKey { return { ...transport, [staticKeySymbol]: key }; } ================================================ FILE: packages/connect-query-core/src/utils.test.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { create, isFieldSet, isMessage } from "@bufbuild/protobuf"; import { Proto2MessageSchema } from "test-utils/gen/proto2_pb.js"; import { describe, expect, it } from "vitest"; import { assert, createProtobufSafeUpdater, isAbortController, } from "./utils.js"; describe("assert", () => { const message = "assertion message"; it("throws on a false condition", () => { expect(() => { assert(false, message); }).toThrow(`Invalid assertion: ${message}`); }); it("does not throw on a true condition", () => { expect(() => { assert(true, message); }).not.toThrow(); }); }); describe("isAbortController", () => { it("returns false for non-objects", () => { expect(isAbortController(true)).toBeFalsy(); expect(isAbortController(false)).toBeFalsy(); expect(isAbortController(0)).toBeFalsy(); expect(isAbortController(1)).toBeFalsy(); expect(isAbortController("a")).toBeFalsy(); expect(isAbortController(undefined)).toBeFalsy(); expect(isAbortController([])).toBeFalsy(); expect(isAbortController(null)).toBeFalsy(); }); it("returns false for objects missing the AbortController properties", () => { expect(isAbortController({})).toBeFalsy(); expect(isAbortController({ signal: undefined })).toBeFalsy(); expect(isAbortController({ signal: null })).toBeFalsy(); expect(isAbortController({ signal: {} })).toBeFalsy(); expect(isAbortController({ signal: { aborted: undefined } })).toBeFalsy(); expect(isAbortController({ signal: { aborted: true } })).toBeFalsy(); expect( isAbortController({ signal: { aborted: true }, abort: undefined }), ).toBeFalsy(); }); it("returns true for the two necessary AbortController properties", () => { expect( isAbortController({ signal: { aborted: false, }, abort: () => {}, }), ).toBeTruthy(); expect(isAbortController(new AbortController())).toBeTruthy(); }); }); describe("createProtobufSafeUpdater", () => { describe("with update message", () => { const schema = { output: Proto2MessageSchema }; const update = create(Proto2MessageSchema, { int32Field: 999, }); const safeUpdater = createProtobufSafeUpdater(schema, update); it("returns update message for previous value undefined", () => { const next = safeUpdater(undefined); expect(next).toBe(update); }); it("returns update message for previous value", () => { const prev = create(Proto2MessageSchema, { int32Field: 123, }); const next = safeUpdater(prev); expect(next).toBe(update); }); }); describe("with update message init", () => { const schema = { output: Proto2MessageSchema }; const update = { int32Field: 999, }; const safeUpdater = createProtobufSafeUpdater(schema, update); it("returns update message for previous value undefined", () => { const next = safeUpdater(undefined); expect(next?.int32Field).toBe(999); }); it("returns update message for previous value", () => { const prev = create(Proto2MessageSchema, { int32Field: 123, }); const next = safeUpdater(prev); expect(next?.$typeName).toBe(Proto2MessageSchema.typeName); expect(next?.int32Field).toBe(999); }); }); describe("with updater function", () => { const schema = { output: Proto2MessageSchema }; const safeUpdater = createProtobufSafeUpdater(schema, (prev) => { if (prev === undefined) { return undefined; } return { ...prev, int32Field: 999, }; }); it("accepts undefined", () => { const next = safeUpdater(undefined); expect(next).toBeUndefined(); }); it("accepts previous message", () => { const prev = create(Proto2MessageSchema, { int32Field: 123, }); const next = safeUpdater(prev); expect(next).toBeDefined(); }); it("returns message", () => { const prev = create(Proto2MessageSchema); const next = safeUpdater(prev); expect(isMessage(next, Proto2MessageSchema)).toBe(true); }); it("updates field", () => { const prev = create(Proto2MessageSchema); const next = safeUpdater(prev); expect(next?.int32Field).toBe(999); }); it("keeps existing fields", () => { const prev = create(Proto2MessageSchema, { stringField: "abc", }); const next = safeUpdater(prev); expect(next?.stringField).toBe("abc"); }); describe("keeps field presence", () => { it("for unset field", () => { const prev = create(Proto2MessageSchema); expect(isFieldSet(prev, Proto2MessageSchema.field.stringField)).toBe( false, ); const next = safeUpdater(prev); const hasStringField = next === undefined ? undefined : isFieldSet(next, Proto2MessageSchema.field.stringField); expect(hasStringField).toBe(false); }); it("for set field", () => { const prev = create(Proto2MessageSchema, { stringField: "abc", }); expect(isFieldSet(prev, Proto2MessageSchema.field.stringField)).toBe( true, ); const next = safeUpdater(prev); const hasStringField = next === undefined ? undefined : isFieldSet(next, Proto2MessageSchema.field.stringField); expect(hasStringField).toBe(true); }); }); }); }); ================================================ FILE: packages/connect-query-core/src/utils.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescMessage, DescMethodUnary, MessageInitShape, MessageShape, } from "@bufbuild/protobuf"; import { create, isMessage } from "@bufbuild/protobuf"; /** * Throws an error with the provided message when the condition is `false` */ export function assert(condition: boolean, message: string): asserts condition { if (!condition) { throw new Error(`Invalid assertion: ${message}`); } } /** * Verifies that the provided input is a valid AbortController */ export const isAbortController = (input: unknown): input is AbortController => { if ( typeof input === "object" && input !== null && "signal" in input && typeof input.signal === "object" && input.signal !== null && "aborted" in input.signal && typeof input.signal.aborted === "boolean" && "abort" in input && typeof input.abort === "function" // 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`). ) { return true; } return false; }; /** * @see `Updater` from `@tanstack/react-query` */ export type ConnectUpdater = | MessageInitShape | undefined | ((prev?: MessageShape) => MessageShape | undefined); /** * This helper makes sure that the type for the original response message is returned. * * @deprecated the ConnectQueryKey type now links to the return data type so `setQueryData` can be called safely without this helper. */ export const createProtobufSafeUpdater = ( schema: Pick, "output">, updater: ConnectUpdater, ) => (prev?: MessageShape): MessageShape | undefined => { if (typeof updater !== "function") { if (updater === undefined) { return undefined; } if (isMessage(updater, schema.output)) { return updater; } return create(schema.output, updater); } return updater(prev); }; ================================================ FILE: packages/connect-query-core/tsconfig.build.json ================================================ { "extends": "../../tsconfig.base.json", "files": ["src/index.ts"] } ================================================ FILE: packages/connect-query-core/tsconfig.json ================================================ { "include": [ "**/*.test.ts", "**/*.test.tsx", "src/test/**.tsx", "vite.config.ts" ], "extends": "./tsconfig.build.json" } ================================================ FILE: packages/connect-query-core/vite.config.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/ export default defineConfig({ test: { environment: "jsdom", typecheck: { enabled: true, // Modified to typecheck definition files as well as source files include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], }, coverage: { provider: "istanbul", thresholds: { branches: 100, functions: 100, lines: 100, statements: 100, }, }, }, }); ================================================ FILE: packages/examples/react/basic/.gitignore ================================================ # Logs logs *.log npm-debug.log* pnpm-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: packages/examples/react/basic/buf.gen.yaml ================================================ # buf.gen.yaml defines a local generation template. # For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml version: v2 inputs: - proto_file: eliza.proto # Deletes the directories specified in the `out` field for all plugins before running code generation. clean: true plugins: - local: protoc-gen-es out: src/gen opt: - target=ts - local: protoc-gen-connect-query out: src/gen opt: - target=ts ================================================ FILE: packages/examples/react/basic/eliza.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package connectrpc.eliza.v1; // ElizaService provides a way to talk to Eliza, a port of the DOCTOR script // for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at // the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the // superficiality of human-computer communication. DOCTOR simulates a // psychotherapist, and is commonly found as an Easter egg in emacs // distributions. service ElizaService { // Say is a unary RPC. Eliza responds to the prompt with a single sentence. rpc Say(SayRequest) returns (SayResponse) {} } // SayRequest is a single-sentence request. message SayRequest { string sentence = 1; } // SayResponse is a single-sentence response. message SayResponse { string sentence = 1; } ================================================ FILE: packages/examples/react/basic/index.html ================================================ Vite + React + TS
================================================ FILE: packages/examples/react/basic/package.json ================================================ { "name": "@connectrpc/connect-query-example-basic", "version": "2.2.0", "private": true, "type": "module", "scripts": { "dev": "vite", "generate": "buf generate", "license-header": "license-header", "test": "vitest --run", "test:watch": "vitest --watch --ui", "lint": "eslint --max-warnings 0 .", "format": "prettier --write . '!src/gen'" }, "dependencies": { "@bufbuild/buf": "1.54.0", "@bufbuild/protobuf": "^2.5.1", "@bufbuild/protoc-gen-es": "^2.5.1", "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-query": "^2.2.0", "@connectrpc/connect-web": "^2.0.2", "@connectrpc/protoc-gen-connect-query": "^2.2.0", "@tanstack/react-query": "^5.79.0", "@tanstack/react-query-devtools": "^5.79.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^4.5.0", "react": "^19.1.0", "react-dom": "^19.1.0", "typescript": "^5.8.3", "vite": "^6.3.5" } } ================================================ FILE: packages/examples/react/basic/src/css.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. export const borderRadius = 6; export const margin = 6; export const padding = 6; export const boxShadow = "0px 1px 2px rgba(15, 16, 77, 0.05)"; export const border = "1px solid #E4E9EF"; export const lightBlue = "#C4E8FC"; export const white = "#FFFFFF"; ================================================ FILE: packages/examples/react/basic/src/datum.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { type FC, type ReactNode, useId } from "react"; import { border, borderRadius, boxShadow, lightBlue, margin, padding, white, } from "./css"; interface DatumProps { datum: string; label: string; } /** * A single data point */ export const Datum: FC = ({ datum, label }) => { const id = useId(); return (
{datum}
); }; /** * Wrapper Datum children */ export const Data: FC<{ children: ReactNode[]; }> = ({ children }) => (
{children}
); ================================================ FILE: packages/examples/react/basic/src/example.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { useQuery } from "@connectrpc/connect-query"; import type { FC } from "react"; import { Data, Datum } from "./datum"; import { say } from "./gen/eliza-ElizaService_connectquery"; import { Indicator, Indicators } from "./indicator"; import { Page } from "./page"; /** * This example demonstrates a basic usage of Connect-Query with `useQuery` */ export const Example: FC = () => { const { status, fetchStatus, error, data } = useQuery(say, { sentence: "Hello", }); return ( Status: {status} ); }; ================================================ FILE: packages/examples/react/basic/src/gen/eliza-ElizaService_connectquery.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-connect-query v2.2.0 with parameter "target=ts" // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) /* eslint-disable */ import { ElizaService } from "./eliza_pb"; /** * Say is a unary RPC. Eliza responds to the prompt with a single sentence. * * @generated from rpc connectrpc.eliza.v1.ElizaService.Say */ export const say = ElizaService.method.say; ================================================ FILE: packages/examples/react/basic/src/gen/eliza_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file eliza.proto. */ export const file_eliza: GenFile = /*@__PURE__*/ fileDesc("CgtlbGl6YS5wcm90bxITY29ubmVjdHJwYy5lbGl6YS52MSIeCgpTYXlSZXF1ZXN0EhAKCHNlbnRlbmNlGAEgASgJIh8KC1NheVJlc3BvbnNlEhAKCHNlbnRlbmNlGAEgASgJMloKDEVsaXphU2VydmljZRJKCgNTYXkSHy5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlcXVlc3QaIC5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlc3BvbnNlIgBiBnByb3RvMw"); /** * SayRequest is a single-sentence request. * * @generated from message connectrpc.eliza.v1.SayRequest */ export type SayRequest = Message<"connectrpc.eliza.v1.SayRequest"> & { /** * @generated from field: string sentence = 1; */ sentence: string; }; /** * Describes the message connectrpc.eliza.v1.SayRequest. * Use `create(SayRequestSchema)` to create a new message. */ export const SayRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_eliza, 0); /** * SayResponse is a single-sentence response. * * @generated from message connectrpc.eliza.v1.SayResponse */ export type SayResponse = Message<"connectrpc.eliza.v1.SayResponse"> & { /** * @generated from field: string sentence = 1; */ sentence: string; }; /** * Describes the message connectrpc.eliza.v1.SayResponse. * Use `create(SayResponseSchema)` to create a new message. */ export const SayResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_eliza, 1); /** * ElizaService provides a way to talk to Eliza, a port of the DOCTOR script * for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at * the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the * superficiality of human-computer communication. DOCTOR simulates a * psychotherapist, and is commonly found as an Easter egg in emacs * distributions. * * @generated from service connectrpc.eliza.v1.ElizaService */ export const ElizaService: GenService<{ /** * Say is a unary RPC. Eliza responds to the prompt with a single sentence. * * @generated from rpc connectrpc.eliza.v1.ElizaService.Say */ say: { methodKind: "unary"; input: typeof SayRequestSchema; output: typeof SayResponseSchema; }, }> = /*@__PURE__*/ serviceDesc(file_eliza, 0); ================================================ FILE: packages/examples/react/basic/src/index.css ================================================ :root { font-family: Inter, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; background-color: #f5f7fa; } body { margin: 0; display: flex; } ================================================ FILE: packages/examples/react/basic/src/indicator.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { FC, ReactNode } from "react"; import { border, borderRadius, boxShadow, margin } from "./css"; /** * a single Indicator */ export const Indicator = ({ label, parent, }: { label: U; parent: T; }) => { const height = "50px"; const active = label === parent; return (
{label}
); }; /** * A wrapper for `Indicator`s */ export const Indicators: FC<{ children: ReactNode; label: string; }> = ({ children, label }) => { return (
{label}
{children}
); }; ================================================ FILE: packages/examples/react/basic/src/main.test.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import "@testing-library/jest-dom/vitest"; import { createRouterTransport } from "@connectrpc/connect"; import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import * as methods from "./gen/eliza-ElizaService_connectquery"; import Main from "./main"; describe("Application", () => { it("should show success status and response data", async () => { const transport = createRouterTransport(({ rpc }) => { rpc(methods.say, () => ({ sentence: "Hello, world!", })); }); render(
); const text = await screen.findByText("Status: success"); expect(text).toBeInTheDocument(); const response = await screen.findByLabelText("data"); expect(response).toHaveTextContent( '{"$typeName":"connectrpc.eliza.v1.SayResponse","sentence":"Hello, world!"}', ); }); }); ================================================ FILE: packages/examples/react/basic/src/main.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import "./index.css"; import type { Transport } from "@connectrpc/connect"; import { TransportProvider } from "@connectrpc/connect-query"; import { createConnectTransport } from "@connectrpc/connect-web"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import * as ReactDOM from "react-dom/client"; import { Example } from "./example"; const queryClient = new QueryClient(); /** * The application root */ export default function App({ transport }: { transport?: Transport }) { const finalTransport = transport ?? createConnectTransport({ baseUrl: "https://demo.connectrpc.com", }); return ( ); } const rootElement = document.getElementById("root"); if (rootElement) { ReactDOM.createRoot(rootElement).render(); } ================================================ FILE: packages/examples/react/basic/src/page.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { FC, PropsWithChildren } from "react"; import { margin } from "./css"; /** * The wrapper for the whole page */ export const Page: FC = ({ children }) => (
{children}
); ================================================ FILE: packages/examples/react/basic/src/vite-env.d.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /// ================================================ FILE: packages/examples/react/basic/tsconfig.json ================================================ { "compilerOptions": { "allowJs": false, "allowSyntheticDefaultImports": true, "esModuleInterop": false, "exactOptionalPropertyTypes": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "jsx": "react-jsx", "lib": ["DOM", "DOM.Iterable", "ESNext"], "module": "ESNext", "moduleResolution": "Node", "noEmit": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "ESNext", "useDefineForClassFields": true, "types": ["node"], "declaration": true // necessary to check if generated code can be published }, "include": ["src", "./*.config.ts", "__mocks__"] } ================================================ FILE: packages/examples/react/basic/vite.config.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import react from "@vitejs/plugin-react"; import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], test: { environment: "jsdom", typecheck: { enabled: true, // Modified to typecheck definition files as well as source files include: ["**/*.{test,spec}?(-d).?(c|m)[jt]s?(x)"], }, }, }); ================================================ FILE: packages/protoc-gen-connect-query/.eslintignore ================================================ snapshots .type-dump ================================================ FILE: packages/protoc-gen-connect-query/.gitignore ================================================ # A directory that exists solely to hold the result of tsc since --noEmit doesn't discover to protability issue found # by https://github.com/connectrpc/connect-query-es/issues/209 .type-dump ================================================ FILE: packages/protoc-gen-connect-query/README.md ================================================ # @connectrpc/protoc-gen-connect-query - [@connectrpc/protoc-gen-connect-query](#connectrpcprotoc-gen-connect-query) - [Installation](#installation) - [Generating Code](#generating-code) - [`example.proto`](#exampleproto) - [`buf.gen.yaml`](#bufgenyaml) - [With the `buf` CLI](#with-the-buf-cli) - [With `protoc`](#with-protoc) - [With Node](#with-node) - [Generated Output](#generated-output) - [Plugin options](#plugin-options) - [`target`](#target) - [`import_extension`](#import_extension) - [`keep_empty_files=true`](#keep_empty_filestrue) - [`js_import_style`](#js_import_style) - [`ts_nocheck=true`](#ts_nochecktrue) - [Example Generated Code](#example-generated-code) 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). Learn more about Connect-Query at [github.com/connectrpc/connect-query-es](https://github.com/connectrpc/connect-query-es). ## Installation `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 [@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). To install the plugins and their runtime libraries, run: ```shell npm install --save-dev @connectrpc/protoc-gen-connect-query @bufbuild/protoc-gen-es npm install @connectrpc/connect-query @bufbuild/protobuf ``` 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. ## Generating Code ### `example.proto` For these examples, consider the following example proto file `example.proto`: ```protobuf syntax = "proto3"; package example.v1; message Nothing {} message Todo { string id = 1; string name = 2; bool completed = 3; } message Todos { repeated Todo todos = 1; } service TodoService { rpc GetTodos(Nothing) returns (Todos); rpc AddTodo(Todo) returns (Nothing); } ``` This file creates an RPC service with the following: - `GetTodos` takes no inputs and returns an array of `Todo`s. - `AddTodo` adds a new `Todo` and returns nothing. ### `buf.gen.yaml` Add a new configuration file `buf.gen.yaml` ```yaml version: v2 plugins: # This will invoke protoc-gen-es and write output to src/gen - local: protoc-gen-es out: src/gen opt: target=ts # This will invoke protoc-gen-connect-query - local: protoc-gen-connect-query out: src/gen opt: target=ts ``` ### With the `buf` CLI 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: ```bash npx @bufbuild/buf generate ``` > 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. ### With `protoc` ```bash PATH=$PATH:$(pwd)/node_modules/.bin \ protoc -I . \ --es_out src/gen \ --es_opt target=ts \ --connect-query_out src/gen \ --connect-query_opt target=ts \ example.proto ``` 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. > Note: Since yarn v2 and above does not use a `node_modules` directory, you need to change the variable a bit: > > ```bash > PATH=$(dirname $(yarn bin protoc-gen-es)):$(dirname $(yarn bin protoc-gen-connect-es)):$PATH > ``` ### With Node Add a line to the `scripts` section of your `package.json` to run `buf generate`. ```json "scripts": { ... "buf:generate": "buf generate" }, ``` Finally, tell Buf to generate code by running your command: ```bash npm run buf:generate ``` Now you should see your generated code: ```tree . └── gen/ ├── example_pb.ts └── example-TodoService_connectquery.ts ``` ## Generated Output Connect-Query will create one output file for every service in every protofile. Say you have the following file structure: ```tree . └── proto/ ├── pizza.proto └── curry.proto ``` Where `pizza.proto` contains `DetroitStyleService` and `ChicagoStyleService`, and where `curry.proto` contains `VindalooService`. Your generated output will look like this: ```tree . └── gen/ ├── pizza_pb.ts ├── pizza-DetroitStyleService_connectquery.ts ├── pizza-ChicagoStyleService_connectquery.ts ├── curry_pb.ts └── curry-VindalooService_connectquery.ts ``` 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. ## Plugin options ### `target` This option controls whether the plugin generates JavaScript, TypeScript, or TypeScript declaration files. Say, for example, you used [`example.proto`](#exampleproto): | Target | Generated output | | ------------ | --------------------------------------- | | `target=js` | `example-TodoService_connectquery.js` | | `target=ts` | `example-TodoService_connectquery.ts` | | `target=dts` | `example-TodoService_connectquery.d.ts` | Multiple values can be given by separating them with `+`, for example `target=js+dts`. 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`. ### `import_extension` 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: - `import_extension=none`: Doesn't add an extension. (Default) - `import_extension=js`: Adds the `.js` extension. - `import_extension=ts`. Adds the `.ts` extension. ### `js_import_style` 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)) generate ECMAScript `import` and `export` statements. For use cases where CommonJS is difficult to avoid, this option can be used to generate CommonJS `require()` calls. Possible values: - `js_import_style=module` generate ECMAScript `import` / `export` statements - the default behavior. - `js_import_style=legacy_commonjs` generate CommonJS `require()` calls. ### `keep_empty_files=true` 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. ### `ts_nocheck=true` [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`. ## Example Generated Code 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. ================================================ FILE: packages/protoc-gen-connect-query/bin/protoc-gen-connect-query ================================================ #!/usr/bin/env node const { runNodeJs } = require("@bufbuild/protoplugin"); const { protocGenConnectQuery, } = require("../dist/cjs/src/protoc-gen-connect-query-plugin.js"); runNodeJs(protocGenConnectQuery); ================================================ FILE: packages/protoc-gen-connect-query/package.json ================================================ { "name": "@connectrpc/protoc-gen-connect-query", "version": "2.2.0", "description": "Code generator for connect-query", "license": "Apache-2.0", "sideEffects": false, "repository": { "type": "git", "url": "https://github.com/connectrpc/connect-query-es.git", "directory": "packages/protoc-gen-connect-query" }, "files": [ "dist/**" ], "bin": { "protoc-gen-connect-query": "bin/protoc-gen-connect-query" }, "engines": { "node": ">=20" }, "scripts": { "prebuild": "rm -rf ./dist/*", "build": "tsc --project tsconfig.json --module commonjs --verbatimModuleSyntax false --moduleResolution node10 --outDir ./dist/cjs", "format": "prettier --write --ignore-unknown '.' '!dist'", "license-header": "license-header", "lint": "eslint --max-warnings 0 ." }, "preferUnplugged": true, "devDependencies": { "@bufbuild/buf": "1.54.0", "@bufbuild/protoc-gen-es": "^2.5.1", "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-query": "^2.2.0", "@tanstack/react-query": "^5.79.0", "typescript": "^5.8.3" }, "dependencies": { "@bufbuild/protobuf": "^2.5.1", "@bufbuild/protoplugin": "^2.2.1" }, "peerDependencies": { "@bufbuild/protoc-gen-es": "2.x" }, "peerDependenciesMeta": { "@bufbuild/protoc-gen-es": { "optional": true } } } ================================================ FILE: packages/protoc-gen-connect-query/src/generateDts.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescFile, DescService } from "@bufbuild/protobuf"; import type { Schema } from "@bufbuild/protoplugin"; import { safeIdentifier } from "@bufbuild/protoplugin"; import type { PluginInit } from "./utils.js"; // prettier-ignore /** * Handles generating a TypeScript Declaration file for a given Schema, DescFile (protobuf definition) and protobuf Service. */ const generateServiceFile = (schema: Schema, protoFile: DescFile) => (service: DescService) => { const f = schema.generateFile( `${protoFile.name}-${service.name}_connectquery.d.ts`, ); f.preamble(protoFile); service.methods.forEach((method) => { switch (method.methodKind) { case "unary": { f.print(f.jsDoc(method)); f.print(f.export("const", safeIdentifier(method.localName)), ": typeof ", f.importSchema(service), '["method"]["', method.localName, '"];'); } break; default: return; } }); }; /** * This function generates the TypeScript Definition output files */ export const generateDts: PluginInit["generateDts"] = (schema) => { schema.files.forEach((protoFile) => { protoFile.services.forEach(generateServiceFile(schema, protoFile)); }); }; ================================================ FILE: packages/protoc-gen-connect-query/src/generateTs.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { DescFile, DescService } from "@bufbuild/protobuf"; import type { Schema } from "@bufbuild/protoplugin"; import { safeIdentifier } from "@bufbuild/protoplugin"; import type { PluginInit } from "./utils.js"; // prettier-ignore /** * Handles generating a source code file for a given Schema, DescFile (protobuf definition) and protobuf Service. * * By pure luck, this file happens to be completely valid JavaScript since all the types are inferred. */ const generateServiceFile = (schema: Schema, protoFile: DescFile, extension: 'js' | 'ts') => (service: DescService) => { const f = schema.generateFile( `${protoFile.name}-${service.name}_connectquery.${extension}`, ); f.preamble(protoFile); service.methods .filter((method) => method.methodKind === "unary") .forEach((method, index, filteredMethods) => { f.print(f.jsDoc(method)); f.print(f.export("const", safeIdentifier(method.localName)), " = ", f.importSchema(service), ".method.", method.localName, ";"); const lastIndex = index === filteredMethods.length - 1; if (!lastIndex) { f.print(); } }); }; /** * This function generates the TypeScript output files */ export const generateTs: PluginInit["generateJs"] & PluginInit["generateTs"] = ( schema, extension, ) => { schema.files.forEach((protoFile) => { protoFile.services.forEach( generateServiceFile(schema, protoFile, extension), ); }); }; ================================================ FILE: packages/protoc-gen-connect-query/src/protoc-gen-connect-query-plugin.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; import { version } from "../package.json"; import { generateDts } from "./generateDts.js"; import { generateTs } from "./generateTs.js"; export const protocGenConnectQuery = createEcmaScriptPlugin({ name: "protoc-gen-connect-query", version: `v${String(version)}`, generateTs, // The generated TypeScript output is completely valid JavaScript since all the types are inferred generateJs: generateTs, generateDts, }); ================================================ FILE: packages/protoc-gen-connect-query/src/utils.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { createEcmaScriptPlugin } from "@bufbuild/protoplugin"; /** * Extracts the type of PluginInit from @bufbuild/protoplugin */ export type PluginInit = Required[0]>; ================================================ FILE: packages/protoc-gen-connect-query/tsconfig.json ================================================ { "files": ["src/protoc-gen-connect-query-plugin.ts"], "extends": "../../tsconfig.base.json", "compilerOptions": { // We import the plugin's version number from package.json "resolveJsonModule": true, // This package is CommonJS "verbatimModuleSyntax": false, "module": "CommonJS", "moduleResolution": "Node10" } } ================================================ FILE: packages/test-utils/buf.gen.yaml ================================================ # buf.gen.yaml defines a local generation template. # For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml version: v2 inputs: - directory: proto # Deletes the directories specified in the `out` field for all plugins before running code generation. clean: true plugins: - local: protoc-gen-es out: src/gen opt: - target=ts ================================================ FILE: packages/test-utils/package.json ================================================ { "name": "test-utils", "private": true, "version": "2.2.0", "type": "module", "scripts": { "generate": "buf generate", "postgenerate": "license-header gen", "prebuild": "rm -rf ./dist/*", "build": "npm run build:cjs && npm run build:esm", "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\"}'", "build:esm": "tsc --project tsconfig.json --outDir ./dist/esm --declaration --declarationDir ./dist/esm", "format": "prettier --write --ignore-unknown '.' '!dist' '!src/gen'", "license-header": "license-header --ignore 'src/gen/**'", "lint": "eslint --max-warnings 0 ." }, "main": "./dist/cjs/index.js", "types": "./dist/cjs/index.d.ts", "exports": { ".": { "import": "./dist/esm/index.js" }, "./gen/*": { "import": "./dist/esm/gen/*" } }, "devDependencies": { "@bufbuild/buf": "^1.54.0", "@bufbuild/protobuf": "^2.5.1", "@bufbuild/protoc-gen-es": "^2.5.1", "@connectrpc/connect": "^2.0.2", "@connectrpc/connect-web": "^2.0.2", "@types/react": "^19.1.6", "react": "^19.1.0" }, "files": [ "dist/**" ] } ================================================ FILE: packages/test-utils/proto/bigint.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; import "google/protobuf/empty.proto"; service BigIntService { rpc Count(CountRequest) returns (CountResponse); rpc GetCount(google.protobuf.Empty) returns (CountResponse); } message CountRequest { int64 add = 1; } message CountResponse { int64 count = 1; } ================================================ FILE: packages/test-utils/proto/eliza.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package connectrpc.eliza.v1; // ElizaService provides a way to talk to Eliza, a port of the DOCTOR script // for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at // the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the // superficiality of human-computer communication. DOCTOR simulates a // psychotherapist, and is commonly found as an Easter egg in emacs // distributions. service ElizaService { // Say is a unary RPC. Eliza responds to the prompt with a single sentence. rpc Say(SayRequest) returns (SayResponse) {} } // SayRequest is a single-sentence request. message SayRequest { string sentence = 1; } // SayResponse is a single-sentence response. message SayResponse { string sentence = 1; } ================================================ FILE: packages/test-utils/proto/list.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; service ListService { rpc List(ListRequest) returns (ListResponse); rpc NestedList(NestedListRequest) returns (NestedListResponse); } message ListRequest { int64 page = 1; bool preview = 2; } message ListResponse { int64 page = 1; repeated string items = 2; } message NestedListRequest { message Nested { int64 page = 1; bool preview = 2; } Nested nested = 1; } message NestedListResponse { message Nested { int64 page = 1; } Nested nested = 1; repeated string items = 2; } ================================================ FILE: packages/test-utils/proto/proto2.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto2"; package test; message Proto2Message { optional string string_field = 1; optional int32 int32_field = 3; } ================================================ FILE: packages/test-utils/proto/proto3.proto ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package test; // Note: We do not exhaust all field types message Proto3Message { string string_field = 1; bytes bytes_field = 2; int32 int32_field = 3; int64 int64_field = 4; double double_field = 5; bool bool_field = 6; Proto3Enum enum_field = 7; Proto3Message message_field = 8; optional string optional_string_field = 9; repeated string repeated_string_field = 17; repeated Proto3Message repeated_message_field = 18; repeated Proto3Enum repeated_enum_field = 19; oneof either { string oneof_string_field = 31; int32 oneof_int32_field = 33; } map map_string_int64_field = 39; map map_string_message_field = 40; map map_string_enum_field = 41; } enum Proto3Enum { PROTO3_ENUM_UNSPECIFIED = 0; PROTO3_ENUM_YES = 1; PROTO3_ENUM_NO = 2; } ================================================ FILE: packages/test-utils/src/gen/bigint_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file bigint.proto (syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import type { EmptySchema } from "@bufbuild/protobuf/wkt"; import { file_google_protobuf_empty } from "@bufbuild/protobuf/wkt"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file bigint.proto. */ export const file_bigint: GenFile = /*@__PURE__*/ fileDesc("CgxiaWdpbnQucHJvdG8iGwoMQ291bnRSZXF1ZXN0EgsKA2FkZBgBIAEoAyIeCg1Db3VudFJlc3BvbnNlEg0KBWNvdW50GAEgASgDMmsKDUJpZ0ludFNlcnZpY2USJgoFQ291bnQSDS5Db3VudFJlcXVlc3QaDi5Db3VudFJlc3BvbnNlEjIKCEdldENvdW50EhYuZ29vZ2xlLnByb3RvYnVmLkVtcHR5Gg4uQ291bnRSZXNwb25zZWIGcHJvdG8z", [file_google_protobuf_empty]); /** * @generated from message CountRequest */ export type CountRequest = Message<"CountRequest"> & { /** * @generated from field: int64 add = 1; */ add: bigint; }; /** * Describes the message CountRequest. * Use `create(CountRequestSchema)` to create a new message. */ export const CountRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_bigint, 0); /** * @generated from message CountResponse */ export type CountResponse = Message<"CountResponse"> & { /** * @generated from field: int64 count = 1; */ count: bigint; }; /** * Describes the message CountResponse. * Use `create(CountResponseSchema)` to create a new message. */ export const CountResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_bigint, 1); /** * @generated from service BigIntService */ export const BigIntService: GenService<{ /** * @generated from rpc BigIntService.Count */ count: { methodKind: "unary"; input: typeof CountRequestSchema; output: typeof CountResponseSchema; }, /** * @generated from rpc BigIntService.GetCount */ getCount: { methodKind: "unary"; input: typeof EmptySchema; output: typeof CountResponseSchema; }, }> = /*@__PURE__*/ serviceDesc(file_bigint, 0); ================================================ FILE: packages/test-utils/src/gen/eliza_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file eliza.proto (package connectrpc.eliza.v1, syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file eliza.proto. */ export const file_eliza: GenFile = /*@__PURE__*/ fileDesc("CgtlbGl6YS5wcm90bxITY29ubmVjdHJwYy5lbGl6YS52MSIeCgpTYXlSZXF1ZXN0EhAKCHNlbnRlbmNlGAEgASgJIh8KC1NheVJlc3BvbnNlEhAKCHNlbnRlbmNlGAEgASgJMloKDEVsaXphU2VydmljZRJKCgNTYXkSHy5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlcXVlc3QaIC5jb25uZWN0cnBjLmVsaXphLnYxLlNheVJlc3BvbnNlIgBiBnByb3RvMw"); /** * SayRequest is a single-sentence request. * * @generated from message connectrpc.eliza.v1.SayRequest */ export type SayRequest = Message<"connectrpc.eliza.v1.SayRequest"> & { /** * @generated from field: string sentence = 1; */ sentence: string; }; /** * Describes the message connectrpc.eliza.v1.SayRequest. * Use `create(SayRequestSchema)` to create a new message. */ export const SayRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_eliza, 0); /** * SayResponse is a single-sentence response. * * @generated from message connectrpc.eliza.v1.SayResponse */ export type SayResponse = Message<"connectrpc.eliza.v1.SayResponse"> & { /** * @generated from field: string sentence = 1; */ sentence: string; }; /** * Describes the message connectrpc.eliza.v1.SayResponse. * Use `create(SayResponseSchema)` to create a new message. */ export const SayResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_eliza, 1); /** * ElizaService provides a way to talk to Eliza, a port of the DOCTOR script * for Joseph Weizenbaum's original ELIZA program. Created in the mid-1960s at * the MIT Artificial Intelligence Laboratory, ELIZA demonstrates the * superficiality of human-computer communication. DOCTOR simulates a * psychotherapist, and is commonly found as an Easter egg in emacs * distributions. * * @generated from service connectrpc.eliza.v1.ElizaService */ export const ElizaService: GenService<{ /** * Say is a unary RPC. Eliza responds to the prompt with a single sentence. * * @generated from rpc connectrpc.eliza.v1.ElizaService.Say */ say: { methodKind: "unary"; input: typeof SayRequestSchema; output: typeof SayResponseSchema; }, }> = /*@__PURE__*/ serviceDesc(file_eliza, 0); ================================================ FILE: packages/test-utils/src/gen/list_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file list.proto (syntax proto3) /* eslint-disable */ import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file list.proto. */ export const file_list: GenFile = /*@__PURE__*/ fileDesc("CgpsaXN0LnByb3RvIiwKC0xpc3RSZXF1ZXN0EgwKBHBhZ2UYASABKAMSDwoHcHJldmlldxgCIAEoCCIrCgxMaXN0UmVzcG9uc2USDAoEcGFnZRgBIAEoAxINCgVpdGVtcxgCIAMoCSJnChFOZXN0ZWRMaXN0UmVxdWVzdBIpCgZuZXN0ZWQYASABKAsyGS5OZXN0ZWRMaXN0UmVxdWVzdC5OZXN0ZWQaJwoGTmVzdGVkEgwKBHBhZ2UYASABKAMSDwoHcHJldmlldxgCIAEoCCJnChJOZXN0ZWRMaXN0UmVzcG9uc2USKgoGbmVzdGVkGAEgASgLMhouTmVzdGVkTGlzdFJlc3BvbnNlLk5lc3RlZBINCgVpdGVtcxgCIAMoCRoWCgZOZXN0ZWQSDAoEcGFnZRgBIAEoAzJpCgtMaXN0U2VydmljZRIjCgRMaXN0EgwuTGlzdFJlcXVlc3QaDS5MaXN0UmVzcG9uc2USNQoKTmVzdGVkTGlzdBISLk5lc3RlZExpc3RSZXF1ZXN0GhMuTmVzdGVkTGlzdFJlc3BvbnNlYgZwcm90bzM"); /** * @generated from message ListRequest */ export type ListRequest = Message<"ListRequest"> & { /** * @generated from field: int64 page = 1; */ page: bigint; /** * @generated from field: bool preview = 2; */ preview: boolean; }; /** * Describes the message ListRequest. * Use `create(ListRequestSchema)` to create a new message. */ export const ListRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 0); /** * @generated from message ListResponse */ export type ListResponse = Message<"ListResponse"> & { /** * @generated from field: int64 page = 1; */ page: bigint; /** * @generated from field: repeated string items = 2; */ items: string[]; }; /** * Describes the message ListResponse. * Use `create(ListResponseSchema)` to create a new message. */ export const ListResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 1); /** * @generated from message NestedListRequest */ export type NestedListRequest = Message<"NestedListRequest"> & { /** * @generated from field: NestedListRequest.Nested nested = 1; */ nested?: NestedListRequest_Nested; }; /** * Describes the message NestedListRequest. * Use `create(NestedListRequestSchema)` to create a new message. */ export const NestedListRequestSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 2); /** * @generated from message NestedListRequest.Nested */ export type NestedListRequest_Nested = Message<"NestedListRequest.Nested"> & { /** * @generated from field: int64 page = 1; */ page: bigint; /** * @generated from field: bool preview = 2; */ preview: boolean; }; /** * Describes the message NestedListRequest.Nested. * Use `create(NestedListRequest_NestedSchema)` to create a new message. */ export const NestedListRequest_NestedSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 2, 0); /** * @generated from message NestedListResponse */ export type NestedListResponse = Message<"NestedListResponse"> & { /** * @generated from field: NestedListResponse.Nested nested = 1; */ nested?: NestedListResponse_Nested; /** * @generated from field: repeated string items = 2; */ items: string[]; }; /** * Describes the message NestedListResponse. * Use `create(NestedListResponseSchema)` to create a new message. */ export const NestedListResponseSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 3); /** * @generated from message NestedListResponse.Nested */ export type NestedListResponse_Nested = Message<"NestedListResponse.Nested"> & { /** * @generated from field: int64 page = 1; */ page: bigint; }; /** * Describes the message NestedListResponse.Nested. * Use `create(NestedListResponse_NestedSchema)` to create a new message. */ export const NestedListResponse_NestedSchema: GenMessage = /*@__PURE__*/ messageDesc(file_list, 3, 0); /** * @generated from service ListService */ export const ListService: GenService<{ /** * @generated from rpc ListService.List */ list: { methodKind: "unary"; input: typeof ListRequestSchema; output: typeof ListResponseSchema; }, /** * @generated from rpc ListService.NestedList */ nestedList: { methodKind: "unary"; input: typeof NestedListRequestSchema; output: typeof NestedListResponseSchema; }, }> = /*@__PURE__*/ serviceDesc(file_list, 0); ================================================ FILE: packages/test-utils/src/gen/proto2_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file proto2.proto (package test, syntax proto2) /* eslint-disable */ import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file proto2.proto. */ export const file_proto2: GenFile = /*@__PURE__*/ fileDesc("Cgxwcm90bzIucHJvdG8SBHRlc3QiOgoNUHJvdG8yTWVzc2FnZRIUCgxzdHJpbmdfZmllbGQYASABKAkSEwoLaW50MzJfZmllbGQYAyABKAU"); /** * @generated from message test.Proto2Message */ export type Proto2Message = Message<"test.Proto2Message"> & { /** * @generated from field: optional string string_field = 1; */ stringField: string; /** * @generated from field: optional int32 int32_field = 3; */ int32Field: number; }; /** * Describes the message test.Proto2Message. * Use `create(Proto2MessageSchema)` to create a new message. */ export const Proto2MessageSchema: GenMessage = /*@__PURE__*/ messageDesc(file_proto2, 0); ================================================ FILE: packages/test-utils/src/gen/proto3_pb.ts ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // @generated by protoc-gen-es v2.6.2 with parameter "target=ts" // @generated from file proto3.proto (package test, syntax proto3) /* eslint-disable */ import type { GenEnum, GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2"; import { enumDesc, fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2"; import type { Message } from "@bufbuild/protobuf"; /** * Describes the file proto3.proto. */ export const file_proto3: GenFile = /*@__PURE__*/ fileDesc("Cgxwcm90bzMucHJvdG8SBHRlc3QirgcKDVByb3RvM01lc3NhZ2USFAoMc3RyaW5nX2ZpZWxkGAEgASgJEhMKC2J5dGVzX2ZpZWxkGAIgASgMEhMKC2ludDMyX2ZpZWxkGAMgASgFEhMKC2ludDY0X2ZpZWxkGAQgASgDEhQKDGRvdWJsZV9maWVsZBgFIAEoARISCgpib29sX2ZpZWxkGAYgASgIEiQKCmVudW1fZmllbGQYByABKA4yEC50ZXN0LlByb3RvM0VudW0SKgoNbWVzc2FnZV9maWVsZBgIIAEoCzITLnRlc3QuUHJvdG8zTWVzc2FnZRIiChVvcHRpb25hbF9zdHJpbmdfZmllbGQYCSABKAlIAYgBARIdChVyZXBlYXRlZF9zdHJpbmdfZmllbGQYESADKAkSMwoWcmVwZWF0ZWRfbWVzc2FnZV9maWVsZBgSIAMoCzITLnRlc3QuUHJvdG8zTWVzc2FnZRItChNyZXBlYXRlZF9lbnVtX2ZpZWxkGBMgAygOMhAudGVzdC5Qcm90bzNFbnVtEhwKEm9uZW9mX3N0cmluZ19maWVsZBgfIAEoCUgAEhsKEW9uZW9mX2ludDMyX2ZpZWxkGCEgASgFSAASTAoWbWFwX3N0cmluZ19pbnQ2NF9maWVsZBgnIAMoCzIsLnRlc3QuUHJvdG8zTWVzc2FnZS5NYXBTdHJpbmdJbnQ2NEZpZWxkRW50cnkSUAoYbWFwX3N0cmluZ19tZXNzYWdlX2ZpZWxkGCggAygLMi4udGVzdC5Qcm90bzNNZXNzYWdlLk1hcFN0cmluZ01lc3NhZ2VGaWVsZEVudHJ5EkoKFW1hcF9zdHJpbmdfZW51bV9maWVsZBgpIAMoCzIrLnRlc3QuUHJvdG8zTWVzc2FnZS5NYXBTdHJpbmdFbnVtRmllbGRFbnRyeRo6ChhNYXBTdHJpbmdJbnQ2NEZpZWxkRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgDOgI4ARpRChpNYXBTdHJpbmdNZXNzYWdlRmllbGRFbnRyeRILCgNrZXkYASABKAkSIgoFdmFsdWUYAiABKAsyEy50ZXN0LlByb3RvM01lc3NhZ2U6AjgBGksKF01hcFN0cmluZ0VudW1GaWVsZEVudHJ5EgsKA2tleRgBIAEoCRIfCgV2YWx1ZRgCIAEoDjIQLnRlc3QuUHJvdG8zRW51bToCOAFCCAoGZWl0aGVyQhgKFl9vcHRpb25hbF9zdHJpbmdfZmllbGQqUgoKUHJvdG8zRW51bRIbChdQUk9UTzNfRU5VTV9VTlNQRUNJRklFRBAAEhMKD1BST1RPM19FTlVNX1lFUxABEhIKDlBST1RPM19FTlVNX05PEAJiBnByb3RvMw"); /** * Note: We do not exhaust all field types * * @generated from message test.Proto3Message */ export type Proto3Message = Message<"test.Proto3Message"> & { /** * @generated from field: string string_field = 1; */ stringField: string; /** * @generated from field: bytes bytes_field = 2; */ bytesField: Uint8Array; /** * @generated from field: int32 int32_field = 3; */ int32Field: number; /** * @generated from field: int64 int64_field = 4; */ int64Field: bigint; /** * @generated from field: double double_field = 5; */ doubleField: number; /** * @generated from field: bool bool_field = 6; */ boolField: boolean; /** * @generated from field: test.Proto3Enum enum_field = 7; */ enumField: Proto3Enum; /** * @generated from field: test.Proto3Message message_field = 8; */ messageField?: Proto3Message; /** * @generated from field: optional string optional_string_field = 9; */ optionalStringField?: string; /** * @generated from field: repeated string repeated_string_field = 17; */ repeatedStringField: string[]; /** * @generated from field: repeated test.Proto3Message repeated_message_field = 18; */ repeatedMessageField: Proto3Message[]; /** * @generated from field: repeated test.Proto3Enum repeated_enum_field = 19; */ repeatedEnumField: Proto3Enum[]; /** * @generated from oneof test.Proto3Message.either */ either: { /** * @generated from field: string oneof_string_field = 31; */ value: string; case: "oneofStringField"; } | { /** * @generated from field: int32 oneof_int32_field = 33; */ value: number; case: "oneofInt32Field"; } | { case: undefined; value?: undefined }; /** * @generated from field: map map_string_int64_field = 39; */ mapStringInt64Field: { [key: string]: bigint }; /** * @generated from field: map map_string_message_field = 40; */ mapStringMessageField: { [key: string]: Proto3Message }; /** * @generated from field: map map_string_enum_field = 41; */ mapStringEnumField: { [key: string]: Proto3Enum }; }; /** * Describes the message test.Proto3Message. * Use `create(Proto3MessageSchema)` to create a new message. */ export const Proto3MessageSchema: GenMessage = /*@__PURE__*/ messageDesc(file_proto3, 0); /** * @generated from enum test.Proto3Enum */ export enum Proto3Enum { /** * @generated from enum value: PROTO3_ENUM_UNSPECIFIED = 0; */ UNSPECIFIED = 0, /** * @generated from enum value: PROTO3_ENUM_YES = 1; */ YES = 1, /** * @generated from enum value: PROTO3_ENUM_NO = 2; */ NO = 2, } /** * Describes the enum test.Proto3Enum. */ export const Proto3EnumSchema: GenEnum = /*@__PURE__*/ enumDesc(file_proto3, 0); ================================================ FILE: packages/test-utils/src/index.tsx ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import type { MessageInitShape } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf"; import { createRouterTransport, type ConnectRouterOptions, } from "@connectrpc/connect"; import { BigIntService, type CountRequest, CountResponseSchema, } from "./gen/bigint_pb.js"; import { ElizaService, type SayRequest, SayResponseSchema, } from "./gen/eliza_pb.js"; import { type ListResponseSchema, ListService, type NestedListResponseSchema, } from "./gen/list_pb.js"; /** * A test-only helper to increase time (necessary for testing react-query) */ export const sleep = async (timeout: number) => new Promise((resolve) => { setTimeout(resolve, timeout); }); /** * a stateless mock for ElizaService */ export const mockEliza = ( override?: MessageInitShape, addDelay = false, options?: { router?: ConnectRouterOptions; }, ) => createRouterTransport( ({ service }) => { service(ElizaService, { say: async (input: SayRequest) => { if (addDelay) { await sleep(1000); } return create( SayResponseSchema, override ?? { sentence: `Hello ${input.sentence}` }, ); }, }); }, { router: options?.router, }, ); /** * a stateless mock for BigIntService */ export const mockBigInt = () => createRouterTransport(({ service }) => { service(BigIntService, { count: () => create(CountResponseSchema, { count: 1n }), }); }); /** * a mock for BigIntService that acts as an impromptu database */ export const mockStatefulBigIntTransport = (addDelay = false) => createRouterTransport(({ service }) => { let count = 0n; service(BigIntService, { count: async (request?: CountRequest) => { if (addDelay) { await sleep(1000); } if (request) { count += request.add; } return create(CountResponseSchema, { count }); }, getCount: () => create(CountResponseSchema, { count }), }); }); /** * a mock for PaginatedService that acts as an impromptu database */ export const mockPaginatedTransport = ( override?: MessageInitShape, addDelay = false, options?: { router?: ConnectRouterOptions; }, ) => createRouterTransport( ({ service }) => { service(ListService, { list: async (request) => { if (addDelay) { await sleep(1000); } if (override !== undefined) { return override; } const base = (request.page - 1n) * 3n; const result = { page: request.page, items: [ `${base + 1n} Item`, `${base + 2n} Item`, `${base + 3n} Item`, ], }; return result; }, }); }, { router: options?.router, }, ); /** * a mock for nested paginated list queries */ export const mockNestedPaginatedTransport = ( override?: MessageInitShape, addDelay = false, options?: { router?: ConnectRouterOptions; }, ) => createRouterTransport( ({ service }) => { service(ListService, { nestedList: async (request) => { if (addDelay) { await sleep(1000); } if (override !== undefined) { return override; } const page = request.nested?.page ?? 0n; const base = (page - 1n) * 3n; return { nested: { page, }, items: [ `${base + 1n} Item`, `${base + 2n} Item`, `${base + 3n} Item`, ], }; }, }); }, { router: options?.router, }, ); ================================================ FILE: packages/test-utils/tsconfig.json ================================================ { "include": ["src/"], "extends": "../../tsconfig.base.json" } ================================================ FILE: scripts/find-workspace-version.js ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { findWorkspaceVersion } from "./utils.js"; process.stdout.write(`${findWorkspaceVersion("packages")}\n`); ================================================ FILE: scripts/gh-diffcheck.js ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { execSync } from "node:child_process"; if (gitUncommitted()) { process.stdout.write( "::error::Uncommitted changes found. Please make sure this branch is up to date, and run the command locally (for example `npx turbo format`). " + "Verify the changes are what you want and commit them.\n", ); execSync("git --no-pager diff", { stdio: "inherit", }); process.exit(1); } /** * @returns {boolean} */ function gitUncommitted() { const out = execSync("git status --porcelain", { encoding: "utf-8", }); return out.trim().length > 0; } ================================================ FILE: scripts/release.js ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { execSync } from "node:child_process"; import { findWorkspaceVersion } from "./utils.js"; /* * Publish connect-query * * Recommended procedure: * 1. Trigger the prepare-release workflow with the version you want to release. * 2. Reviews release notes in the created PR, wait for approval. * 3. Merge the PR. */ const tag = determinePublishTag(findWorkspaceVersion("packages")); const uncommitted = gitUncommitted(); if (uncommitted.length > 0) { throw new Error("Uncommitted changes found: \n" + uncommitted); } npmPublish(); /** * */ function npmPublish() { const command = `npm publish --tag ${tag}` + " --workspace packages/connect-query" + " --workspace packages/connect-query-core" + " --workspace packages/protoc-gen-connect-query"; execSync(command, { stdio: "inherit", }); } /** * @returns {string} */ function gitUncommitted() { const out = execSync("git status --short", { encoding: "utf-8", }); if (out.trim().length === 0) { return ""; } return out; } /** * @param {string} version * @returns {string} */ function determinePublishTag(version) { if (/^\d+\.\d+\.\d+$/.test(version)) { return "latest"; } else if (/^\d+\.\d+\.\d+-alpha.*$/.test(version)) { return "alpha"; } else if (/^\d+\.\d+\.\d+-beta.*$/.test(version)) { return "beta"; } else if (/^\d+\.\d+\.\d+-rc.*$/.test(version)) { return "rc"; } else { throw new Error(`Unable to determine publish tag from version ${version}`); } } ================================================ FILE: scripts/set-workspace-version.js ================================================ #!/usr/bin/env node // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // eslint-disable-next-line n/no-unsupported-features/node-builtins import { readFileSync, writeFileSync, existsSync, globSync } from "node:fs"; import { dirname, join } from "node:path"; // Ensures that a valid semver version is provided // See https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string const versionRegex = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; if (process.argv.length !== 3 || !versionRegex.test(process.argv[2])) { process.stderr.write( [ `USAGE: ${process.argv[1]} `, "", "Walks through all workspace packages and sets the version of each ", "package to the given version.", "If a package depends on another package from the workspace, the", "dependency version is updated as well.", "", ...(versionRegex.test(process.argv[2]) ? [] : [ "Version provided is not a valid semver version.", "Please provide a version in the format MAJOR.MINOR.PATCH[-PRERELEASE+BUILD].", ]), ].join("\n"), ); process.exit(1); } const newVersion = process.argv[2]; const rootPackagePath = "package.json"; const lockFilePath = "package-lock.json"; try { const lock = readLockfile(lockFilePath); const workspaces = readWorkspaces(rootPackagePath); const allPackages = workspaces.map((ws) => ws.pkg); const bumpPackages = allPackages.filter( (pkg) => pkg.version !== undefined && pkg.version !== newVersion, ); /** @type {Log[]} */ const log = []; for (const pkg of bumpPackages) { log.push({ pkg, message: `updated version from ${pkg.version} to ${newVersion}`, }); pkg.version = newVersion; findLockPackage(lock, pkg.name).version = newVersion; } for (const pkg of bumpPackages) { // update deps in workspace package.json for (const other of allPackages) { log.push(...updatePackageDep(other, pkg.name, newVersion)); } // update deps in package-lock.json for (const lockPkg of Object.values(lock.packages)) { updatePackageDep(lockPkg, pkg.name, newVersion); } } if (log.length > 0) { for (const { path, pkg } of workspaces) { writeJson(path, pkg); } writeJson(lockFilePath, lock); process.stdout.write(formatLog(log) + "\n"); } } catch (e) { process.stderr.write(String(e) + "\n"); process.exit(1); } /** * @typedef {{path: string; pkg: Package}} Workspace */ /** * Read the given root package.json file, and return an array of workspace * packages. * * @param {string} rootPackagePath * @return {Workspace[]} */ function readWorkspaces(rootPackagePath) { const root = readRootPackage(rootPackagePath); const rootDir = dirname(rootPackagePath); return root.workspaces .flatMap((ws) => globSync(join(rootDir, ws, "package.json"))) .filter((path) => existsSync(path)) .map((path) => { return { path, pkg: readPackage(path) }; }); } /** * @typedef {{message: string, pkg: Package}} Log */ /** * @param {Package|LockPackage} pkg * @param {string} depName * @param {string} toVersion * @return {Log[]} */ function updatePackageDep(pkg, depName, toVersion) { if (toVersion === undefined) { throw new Error("toVersion undefined"); } /** @type {Log[]} */ const log = []; for (const key of [ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", ]) { // eslint-disable-next-line n/no-unsupported-features/es-builtins,n/no-unsupported-features/es-syntax if (!Object.hasOwn(pkg, key)) { continue; } /** @type { Record } */ const deps = pkg[key]; const from = deps[depName]; if (from === undefined) { continue; } let to; if (from.startsWith("^")) { to = `^${toVersion}`; } else if (from.startsWith("~")) { to = `~${toVersion}`; } else if (from.startsWith("=")) { to = `=${toVersion}`; } else if (from === "*") { to = `*`; } else { to = toVersion; } if (from === to) { continue; } deps[depName] = to; log.push({ pkg, message: `updated ${key}["${depName}"] from ${from} to ${to}`, }); } return log; } /** * * @param {Log[]} log * @return {string} */ function formatLog(log) { const lines = []; const updatesByName = {}; for (const l of log) { if (updatesByName[l.pkg.name] === undefined) { updatesByName[l.pkg.name] = []; } updatesByName[l.pkg.name].push(l); } for (const name of Object.keys(updatesByName).sort()) { lines.push(`${name}:`); for (const update of updatesByName[name]) { lines.push(` ${update.message}`); } } return lines.join("\n"); } /** * @typedef {{name: string; version?: string; private?: boolean}} Package */ /** * @param {string} path * @return {Package} */ function readPackage(path) { const json = JSON.parse(readFileSync(path, "utf-8")); if (typeof json !== "object" || json === null) { throw new Error(`Failed to parse ${path}`); } const lock = JSON.parse(readFileSync(path, "utf-8")); if (typeof lock !== "object" || lock === null) { throw new Error(`Failed to parse ${path}`); } if (!("name" in json) || typeof json.name != "string") { throw new Error(`Missing "name" in ${path}`); } if ("version" in json) { if (typeof json.version != "string") { throw new Error(`Invalid "version" in ${path}`); } } else if (!("private" in json) || json.private !== true) { throw new Error(`Need either "version" or "private":true in ${path}`); } return lock; } /** * @typedef {{packages: Record}} Lockfile */ /** * @typedef {{name?: string; version?: string}} LockPackage */ /** * @param {string} path * @return {Lockfile} */ function readLockfile(path) { const lock = JSON.parse(readFileSync(path, "utf-8")); if (typeof lock !== "object" || lock === null) { throw new Error(`Failed to parse ${path}`); } if (!("lockfileVersion" in lock) || lock.lockfileVersion !== 3) { throw new Error(`Unsupported lock file version in ${path}`); } if (typeof lock.packages != "object" || lock.packages == null) { throw new Error(`Missing "packages" in ${path}`); } return lock; } /** * Locates an entry for a local workspace package in a lock file. * Throws an error if not found. * * @param {Lockfile} lock * @param {string} packageName * @return {LockPackage} */ function findLockPackage(lock, packageName) { for (const [path, lockPkg] of Object.entries(lock.packages)) { // eslint-disable-next-line n/no-unsupported-features/es-builtins,n/no-unsupported-features/es-syntax if (Object.hasOwn(lockPkg, "name") && lockPkg.name === packageName) { return lockPkg; } // In some situations, the entry for a local package doesn't have a "name" property. // We check the path of the entry instead: If the last path element is the same as // the package name without scope, it's the entry we are looking for. if (path.startsWith("node_modules/")) { // Not a local workspace package continue; } const lastPathEle = path.split("/").pop(); const packageShortname = packageName.split("/").pop(); if (lastPathEle === packageShortname) { return lockPkg; } } throw new Error( `Cannot find package ${packageName} in lock file. Run npm install?`, ); } /** * @typedef {{ name?: string; version?: string; workspaces: string[] }} RootPackage */ /** * @param {string} path * @return {RootPackage} */ function readRootPackage(path) { const json = JSON.parse(readFileSync(path, "utf-8")); if (typeof json !== "object" || json === null) { throw new Error(`Failed to parse ${path}`); } if ( !Array.isArray(json.workspaces) || json.workspaces.some((w) => typeof w !== "string") ) { throw new Error(`Missing or malformed "workspaces" array in ${path}`); } return json; } /** * @param {string} path * @param {any} json */ function writeJson(path, json) { writeFileSync(path, JSON.stringify(json, null, 2) + "\n"); } ================================================ FILE: scripts/utils.js ================================================ // Copyright 2021-2023 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { readdirSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; /** * Retrieves the workspace version from the package directory. * * @param {string} packagesDir * @returns {string} */ export function findWorkspaceVersion(packagesDir) { let version = undefined; for (const entry of readdirSync(packagesDir, { withFileTypes: true })) { if (!entry.isDirectory()) { continue; } const path = join(packagesDir, entry.name, "package.json"); if (existsSync(path)) { const pkg = JSON.parse(readFileSync(path, "utf-8")); if (pkg.private === true) { continue; } if (!pkg.version) { throw new Error(`${path} is missing "version"`); } if (version === undefined) { version = pkg.version; } else if (version !== pkg.version) { throw new Error(`${path} has unexpected version ${pkg.version}`); } } } if (version === undefined) { throw new Error(`unable to find workspace version`); } return version; } ================================================ FILE: tsconfig.base.json ================================================ { "compilerOptions": { "target": "ES2020", "lib": [ "ES2017", // DOM for the fetch and streams API "DOM" ], "esModuleInterop": false, "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "noImplicitThis": true, "useUnknownInCatchVariables": true, "noUnusedLocals": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, // We need node's module resolution, so we do not have to skip lib checks "moduleResolution": "Node16", "module": "Node16", "verbatimModuleSyntax": true, "skipLibCheck": false } } ================================================ FILE: turbo.json ================================================ { "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build", "generate"], "outputs": ["dist/**"] }, "generate": { "dependsOn": ["^build"], "outputs": ["src/gen/**"] }, "test": { "dependsOn": ["build"], "cache": false }, "format": {}, "license-header": { "dependsOn": ["generate"] }, "lint": { "dependsOn": ["format", "^build", "generate"] }, "attw": { "dependsOn": ["build"] }, "//#format": { "inputs": ["$TURBO_DEFAULT$", "!packages/**", "package-lock.json"] }, "//#license-header": { "inputs": ["$TURBO_DEFAULT$", "!packages/**"] }, "//#lint": { "dependsOn": ["format"], "inputs": ["$TURBO_DEFAULT$", "!packages/**", "package-lock.json"] } } }