├── .changeset ├── README.md └── config.json ├── .env.local ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── push-migrations.yml │ ├── release-next.yml │ ├── release.yml │ ├── sync-renovate-changesets.yml │ └── update-prs.yml ├── .gitignore ├── .npmrc ├── .nvmrc ├── LICENSE ├── README.md ├── biome.json ├── docs ├── .eslintrc.json ├── .gitignore ├── README.md ├── app │ ├── [[...mdxPath]] │ │ └── page.jsx │ ├── _meta.js │ └── layout.jsx ├── components │ ├── linked-tabs.jsx │ └── tab-context.jsx ├── content │ ├── configuration.mdx │ ├── index.mdx │ ├── postgrest │ │ ├── _meta.ts │ │ ├── custom-cache-updates.mdx │ │ ├── getting-started.mdx │ │ ├── mutations.mdx │ │ ├── queries.mdx │ │ ├── server.mdx │ │ ├── ssr │ │ │ ├── _meta.ts │ │ │ ├── react-query.mdx │ │ │ └── swr.mdx │ │ └── subscriptions.mdx │ └── storage │ │ ├── _meta.ts │ │ ├── getting-started.mdx │ │ ├── mutations.mdx │ │ └── queries.mdx ├── mdx-components.jsx ├── next.config.mjs ├── package.json ├── public │ ├── favicon.ico │ └── og-image.png ├── styles │ └── globals.css └── tsconfig.json ├── examples ├── react-query │ ├── .editorconfig │ ├── .gitignore │ ├── components.json │ ├── components │ │ ├── contact │ │ │ ├── contact-cards.tsx │ │ │ └── upsert-contact.modal.tsx │ │ ├── continent │ │ │ └── continent-select.tsx │ │ ├── icons.tsx │ │ ├── layout.tsx │ │ ├── main-nav.tsx │ │ ├── site-header.tsx │ │ ├── theme-toggle.tsx │ │ ├── typography │ │ │ ├── code.tsx │ │ │ ├── h1.tsx │ │ │ ├── h2.tsx │ │ │ ├── h3.tsx │ │ │ ├── h4.tsx │ │ │ ├── p.tsx │ │ │ ├── small.tsx │ │ │ └── subtle.tsx │ │ └── ui │ │ │ ├── avatar.tsx │ │ │ ├── button.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ └── select.tsx │ ├── config │ │ └── site.ts │ ├── lib │ │ └── utils.ts │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── index.tsx │ │ ├── use-infinite-scroll-query.tsx │ │ ├── use-pagination-query.tsx │ │ └── use-query.tsx │ ├── postcss.config.js │ ├── public │ │ └── favicon.ico │ ├── styles │ │ └── globals.css │ ├── tsconfig.json │ └── types │ │ ├── database.ts │ │ └── nav.ts └── swr │ ├── .editorconfig │ ├── .gitignore │ ├── components.json │ ├── components │ ├── contact │ │ ├── contact-cards.tsx │ │ └── upsert-contact.modal.tsx │ ├── continent │ │ └── continent-select.tsx │ ├── icons.tsx │ ├── layout.tsx │ ├── main-nav.tsx │ ├── site-header.tsx │ ├── theme-toggle.tsx │ ├── typography │ │ ├── code.tsx │ │ ├── h1.tsx │ │ ├── h2.tsx │ │ ├── h3.tsx │ │ ├── h4.tsx │ │ ├── p.tsx │ │ ├── small.tsx │ │ └── subtle.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ └── select.tsx │ ├── config │ └── site.ts │ ├── lib │ └── utils.ts │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── index.tsx │ ├── use-infinite-scroll-query.tsx │ └── use-pagination-query.tsx │ ├── postcss.config.js │ ├── public │ └── favicon.ico │ ├── styles │ └── globals.css │ ├── tsconfig.json │ └── types │ ├── database.ts │ └── nav.ts ├── hackathon.md ├── package.json ├── packages ├── postgrest-core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cursor-pagination-fetcher.ts │ │ ├── delete-fetcher.ts │ │ ├── delete-item.ts │ │ ├── fetch │ │ │ ├── build-mutation-fetcher-response.ts │ │ │ ├── build-normalized-query.ts │ │ │ ├── build-select-statement.ts │ │ │ └── dedupe.ts │ │ ├── fetcher.ts │ │ ├── filter │ │ │ └── denormalize.ts │ │ ├── index.ts │ │ ├── insert-fetcher.ts │ │ ├── lib │ │ │ ├── binary-search.ts │ │ │ ├── cache-data-types.ts │ │ │ ├── encode-object.ts │ │ │ ├── extract-paths-from-filter.ts │ │ │ ├── filter-filter-definitions-by-paths.ts │ │ │ ├── find-filters.ts │ │ │ ├── find-index-ordered.ts │ │ │ ├── find-last-index.ts │ │ │ ├── get-table-from-url.ts │ │ │ ├── get-table.ts │ │ │ ├── get.ts │ │ │ ├── group-paths-recursive.ts │ │ │ ├── if-date-get-time.ts │ │ │ ├── is-iso-date-string.ts │ │ │ ├── is-not-null.ts │ │ │ ├── is-object.ts │ │ │ ├── is-plain-object.ts │ │ │ ├── is-postgrest-builder.ts │ │ │ ├── is-postgrest-transform-builder.ts │ │ │ ├── like-postgrest-builder.ts │ │ │ ├── like-query-builder.ts │ │ │ ├── operators.ts │ │ │ ├── parse-order-by-key.ts │ │ │ ├── parse-order-by.ts │ │ │ ├── parse-select-param.ts │ │ │ ├── parse-value.ts │ │ │ ├── query-types.ts │ │ │ ├── remove-alias-from-declaration.ts │ │ │ ├── remove-first-path-element.ts │ │ │ ├── response-types.ts │ │ │ ├── set-filter-value.ts │ │ │ ├── sort-search-param.ts │ │ │ └── sorted-comparator.ts │ │ ├── mutate-item.ts │ │ ├── mutate │ │ │ ├── should-revalidate-relation.ts │ │ │ ├── should-revalidate-table.ts │ │ │ ├── transformers.ts │ │ │ └── types.ts │ │ ├── offset-pagination-fetcher.ts │ │ ├── postgrest-filter.ts │ │ ├── postgrest-parser.ts │ │ ├── postgrest-query-parser.ts │ │ ├── revalidate-tables.ts │ │ ├── update-fetcher.ts │ │ ├── upsert-fetcher.ts │ │ └── upsert-item.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── fetcher.spec.ts.snap │ │ ├── cursor-pagination-fetcher.spec.ts │ │ ├── database.types.ts │ │ ├── delete-fetcher.spec.ts │ │ ├── delete-item.spec.ts │ │ ├── fetch │ │ │ ├── build-mutation-fetcher-response.spec.ts │ │ │ ├── build-normalized-query.spec.ts │ │ │ └── build-select-statement.spec.ts │ │ ├── fetcher.spec.ts │ │ ├── filter │ │ │ └── denormalize.spec.ts │ │ ├── index.spec.ts │ │ ├── insert-fetcher.spec.ts │ │ ├── lib │ │ │ ├── extract-paths-from-filters.spec.ts │ │ │ ├── find-filters.spec.ts │ │ │ ├── find-index-ordered.spec.ts │ │ │ ├── get-table.spec.ts │ │ │ ├── get.spec.ts │ │ │ ├── group-paths-recursive.spec.ts │ │ │ ├── if-date-get-time.spec.ts │ │ │ ├── operators.spec.ts │ │ │ ├── parse-order-by-key.spec.ts │ │ │ ├── parse-select-param.spec.ts │ │ │ └── sorted-comparator.spec.ts │ │ ├── mutate-item.spec.ts │ │ ├── mutate │ │ │ ├── should-revalidate-relation.spec.ts │ │ │ ├── should-revalidate-table.spec.ts │ │ │ └── transformers.spec.ts │ │ ├── offset-pagination-fetcher.spec.ts │ │ ├── postgrest-filter.integration.spec.ts │ │ ├── postgrest-filter.spec.ts │ │ ├── postgrest-parser.spec.ts │ │ ├── postgrest-query-parser.spec.ts │ │ ├── revalidate-tables.spec.ts │ │ ├── update-fetcher.spec.ts │ │ ├── upsert-fetcher.spec.ts │ │ ├── upsert-item.spec.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── postgrest-react-query │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cache │ │ │ ├── index.ts │ │ │ ├── use-delete-item.ts │ │ │ ├── use-mutate-item.ts │ │ │ ├── use-revalidate-tables.ts │ │ │ └── use-upsert-item.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── index.ts │ │ │ ├── key.ts │ │ │ ├── use-postgrest-filter-cache.ts │ │ │ └── use-queries-for-table-loader.ts │ │ ├── mutate │ │ │ ├── get-user-response.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── use-delete-many-mutation.ts │ │ │ ├── use-delete-mutation.ts │ │ │ ├── use-insert-mutation.ts │ │ │ ├── use-update-mutation.ts │ │ │ └── use-upsert-mutation.ts │ │ ├── query │ │ │ ├── build-query-opts.ts │ │ │ ├── fetch.ts │ │ │ ├── index.ts │ │ │ ├── prefetch.ts │ │ │ └── use-query.ts │ │ └── subscribe │ │ │ ├── index.ts │ │ │ ├── use-subscription-query.ts │ │ │ └── use-subscription.ts │ ├── tests │ │ ├── cache │ │ │ └── use-mutate-item.spec.tsx │ │ ├── database.types.ts │ │ ├── mutate │ │ │ ├── use-delete-many-mutation.integration.spec.tsx │ │ │ ├── use-delete-mutation.integration.spec.tsx │ │ │ ├── use-insert-mutation.integration.spec.tsx │ │ │ ├── use-update-mutation.integration.spec.tsx │ │ │ └── use-upsert-mutation.integration.spec.tsx │ │ ├── query │ │ │ ├── fetch.spec.ts │ │ │ ├── prefetch.integration.spec.ts │ │ │ └── use-query.integration.spec.tsx │ │ ├── subscribe │ │ │ ├── use-subscription-query.integration.spec.tsx │ │ │ └── use-subscription.integration.spec.tsx │ │ └── utils.tsx │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── postgrest-server │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── context.ts │ │ ├── index.ts │ │ ├── key.ts │ │ ├── query-cache.ts │ │ ├── stores │ │ │ ├── entry.ts │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ ├── memory.ts │ │ │ └── redis.ts │ │ ├── swr-cache.ts │ │ ├── tiered-store.ts │ │ └── utils.ts │ ├── tests │ │ ├── database.types.ts │ │ ├── query-cache.test.ts │ │ ├── stores │ │ │ └── memory.test.ts │ │ ├── swr-cache.test.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── postgrest-swr │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cache │ │ │ ├── index.ts │ │ │ ├── use-delete-item.ts │ │ │ ├── use-mutate-item.ts │ │ │ ├── use-revalidate-tables.ts │ │ │ └── use-upsert-item.ts │ │ ├── index.react-server.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── create-key-getter.ts │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ ├── index.ts │ │ │ ├── middleware.ts │ │ │ ├── mutable-keys.ts │ │ │ ├── parse-order-by.ts │ │ │ ├── types.ts │ │ │ ├── use-postgrest-filter-cache.ts │ │ │ └── use-queries-for-table-loader.ts │ │ ├── mutate │ │ │ ├── get-user-response.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── use-delete-many-mutation.ts │ │ │ ├── use-delete-mutation.ts │ │ │ ├── use-insert-mutation.ts │ │ │ ├── use-random-key.ts │ │ │ ├── use-update-mutation.ts │ │ │ └── use-upsert-mutation.ts │ │ ├── query │ │ │ ├── index.ts │ │ │ ├── prefetch.ts │ │ │ ├── use-cursor-infinite-scroll-query.ts │ │ │ ├── use-infinite-offset-pagination-query.ts │ │ │ ├── use-offset-infinite-query.ts │ │ │ ├── use-offset-infinite-scroll-query.ts │ │ │ └── use-query.ts │ │ └── subscribe │ │ │ ├── index.ts │ │ │ ├── use-subscription-query.ts │ │ │ └── use-subscription.ts │ ├── tests │ │ ├── cache │ │ │ └── use-mutate-item.spec.tsx │ │ ├── database.types.ts │ │ ├── lib │ │ │ └── get-mutable-keys.spec.ts │ │ ├── mutate │ │ │ ├── use-delete-many-mutation.spec.tsx │ │ │ ├── use-delete-mutation.integration.spec.tsx │ │ │ ├── use-insert-mutation.integration.spec.tsx │ │ │ ├── use-update-mutation.integration.spec.tsx │ │ │ └── use-upsert-mutation.integration.spec.tsx │ │ ├── query │ │ │ ├── prefetch.integration.spec.ts │ │ │ ├── use-cursor-infinite-scroll-query.spec.tsx │ │ │ ├── use-infinite-offset-pagination-query.spec.tsx │ │ │ ├── use-offset-infinite-query.spec.tsx │ │ │ ├── use-offset-infinite-scroll-query.spec.tsx │ │ │ └── use-query.integration.spec.tsx │ │ ├── subscribe │ │ │ ├── use-subscription-query.integration.spec.tsx │ │ │ └── use-subscription.integration.spec.tsx │ │ └── utils.tsx │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── storage-core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── directory-fetcher.ts │ │ ├── directory-urls-fetcher.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── get-minimal-paths.ts │ │ │ └── types.ts │ │ ├── mutate-paths.ts │ │ ├── remove-directory.ts │ │ ├── remove-files.ts │ │ ├── upload.ts │ │ └── url-fetcher.ts │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ └── 4.jpg │ │ ├── directory-fetcher.spec.ts │ │ ├── directory-urls-fetcher.spec.ts │ │ ├── index.spec.ts │ │ ├── mutate-paths.spec.ts │ │ ├── remove-directory.spec.ts │ │ ├── remove-files.spec.ts │ │ ├── upload.spec.ts │ │ ├── url-fetcher.spec.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── storage-react-query │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ ├── get-bucket-id.ts │ │ │ ├── index.ts │ │ │ ├── key.ts │ │ │ ├── truthy.ts │ │ │ └── types.ts │ │ ├── mutate │ │ │ ├── index.ts │ │ │ ├── use-remove-directory.ts │ │ │ ├── use-remove-files.ts │ │ │ └── use-upload.ts │ │ └── query │ │ │ ├── index.ts │ │ │ ├── use-directory-urls.ts │ │ │ ├── use-directory.ts │ │ │ └── use-file-url.ts │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ └── 4.jpg │ │ ├── lib │ │ │ ├── decode.spec.ts │ │ │ └── key.spec.ts │ │ ├── mutate │ │ │ ├── use-remove-directory.spec.tsx │ │ │ ├── use-remove-files.spec.tsx │ │ │ └── use-upload.spec.tsx │ │ ├── query │ │ │ ├── use-directory-urls.spec.tsx │ │ │ ├── use-directory.spec.tsx │ │ │ └── use-file-url.spec.tsx │ │ └── utils.tsx │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── storage-swr │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── decode.ts │ │ │ ├── encode.ts │ │ │ ├── get-bucket-id.ts │ │ │ ├── index.ts │ │ │ ├── key.ts │ │ │ ├── middleware.ts │ │ │ ├── truthy.ts │ │ │ └── types.ts │ │ ├── mutate │ │ │ ├── index.ts │ │ │ ├── use-random-key.ts │ │ │ ├── use-remove-directory.ts │ │ │ ├── use-remove-files.ts │ │ │ └── use-upload.ts │ │ └── query │ │ │ ├── index.ts │ │ │ ├── use-directory-urls.ts │ │ │ ├── use-directory.ts │ │ │ └── use-file-url.ts │ ├── tests │ │ ├── __fixtures__ │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ └── 4.jpg │ │ ├── lib │ │ │ ├── decode.spec.ts │ │ │ └── key.spec.ts │ │ ├── mutate │ │ │ ├── use-remove-directory.spec.tsx │ │ │ ├── use-remove-files.spec.tsx │ │ │ └── use-upload.spec.tsx │ │ ├── query │ │ │ ├── use-directory-urls.spec.tsx │ │ │ ├── use-directory.spec.tsx │ │ │ └── use-file-url.spec.tsx │ │ └── utils.tsx │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts └── tsconfig │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── node.json │ ├── package.json │ ├── react-library.json │ └── web.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── supabase ├── .gitignore ├── config.toml └── migrations │ ├── 1660333326_test_schema.sql │ ├── 20230912094327_add_serial_pk_table.sql │ ├── 20231114093521_add_multi_pk_table.sql │ ├── 20240229212226_add_many_to_many_test_tables.sql │ ├── 20240311151146_add_multi_fkeys.sql │ ├── 20240311151147_allow_aggregates.sql │ └── 20240311151148_add_rpcs.sql ├── tsconfig.json └── turbo.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | SUPABASE_URL=http://localhost:54321 2 | SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 3 | 4 | NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321 5 | NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0 6 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [psteinroe] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | If possible, open a PR with a failing test. Otherwise, describe how to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/workflows/push-migrations.yml: -------------------------------------------------------------------------------- 1 | name: Push Migrations 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | push-migrations: 15 | name: Deploy 16 | timeout-minutes: 30 17 | runs-on: ubuntu-latest 18 | env: 19 | # dev environment secrets 20 | SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }} 21 | SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} 22 | SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: supabase/setup-cli@v1 27 | - run: supabase link --project-ref $SUPABASE_PROJECT_REF 28 | - run: supabase db push 29 | -------------------------------------------------------------------------------- /.github/workflows/release-next.yml: -------------------------------------------------------------------------------- 1 | name: Release @next 2 | 3 | on: 4 | push: 5 | branches: 6 | - next 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | env: 14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 15 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: pnpm/action-setup@v3.0.0 20 | with: 21 | version: 8.x.x 22 | 23 | - name: Set up Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | 29 | - name: Install Dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Build Packages 33 | run: pnpm run build --filter='./packages/*' 34 | 35 | - name: Create a release 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | version: pnpm ci:version --snapshot next 40 | commit: "chore: update versions" 41 | title: "chore: update versions" 42 | publish: pnpm ci:release --tag next 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | env: 14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 15 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: pnpm/action-setup@v3.0.0 20 | with: 21 | version: 8.x.x 22 | 23 | - name: Set up Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | cache: pnpm 28 | 29 | - name: Install Dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Build Packages 33 | run: pnpm run build --filter='./packages/*' 34 | 35 | - name: Create a release 36 | id: changesets 37 | uses: changesets/action@v1 38 | with: 39 | version: pnpm ci:version 40 | commit: "chore: update versions" 41 | title: "chore: update versions" 42 | publish: pnpm ci:release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/update-prs.yml: -------------------------------------------------------------------------------- 1 | name: Update stale PRs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | update-prs: 9 | name: Run auto-update to update stale PRs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: tibdex/auto-update@v2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # turbo 27 | .turbo 28 | 29 | # dist 30 | **/dist/ 31 | 32 | tsconfig.tsbuildinfo 33 | 34 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.14.0 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Philipp Steinrötter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": ["**/.next", "node_modules"] 8 | }, 9 | "linter": { 10 | "enabled": false 11 | }, 12 | "formatter": { 13 | "indentStyle": "space", 14 | "indentWidth": 2, 15 | "lineWidth": 80 16 | }, 17 | "javascript": { 18 | "formatter": { 19 | "quoteStyle": "single", 20 | "jsxQuoteStyle": "double" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/docs/README.md -------------------------------------------------------------------------------- /docs/app/[[...mdxPath]]/page.jsx: -------------------------------------------------------------------------------- 1 | import { LinkedTabs } from '@/components/linked-tabs'; 2 | import { generateStaticParamsFor, importPage } from 'nextra/pages'; 3 | import { useMDXComponents as getMDXComponents } from '../../mdx-components'; 4 | 5 | export const generateStaticParams = generateStaticParamsFor('mdxPath'); 6 | 7 | export async function generateMetadata(props) { 8 | const params = await props.params; 9 | const { metadata } = await importPage(params.mdxPath); 10 | return metadata; 11 | } 12 | 13 | const Wrapper = getMDXComponents({ LinkedTabs }).wrapper; 14 | 15 | export default async function Page(props) { 16 | const params = await props.params; 17 | const result = await importPage(params.mdxPath); 18 | const { default: MDXContent, toc, metadata } = result; 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /docs/app/_meta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | title: 'Introduction', 4 | }, 5 | configuration: 'Configuration', 6 | postgrest: 'PostgREST', 7 | storage: 'Storage', 8 | }; 9 | -------------------------------------------------------------------------------- /docs/components/linked-tabs.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Tabs } from 'nextra/components'; 4 | import React from 'react'; 5 | import { useTabContext } from './tab-context'; 6 | 7 | export const LinkedTabs = ({ id, items, children }) => { 8 | const { selectedTabs, setSelectedTab } = useTabContext(); 9 | const selectedTab = selectedTabs[id] || items[0]; 10 | 11 | const index = items.indexOf(selectedTab); 12 | const selectedIndex = index !== -1 ? index : 0; 13 | 14 | return ( 15 | setSelectedTab(id, items[index])} 19 | > 20 | {children} 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /docs/components/tab-context.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createContext, useContext, useEffect, useState } from 'react'; 4 | 5 | const TabContext = createContext({ 6 | selectedTabs: {}, 7 | setSelectedTab: () => {}, 8 | }); 9 | 10 | export const useTabContext = () => useContext(TabContext); 11 | 12 | export const TabProvider = ({ children }) => { 13 | const [selectedTabs, setSelectedTabs] = useState({}); 14 | 15 | useEffect(() => { 16 | if (typeof window !== 'undefined') { 17 | const storedTabs = localStorage.getItem('selectedTabs'); 18 | if (storedTabs) { 19 | try { 20 | const parsedTabs = JSON.parse(storedTabs); 21 | setSelectedTabs(parsedTabs); 22 | } catch (error) { 23 | console.error( 24 | 'Failed to parse selectedTabs from localStorage', 25 | error, 26 | ); 27 | } 28 | } 29 | } 30 | }, []); 31 | 32 | useEffect(() => { 33 | if (typeof window !== 'undefined') { 34 | localStorage.setItem('selectedTabs', JSON.stringify(selectedTabs)); 35 | } 36 | }, [selectedTabs]); 37 | 38 | const setSelectedTab = (id, tab) => { 39 | setSelectedTabs((prev) => ({ 40 | ...prev, 41 | [id]: tab, 42 | })); 43 | }; 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /docs/content/configuration.mdx: -------------------------------------------------------------------------------- 1 | import { Callout, Tabs } from 'nextra/components'; 2 | import { LinkedTabs } from '@/components/linked-tabs'; 3 | 4 | # Configuration 5 | 6 | Supabase Cache Helpers does a decent job at keeping your data up-to-date. This allows you to deviate from the standard configuration and reduce the number of requests to your backend while keeping your app fresh. 7 | 8 | 11 | 12 | ```tsx 13 | function Page() { 14 | return ( 15 | 21 | ... 22 | 23 | ) 24 | } 25 | ``` 26 | 27 | 28 | 29 | You can find more details on the [React Query documentation](https://tanstack.com/query/latest/docs/framework/react/quick-start). 30 | 31 | ```tsx 32 | function Page() { 33 | const queryClient = new QueryClient({ 34 | defaultOptions: { 35 | queries: { 36 | refetchOnWindowFocus: false, 37 | staleTime: Infinity, 38 | gcTime: Infinity, 39 | }, 40 | }, 41 | }); 42 | 43 | return ( 44 | 45 | {/* Your components */} 46 | 47 | ); 48 | } 49 | ``` 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/content/postgrest/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'getting-started': 'Getting Started', 3 | queries: 'Queries', 4 | mutations: 'Mutations', 5 | subscriptions: 'Subscriptions', 6 | 'custom-cache-updates': 'Custom Cache Updates', 7 | ssr: 'Server Side Rendering', 8 | server: 'Server Side Caching', 9 | }; 10 | -------------------------------------------------------------------------------- /docs/content/postgrest/ssr/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | swr: 'SWR', 3 | 'react-query': 'React Query', 4 | }; 5 | -------------------------------------------------------------------------------- /docs/content/storage/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'getting-started': 'Getting Started', 3 | queries: 'Queries', 4 | mutations: 'Mutations', 5 | }; 6 | -------------------------------------------------------------------------------- /docs/mdx-components.jsx: -------------------------------------------------------------------------------- 1 | import { useMDXComponents as getDocsMDXComponents } from 'nextra-theme-docs'; 2 | 3 | const docsComponents = getDocsMDXComponents(); 4 | 5 | export const useMDXComponents = (components) => ({ 6 | ...docsComponents, 7 | ...components, 8 | }); 9 | -------------------------------------------------------------------------------- /docs/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | import nextra from 'nextra'; 4 | 5 | const withNextra = nextra({}); 6 | 7 | export default withNextra(); 8 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3002 --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "clean": "rm -rf .turbo && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", 10 | "typecheck": "tsc --pretty --noEmit" 11 | }, 12 | "dependencies": { 13 | "@vercel/analytics": "1.5.0", 14 | "next": "15.3.1", 15 | "nextra": "4.2.17", 16 | "nextra-theme-docs": "4.2.17", 17 | "react": "19.1.0", 18 | "react-dom": "19.1.0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "22.14.1", 22 | "@types/react": "19.1.2", 23 | "@types/react-dom": "19.1.2", 24 | "autoprefixer": "^10.4.13", 25 | "postcss": "^8.4.14", 26 | "tailwindcss": "4.1.4", 27 | "typescript": "5.8.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/docs/public/og-image.png -------------------------------------------------------------------------------- /docs/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | /* 4 | The default border color has changed to `currentcolor` in Tailwind CSS v4, 5 | so we've added these compatibility styles to make sure everything still 6 | looks the same as it did with Tailwind CSS v3. 7 | 8 | If we ever want to remove these styles, we need to add an explicit border 9 | color utility to any element that depends on these defaults. 10 | */ 11 | @layer base { 12 | *, 13 | ::after, 14 | ::before, 15 | ::backdrop, 16 | ::file-selector-button { 17 | border-color: var(--color-gray-200, currentcolor); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ] 25 | }, 26 | "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /examples/react-query/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /examples/react-query/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | .env -------------------------------------------------------------------------------- /examples/react-query/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-query/components/continent/continent-select.tsx: -------------------------------------------------------------------------------- 1 | import type * as SelectPrimitive from '@radix-ui/react-select'; 2 | import { useQuery } from '@supabase-cache-helpers/postgrest-react-query'; 3 | import { useSupabaseClient } from '@supabase/auth-helpers-react'; 4 | import type { FC } from 'react'; 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from '@/components/ui/select'; 13 | import { cn } from '@/lib/utils'; 14 | import type { Database } from '@/types/database'; 15 | 16 | export type ContinentSelectProps = { 17 | containerProps?: SelectPrimitive.SelectProps; 18 | triggerProps?: SelectPrimitive.SelectTriggerProps; 19 | }; 20 | 21 | export const ContinentSelect: FC = ({ 22 | containerProps, 23 | triggerProps, 24 | }) => { 25 | const supabase = useSupabaseClient(); 26 | const { data, isLoading } = useQuery( 27 | supabase.from('continent').select('code,name'), 28 | ); 29 | 30 | const { disabled, ...props } = containerProps; 31 | 32 | return ( 33 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /examples/react-query/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { useSubscriptionQuery } from '@supabase-cache-helpers/postgrest-react-query'; 2 | import { useSupabaseClient } from '@supabase/auth-helpers-react'; 3 | 4 | import { SiteHeader } from '@/components/site-header'; 5 | import type { Database } from '@/types/database'; 6 | 7 | interface LayoutProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | export function Layout({ children }: LayoutProps) { 12 | const supabase = useSupabaseClient(); 13 | 14 | useSubscriptionQuery( 15 | supabase, 16 | 'contacts', 17 | { event: '*', table: 'contact', schema: 'public' }, 18 | ['id'], 19 | ); 20 | 21 | return ( 22 | <> 23 | 24 |
{children}
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/react-query/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | import * as React from 'react'; 3 | 4 | import { Icons } from '@/components/icons'; 5 | import { Button } from '@/components/ui/button'; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from '@/components/ui/dropdown-menu'; 12 | 13 | export function ThemeToggle() { 14 | const { setTheme } = useTheme(); 15 | 16 | return ( 17 | 18 | 19 | 24 | 25 | 26 | setTheme('light')}> 27 | 28 | Light 29 | 30 | setTheme('dark')}> 31 | 32 | Dark 33 | 34 | setTheme('system')}> 35 | 36 | System 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/code.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Code: FC< 6 | DetailedHTMLProps, HTMLElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 | 16 | {children} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/h1.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H1: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/h2.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H2: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/h3.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H3: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/h4.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H4: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/p.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const P: FC< 6 | DetailedHTMLProps, HTMLParagraphElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

10 | {children} 11 |

12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/small.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Small: FC< 6 | DetailedHTMLProps, HTMLElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/react-query/components/typography/subtle.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Subtle: FC< 6 | DetailedHTMLProps, HTMLParagraphElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

13 | {children} 14 |

15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/react-query/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ); 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback }; 52 | -------------------------------------------------------------------------------- /examples/react-query/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<'input'>) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /examples/react-query/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /examples/react-query/config/site.ts: -------------------------------------------------------------------------------- 1 | import type { NavItem } from '@/types/nav'; 2 | 3 | interface SiteConfig { 4 | name: string; 5 | description: string; 6 | mainNav: NavItem[]; 7 | links: { 8 | twitter: string; 9 | github: string; 10 | docs: string; 11 | }; 12 | } 13 | 14 | export const siteConfig: SiteConfig = { 15 | name: 'Supabase Cache Helpers for React Query', 16 | description: 17 | 'A collection of React Query utilities for working with Supabase.', 18 | mainNav: [ 19 | { 20 | title: 'Home', 21 | href: '/', 22 | }, 23 | { 24 | title: 'useQuery', 25 | href: '/use-query', 26 | }, 27 | // { 28 | // title: "useInfiniteScrollQuery", 29 | // href: "/use-infinite-scroll-query", 30 | // }, 31 | // { 32 | // title: "usePaginationQuery", 33 | // href: "/use-pagination-query", 34 | // }, 35 | ], 36 | links: { 37 | twitter: 'https://twitter.com/psteinroe', 38 | github: 'https://github.com/psteinroe/supabase-cache-helpers', 39 | docs: 'https://supabase-cache-helpers.vercel.app', 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /examples/react-query/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /examples/react-query/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/react-query/next.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import * as url from 'url'; 3 | import { config } from 'dotenv'; 4 | 5 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | /** @type {import('next').NextConfig} */ 8 | const nextConfig = { 9 | ...(process.env.NODE_ENV === 'development' 10 | ? { 11 | env: config({ 12 | path: path.resolve(__dirname, `../../.env.local`), 13 | }).parsed, 14 | } 15 | : {}), 16 | reactStrictMode: true, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /examples/react-query/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/react-query/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Link from 'next/link'; 3 | 4 | import { Layout } from '@/components/layout'; 5 | import { buttonVariants } from '@/components/ui/button'; 6 | import { siteConfig } from '@/config/site'; 7 | 8 | export default function IndexPage() { 9 | return ( 10 | 11 | 12 | {siteConfig.name} 13 | 14 | 15 | 16 | 17 |
18 |
19 |

20 | {siteConfig.name} 21 |

22 |

23 | {siteConfig.description} 24 |

25 |
26 |
27 | 33 | Documentation 34 | 35 | 41 | GitHub 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/react-query/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/react-query/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/examples/react-query/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-query/types/nav.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | title: string; 3 | href?: string; 4 | disabled?: boolean; 5 | external?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /examples/swr/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /examples/swr/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | .env -------------------------------------------------------------------------------- /examples/swr/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/swr/components/continent/continent-select.tsx: -------------------------------------------------------------------------------- 1 | import type * as SelectPrimitive from '@radix-ui/react-select'; 2 | import { useQuery } from '@supabase-cache-helpers/postgrest-swr'; 3 | import { useSupabaseClient } from '@supabase/auth-helpers-react'; 4 | import type { FC } from 'react'; 5 | 6 | import { 7 | Select, 8 | SelectContent, 9 | SelectItem, 10 | SelectTrigger, 11 | SelectValue, 12 | } from '@/components/ui/select'; 13 | import { cn } from '@/lib/utils'; 14 | import type { Database } from '@/types/database'; 15 | 16 | export type ContinentSelectProps = { 17 | containerProps?: SelectPrimitive.SelectProps; 18 | triggerProps?: SelectPrimitive.SelectTriggerProps; 19 | }; 20 | 21 | export const ContinentSelect: FC = ({ 22 | containerProps, 23 | triggerProps, 24 | }) => { 25 | const supabase = useSupabaseClient(); 26 | const { data, isLoading } = useQuery( 27 | supabase.from('continent').select('code,name'), 28 | { 29 | revalidateOnFocus: false, 30 | revalidateIfStale: false, 31 | revalidateOnReconnect: false, 32 | }, 33 | ); 34 | 35 | const { disabled, ...props } = containerProps; 36 | 37 | return ( 38 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /examples/swr/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { useSubscriptionQuery } from '@supabase-cache-helpers/postgrest-swr'; 2 | import { useSupabaseClient } from '@supabase/auth-helpers-react'; 3 | 4 | import { SiteHeader } from '@/components/site-header'; 5 | import type { Database } from '@/types/database'; 6 | 7 | interface LayoutProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | export function Layout({ children }: LayoutProps) { 12 | const supabase = useSupabaseClient(); 13 | 14 | useSubscriptionQuery( 15 | supabase, 16 | 'contacts', 17 | { event: '*', table: 'contact', schema: 'public' }, 18 | ['id'], 19 | ); 20 | 21 | return ( 22 | <> 23 | 24 |
{children}
25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/swr/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | import * as React from 'react'; 3 | 4 | import { Icons } from '@/components/icons'; 5 | import { Button } from '@/components/ui/button'; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | } from '@/components/ui/dropdown-menu'; 12 | 13 | export function ThemeToggle() { 14 | const { setTheme } = useTheme(); 15 | 16 | return ( 17 | 18 | 19 | 24 | 25 | 26 | setTheme('light')}> 27 | 28 | Light 29 | 30 | setTheme('dark')}> 31 | 32 | Dark 33 | 34 | setTheme('system')}> 35 | 36 | System 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /examples/swr/components/typography/code.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Code: FC< 6 | DetailedHTMLProps, HTMLElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 | 16 | {children} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/swr/components/typography/h1.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H1: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/swr/components/typography/h2.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H2: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/swr/components/typography/h3.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H3: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/swr/components/typography/h4.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const H4: FC< 6 | DetailedHTMLProps, HTMLHeadingElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

16 | {children} 17 |

18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /examples/swr/components/typography/p.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const P: FC< 6 | DetailedHTMLProps, HTMLParagraphElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

10 | {children} 11 |

12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /examples/swr/components/typography/small.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Small: FC< 6 | DetailedHTMLProps, HTMLElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/swr/components/typography/subtle.tsx: -------------------------------------------------------------------------------- 1 | import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export const Subtle: FC< 6 | DetailedHTMLProps, HTMLParagraphElement> 7 | > = ({ className, children, ...props }) => { 8 | return ( 9 |

13 | {children} 14 |

15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/swr/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from '@radix-ui/react-avatar'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Avatar({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | function AvatarImage({ 23 | className, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | function AvatarFallback({ 36 | className, 37 | ...props 38 | }: React.ComponentProps) { 39 | return ( 40 | 48 | ); 49 | } 50 | 51 | export { Avatar, AvatarImage, AvatarFallback }; 52 | -------------------------------------------------------------------------------- /examples/swr/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<'input'>) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /examples/swr/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label'; 2 | import * as React from 'react'; 3 | 4 | import { cn } from '@/lib/utils'; 5 | 6 | function Label({ 7 | className, 8 | ...props 9 | }: React.ComponentProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /examples/swr/config/site.ts: -------------------------------------------------------------------------------- 1 | import type { NavItem } from '@/types/nav'; 2 | 3 | interface SiteConfig { 4 | name: string; 5 | description: string; 6 | mainNav: NavItem[]; 7 | links: { 8 | twitter: string; 9 | github: string; 10 | docs: string; 11 | }; 12 | } 13 | 14 | export const siteConfig: SiteConfig = { 15 | name: 'Supabase Cache Helpers for SWR', 16 | description: 'A collection of SWR utilities for working with Supabase.', 17 | mainNav: [ 18 | { 19 | title: 'Home', 20 | href: '/', 21 | }, 22 | { 23 | title: 'useInfiniteScrollQuery', 24 | href: '/use-infinite-scroll-query', 25 | }, 26 | { 27 | title: 'usePaginationQuery', 28 | href: '/use-pagination-query', 29 | }, 30 | ], 31 | links: { 32 | twitter: 'https://twitter.com/psteinroe', 33 | github: 'https://github.com/psteinroe/supabase-cache-helpers', 34 | docs: 'https://supabase-cache-helpers.vercel.app', 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /examples/swr/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /examples/swr/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /examples/swr/next.config.mjs: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import * as url from 'url'; 3 | import { config } from 'dotenv'; 4 | 5 | const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | /** @type {import('next').NextConfig} */ 8 | const nextConfig = { 9 | ...(process.env.NODE_ENV === 'development' 10 | ? { 11 | env: config({ 12 | path: path.resolve(__dirname, `../../.env.local`), 13 | }).parsed, 14 | } 15 | : {}), 16 | reactStrictMode: true, 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /examples/swr/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/swr/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import Link from 'next/link'; 3 | 4 | import { Layout } from '@/components/layout'; 5 | import { buttonVariants } from '@/components/ui/button'; 6 | import { siteConfig } from '@/config/site'; 7 | 8 | export default function IndexPage() { 9 | return ( 10 | 11 | 12 | {siteConfig.name} 13 | 14 | 15 | 16 | 17 |
18 |
19 |

20 | {siteConfig.name} 21 |

22 |

23 | {siteConfig.description} 24 |

25 |
26 |
27 | 33 | Documentation 34 | 35 | 41 | GitHub 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /examples/swr/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/swr/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/examples/swr/public/favicon.ico -------------------------------------------------------------------------------- /examples/swr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "target": "ES2017" 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/swr/types/nav.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | title: string; 3 | href?: string; 4 | disabled?: boolean; 5 | external?: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-cache-helpers", 3 | "homepage": "https://supabase-cache-helpers.vercel.app", 4 | "version": "0.0.0", 5 | "private": true, 6 | "workspaces": ["packages/*"], 7 | "scripts": { 8 | "turbo": "turbo", 9 | "build": "turbo run build", 10 | "build:packages": "turbo run build --filter='./packages/*'", 11 | "dev": "turbo run dev --parallel", 12 | "check": "biome check", 13 | "fix": "biome check --write", 14 | "clean": "turbo run clean && rm -rf node_modules", 15 | "test": "turbo run test --concurrency=1", 16 | "typecheck": "turbo run typecheck", 17 | "typegen": "supabase gen types typescript --local > packages/postgrest-swr/tests/database.types.ts && supabase gen types typescript --local > packages/postgrest-react-query/tests/database.types.ts && supabase gen types typescript --local > packages/postgrest-core/tests/database.types.ts && supabase gen types typescript --local > examples/swr/types/database.ts && supabase gen types typescript --local > examples/react-query/types/database.ts", 18 | "clear-branches": "git branch --merged | egrep -v \"(^\\*|main)\" | xargs git branch -d", 19 | "merge-main": "git fetch origin main:main && git merge main", 20 | "reset-git": "git checkout main && git pull && pnpm run clear-branches", 21 | "changeset": "changeset", 22 | "ci:version": "changeset version && pnpm run fix", 23 | "ci:release": "changeset publish" 24 | }, 25 | "devDependencies": { 26 | "@biomejs/biome": "1.9.4", 27 | "@changesets/cli": "2.29.0", 28 | "supabase": "2.24.0", 29 | "turbo": "2.5.0" 30 | }, 31 | "engines": { 32 | "pnpm": "8", 33 | "node": ">=22.0.0" 34 | }, 35 | "pnpm": { 36 | "overrides": { 37 | "react": "19.1.0", 38 | "react-dom": "19.1.0" 39 | } 40 | }, 41 | "packageManager": "pnpm@8.15.8" 42 | } 43 | -------------------------------------------------------------------------------- /packages/postgrest-core/README.md: -------------------------------------------------------------------------------- 1 | # PostgREST Core 2 | 3 | A collection of cache utilities for working with the Supabase REST API. It is not meant to be used standalone. 4 | -------------------------------------------------------------------------------- /packages/postgrest-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase-cache-helpers/postgrest-core", 3 | "version": "0.12.1", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "types": "./dist/index.d.ts", 16 | "files": ["dist/**"], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "keywords": ["Supabase", "PostgREST", "Cache"], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", 24 | "directory": "packages/postgrest-shared" 25 | }, 26 | "license": "MIT", 27 | "scripts": { 28 | "build": "tsup", 29 | "clean": "rm -rf .turbo && rm -rf coverage && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", 30 | "test": "vitest --coverage", 31 | "typecheck": "tsc --pretty --noEmit" 32 | }, 33 | "peerDependencies": { 34 | "@supabase/postgrest-js": "^1.19.4" 35 | }, 36 | "dependencies": { 37 | "fast-equals": "5.2.2", 38 | "flat": "6.0.1", 39 | "merge-anything": "5.1.7", 40 | "xregexp": "5.1.1" 41 | }, 42 | "devDependencies": { 43 | "@supabase-cache-helpers/tsconfig": "workspace:*", 44 | "@supabase/postgrest-js": "1.19.4", 45 | "@supabase/supabase-js": "2.49.4", 46 | "@types/flat": "5.0.2", 47 | "@types/lodash": "4.17.0", 48 | "@vitest/coverage-istanbul": "^3.0.0", 49 | "dotenv": "16.5.0", 50 | "tsup": "8.5.0", 51 | "typescript": "5.8.3", 52 | "vitest": "3.2.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/fetch/build-select-statement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type NestedPath, 3 | groupPathsRecursive, 4 | isNestedPath, 5 | } from '../lib/group-paths-recursive'; 6 | import type { Path } from '../lib/query-types'; 7 | 8 | // Transforms a list of Path[] into a select statement 9 | export const buildSelectStatement = (paths: Path[]): string => { 10 | return buildSelectStatementFromGroupedPaths(groupPathsRecursive(paths)); 11 | }; 12 | 13 | // Transforms a list of (Path | NestedPath)[] grouped statements into a select statement 14 | export const buildSelectStatementFromGroupedPaths = ( 15 | paths: (Path | NestedPath)[], 16 | ): string => 17 | paths 18 | .map((i) => { 19 | if (isNestedPath(i)) { 20 | return `${i.declaration}(${buildSelectStatement(i.paths)})`; 21 | } 22 | return `${i.alias ? `${i.alias}:` : ''}${i.path}`; 23 | }) 24 | .join(','); 25 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/fetch/dedupe.ts: -------------------------------------------------------------------------------- 1 | import { type NestedPath, isNestedPath } from '../lib/group-paths-recursive'; 2 | import type { Path } from '../lib/query-types'; 3 | 4 | export const DEDUPE_ALIAS_PREFIX = 'd'; 5 | 6 | export const dedupeGroupedPathsRecursive = ( 7 | grouped: (Path | NestedPath)[], 8 | ): (Path | NestedPath)[] => { 9 | const dedupeCounters = new Map(); 10 | 11 | return grouped.map((p, idx, a) => { 12 | // never dedupe non-nested paths because even if there is a duplicate we always want to dedupe the nested path instead 13 | // e.g. inbox_id,inbox_id(name) should be deduped to inbox_id,d_0_inbox_id:inbox_id(name) 14 | if (!isNestedPath(p)) return p; 15 | 16 | // dedupe current nested path if there is another path with the same `path` 17 | if (a.some((i, itemIdx) => i.path === p.path && idx !== itemIdx)) { 18 | const counter = dedupeCounters.get(p.path) || 0; 19 | dedupeCounters.set(p.path, counter + 1); 20 | const alias = [DEDUPE_ALIAS_PREFIX, counter, p.path].join('_'); 21 | return { 22 | ...p, 23 | alias, 24 | declaration: `${alias}:${p.declaration}`, 25 | paths: dedupeGroupedPathsRecursive(p.paths), 26 | }; 27 | } 28 | 29 | return { 30 | ...p, 31 | paths: dedupeGroupedPathsRecursive(p.paths), 32 | }; 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/fetcher.ts: -------------------------------------------------------------------------------- 1 | import { isPostgrestBuilder } from './lib/is-postgrest-builder'; 2 | import type { AnyPostgrestResponse } from './lib/response-types'; 3 | 4 | export const fetcher = async ( 5 | q: PromiseLike>, 6 | ) => { 7 | if (isPostgrestBuilder(q)) { 8 | q = q.throwOnError(); 9 | } 10 | return await q; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/index.ts: -------------------------------------------------------------------------------- 1 | // cherry pick exports that are used by the adapter packages 2 | export * from './fetch/build-normalized-query'; 3 | export * from './fetch/build-mutation-fetcher-response'; 4 | export * from './mutate/types'; 5 | export * from './lib/query-types'; 6 | export * from './lib/get-table'; 7 | export * from './lib/cache-data-types'; 8 | export * from './lib/response-types'; 9 | export * from './lib/encode-object'; 10 | export * from './lib/is-postgrest-builder'; 11 | export * from './lib/is-postgrest-transform-builder'; 12 | export * from './lib/get'; 13 | export * from './lib/set-filter-value'; 14 | export * from './lib/parse-value'; 15 | export * from './lib/parse-order-by-key'; 16 | export * from './lib/parse-order-by'; 17 | export * from './lib/find-filters'; 18 | export * from './lib/is-plain-object'; 19 | 20 | export * from './cursor-pagination-fetcher'; 21 | export * from './delete-fetcher'; 22 | export * from './delete-item'; 23 | export * from './fetcher'; 24 | export * from './insert-fetcher'; 25 | export * from './mutate-item'; 26 | export * from './offset-pagination-fetcher'; 27 | export * from './postgrest-filter'; 28 | export * from './postgrest-parser'; 29 | export * from './postgrest-query-parser'; 30 | export * from './update-fetcher'; 31 | export * from './upsert-fetcher'; 32 | export * from './upsert-item'; 33 | export * from './revalidate-tables'; 34 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/binary-search.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Binary search in JavaScript. 3 | * Returns the index of of the element in a sorted array or (-n-1) where n is the insertion point for the new element. 4 | * Parameters: 5 | * ar - A sorted array 6 | * el - An element to search for 7 | * compare - A comparator function. The function takes two arguments: (a, b) and returns: 8 | * a negative number if a is less than b; 9 | * 0 if a is equal to b; 10 | * a positive number of a is greater than b. 11 | * The array may contain duplicate elements. If there are more than one equal elements in the array, 12 | * the returned value can be the index of any one of the equal elements. 13 | */ 14 | export function binarySearch( 15 | arr: Type[], 16 | el: Type, 17 | compare: (a: Type, b: Type) => number, 18 | ) { 19 | let m = 0; 20 | let n = arr.length - 1; 21 | while (m <= n) { 22 | const k = (n + m) >> 1; 23 | const cmp = compare(el, arr[k]); 24 | if (cmp > 0) { 25 | m = k + 1; 26 | } else if (cmp < 0) { 27 | n = k - 1; 28 | } else { 29 | return k; 30 | } 31 | } 32 | return m; 33 | } 34 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/cache-data-types.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestHasMorePaginationResponse } from './response-types'; 2 | 3 | export type PostgrestPaginationCacheData = Result[][]; 4 | 5 | export const isPostgrestPaginationCacheData = ( 6 | q: unknown, 7 | ): q is PostgrestPaginationCacheData => { 8 | if (!Array.isArray(q)) return false; 9 | return q.length === 0 || Array.isArray(q[0]); 10 | }; 11 | 12 | export type PostgrestHasMorePaginationCacheData = 13 | PostgrestHasMorePaginationResponse[]; 14 | 15 | export const isPostgrestHasMorePaginationCacheData = ( 16 | q: unknown, 17 | ): q is PostgrestHasMorePaginationCacheData => { 18 | if (!Array.isArray(q)) return false; 19 | if (q.length === 0) return true; 20 | const firstPage = q[0]; 21 | return ( 22 | Array.isArray( 23 | (firstPage as PostgrestHasMorePaginationResponse).data, 24 | ) && 25 | typeof (firstPage as PostgrestHasMorePaginationResponse).hasMore === 26 | 'boolean' 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/encode-object.ts: -------------------------------------------------------------------------------- 1 | import { flatten, unflatten } from 'flat'; 2 | 3 | import { sortSearchParams } from './sort-search-param'; 4 | 5 | /** 6 | * Encodes an object by url-encoding an ordered lists of all paths and their values. 7 | */ 8 | export const encodeObject = (obj: Record): string => { 9 | const sortedEntries = Object.entries( 10 | flatten(obj) as Record, 11 | ).sort(([a], [b]) => a.length - b.length); 12 | const bodyParams = new URLSearchParams(); 13 | sortedEntries.forEach(([key, value]) => { 14 | bodyParams.append(key, String(value)); 15 | }); 16 | return sortSearchParams(bodyParams).toString(); 17 | }; 18 | 19 | /** 20 | * Decodes a URL-encoded string back into a nested object. 21 | * This is the reverse operation of encodeObject. 22 | */ 23 | export const decodeObject = ( 24 | encodedString: string, 25 | ): Record => { 26 | const params = new URLSearchParams(encodedString); 27 | const flatObject: Record = {}; 28 | 29 | // Convert URLSearchParams back to a flat object 30 | params.forEach((value, key) => { 31 | // Try to convert string values to appropriate types 32 | let parsedValue: unknown = value; 33 | 34 | // Try to parse numbers 35 | if (/^-?\d+$/.test(value)) { 36 | parsedValue = parseInt(value, 10); 37 | } else if (/^-?\d+\.\d+$/.test(value)) { 38 | parsedValue = parseFloat(value); 39 | } else if (value === 'true') { 40 | parsedValue = true; 41 | } else if (value === 'false') { 42 | parsedValue = false; 43 | } else if (value === 'null') { 44 | parsedValue = null; 45 | } 46 | 47 | flatObject[key] = parsedValue; 48 | }); 49 | 50 | // Unflatten the object to restore nested structure 51 | return unflatten(flatObject); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/extract-paths-from-filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type FilterDefinitions, 3 | type Path, 4 | isAndFilter, 5 | isFilterDefinition, 6 | isOrFilter, 7 | } from './query-types'; 8 | 9 | export const extractPathsFromFilters = (f: FilterDefinitions, p: Path[]) => { 10 | return f.reduce((prev, filter) => { 11 | if (isAndFilter(filter)) { 12 | prev.push(...extractPathsFromFilters(filter.and, p)); 13 | } else if (isOrFilter(filter)) { 14 | prev.push(...extractPathsFromFilters(filter.or, p)); 15 | } else if (isFilterDefinition(filter)) { 16 | const relatedPath = p.find((p) => p.path === filter.path); 17 | const pathElements = filter.path.split('.'); 18 | const aliasElements = filter.alias?.split('.'); 19 | const declaration = pathElements 20 | .map( 21 | (el, idx) => 22 | `${aliasElements && aliasElements[idx] !== el ? `${aliasElements[idx]}:` : ''}${el}`, 23 | ) 24 | .join('.'); 25 | prev.push({ 26 | path: filter.path, 27 | alias: filter.alias, 28 | declaration: relatedPath ? relatedPath.declaration : declaration, 29 | }); 30 | } 31 | return prev; 32 | }, []); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/filter-filter-definitions-by-paths.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type FilterDefinitions, 3 | isAndFilter, 4 | isFilterDefinition, 5 | isOrFilter, 6 | } from './query-types'; 7 | 8 | export const filterFilterDefinitionsByPaths = ( 9 | f: FilterDefinitions, 10 | paths: string[], 11 | ) => { 12 | return f.reduce((prev, filter) => { 13 | if (isAndFilter(filter)) { 14 | const filters = filterFilterDefinitionsByPaths(filter.and, paths); 15 | if (filters.length > 0) { 16 | prev.push({ and: filters }); 17 | } 18 | } else if (isOrFilter(filter)) { 19 | const filters = filterFilterDefinitionsByPaths(filter.or, paths); 20 | if (filters.length > 0) { 21 | prev.push({ or: filters }); 22 | } 23 | } else if (isFilterDefinition(filter) && paths.includes(filter.path)) { 24 | prev.push(filter); 25 | } 26 | return prev; 27 | }, []); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/find-filters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type FilterDefinition, 3 | type FilterDefinitions, 4 | isAndFilter, 5 | isFilterDefinition, 6 | isOrFilter, 7 | } from './query-types'; 8 | 9 | // Helper to search for filters in a filter definition 10 | export const findFilters = ( 11 | f: FilterDefinitions, 12 | by: Partial, 13 | ) => { 14 | const filters: FilterDefinition[] = []; 15 | f.forEach((filter) => { 16 | if (isAndFilter(filter)) { 17 | filters.push(...findFilters(filter.and, by)); 18 | } 19 | if (isOrFilter(filter)) { 20 | filters.push(...findFilters(filter.or, by)); 21 | } 22 | if (isFilterDefinition(filter)) { 23 | if ( 24 | (typeof by.path === 'undefined' || filter.path === by.path) && 25 | (typeof by.alias === 'undefined' || filter.alias === by.alias) && 26 | (typeof by.value === 'undefined' || filter.value === by.value) && 27 | (typeof by.negate === 'undefined' || filter.negate === by.negate) && 28 | (typeof by.operator === 'undefined' || filter.operator === by.operator) 29 | ) { 30 | filters.push(filter); 31 | } 32 | } 33 | }); 34 | return filters; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/find-index-ordered.ts: -------------------------------------------------------------------------------- 1 | import { binarySearch } from './binary-search'; 2 | import type { OrderDefinition } from './query-types'; 3 | import { buildSortedComparator } from './sorted-comparator'; 4 | 5 | export const findIndexOrdered = >( 6 | input: Type, 7 | currentData: Type[], 8 | orderBy: OrderDefinition[], 9 | ): number => binarySearch(currentData, input, buildSortedComparator(orderBy)); 10 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/find-last-index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the index of the last element in the array where predicate is true, and -1 3 | * otherwise. 4 | * @param array The source array to search in 5 | * @param predicate find calls predicate once for each element of the array, in descending 6 | * order, until it finds one where predicate returns true. If such an element is found, 7 | * findLastIndex immediately returns that element index. Otherwise, findLastIndex returns -1. 8 | */ 9 | export function findLastIndex( 10 | array: T[], 11 | predicate: (value: T, index: number, obj: T[]) => boolean, 12 | ): number { 13 | let l = array.length; 14 | while (l--) { 15 | if (predicate(array[l], l, array)) return l; 16 | } 17 | return -1; 18 | } 19 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/get-table-from-url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses a url and returns the table name the url is interacting with. 3 | * 4 | * For mutations, the .split('?') goes unused. 5 | * 6 | * @param url The url we are pulling the table name from 7 | * @returns Table name 8 | */ 9 | export const getTableFromUrl = (url: string): string => { 10 | // Split the url 11 | const split = url.toString().split('/'); 12 | // Pop the last part of the path off and remove any params if they exist 13 | const table = split.pop()?.split('?').shift() as string; 14 | // Pop an additional position to check for rpc 15 | const maybeRpc = split.pop() as string; 16 | // Rejoin the result to include rpc otherwise just table name 17 | return [maybeRpc === 'rpc' ? maybeRpc : null, table] 18 | .filter(Boolean) 19 | .join('/'); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/get-table.ts: -------------------------------------------------------------------------------- 1 | import { getTableFromUrl } from './get-table-from-url'; 2 | import { MaybeLikePostgrestBuilder } from './like-postgrest-builder'; 3 | import { isLikeQueryBuilder } from './like-query-builder'; 4 | 5 | export const getTable = ( 6 | query: MaybeLikePostgrestBuilder, 7 | ): string => { 8 | if (!isLikeQueryBuilder(query)) { 9 | throw new Error('Invalid PostgrestBuilder'); 10 | } 11 | 12 | return getTableFromUrl(query['url'].pathname); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/get.ts: -------------------------------------------------------------------------------- 1 | export const get = (obj: any, path: string, defaultValue: any = undefined) => { 2 | const split = path.split(/((?:\.|,|\[|\]|->>|->)+)/g); 3 | let result: any = obj; 4 | for (let i = -1; i < split.length; i += 2) { 5 | const separator = split[i]; 6 | let key: string | number = split[i + 1]; 7 | if (!key) { 8 | continue; 9 | } 10 | if (separator?.endsWith('->') || separator?.endsWith('->>')) { 11 | if (/^\d+$/.test(key)) { 12 | key = Number.parseInt(key, 10); 13 | } 14 | } 15 | if (separator?.endsWith('->>')) { 16 | result = `${result ? result[key] : result}`; 17 | } else { 18 | result = result ? result[key] : result; 19 | } 20 | } 21 | return result === undefined || result === obj ? defaultValue : result; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/group-paths-recursive.ts: -------------------------------------------------------------------------------- 1 | import type { Path } from './query-types'; 2 | import { removeFirstPathElement } from './remove-first-path-element'; 3 | 4 | export type NestedPath = { 5 | alias?: string; 6 | path: string; 7 | declaration: string; 8 | paths: (Path | NestedPath)[]; 9 | }; 10 | 11 | export const isNestedPath = (p: Path | NestedPath): p is NestedPath => 12 | Array.isArray((p as NestedPath).paths); 13 | 14 | // group paths by first path elements declaration 15 | // returns [Path, Path, NestedPath, NestedPath, Path] 16 | export const groupPathsRecursive = (paths: Path[]): (Path | NestedPath)[] => { 17 | const grouped = paths.reduce<(Path | NestedPath)[]>((prev, curr) => { 18 | const levels = curr.path.split('.').length; 19 | if (levels === 1) { 20 | prev.push(curr); 21 | return prev; 22 | } 23 | 24 | // if has more than one level left, 25 | const firstLevelDeclaration = curr.declaration.split('.')[0]; 26 | const indexOfNested = prev.findIndex( 27 | (p) => isNestedPath(p) && p.declaration === firstLevelDeclaration, 28 | ); 29 | const pathWithoutCurrentLevel = removeFirstPathElement(curr); 30 | if (indexOfNested !== -1) { 31 | // add to nested 32 | (prev[indexOfNested] as NestedPath).paths.push(pathWithoutCurrentLevel); 33 | return prev; 34 | } 35 | // create nested 36 | prev.push({ 37 | declaration: firstLevelDeclaration, 38 | path: curr.path.split('.')[0], 39 | paths: [pathWithoutCurrentLevel], 40 | ...(curr.alias ? { alias: curr.alias.split('.')[0] } : {}), 41 | }); 42 | return prev; 43 | }, []); 44 | 45 | return grouped.map((p) => 46 | isNestedPath(p) ? { ...p, paths: groupPathsRecursive(p.paths) } : p, 47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/if-date-get-time.ts: -------------------------------------------------------------------------------- 1 | export const ifDateGetTime = (v: unknown) => { 2 | if (v instanceof Date) return v.getTime(); 3 | if (typeof v === 'string') { 4 | const t = new Date(v).getTime(); 5 | if (!isNaN(t)) return t; 6 | } 7 | return v; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-iso-date-string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if a value is a valid ISO DateTime string 3 | * @param v 4 | * @returns 5 | */ 6 | export const isISODateString = (v: unknown): boolean => 7 | typeof v === 'string' && 8 | /(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))/.test( 9 | v, 10 | ); 11 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-not-null.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param i Ahhh gotta love typescript 4 | * @returns 5 | */ 6 | export const isNotNull = (i: I | null): i is I => i !== null; 7 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-object.ts: -------------------------------------------------------------------------------- 1 | export const isObject = (v: unknown): v is Record => 2 | typeof v === 'object' && !Array.isArray(v) && v !== null; 3 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-plain-object.ts: -------------------------------------------------------------------------------- 1 | export function isPlainObject( 2 | value: unknown, 3 | ): value is Record { 4 | return Object.prototype.toString.call(value) === '[object Object]'; 5 | } 6 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-postgrest-builder.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestBuilder } from '@supabase/postgrest-js'; 2 | 3 | export const isPostgrestBuilder = ( 4 | q: unknown, 5 | ): q is PostgrestBuilder => { 6 | return typeof (q as PostgrestBuilder).throwOnError === 'function'; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/is-postgrest-transform-builder.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestTransformBuilder } from '@supabase/postgrest-js'; 2 | import type { GenericSchema } from '@supabase/postgrest-js/dist/cjs/types'; 3 | 4 | export const isPostgrestTransformBuilder = < 5 | Schema extends GenericSchema, 6 | Row extends Record, 7 | Result, 8 | RelationName = unknown, 9 | Relationships = unknown, 10 | >( 11 | q: unknown, 12 | ): q is PostgrestTransformBuilder< 13 | Schema, 14 | Row, 15 | Result, 16 | RelationName, 17 | Relationships 18 | > => { 19 | return ( 20 | typeof ( 21 | q as PostgrestTransformBuilder< 22 | Schema, 23 | Row, 24 | Result, 25 | RelationName, 26 | Relationships 27 | > 28 | ).abortSignal === 'function' 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/like-postgrest-builder.ts: -------------------------------------------------------------------------------- 1 | export type MaybeLikePostgrestBuilder = unknown; 2 | 3 | export type LikePostgrestBuilder = { 4 | url: URL; 5 | method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'; 6 | headers: Record; 7 | schema?: string; 8 | body?: unknown; 9 | }; 10 | 11 | export const isLikePostgrestBuilder = ( 12 | v: MaybeLikePostgrestBuilder, 13 | ): v is LikePostgrestBuilder => { 14 | if (typeof v !== 'object' || v === null) return false; 15 | const obj = v as LikePostgrestBuilder; 16 | 17 | return ( 18 | typeof obj['url'] === 'object' && 19 | typeof obj['headers'] === 'object' && 20 | typeof obj['method'] === 'string' 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/like-query-builder.ts: -------------------------------------------------------------------------------- 1 | export type MaybeLikeQueryBuilder = unknown; 2 | 3 | export type LikeQueryBuilder = { 4 | url: URL; 5 | }; 6 | 7 | export const isLikeQueryBuilder = ( 8 | v: MaybeLikeQueryBuilder, 9 | ): v is LikeQueryBuilder => { 10 | if (typeof v !== 'object' || v === null) return false; 11 | const obj = v as LikeQueryBuilder; 12 | 13 | return typeof obj['url'] === 'object'; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/parse-order-by-key.ts: -------------------------------------------------------------------------------- 1 | import type { OrderDefinition } from './query-types'; 2 | 3 | /** 4 | * Parses orderByKey back to OrderDefinition 5 | * @param key generated by PostgrestParser 6 | * @returns The parsed OrderDefinition 7 | */ 8 | export const parseOrderByKey = (v: string): OrderDefinition[] => { 9 | return v.split('|').map((orderBy) => { 10 | const [tableDef, orderDef] = orderBy.split(':'); 11 | const [foreignTableOrCol, maybeCol] = tableDef.split('.'); 12 | const [dir, nulls] = orderDef.split('.'); 13 | return { 14 | ascending: dir === 'asc', 15 | nullsFirst: nulls === 'nullsFirst', 16 | foreignTable: maybeCol ? foreignTableOrCol : undefined, 17 | column: maybeCol ? maybeCol : foreignTableOrCol, 18 | }; 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/parse-order-by.ts: -------------------------------------------------------------------------------- 1 | import { OrderDefinition } from './query-types'; 2 | 3 | export const parseOrderBy = (searchParams: URLSearchParams) => { 4 | const orderBy: OrderDefinition[] = []; 5 | searchParams.forEach((value, key) => { 6 | const split = key.split('.'); 7 | if (split[split.length === 2 ? 1 : 0] === 'order') { 8 | // separated by , 9 | const orderByDefs = value.split(','); 10 | orderByDefs.forEach((def) => { 11 | const [column, ascending, nullsFirst] = def.split('.'); 12 | orderBy.push({ 13 | ascending: ascending === 'asc', 14 | column, 15 | nullsFirst: nullsFirst === 'nullsfirst', 16 | foreignTable: split.length === 2 ? split[0] : undefined, 17 | }); 18 | }); 19 | } 20 | }); 21 | 22 | return orderBy; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/parse-value.ts: -------------------------------------------------------------------------------- 1 | import { isISODateString } from './is-iso-date-string'; 2 | import type { ValueType } from './query-types'; 3 | 4 | /** 5 | * Safely parse any value to a ValueType 6 | * @param v Any value 7 | * @returns a ValueType 8 | */ 9 | export const parseValue = (v: any): ValueType => { 10 | if (isISODateString(v)) return new Date(v); 11 | try { 12 | return JSON.parse(v); 13 | } catch { 14 | return v; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/remove-alias-from-declaration.ts: -------------------------------------------------------------------------------- 1 | // removes alias from every level of declaration 2 | export const removeAliasFromDeclaration = (d: string) => 3 | d 4 | .split('.') 5 | .map((el) => el.split(':').pop() as string) 6 | .join('.'); 7 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/remove-first-path-element.ts: -------------------------------------------------------------------------------- 1 | import type { Path } from './query-types'; 2 | 3 | export const removeFirstPathElement = (p: Path): Path => { 4 | const aliasWithoutFirstElement = p.alias 5 | ? p.alias.split('.').slice(1).join('.') 6 | : undefined; 7 | const pathWithoutFirstEelment = p.path.split('.').slice(1).join('.'); 8 | 9 | return { 10 | declaration: p.declaration.split('.').slice(1).join('.'), 11 | path: pathWithoutFirstEelment, 12 | alias: 13 | aliasWithoutFirstElement && 14 | (aliasWithoutFirstElement.split('.').length > 1 || 15 | aliasWithoutFirstElement !== pathWithoutFirstEelment) 16 | ? aliasWithoutFirstElement 17 | : undefined, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/response-types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PostgrestMaybeSingleResponse, 3 | PostgrestResponse, 4 | PostgrestSingleResponse, 5 | } from '@supabase/postgrest-js'; 6 | 7 | // Convencience type to not bloat up implementation 8 | export type AnyPostgrestResponse = 9 | | PostgrestSingleResponse 10 | | PostgrestMaybeSingleResponse 11 | | PostgrestResponse; 12 | 13 | export const isAnyPostgrestResponse = ( 14 | q: unknown, 15 | ): q is AnyPostgrestResponse => { 16 | if (!q) return false; 17 | return ( 18 | typeof (q as AnyPostgrestResponse).data === 'object' || 19 | Array.isArray((q as AnyPostgrestResponse).data) 20 | ); 21 | }; 22 | 23 | export type PostgrestPaginationResponse = Result[]; 24 | 25 | export const isPostgrestPaginationResponse = ( 26 | q: unknown, 27 | ): q is PostgrestPaginationResponse => { 28 | return Array.isArray(q); 29 | }; 30 | 31 | export type PostgrestHasMorePaginationResponse = { 32 | data: Result[]; 33 | hasMore: boolean; 34 | }; 35 | 36 | export const isPostgrestHasMorePaginationResponse = ( 37 | q: unknown, 38 | ): q is PostgrestHasMorePaginationResponse => { 39 | if (!q) return false; 40 | return ( 41 | Array.isArray((q as PostgrestHasMorePaginationResponse).data) && 42 | typeof (q as PostgrestHasMorePaginationResponse).hasMore === 43 | 'boolean' 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/set-filter-value.ts: -------------------------------------------------------------------------------- 1 | export const setFilterValue = ( 2 | searchParams: URLSearchParams, 3 | path: string, 4 | op: string, 5 | value: string, 6 | ) => { 7 | const filters = searchParams.getAll(path); 8 | // delete all 9 | searchParams.delete(path); 10 | 11 | // re-create 12 | for (const f of filters) { 13 | if (f.startsWith(`${op}.`)) { 14 | continue; 15 | } 16 | searchParams.append(path, f); 17 | } 18 | 19 | searchParams.append(path, `${op}.${value}`); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/sort-search-param.ts: -------------------------------------------------------------------------------- 1 | export const sortSearchParams = (params: URLSearchParams) => 2 | new URLSearchParams( 3 | Array.from(params.entries()).sort((a, b) => { 4 | const x = `${a[0]}${a[1]}`; 5 | const y = `${b[0]}${b[1]}`; 6 | return x > y ? 1 : -1; 7 | }), 8 | ); 9 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/lib/sorted-comparator.ts: -------------------------------------------------------------------------------- 1 | import { get as defaultGet } from './get'; 2 | import { ifDateGetTime } from './if-date-get-time'; 3 | import type { OrderDefinition } from './query-types'; 4 | 5 | export const buildSortedComparator = >( 6 | orderBy: OrderDefinition[], 7 | ) => { 8 | return (a: Type, b: Type) => { 9 | for (const { column, ascending, nullsFirst, foreignTable } of orderBy) { 10 | const aValue = ifDateGetTime( 11 | defaultGet( 12 | a, 13 | `${foreignTable ? `${foreignTable}.` : ''}${column}`, 14 | null, 15 | ), 16 | ); 17 | 18 | const bValue = ifDateGetTime( 19 | defaultGet( 20 | b, 21 | `${foreignTable ? `${foreignTable}.` : ''}${column}`, 22 | null, 23 | ), 24 | ); 25 | 26 | // go to next if value is equals 27 | if (aValue === bValue) continue; 28 | 29 | // nullsFirst / nullsLast 30 | if (aValue === null || aValue === undefined) { 31 | return nullsFirst ? -1 : 1; 32 | } 33 | 34 | if (bValue === null || bValue === undefined) { 35 | return nullsFirst ? 1 : -1; 36 | } 37 | 38 | // otherwise, if we're ascending, lowest sorts first 39 | if (ascending) { 40 | return aValue < bValue ? -1 : 1; 41 | } 42 | 43 | // if descending, highest sorts first 44 | return aValue < bValue ? 1 : -1; 45 | } 46 | 47 | return 0; 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/mutate/should-revalidate-relation.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestFilter } from '../postgrest-filter'; 2 | import type { PostgrestQueryParserOptions } from '../postgrest-query-parser'; 3 | import type { DecodedKey } from './types'; 4 | 5 | export type RevalidateRelationOpt = { 6 | schema?: string; 7 | relation: string; 8 | relationIdColumn: string; 9 | fKeyColumn: keyof Type; 10 | }; 11 | 12 | export type RevalidateRelations> = 13 | RevalidateRelationOpt[]; 14 | 15 | export type RevalidateRelationsProps> = { 16 | input: Partial; 17 | decodedKey: Pick; 18 | getPostgrestFilter: ( 19 | query: string, 20 | opts?: PostgrestQueryParserOptions, 21 | ) => Pick, 'applyFilters'>; 22 | }; 23 | 24 | export const shouldRevalidateRelation = >( 25 | relations: RevalidateRelations, 26 | { 27 | input, 28 | getPostgrestFilter, 29 | decodedKey: { schema, table, queryKey }, 30 | }: RevalidateRelationsProps, 31 | ): boolean => 32 | Boolean( 33 | relations.find( 34 | (r) => 35 | (!r.schema || r.schema === schema) && 36 | r.relation === table && 37 | typeof input[r.fKeyColumn] !== 'undefined' && 38 | getPostgrestFilter(queryKey, { 39 | exclusivePaths: [r.relationIdColumn], 40 | }).applyFilters({ 41 | [r.relationIdColumn]: input[r.fKeyColumn], 42 | }), 43 | ), 44 | ); 45 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/mutate/should-revalidate-table.ts: -------------------------------------------------------------------------------- 1 | import type { DecodedKey } from './types'; 2 | 3 | export type RevalidateTableOpt = { schema?: string; table: string }; 4 | 5 | export type RevalidateTables = RevalidateTableOpt[]; 6 | 7 | export type RevalidateTablesProps = { 8 | decodedKey: Pick; 9 | }; 10 | 11 | export const shouldRevalidateTable = ( 12 | tables: RevalidateTables, 13 | { decodedKey: { schema, table } }: RevalidateTablesProps, 14 | ): boolean => 15 | Boolean( 16 | tables.find((t) => (!t.schema || t.schema === schema) && t.table === table), 17 | ); 18 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/mutate/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnyPostgrestResponse, 3 | PostgrestHasMorePaginationResponse, 4 | } from '../lib/response-types'; 5 | import type { RevalidateRelations } from './should-revalidate-relation'; 6 | import type { RevalidateTables } from './should-revalidate-table'; 7 | 8 | export type RevalidateOpts> = { 9 | revalidateTables?: RevalidateTables; 10 | revalidateRelations?: RevalidateRelations; 11 | }; 12 | 13 | export type MutatorFn = ( 14 | currentData: 15 | | AnyPostgrestResponse 16 | | PostgrestHasMorePaginationResponse 17 | | unknown, 18 | ) => 19 | | AnyPostgrestResponse 20 | | PostgrestHasMorePaginationResponse 21 | | unknown; 22 | 23 | export type DecodedKey = { 24 | bodyKey: string | undefined; 25 | orderByKey: string | undefined; 26 | queryKey: string; 27 | count: string | null; 28 | schema: string | undefined; 29 | table: string; 30 | isHead: boolean | undefined; 31 | limit: number | undefined; 32 | offset: number | undefined; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/postgrest-core/src/revalidate-tables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RevalidateTables, 3 | shouldRevalidateTable, 4 | } from './mutate/should-revalidate-table'; 5 | import type { DecodedKey } from './mutate/types'; 6 | 7 | export type RevalidateTablesOperation = RevalidateTables; 8 | 9 | export type RevalidateTablesCache = { 10 | /** 11 | * The keys currently present in the cache 12 | */ 13 | cacheKeys: KeyType[]; 14 | /** 15 | * Decode a key. Should return null if not a PostgREST key. 16 | */ 17 | decode: (k: KeyType) => DecodedKey | null; 18 | /** 19 | * The revalidation function from the cache library 20 | */ 21 | revalidate: (key: KeyType) => Promise | void; 22 | }; 23 | 24 | export const revalidateTables = async ( 25 | tables: RevalidateTablesOperation, 26 | cache: RevalidateTablesCache, 27 | ) => { 28 | const { cacheKeys, decode, revalidate } = cache; 29 | 30 | const mutations = []; 31 | for (const k of cacheKeys) { 32 | const key = decode(k); 33 | 34 | // Exit early if not a postgrest key 35 | if (!key) continue; 36 | 37 | if (shouldRevalidateTable(tables, { decodedKey: key })) { 38 | mutations.push(revalidate(k)); 39 | } 40 | } 41 | 42 | await Promise.all(mutations); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/__snapshots__/fetcher.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`fetcher > should throw on error > column contact.unknown does not exist 1`] = `[PostgrestError: column contact.unknown does not exist]`; 4 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as Import from '../src'; 3 | 4 | describe('index exports', () => { 5 | it('should export', () => { 6 | expect(Object.keys(Import)).toHaveLength(45); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/lib/extract-paths-from-filters.spec.ts: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { beforeAll, describe, expect, it } from 'vitest'; 3 | 4 | import { extractPathsFromFilters } from '../../src/lib/extract-paths-from-filter'; 5 | import { PostgrestParser } from '../../src/postgrest-parser'; 6 | 7 | describe('extractPathsFromFilters', () => { 8 | let c: SupabaseClient; 9 | 10 | beforeAll(() => { 11 | c = createClient('https://localhost', '1234'); 12 | }); 13 | 14 | it('should add declarations from path to matching filter path', () => { 15 | const parser = new PostgrestParser( 16 | c 17 | .from('conversation') 18 | .select('inbox:inbox_id!inner(id,name,emoji)') 19 | .eq('inbox_id.id', 'inbox-id'), 20 | ); 21 | expect(extractPathsFromFilters(parser.filters, parser.paths)).toEqual([ 22 | { 23 | alias: 'inbox.id', 24 | declaration: 'inbox:inbox_id!inner.id', 25 | path: 'inbox_id.id', 26 | }, 27 | ]); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/lib/get-table.spec.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js'; 2 | import { describe, expect, it } from 'vitest'; 3 | 4 | import { getTable } from '../../src/lib/get-table'; 5 | 6 | const c = createClient('http://localhost:3000', 'test'); 7 | 8 | describe('getTable', () => { 9 | it('should return table name', () => { 10 | expect(getTable(c.from('test').select('id').eq('id', 1))).toEqual('test'); 11 | }); 12 | 13 | it('should throw if not a query', () => { 14 | expect(() => getTable({})).toThrow(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/lib/get.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { get } from '../../src/lib/get'; 3 | 4 | describe('get', () => { 5 | it.each([ 6 | [{ a: 1 }, 'a', undefined, 1], // simple case 7 | [{ a: 1 }, 'b', 2, 2], // default case 8 | [{ a: 1 }, '', undefined, undefined], // empty case 9 | [{ a: { b: 1 } }, 'a.b', undefined, 1], // dot syntax 10 | [{ a: { b: 1 } }, 'a,b', undefined, 1], // comma syntax 11 | [{ a: { b: 1 } }, 'a[b]', undefined, 1], // bracket syntax 12 | [{ a: { b: { c: { d: 1 } } } }, 'a.b,c.[d]', undefined, 1], // combination syntax 13 | [{ a: { b: 1 } }, 'a->b', undefined, 1], // json value syntax 14 | [{ a: { b: 1 } }, 'a->>b', undefined, '1'], // json string syntax 15 | [{ a: [1, 2] }, 'a->0', undefined, 1], // json array value syntax 16 | [{ a: [1, 2] }, 'a->>0', undefined, '1'], // json array string syntax 17 | [{ a: { b: { c: 1 } } }, 'a->b->c', undefined, 1], // nested json syntax 18 | [{ a: { b: { c: 1 } } }, 'a->b->>c', undefined, '1'], 19 | [{ a: { b: [1, 2] } }, 'a.b->0', undefined, 1], 20 | [{ a: { b: [1, 2] } }, 'a.b->>0', undefined, '1'], 21 | [{ a: { b: 1 } }, 'a->0', undefined, undefined], // not an array 22 | [{ a: [1, 2] }, 'a->2', undefined, undefined], // missing array value 23 | ])('get(%j, "%s", %s) should be %s', (obj, path, defaultValue, expected) => { 24 | expect(get(obj, path, defaultValue)).toEqual(expected); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/lib/if-date-get-time.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { ifDateGetTime } from '../../src/lib/if-date-get-time'; 3 | 4 | describe('ifDateGetTime', () => { 5 | it('should return input if its a number', () => { 6 | expect(ifDateGetTime(20)).toEqual(20); 7 | }); 8 | 9 | it('should return input if its an arbitrary string', () => { 10 | expect(ifDateGetTime('test')).toEqual('test'); 11 | }); 12 | 13 | it('should return time if input is date', () => { 14 | const d = new Date(); 15 | expect(ifDateGetTime(d)).toEqual(d.getTime()); 16 | }); 17 | 18 | it('should return time if input is iso string', () => { 19 | const t = '2023-05-09T12:33:26.688932+00:00'; 20 | expect(ifDateGetTime(t)).toEqual(new Date(t).getTime()); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/lib/parse-order-by-key.spec.ts: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { beforeAll, describe, expect, it } from 'vitest'; 3 | 4 | import { parseOrderByKey } from '../../src/lib/parse-order-by-key'; 5 | import { PostgrestParser } from '../../src/postgrest-parser'; 6 | 7 | describe('parseOrderByKey', () => { 8 | let c: SupabaseClient; 9 | 10 | beforeAll(() => { 11 | c = createClient('https://localhost', '1234'); 12 | }); 13 | 14 | it('should parse forth and back correctly', () => { 15 | const parser = new PostgrestParser( 16 | c 17 | .from('test') 18 | .select('*', { head: true, count: 'exact' }) 19 | .eq('id', '123') 20 | .order('one', { 21 | ascending: true, 22 | foreignTable: 'foreignTable', 23 | nullsFirst: false, 24 | }) 25 | .order('two', { ascending: false, nullsFirst: true }), 26 | ); 27 | expect(parseOrderByKey(parser.orderByKey)).toEqual(parser.orderBy); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/mutate/should-revalidate-relation.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { shouldRevalidateRelation } from '../../src/mutate/should-revalidate-relation'; 3 | 4 | describe('should-revalidate-relation', () => { 5 | it('should set relations defined in revalidateRelations to stale if fkey from input matches id', async () => { 6 | expect( 7 | shouldRevalidateRelation( 8 | [ 9 | { 10 | relation: 'relation', 11 | fKeyColumn: 'fkey', 12 | relationIdColumn: 'id', 13 | schema: 'schema', 14 | }, 15 | ], 16 | { 17 | input: { 18 | fkey: '1', 19 | }, 20 | getPostgrestFilter: () => ({ 21 | applyFilters: (obj: unknown): obj is any => true, 22 | }), 23 | decodedKey: { 24 | schema: 'schema', 25 | table: 'relation', 26 | queryKey: 'queryKey', 27 | }, 28 | }, 29 | ), 30 | ).toBe(true); 31 | }); 32 | 33 | it('should use same schema as table if none is set on revalidateRelations', async () => { 34 | expect( 35 | shouldRevalidateRelation( 36 | [ 37 | { 38 | relation: 'relation', 39 | fKeyColumn: 'fkey', 40 | relationIdColumn: 'id', 41 | }, 42 | ], 43 | { 44 | input: { 45 | fkey: '1', 46 | }, 47 | getPostgrestFilter: () => ({ 48 | applyFilters: (obj: unknown): obj is any => true, 49 | }), 50 | decodedKey: { 51 | schema: 'schema', 52 | table: 'relation', 53 | queryKey: 'queryKey', 54 | }, 55 | }, 56 | ), 57 | ).toBe(true); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/mutate/should-revalidate-table.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { shouldRevalidateTable } from '../../src/mutate/should-revalidate-table'; 3 | 4 | describe('should-revalidate-table', () => { 5 | it('should set tables defined in revalidateTables to stale', async () => { 6 | expect( 7 | shouldRevalidateTable([{ schema: 'schema', table: 'relation' }], { 8 | decodedKey: { 9 | schema: 'schema', 10 | table: 'relation', 11 | }, 12 | }), 13 | ).toBe(true); 14 | }); 15 | 16 | it('should use same schema as table if none is defined in revalidateTables', async () => { 17 | expect( 18 | shouldRevalidateTable([{ table: 'relation' }], { 19 | decodedKey: { 20 | schema: 'schema', 21 | table: 'relation', 22 | }, 23 | }), 24 | ).toBe(true); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/mutate/transformers.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { toHasMorePaginationCacheData } from '../../src/mutate/transformers'; 3 | 4 | describe('toHasMorePaginationCacheData', () => { 5 | it('should set hasMore to false if there are no items in the cache currently', async () => { 6 | expect(toHasMorePaginationCacheData([{ test: 'a' }], [], 21)).toEqual([ 7 | { 8 | data: [ 9 | { 10 | test: 'a', 11 | }, 12 | ], 13 | hasMore: false, 14 | }, 15 | ]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/postgrest-query-parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { PostgrestQueryParser } from '../src/postgrest-query-parser'; 3 | 4 | describe('PostgrestQueryParser', () => { 5 | it('should work if the column has the name as the operator', () => { 6 | expect( 7 | new PostgrestQueryParser( 8 | 'fts=fts.12%3A*&limit=50&offset=0&order=name.asc&organisation_id=eq.7ffe8eab-8e99-4b63-be2d-a418d4cb767b&select=id%2Cname%2Cwhatsapp_status%2Ctype%2Crequest_approvals%2Clanguage%28name%29%2Ctext%2Cupdated_at%2Cprovider_template_approval%28status%2Ccategory%29', 9 | ).filters, 10 | ).toEqual([ 11 | { 12 | alias: undefined, 13 | negate: false, 14 | operator: 'fts', 15 | path: 'fts', 16 | value: '12:*', 17 | }, 18 | { 19 | alias: undefined, 20 | negate: false, 21 | operator: 'eq', 22 | path: 'organisation_id', 23 | value: '7ffe8eab-8e99-4b63-be2d-a418d4cb767b', 24 | }, 25 | ]); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/postgrest-core/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); 5 | -------------------------------------------------------------------------------- /packages/postgrest-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/web.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/postgrest-core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/postgrest-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | coverage: { 7 | provider: 'istanbul', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-delete-item'; 2 | export * from './use-mutate-item'; 3 | export * from './use-revalidate-tables'; 4 | export * from './use-upsert-item'; 5 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/cache/use-delete-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DeleteItemOperation, 3 | deleteItem, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { useQueryClient } from '@tanstack/react-query'; 6 | import { flatten } from 'flat'; 7 | 8 | import { decode, usePostgrestFilterCache } from '../lib'; 9 | 10 | /** 11 | * Convenience hook to delete an item from the react query cache. Does not make any http requests, and is supposed to be used for custom cache updates. 12 | * @param opts The mutation options 13 | * @returns void 14 | */ 15 | export function useDeleteItem>( 16 | opts: Omit, 'input'>, 17 | ) { 18 | const queryClient = useQueryClient(); 19 | const getPostgrestFilter = usePostgrestFilterCache(); 20 | 21 | return async (input: Type) => 22 | await deleteItem( 23 | { 24 | input: flatten(input) as Type, 25 | ...opts, 26 | }, 27 | { 28 | cacheKeys: queryClient 29 | .getQueryCache() 30 | .getAll() 31 | .map((c) => c.queryKey), 32 | getPostgrestFilter, 33 | revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), 34 | mutate: (key, fn) => { 35 | queryClient.setQueriesData({ queryKey: key }, fn); 36 | }, 37 | decode, 38 | }, 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/cache/use-mutate-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type MutateItemOperation, 3 | mutateItem, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { useQueryClient } from '@tanstack/react-query'; 6 | import { flatten } from 'flat'; 7 | 8 | import { decode, usePostgrestFilterCache } from '../lib'; 9 | 10 | /** 11 | * Convenience hook to mutate an item within the react query cache. Does not make any http requests, and is supposed to be used for custom cache updates. 12 | * @param opts The mutation options 13 | * @returns void 14 | */ 15 | export function useMutateItem>( 16 | opts: Omit, 'input' | 'mutate'>, 17 | ): (input: Partial, mutateFn: (current: Type) => Type) => Promise { 18 | const queryClient = useQueryClient(); 19 | const getPostgrestFilter = usePostgrestFilterCache(); 20 | 21 | return async (input: Partial, mutateFn: (current: Type) => Type) => 22 | await mutateItem( 23 | { 24 | input: flatten(input) as Partial, 25 | mutate: mutateFn, 26 | ...opts, 27 | }, 28 | { 29 | cacheKeys: queryClient 30 | .getQueryCache() 31 | .getAll() 32 | .map((c) => c.queryKey), 33 | getPostgrestFilter, 34 | revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), 35 | mutate: (key, fn) => { 36 | queryClient.setQueriesData({ queryKey: key }, fn); 37 | }, 38 | decode, 39 | }, 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/cache/use-revalidate-tables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type RevalidateTablesOperation, 3 | revalidateTables, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { useQueryClient } from '@tanstack/react-query'; 6 | 7 | import { decode } from '../lib'; 8 | 9 | /** 10 | * Returns a function that can be used to revalidate all queries in the cache that match the tables provided in the `RevalidateTablesOperation` 11 | * This hook does not make any HTTP requests and is intended to be used for custom cache revalidations. 12 | * 13 | * @param opts - The tables to revalidate 14 | * 15 | * @returns A function that will revalidate all defined tables when called. 16 | * **/ 17 | export function useRevalidateTables( 18 | tables: RevalidateTablesOperation, 19 | ): () => Promise { 20 | const queryClient = useQueryClient(); 21 | 22 | return async () => 23 | await revalidateTables(tables, { 24 | cacheKeys: queryClient 25 | .getQueryCache() 26 | .getAll() 27 | .map((c) => c.queryKey), 28 | revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), 29 | decode, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/cache/use-upsert-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type UpsertItemOperation, 3 | upsertItem, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { useQueryClient } from '@tanstack/react-query'; 6 | import { flatten } from 'flat'; 7 | 8 | import { decode, usePostgrestFilterCache } from '../lib'; 9 | 10 | /** 11 | * Convenience hook to upsert an item into the react query cache. Does not make any http requests, and is supposed to be used for custom cache updates. 12 | * @param opts The mutation options 13 | * @returns void 14 | */ 15 | export function useUpsertItem>( 16 | opts: Omit, 'input'>, 17 | ) { 18 | const queryClient = useQueryClient(); 19 | const getPostgrestFilter = usePostgrestFilterCache(); 20 | 21 | return async (input: Type) => 22 | await upsertItem( 23 | { 24 | input: flatten(input) as Type, 25 | ...opts, 26 | }, 27 | { 28 | cacheKeys: queryClient 29 | .getQueryCache() 30 | .getAll() 31 | .map((c) => c.queryKey), 32 | getPostgrestFilter, 33 | revalidate: (key) => queryClient.invalidateQueries({ queryKey: key }), 34 | mutate: (key, fn) => { 35 | queryClient.setQueriesData({ queryKey: key }, fn); 36 | }, 37 | decode, 38 | }, 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | PostgrestHasMorePaginationCacheData, 3 | PostgrestPaginationCacheData, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | 6 | export * from './cache'; 7 | export * from './lib'; 8 | export * from './mutate'; 9 | export * from './query'; 10 | export * from './subscribe'; 11 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-postgrest-filter-cache'; 2 | export * from './key'; 3 | export * from './use-queries-for-table-loader'; 4 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/lib/use-postgrest-filter-cache.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PostgrestFilter, 3 | type PostgrestQueryParserOptions, 4 | encodeObject, 5 | } from '@supabase-cache-helpers/postgrest-core'; 6 | import { useQueryClient } from '@tanstack/react-query'; 7 | 8 | export const POSTGREST_FILTER_KEY_PREFIX = 'postgrest-filter'; 9 | 10 | export const usePostgrestFilterCache = < 11 | R extends Record, 12 | >() => { 13 | const queryClient = useQueryClient(); 14 | 15 | return (query: string, opts?: PostgrestQueryParserOptions) => { 16 | const key = [ 17 | POSTGREST_FILTER_KEY_PREFIX, 18 | query, 19 | opts ? encodeObject(opts) : null, 20 | ]; 21 | const cacheData = queryClient.getQueryData(key); 22 | if (cacheData instanceof PostgrestFilter) { 23 | return cacheData; 24 | } 25 | const filter = PostgrestFilter.fromQuery(query, opts); 26 | queryClient.setQueryData(key, filter); 27 | return filter as PostgrestFilter; 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/lib/use-queries-for-table-loader.ts: -------------------------------------------------------------------------------- 1 | import type { BuildNormalizedQueryOps } from '@supabase-cache-helpers/postgrest-core'; 2 | import { useQueryClient } from '@tanstack/react-query'; 3 | 4 | import { decode } from './key'; 5 | import { usePostgrestFilterCache } from './use-postgrest-filter-cache'; 6 | 7 | export const useQueriesForTableLoader = (table: string) => { 8 | const queryClient = useQueryClient(); 9 | const getPostgrestFilter = usePostgrestFilterCache(); 10 | 11 | return () => 12 | queryClient 13 | .getQueryCache() 14 | .getAll() 15 | .map((c) => c.queryKey) 16 | .reduce>( 17 | (prev, curr) => { 18 | const decodedKey = decode(curr); 19 | if (decodedKey?.table === table) { 20 | prev.push(getPostgrestFilter(decodedKey.queryKey).params); 21 | } 22 | return prev; 23 | }, 24 | [], 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/mutate/get-user-response.ts: -------------------------------------------------------------------------------- 1 | import type { MutationFetcherResponse } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash 4 | 5 | export function truthy(value: T): value is Truthy { 6 | return !!value; 7 | } 8 | 9 | export const getUserResponse = ( 10 | d: MutationFetcherResponse[] | null | undefined, 11 | ) => { 12 | if (!d) return d; 13 | return d.map((r) => r.userQueryData).filter(truthy); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/mutate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-delete-many-mutation'; 2 | export * from './use-delete-mutation'; 3 | export * from './use-insert-mutation'; 4 | export * from './use-update-mutation'; 5 | export * from './use-upsert-mutation'; 6 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/query/build-query-opts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type AnyPostgrestResponse, 3 | isPostgrestBuilder, 4 | isPostgrestTransformBuilder, 5 | } from '@supabase-cache-helpers/postgrest-core'; 6 | import type { PostgrestError } from '@supabase/postgrest-js'; 7 | import type { UseQueryOptions as UseReactQueryOptions } from '@tanstack/react-query'; 8 | 9 | import { encode } from '../lib/key'; 10 | 11 | export function buildQueryOpts( 12 | query: PromiseLike>, 13 | config?: Omit< 14 | UseReactQueryOptions, PostgrestError>, 15 | 'queryKey' | 'queryFn' 16 | >, 17 | ): UseReactQueryOptions, PostgrestError> { 18 | return { 19 | queryKey: encode(query, false), 20 | queryFn: async ({ signal }) => { 21 | if (isPostgrestTransformBuilder(query)) { 22 | query = query.abortSignal(signal); 23 | } 24 | if (isPostgrestBuilder(query)) { 25 | query = query.throwOnError(); 26 | } 27 | return await query; 28 | }, 29 | ...config, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/query/fetch.ts: -------------------------------------------------------------------------------- 1 | import type { AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core'; 2 | import type { 3 | PostgrestError, 4 | PostgrestMaybeSingleResponse, 5 | PostgrestResponse, 6 | PostgrestSingleResponse, 7 | } from '@supabase/postgrest-js'; 8 | import type { FetchQueryOptions, QueryClient } from '@tanstack/react-query'; 9 | 10 | import { buildQueryOpts } from './build-query-opts'; 11 | 12 | function fetchQuery( 13 | queryClient: QueryClient, 14 | query: PromiseLike>, 15 | config?: Omit< 16 | FetchQueryOptions, PostgrestError>, 17 | 'queryKey' | 'queryFn' 18 | >, 19 | ): Promise>; 20 | function fetchQuery( 21 | queryClient: QueryClient, 22 | query: PromiseLike>, 23 | config?: Omit< 24 | FetchQueryOptions, PostgrestError>, 25 | 'queryKey' | 'queryFn' 26 | >, 27 | ): Promise>; 28 | function fetchQuery( 29 | queryClient: QueryClient, 30 | query: PromiseLike>, 31 | config?: Omit< 32 | FetchQueryOptions, PostgrestError>, 33 | 'queryKey' | 'queryFn' 34 | >, 35 | ): Promise>; 36 | 37 | async function fetchQuery( 38 | queryClient: QueryClient, 39 | query: PromiseLike>, 40 | config?: Omit< 41 | FetchQueryOptions, PostgrestError>, 42 | 'queryKey' | 'queryFn' 43 | >, 44 | ): Promise> { 45 | return await queryClient.fetchQuery< 46 | AnyPostgrestResponse, 47 | PostgrestError 48 | >(buildQueryOpts(query, config)); 49 | } 50 | 51 | export { fetchQuery }; 52 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build-query-opts'; 2 | export * from './fetch'; 3 | export * from './prefetch'; 4 | export * from './use-query'; 5 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/src/subscribe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-subscription-query'; 2 | export * from './use-subscription'; 3 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/tests/query/fetch.spec.ts: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { QueryClient } from '@tanstack/react-query'; 3 | import { beforeAll, describe, expect, it } from 'vitest'; 4 | 5 | import { fetchQuery } from '../../src'; 6 | import type { Database } from '../database.types'; 7 | import '../utils'; 8 | 9 | const TEST_PREFIX = 'postgrest-react-query-fetch'; 10 | 11 | describe('fetchQuery', () => { 12 | let client: SupabaseClient; 13 | let testRunPrefix: string; 14 | let contacts: Database['public']['Tables']['contact']['Row'][]; 15 | 16 | beforeAll(async () => { 17 | testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; 18 | client = createClient( 19 | process.env.SUPABASE_URL as string, 20 | process.env.SUPABASE_ANON_KEY as string, 21 | ); 22 | await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); 23 | 24 | const { data } = await client 25 | .from('contact') 26 | .insert([ 27 | { username: `${testRunPrefix}-username-1` }, 28 | { username: `${testRunPrefix}-username-2` }, 29 | { username: `${testRunPrefix}-username-3` }, 30 | { username: `${testRunPrefix}-username-4` }, 31 | ]) 32 | .select('*') 33 | .throwOnError(); 34 | contacts = data ?? []; 35 | expect(contacts).toHaveLength(4); 36 | }); 37 | 38 | it('fetchQuery should work', async () => { 39 | const queryClient = new QueryClient(); 40 | const { data } = await fetchQuery( 41 | queryClient, 42 | client.from('contact').select('*').ilike('username', `${testRunPrefix}%`), 43 | ); 44 | expect(data).toEqual(contacts); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/tests/utils.tsx: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { type QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { render } from '@testing-library/react'; 4 | import * as dotenv from 'dotenv'; 5 | import type React from 'react'; 6 | 7 | dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); 8 | 9 | export const renderWithConfig = ( 10 | element: React.ReactElement, 11 | queryClient: QueryClient, 12 | ): ReturnType => { 13 | const TestQueryClientProvider = ({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) => ( 18 | {children} 19 | ); 20 | return render(element, { wrapper: TestQueryClientProvider }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/react-library.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/postgrest-react-query/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | environment: 'happy-dom', 7 | coverage: { 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/postgrest-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase-cache-helpers/postgrest-server", 3 | "version": "0.2.1", 4 | "author": "Philipp Steinrötter ", 5 | "homepage": "https://supabase-cache-helpers.vercel.app", 6 | "bugs": { 7 | "url": "https://github.com/psteinroe/supabase-cache-helpers/issues" 8 | }, 9 | "type": "module", 10 | "main": "./dist/index.js", 11 | "source": "./src/index.ts", 12 | "types": "./dist/index.d.ts", 13 | "files": ["dist/**"], 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "license": "MIT", 18 | "scripts": { 19 | "build": "tsup", 20 | "test": "vitest --coverage --no-file-parallelism --dangerouslyIgnoreUnhandledErrors", 21 | "clean": "rm -rf .turbo && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", 22 | "typecheck": "tsc --pretty --noEmit" 23 | }, 24 | "keywords": ["Supabase", "PostgREST", "Cache", "SWR"], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", 28 | "directory": "packages/postgrest-server" 29 | }, 30 | "peerDependencies": { 31 | "@supabase/postgrest-js": "^1.19.4" 32 | }, 33 | "devDependencies": { 34 | "@supabase-cache-helpers/tsconfig": "workspace:*", 35 | "@supabase/postgrest-js": "1.19.4", 36 | "@supabase/supabase-js": "2.49.4", 37 | "@vitest/coverage-istanbul": "^3.0.0", 38 | "ioredis": "5.6.1", 39 | "dotenv": "16.5.0", 40 | "tsup": "8.5.0", 41 | "typescript": "5.8.3", 42 | "vitest": "3.2.0" 43 | }, 44 | "dependencies": { 45 | "@supabase-cache-helpers/postgrest-core": "workspace:*" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/context.ts: -------------------------------------------------------------------------------- 1 | export interface Context { 2 | waitUntil: (p: Promise) => void; 3 | } 4 | 5 | export class DefaultStatefulContext implements Context { 6 | public waitUntil(_p: Promise) { 7 | // do nothing, the promise will resolve on its own 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stores'; 2 | export * from './context'; 3 | export * from './query-cache'; 4 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/key.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyPostgrestResponse, 3 | PostgrestParser, 4 | isPostgrestBuilder, 5 | } from '@supabase-cache-helpers/postgrest-core'; 6 | 7 | const SEPARATOR = '$'; 8 | 9 | export function encode( 10 | query: PromiseLike>, 11 | ) { 12 | if (!isPostgrestBuilder(query)) { 13 | throw new Error('Query is not a PostgrestBuilder'); 14 | } 15 | 16 | const parser = new PostgrestParser(query); 17 | return [ 18 | parser.schema, 19 | parser.table, 20 | parser.queryKey, 21 | parser.bodyKey ?? 'null', 22 | `count=${parser.count}`, 23 | `head=${parser.isHead}`, 24 | parser.orderByKey, 25 | ].join(SEPARATOR); 26 | } 27 | 28 | export function buildTablePrefix(schema: string, table: string) { 29 | return [schema, table].join(SEPARATOR); 30 | } 31 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/stores/entry.ts: -------------------------------------------------------------------------------- 1 | import { type AnyPostgrestResponse } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | export type Value = AnyPostgrestResponse; 4 | 5 | export type Entry = { 6 | value: Value; 7 | 8 | // Before this time the entry is considered fresh and valid 9 | // UnixMilli 10 | freshUntil: number; 11 | 12 | /** 13 | * Unix timestamp in milliseconds. 14 | * 15 | * Do not use data after this point as it is considered no longer valid. 16 | * 17 | * You can use this field to configure automatic eviction in your store implementation. * 18 | */ 19 | staleUntil: number; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interface'; 2 | export * from './entry'; 3 | export * from './memory'; 4 | export * from './redis'; 5 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/stores/interface.ts: -------------------------------------------------------------------------------- 1 | import { Entry } from './entry'; 2 | 3 | /** 4 | * A store is a common interface for storing, reading and deleting key-value pairs. 5 | * 6 | * The store implementation is responsible for cleaning up expired data on its own. 7 | */ 8 | export interface Store { 9 | /** 10 | * A name for metrics/tracing. 11 | * 12 | * @example: memory | zone 13 | */ 14 | name: string; 15 | 16 | /** 17 | * Return the cached value 18 | * 19 | * The response must be `undefined` for cache misses 20 | */ 21 | get(key: string): Promise | undefined>; 22 | 23 | /** 24 | * Sets the value for the given key. 25 | * 26 | * You are responsible for evicting expired values in your store implementation. 27 | * Use the `entry.staleUntil` (unix milli timestamp) field to configure expiration 28 | */ 29 | set(key: string, value: Entry): Promise; 30 | 31 | /** 32 | * Removes the key from the store. 33 | */ 34 | remove(key: string | string[]): Promise; 35 | 36 | /** 37 | * Removes all keys with the given prefix. 38 | */ 39 | removeByPrefix(prefix: string): Promise; 40 | } 41 | -------------------------------------------------------------------------------- /packages/postgrest-server/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Value } from './stores/entry'; 2 | 3 | /** 4 | * A result is empty if 5 | * - it does not contain a truhty data field 6 | * - it does not contain a count field 7 | * - data is an empty array 8 | * 9 | * @template Result - The Result of the query 10 | * @param result - The value to check 11 | * @returns true if the result is empty 12 | */ 13 | export function isEmpty(result: Value) { 14 | if (typeof result.count === 'number') { 15 | return false; 16 | } 17 | 18 | if (!result.data) { 19 | return true; 20 | } 21 | 22 | if (Array.isArray(result.data)) { 23 | return result.data.length === 0; 24 | } 25 | 26 | return false; 27 | } 28 | -------------------------------------------------------------------------------- /packages/postgrest-server/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import * as dotenv from 'dotenv'; 3 | 4 | dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); 5 | -------------------------------------------------------------------------------- /packages/postgrest-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/node.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/postgrest-server/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entry: { 6 | index: 'src/index.ts', 7 | stores: 'src/stores/index.ts', 8 | }, 9 | external: [/^@supabase\//], 10 | format: ['cjs', 'esm'], 11 | // inject: ['src/react-shim.js'], 12 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 13 | legacyOutput: false, 14 | sourcemap: true, 15 | splitting: false, 16 | bundle: true, 17 | clean: true, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/postgrest-server/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | coverage: { 7 | provider: 'istanbul', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-delete-item'; 2 | export * from './use-mutate-item'; 3 | export * from './use-revalidate-tables'; 4 | export * from './use-upsert-item'; 5 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/cache/use-delete-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type DeleteItemOperation, 3 | deleteItem, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { flatten } from 'flat'; 6 | import { type MutatorOptions, useSWRConfig } from 'swr'; 7 | 8 | import { decode, usePostgrestFilterCache } from '../lib'; 9 | import { getMutableKeys } from '../lib/mutable-keys'; 10 | 11 | /** 12 | * Returns a function that can be used to delete an item into the SWR cache. 13 | * This hook does not make any HTTP requests and is intended to be used for custom cache updates. 14 | * 15 | * @param opts - Options for the delete operation, excluding the input record. 16 | * 17 | * @returns A function that takes a record of type `Type` and returns a promise that resolves once the record has been deleted from the cache. 18 | * **/ 19 | export function useDeleteItem>( 20 | opts: Omit, 'input'> & MutatorOptions, 21 | ): (input: Type) => Promise { 22 | const { mutate, cache } = useSWRConfig(); 23 | const getPostgrestFilter = usePostgrestFilterCache(); 24 | 25 | return async (input: Type) => 26 | await deleteItem( 27 | { 28 | input: flatten(input) as Type, 29 | ...opts, 30 | }, 31 | { 32 | cacheKeys: getMutableKeys(Array.from(cache.keys())), 33 | getPostgrestFilter, 34 | revalidate: (key) => { 35 | mutate(key); 36 | }, 37 | mutate: (key, data) => { 38 | mutate(key, data, { ...opts, revalidate: opts?.revalidate ?? false }); 39 | }, 40 | decode, 41 | }, 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/cache/use-revalidate-tables.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type RevalidateTablesOperation, 3 | revalidateTables, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { useSWRConfig } from 'swr'; 6 | 7 | import { decode } from '../lib'; 8 | import { getMutableKeys } from '../lib/mutable-keys'; 9 | 10 | /** 11 | * Returns a function that can be used to revalidate all queries in the cache that match the tables provided in the `RevalidateTablesOperation` 12 | * This hook does not make any HTTP requests and is intended to be used for custom cache revalidations. 13 | * 14 | * @param tables - The tables to revalidate 15 | * 16 | * @returns A function that will revalidate all defined tables when called. 17 | * **/ 18 | export function useRevalidateTables( 19 | tables: RevalidateTablesOperation, 20 | ): () => Promise { 21 | const { mutate, cache } = useSWRConfig(); 22 | 23 | return async () => 24 | await revalidateTables(tables, { 25 | cacheKeys: getMutableKeys(Array.from(cache.keys())), 26 | revalidate: (key) => { 27 | mutate(key); 28 | }, 29 | decode, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/cache/use-upsert-item.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type UpsertItemOperation, 3 | upsertItem, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import { flatten } from 'flat'; 6 | import { type MutatorOptions, useSWRConfig } from 'swr'; 7 | 8 | import { decode, usePostgrestFilterCache } from '../lib'; 9 | import { getMutableKeys } from '../lib/mutable-keys'; 10 | 11 | /** 12 | * Returns a function that can be used to upsert an item into the SWR cache. 13 | * This hook does not make any HTTP requests and is intended to be used for custom cache updates. 14 | * 15 | * @param opts - Options for the upsert operation, excluding the input record. 16 | * 17 | * @returns A function that takes a record of type `Type` and returns a promise that resolves once the record has been upserted into the cache. 18 | * **/ 19 | export function useUpsertItem>( 20 | opts: Omit, 'input'> & MutatorOptions, 21 | ): (input: Type) => Promise { 22 | const { mutate, cache } = useSWRConfig(); 23 | const getPostgrestFilter = usePostgrestFilterCache(); 24 | 25 | return async (input: Type) => 26 | await upsertItem( 27 | { 28 | input: flatten(input) as Type, 29 | ...opts, 30 | }, 31 | { 32 | cacheKeys: getMutableKeys(Array.from(cache.keys())), 33 | getPostgrestFilter, 34 | revalidate: (key) => { 35 | mutate(key); 36 | }, 37 | mutate: (key, data) => { 38 | mutate(key, data, { 39 | ...opts, 40 | revalidate: opts?.revalidate ?? false, 41 | }); 42 | }, 43 | decode, 44 | }, 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/index.react-server.ts: -------------------------------------------------------------------------------- 1 | // Server only flag in case other RSC compatible exports are added here later 2 | import 'server-only'; 3 | 4 | export * from './query/prefetch'; 5 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | PostgrestHasMorePaginationCacheData, 3 | PostgrestPaginationCacheData, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | 6 | export * from './cache'; 7 | export * from './lib'; 8 | export * from './mutate'; 9 | export * from './query'; 10 | export * from './subscribe'; 11 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const KEY_PREFIX = 'postgrest'; 2 | export const POSTGREST_FILTER_KEY_PREFIX = 'postgrest-filter'; 3 | export const KEY_SEPARATOR = '$'; 4 | export const INFINITE_PREFIX = '$inf$'; 5 | export const INFINITE_KEY_PREFIX = 'page'; 6 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/decode.ts: -------------------------------------------------------------------------------- 1 | import type { Key } from 'swr'; 2 | 3 | import { 4 | INFINITE_KEY_PREFIX, 5 | INFINITE_PREFIX, 6 | KEY_PREFIX, 7 | KEY_SEPARATOR, 8 | } from './constants'; 9 | import type { DecodedSWRKey } from './types'; 10 | 11 | export const decode = (key: Key): DecodedSWRKey | null => { 12 | if (typeof key !== 'string') return null; 13 | 14 | const isInfinite = key.startsWith(INFINITE_PREFIX); 15 | let parsedKey = key.replace(INFINITE_PREFIX, ''); 16 | 17 | // Exit early if not a postgrest key 18 | const isPostgrestKey = parsedKey.startsWith(`${KEY_PREFIX}${KEY_SEPARATOR}`); 19 | if (!isPostgrestKey) { 20 | return null; 21 | } 22 | parsedKey = parsedKey.replace(`${KEY_PREFIX}${KEY_SEPARATOR}`, ''); 23 | 24 | const [ 25 | pagePrefix, 26 | schema, 27 | table, 28 | queryKey, 29 | bodyKey, 30 | count, 31 | head, 32 | orderByKey, 33 | ] = parsedKey.split(KEY_SEPARATOR); 34 | 35 | const params = new URLSearchParams(queryKey); 36 | const limit = params.get('limit'); 37 | const offset = params.get('offset'); 38 | 39 | const countValue = count.replace('count=', ''); 40 | 41 | return { 42 | limit: limit ? Number(limit) : undefined, 43 | offset: offset ? Number(offset) : undefined, 44 | bodyKey, 45 | count: countValue === 'null' ? null : countValue, 46 | isHead: head === 'head=true', 47 | isInfinite, 48 | key, 49 | isInfiniteKey: pagePrefix === INFINITE_KEY_PREFIX, 50 | queryKey, 51 | schema, 52 | table, 53 | orderByKey, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/encode.ts: -------------------------------------------------------------------------------- 1 | import type { PostgrestParser } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | import { INFINITE_KEY_PREFIX, KEY_PREFIX, KEY_SEPARATOR } from './constants'; 4 | 5 | export const encode = ( 6 | parser: PostgrestParser, 7 | isInfinite: boolean, 8 | ) => { 9 | return [ 10 | KEY_PREFIX, 11 | isInfinite ? INFINITE_KEY_PREFIX : 'null', 12 | parser.schema, 13 | parser.table, 14 | parser.queryKey, 15 | parser.bodyKey ?? 'null', 16 | `count=${parser.count}`, 17 | `head=${parser.isHead}`, 18 | parser.orderByKey, 19 | ].join(KEY_SEPARATOR); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './create-key-getter'; 3 | export * from './decode'; 4 | export * from './encode'; 5 | export * from './middleware'; 6 | export * from './types'; 7 | export * from './use-postgrest-filter-cache'; 8 | export * from './use-queries-for-table-loader'; 9 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PostgrestParser, 3 | isPostgrestBuilder, 4 | } from '@supabase-cache-helpers/postgrest-core'; 5 | import type { 6 | SWRInfiniteConfiguration, 7 | SWRInfiniteFetcher, 8 | SWRInfiniteHook, 9 | SWRInfiniteKeyLoader, 10 | } from 'swr/infinite'; 11 | 12 | import { encode } from './encode'; 13 | 14 | export const infiniteMiddleware = ( 15 | useSWRInfiniteNext: SWRInfiniteHook, 16 | ) => { 17 | return ( 18 | keyFn: SWRInfiniteKeyLoader, 19 | fetcher: SWRInfiniteFetcher, 20 | config: SWRInfiniteConfiguration, 21 | ) => { 22 | return useSWRInfiniteNext( 23 | (index, previousPageData) => { 24 | // todo use type guard 25 | const query = keyFn(index, previousPageData); 26 | if (!query) return null; 27 | if (!isPostgrestBuilder(query)) { 28 | throw new Error('Key is not a PostgrestBuilder'); 29 | } 30 | 31 | return encode(new PostgrestParser(query), true); 32 | }, 33 | typeof fetcher === 'function' ? (query) => fetcher(query) : fetcher, 34 | config, 35 | ); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/mutable-keys.ts: -------------------------------------------------------------------------------- 1 | import { decode } from './decode'; 2 | 3 | export const getMutableKeys = (keys: string[]) => { 4 | return keys.filter((k) => { 5 | const decoded = decode(k); 6 | return decoded && (decoded.isInfinite || !decoded.isInfiniteKey); 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/parse-order-by.ts: -------------------------------------------------------------------------------- 1 | import { parseOrderBy as core } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | export function parseOrderBy( 4 | searchParams: URLSearchParams, 5 | { 6 | orderByPath, 7 | uqOrderByPath, 8 | }: { orderByPath: string; uqOrderByPath?: string }, 9 | ) { 10 | const orderByDef = core(searchParams); 11 | const orderBy = orderByDef.find((o) => o.column === orderByPath); 12 | 13 | if (!orderBy) { 14 | throw new Error(`No ordering key found for path ${orderByPath}`); 15 | } 16 | 17 | const uqOrderBy = uqOrderByPath 18 | ? orderByDef.find((o) => o.column === uqOrderByPath) 19 | : null; 20 | 21 | return { 22 | orderBy, 23 | uqOrderBy, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { DecodedKey } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | export type DecodedSWRKey = DecodedKey & { 4 | isInfinite: boolean; 5 | key: string; 6 | isInfiniteKey: boolean; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/use-postgrest-filter-cache.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PostgrestFilter, 3 | type PostgrestQueryParserOptions, 4 | encodeObject, 5 | } from '@supabase-cache-helpers/postgrest-core'; 6 | import { useSWRConfig } from 'swr'; 7 | 8 | import { KEY_SEPARATOR, POSTGREST_FILTER_KEY_PREFIX } from './constants'; 9 | 10 | export const usePostgrestFilterCache = < 11 | R extends Record, 12 | >() => { 13 | const { cache } = useSWRConfig(); 14 | 15 | return (query: string, opts?: PostgrestQueryParserOptions) => { 16 | const key = [ 17 | POSTGREST_FILTER_KEY_PREFIX, 18 | query, 19 | opts ? encodeObject(opts) : null, 20 | ] 21 | .filter(Boolean) 22 | .join(KEY_SEPARATOR); 23 | const cacheData = cache.get(key); 24 | if (cacheData && cacheData.data instanceof PostgrestFilter) { 25 | return cacheData.data; 26 | } 27 | const filter = PostgrestFilter.fromQuery(query, opts); 28 | cache.set(key, { data: filter }); 29 | return filter as PostgrestFilter; 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/lib/use-queries-for-table-loader.ts: -------------------------------------------------------------------------------- 1 | import type { BuildNormalizedQueryOps } from '@supabase-cache-helpers/postgrest-core'; 2 | import { useSWRConfig } from 'swr'; 3 | 4 | import { decode } from './decode'; 5 | import { usePostgrestFilterCache } from './use-postgrest-filter-cache'; 6 | 7 | export const useQueriesForTableLoader = (table: string) => { 8 | const { cache } = useSWRConfig(); 9 | const getPostgrestFilter = usePostgrestFilterCache(); 10 | 11 | return () => 12 | Array.from(cache.keys()).reduce< 13 | ReturnType 14 | >((prev, curr) => { 15 | const decodedKey = decode(curr); 16 | if (decodedKey?.table === table) { 17 | prev.push(getPostgrestFilter(decodedKey.queryKey).params); 18 | } 19 | return prev; 20 | }, []); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/mutate/get-user-response.ts: -------------------------------------------------------------------------------- 1 | import type { MutationFetcherResponse } from '@supabase-cache-helpers/postgrest-core'; 2 | 3 | type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash 4 | 5 | export function truthy(value: T): value is Truthy { 6 | return !!value; 7 | } 8 | 9 | export const getUserResponse = (d: MutationFetcherResponse[] | null) => { 10 | if (!d) return d; 11 | return d.map((r) => r.userQueryData).filter(truthy); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/mutate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './use-delete-many-mutation'; 3 | export * from './use-delete-mutation'; 4 | export * from './use-insert-mutation'; 5 | export * from './use-update-mutation'; 6 | export * from './use-upsert-mutation'; 7 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/mutate/use-random-key.ts: -------------------------------------------------------------------------------- 1 | import { useId } from 'react'; 2 | 3 | import { KEY_SEPARATOR } from '../lib'; 4 | 5 | const PREFIX = 'random-mutation-key'; 6 | 7 | export const useRandomKey = () => { 8 | const key = useId(); 9 | 10 | return [PREFIX, key].join(KEY_SEPARATOR); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prefetch'; 2 | export * from './use-cursor-infinite-scroll-query'; 3 | export * from './use-offset-infinite-query'; 4 | export * from './use-offset-infinite-scroll-query'; 5 | export * from './use-infinite-offset-pagination-query'; 6 | export * from './use-query'; 7 | -------------------------------------------------------------------------------- /packages/postgrest-swr/src/subscribe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-subscription-query'; 2 | export * from './use-subscription'; 3 | -------------------------------------------------------------------------------- /packages/postgrest-swr/tests/lib/get-mutable-keys.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getMutableKeys } from '../../src/lib/mutable-keys'; 3 | 4 | describe('getMutableKeys', () => { 5 | it('should return correct keys', () => { 6 | expect( 7 | getMutableKeys([ 8 | '$inf$postgrest$page$public$contact$limit=1&offset=0&order=username.asc&select=id%2Cusername&username=ilike.postgrest-swr-pagination-88%25$null$count=null$head=false$undefined.username:asc.nullsLast', 9 | 'postgrest$page$public$contact$limit=1&offset=0&order=username.asc&select=id%2Cusername&username=ilike.postgrest-swr-pagination-88%25$null$count=null$head=false$undefined.username:asc.nullsLast', 10 | 'postgrest$page$public$contact$limit=1&offset=1&order=username.asc&select=id%2Cusername&username=ilike.postgrest-swr-pagination-88%25$null$count=null$head=false$undefined.username:asc.nullsLast', 11 | 'postgrest$page$public$contact$limit=1&offset=2&order=username.asc&select=id%2Cusername&username=ilike.postgrest-swr-pagination-88%25$null$count=null$head=false$undefined.username:asc.nullsLast', 12 | ]), 13 | ).toEqual([ 14 | '$inf$postgrest$page$public$contact$limit=1&offset=0&order=username.asc&select=id%2Cusername&username=ilike.postgrest-swr-pagination-88%25$null$count=null$head=false$undefined.username:asc.nullsLast', 15 | ]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/postgrest-swr/tests/query/prefetch.integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { beforeAll, describe, expect, it } from 'vitest'; 3 | 4 | import { fetchQueryFallbackData } from '../../src'; 5 | import type { Database } from '../database.types'; 6 | import '../utils'; 7 | 8 | const TEST_PREFIX = 'postgrest-swr-prefetch'; 9 | 10 | describe('prefetch', () => { 11 | let client: SupabaseClient; 12 | let testRunPrefix: string; 13 | let contacts: Database['public']['Tables']['contact']['Row'][]; 14 | 15 | beforeAll(async () => { 16 | testRunPrefix = `${TEST_PREFIX}-${Math.floor(Math.random() * 1000)}`; 17 | client = createClient( 18 | process.env.SUPABASE_URL as string, 19 | process.env.SUPABASE_ANON_KEY as string, 20 | ); 21 | await client.from('contact').delete().ilike('username', `${TEST_PREFIX}%`); 22 | 23 | const { data } = await client 24 | .from('contact') 25 | .insert([ 26 | { username: `${testRunPrefix}-username-1` }, 27 | { username: `${testRunPrefix}-username-2` }, 28 | { username: `${testRunPrefix}-username-3` }, 29 | { username: `${testRunPrefix}-username-4` }, 30 | ]) 31 | .select('*') 32 | .throwOnError(); 33 | contacts = data ?? []; 34 | expect(contacts).toHaveLength(4); 35 | }); 36 | 37 | it('should throw if not a postgrest builder', async () => { 38 | try { 39 | await fetchQueryFallbackData('' as any); 40 | } catch (error) { 41 | expect(error).toEqual(new Error('Query is not a PostgrestBuilder')); 42 | } 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/postgrest-swr/tests/utils.tsx: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | import { render } from '@testing-library/react'; 3 | import * as dotenv from 'dotenv'; 4 | import type React from 'react'; 5 | import { SWRConfig } from 'swr'; 6 | 7 | dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); 8 | 9 | export const renderWithConfig = ( 10 | element: React.ReactElement, 11 | config: Parameters[0]['value'], 12 | ): ReturnType => { 13 | const TestSWRConfig = ({ children }: { children: React.ReactNode }) => ( 14 | 17 | {children} 18 | 19 | ); 20 | return render(element, { wrapper: TestSWRConfig }); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/postgrest-swr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/react-library.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/postgrest-swr/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts', 'src/index.react-server.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/postgrest-swr/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | environment: 'happy-dom', 7 | coverage: { 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/storage-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @supabase-cache-helpers/storage-core 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 0f7a0be: fix: first check if it exists before fetching into 8 | - aea1bca: fix: useUpload to support file metadata 9 | 10 | ## 0.1.1 11 | 12 | ### Patch Changes 13 | 14 | - dbafa2f: fix: use new .info() for ensureExistence 15 | 16 | ## 0.1.0 17 | 18 | ### Minor Changes 19 | 20 | - 9b1bba1: fix: make compatible with latest supabase-js 21 | 22 | ### Patch Changes 23 | 24 | - 68f2767: Updated dependency `fast-equals` to `5.2.2`. 25 | 26 | ## 0.0.6 27 | 28 | ### Patch Changes 29 | 30 | - 49a35ad: chore: update to latest supabase-js 31 | 32 | ## 0.0.5 33 | 34 | ### Patch Changes 35 | 36 | - bfdc3ee: chore: update dependencies 37 | 38 | ## 0.0.4 39 | 40 | ### Patch Changes 41 | 42 | - 40a6327: fix: update typescript to 5.4.2 43 | 44 | ## 0.0.3 45 | 46 | ### Patch Changes 47 | 48 | - f2ca765: chore: upgrade supabase-js to 2.38.5 49 | - f2ca765: chore: upgrade storage-js to 2.5.5 50 | 51 | ## 0.0.2 52 | 53 | ### Patch Changes 54 | 55 | - f9dd4e4: fix: expose storage-js configs for transform and download 56 | 57 | ## 0.0.1 58 | 59 | ### Patch Changes 60 | 61 | - 2f1d3cb: refactor: merge internal packages into one core package per product 62 | -------------------------------------------------------------------------------- /packages/storage-core/README.md: -------------------------------------------------------------------------------- 1 | # Storage Core 2 | 3 | A collection of cache utilities for working with the Supabase Storage API. It is not meant to be used standalone. 4 | -------------------------------------------------------------------------------- /packages/storage-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase-cache-helpers/storage-core", 3 | "version": "0.1.2", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "import": "./dist/index.js", 11 | "require": "./dist/index.cjs" 12 | }, 13 | "./package.json": "./package.json" 14 | }, 15 | "types": "./dist/index.d.ts", 16 | "files": ["dist/**"], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", 20 | "directory": "packages/storage-fetcher" 21 | }, 22 | "keywords": ["Supabase", "Storage"], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "MIT", 27 | "scripts": { 28 | "build": "tsup", 29 | "test": "vitest --coverage", 30 | "clean": "rm -rf .turbo && rm -rf coverage && rm -rf .nyc_output && rm -rf node_modules && rm -rf dist", 31 | "typecheck": "tsc --pretty --noEmit" 32 | }, 33 | "devDependencies": { 34 | "@supabase-cache-helpers/tsconfig": "workspace:*", 35 | "happy-dom": "17.6.1", 36 | "@supabase/storage-js": "2.7.1", 37 | "@supabase/supabase-js": "2.49.4", 38 | "@vitest/coverage-istanbul": "^3.0.0", 39 | "dotenv": "16.5.0", 40 | "tsup": "8.5.0", 41 | "typescript": "5.8.3", 42 | "vitest": "3.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/storage-core/src/directory-fetcher.ts: -------------------------------------------------------------------------------- 1 | import type { FileObject } from '@supabase/storage-js'; 2 | import type StorageFileApi from '@supabase/storage-js/dist/module/packages/StorageFileApi'; 3 | 4 | export const fetchDirectory = async ( 5 | fileApi: StorageFileApi, 6 | path: string, 7 | ): Promise => { 8 | const { data, error } = await fileApi.list(path); 9 | if (error) throw error; 10 | if (!Array.isArray(data)) return []; 11 | return data; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/storage-core/src/directory-urls-fetcher.ts: -------------------------------------------------------------------------------- 1 | import type { FileObject } from '@supabase/storage-js'; 2 | import type StorageFileApi from '@supabase/storage-js/dist/module/packages/StorageFileApi'; 3 | 4 | import { fetchDirectory } from './directory-fetcher'; 5 | import type { StoragePrivacy } from './lib/types'; 6 | import { type URLFetcherConfig, createUrlFetcher } from './url-fetcher'; 7 | 8 | type DirectoryURLsFetcher = ( 9 | fileApi: StorageFileApi, 10 | path: string, 11 | ) => Promise<(FileObject & { url: string })[]>; 12 | 13 | export const createDirectoryUrlsFetcher = ( 14 | mode: StoragePrivacy, 15 | config?: Pick, 16 | ): DirectoryURLsFetcher => { 17 | const fetchUrl = createUrlFetcher(mode, config); 18 | return async (fileApi: StorageFileApi, path) => { 19 | const files = await fetchDirectory(fileApi, path); 20 | const filesWithUrls = []; 21 | for (const f of files) { 22 | const url = await fetchUrl(fileApi, `${path}/${f.name}`); 23 | if (url) filesWithUrls.push({ ...f, url }); 24 | } 25 | return filesWithUrls; 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/storage-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/types'; 2 | 3 | export * from './directory-fetcher'; 4 | export * from './directory-urls-fetcher'; 5 | export * from './mutate-paths'; 6 | export * from './remove-directory'; 7 | export * from './remove-files'; 8 | export * from './upload'; 9 | export * from './url-fetcher'; 10 | -------------------------------------------------------------------------------- /packages/storage-core/src/lib/get-minimal-paths.ts: -------------------------------------------------------------------------------- 1 | export const getMinimalPaths = (paths: string[]) => 2 | paths.reduce((paths, path) => { 3 | const matchingPaths = paths.filter((p) => p.startsWith(path)); 4 | return [...paths.filter((p) => !matchingPaths.includes(p)), path]; 5 | }, []); 6 | -------------------------------------------------------------------------------- /packages/storage-core/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type DecodedStorageKey = { bucketId: string; path: string }; 2 | 3 | export type StoragePrivacy = 'public' | 'private'; 4 | -------------------------------------------------------------------------------- /packages/storage-core/src/mutate-paths.ts: -------------------------------------------------------------------------------- 1 | import { getMinimalPaths } from './lib/get-minimal-paths'; 2 | import type { DecodedStorageKey } from './lib/types'; 3 | 4 | export type Cache = { 5 | /** 6 | * The keys currently present in the cache 7 | */ 8 | cacheKeys: KeyType[]; 9 | /** 10 | * Decode a key. Should return null if not a Storage key. 11 | */ 12 | decode: (k: KeyType) => DecodedStorageKey | null; 13 | /** 14 | * The mutation function from the cache library 15 | */ 16 | mutate: (key: KeyType) => Promise; 17 | }; 18 | 19 | export const mutatePaths = async ( 20 | bucketId: string, 21 | paths: string[], 22 | { cacheKeys, decode, mutate }: Cache, 23 | ) => { 24 | const minimalPaths = getMinimalPaths(paths); 25 | if (minimalPaths.length === 0) return; 26 | await Promise.all( 27 | cacheKeys.map(async (key) => { 28 | const decodedKey = decode(key); 29 | if (!decodedKey) return false; 30 | if (decodedKey.bucketId !== bucketId) return false; 31 | if ( 32 | minimalPaths.find( 33 | (p) => p.startsWith(decodedKey.path) || decodedKey.path.startsWith(p), 34 | ) 35 | ) { 36 | mutate(key); 37 | } 38 | }), 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /packages/storage-core/src/remove-directory.ts: -------------------------------------------------------------------------------- 1 | import type StorageFileApi from '@supabase/storage-js/dist/module/packages/StorageFileApi'; 2 | 3 | import { fetchDirectory } from './directory-fetcher'; 4 | import { createRemoveFilesFetcher } from './remove-files'; 5 | 6 | export const createRemoveDirectoryFetcher = (fileApi: StorageFileApi) => { 7 | const removeFiles = createRemoveFilesFetcher(fileApi); 8 | return async (path: string) => { 9 | const files = await fetchDirectory(fileApi, path); 10 | return await removeFiles(files.map((f) => [path, f.name].join('/'))); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/storage-core/src/remove-files.ts: -------------------------------------------------------------------------------- 1 | import type StorageFileApi from '@supabase/storage-js/dist/module/packages/StorageFileApi'; 2 | 3 | export const createRemoveFilesFetcher = 4 | (fileApi: StorageFileApi) => async (paths: string[]) => { 5 | const { data, error } = await fileApi.remove(paths); 6 | if (error) throw error; 7 | return data; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/storage-core/src/url-fetcher.ts: -------------------------------------------------------------------------------- 1 | import type { TransformOptions } from '@supabase/storage-js'; 2 | import type StorageFileApi from '@supabase/storage-js/dist/module/packages/StorageFileApi'; 3 | 4 | import type { StoragePrivacy } from './lib/types'; 5 | 6 | type URLFetcher = ( 7 | fileApi: StorageFileApi, 8 | path: string, 9 | ) => Promise; 10 | 11 | export type URLFetcherConfig = { 12 | expiresIn?: number; 13 | ensureExistence?: boolean; 14 | download?: string | boolean | undefined; 15 | transform?: TransformOptions | undefined; 16 | }; 17 | 18 | export const createUrlFetcher = ( 19 | mode: StoragePrivacy, 20 | config?: URLFetcherConfig, 21 | ): URLFetcher => { 22 | return async ( 23 | fileApi: StorageFileApi, 24 | path: string, 25 | ): Promise => { 26 | let params: Record = {}; 27 | 28 | if (config?.ensureExistence) { 29 | const { data: exists } = await fileApi.exists(path); 30 | if (!exists) return; 31 | const { data: fileInfo } = await fileApi.info(path); 32 | if (!fileInfo) return; 33 | params = { 34 | updated_at: fileInfo.updatedAt, 35 | }; 36 | } 37 | 38 | let url: string | undefined; 39 | if (mode === 'private') { 40 | const { data, error } = await fileApi.createSignedUrl( 41 | path, 42 | config?.expiresIn ?? 1800, 43 | config, 44 | ); 45 | if (error) throw error; 46 | url = data.signedUrl; 47 | } else if (mode === 'public') { 48 | const { data } = fileApi.getPublicUrl(path, config); 49 | url = data.publicUrl; 50 | } else { 51 | throw new Error(`Invalid mode: ${mode}`); 52 | } 53 | const fileURL = new URL(url); 54 | Object.entries(params).forEach(([key, value]) => { 55 | fileURL.searchParams.append(key, value); 56 | }); 57 | return fileURL.toString(); 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/storage-core/tests/__fixtures__/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-core/tests/__fixtures__/1.jpg -------------------------------------------------------------------------------- /packages/storage-core/tests/__fixtures__/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-core/tests/__fixtures__/2.jpg -------------------------------------------------------------------------------- /packages/storage-core/tests/__fixtures__/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-core/tests/__fixtures__/3.jpg -------------------------------------------------------------------------------- /packages/storage-core/tests/__fixtures__/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-core/tests/__fixtures__/4.jpg -------------------------------------------------------------------------------- /packages/storage-core/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import * as Import from '../src'; 3 | 4 | describe('index exports', () => { 5 | it('should export', () => { 6 | expect(Object.keys(Import)).toHaveLength(7); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/storage-core/tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { readFile, readdir } from 'node:fs/promises'; 2 | import { join, resolve } from 'node:path'; 3 | import type { SupabaseClient } from '@supabase/supabase-js'; 4 | import * as dotenv from 'dotenv'; 5 | 6 | dotenv.config({ path: resolve(__dirname, '../../../.env.local') }); 7 | 8 | export const loadFixtures = async () => { 9 | const fixturesDir = resolve(__dirname, '__fixtures__'); 10 | const fileNames = await readdir(fixturesDir); 11 | return { 12 | fileNames, 13 | files: await Promise.all( 14 | fileNames.map(async (f) => await readFile(join(fixturesDir, f))), 15 | ), 16 | }; 17 | }; 18 | 19 | export const upload = async ( 20 | client: SupabaseClient, 21 | bucketName: string, 22 | dirName: string, 23 | ): Promise => { 24 | const fixturesDir = resolve(__dirname, '__fixtures__'); 25 | const fileNames = await readdir(fixturesDir); 26 | await Promise.all( 27 | fileNames.map( 28 | async (f) => 29 | await client.storage 30 | .from(bucketName) 31 | .upload(`${dirName}/${f}`, await readFile(join(fixturesDir, f))), 32 | ), 33 | ); 34 | return fileNames; 35 | }; 36 | 37 | export const cleanup = async ( 38 | client: SupabaseClient, 39 | bucketName: string, 40 | dirName: string, 41 | ) => { 42 | const { data } = await client.storage.from(bucketName).list(dirName); 43 | await client.storage 44 | .from(bucketName) 45 | .remove((data ?? []).map((d) => `${dirName}/${d.name}`)); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/storage-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/web.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/storage-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | environment: 'happy-dom', 7 | coverage: { 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | export * from './mutate'; 3 | export * from './query'; 4 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const KEY_PREFIX = 'storage'; 2 | export const KEY_SEPARATOR = '$'; 3 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/decode.ts: -------------------------------------------------------------------------------- 1 | import type { DecodedStorageKey } from '@supabase-cache-helpers/storage-core'; 2 | import type { QueryKey } from '@tanstack/react-query'; 3 | 4 | import { KEY_PREFIX } from './constants'; 5 | 6 | export const decode = (key: QueryKey): DecodedStorageKey | null => { 7 | if (!Array.isArray(key) || key.length !== 3 || key[0] !== KEY_PREFIX) { 8 | return null; 9 | } 10 | const [_, bucketId, path] = key; 11 | return { bucketId, path }; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/encode.ts: -------------------------------------------------------------------------------- 1 | import type { QueryKey } from '@tanstack/react-query'; 2 | 3 | import { KEY_PREFIX } from './constants'; 4 | import { getBucketId } from './get-bucket-id'; 5 | import { assertStorageKeyInput } from './key'; 6 | 7 | export const encode = (key: QueryKey): string[] => { 8 | const [fileApi, path] = assertStorageKeyInput(key); 9 | return [KEY_PREFIX, getBucketId(fileApi), path]; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/get-bucket-id.ts: -------------------------------------------------------------------------------- 1 | import type { StorageFileApi } from './types'; 2 | 3 | export const getBucketId = (fileApi: StorageFileApi) => 4 | fileApi['bucketId'] as string; 5 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './decode'; 3 | export * from './encode'; 4 | export * from './get-bucket-id'; 5 | export * from './key'; 6 | export * from './truthy'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/key.ts: -------------------------------------------------------------------------------- 1 | import type { QueryKey } from '@tanstack/react-query'; 2 | 3 | import type { StorageFileApi } from './types'; 4 | 5 | export const isStorageKeyInput = (key: QueryKey): key is StorageKeyInput => 6 | Array.isArray(key) && 7 | key.length === 2 && 8 | typeof key[1] === 'string' && 9 | Boolean(key[0]['bucketId']); 10 | 11 | export const assertStorageKeyInput = (key: QueryKey): StorageKeyInput => { 12 | if (!isStorageKeyInput(key)) throw new Error('Invalid key'); 13 | return key; 14 | }; 15 | 16 | export type StorageKeyInput = [StorageFileApi, string]; 17 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/truthy.ts: -------------------------------------------------------------------------------- 1 | type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash 2 | 3 | export function truthy(value: T): value is Truthy { 4 | return !!value; 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { SupabaseClient } from '@supabase/supabase-js'; 2 | 3 | export type StorageFileApi = ReturnType; 4 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/mutate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-remove-directory'; 2 | export * from './use-remove-files'; 3 | export * from './use-upload'; 4 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/mutate/use-remove-directory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRemoveDirectoryFetcher, 3 | mutatePaths, 4 | } from '@supabase-cache-helpers/storage-core'; 5 | import type { FileObject, StorageError } from '@supabase/storage-js'; 6 | import { 7 | type QueryKey, 8 | type UseMutationOptions, 9 | type UseMutationResult, 10 | useMutation, 11 | useQueryClient, 12 | } from '@tanstack/react-query'; 13 | import { useCallback } from 'react'; 14 | 15 | import { type StorageFileApi, decode, getBucketId } from '../lib'; 16 | 17 | /** 18 | * A hook that provides a mutation function to remove a directory and all its contents. 19 | * @param fileApi The `StorageFileApi` instance to use for the removal. 20 | * @param config Optional configuration options for the React Query mutation. 21 | * @returns An object containing the mutation function, loading state, and error state. 22 | */ 23 | function useRemoveDirectory( 24 | fileApi: StorageFileApi, 25 | config?: Omit< 26 | UseMutationOptions, 27 | 'mutationFn' 28 | >, 29 | ): UseMutationResult { 30 | const queryClient = useQueryClient(); 31 | const fetcher = useCallback(createRemoveDirectoryFetcher(fileApi), [fileApi]); 32 | return useMutation({ 33 | mutationFn: async (arg) => { 34 | const result = fetcher(arg); 35 | await mutatePaths(getBucketId(fileApi), [arg], { 36 | cacheKeys: queryClient 37 | .getQueryCache() 38 | .getAll() 39 | .map((c) => c.queryKey), 40 | decode, 41 | mutate: async (key) => { 42 | await queryClient.invalidateQueries({ queryKey: key }); 43 | }, 44 | }); 45 | return result; 46 | }, 47 | ...config, 48 | }); 49 | } 50 | 51 | export { useRemoveDirectory }; 52 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/mutate/use-remove-files.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRemoveFilesFetcher, 3 | mutatePaths, 4 | } from '@supabase-cache-helpers/storage-core'; 5 | import type { FileObject, StorageError } from '@supabase/storage-js'; 6 | import { 7 | type QueryKey, 8 | type UseMutationOptions, 9 | type UseMutationResult, 10 | useMutation, 11 | useQueryClient, 12 | } from '@tanstack/react-query'; 13 | import { useCallback } from 'react'; 14 | 15 | import { type StorageFileApi, decode, getBucketId } from '../lib'; 16 | 17 | /** 18 | * Hook for removing files from storage using React Query mutation 19 | * @param {StorageFileApi} fileApi - The Supabase Storage API 20 | * @param {UseMutationOptions} [config] - The React Query mutation configuration 21 | * @returns {UseMutationOptions} - The React Query mutation response object 22 | */ 23 | function useRemoveFiles( 24 | fileApi: StorageFileApi, 25 | config?: Omit< 26 | UseMutationOptions, 27 | 'mutationFn' 28 | >, 29 | ): UseMutationResult { 30 | const queryClient = useQueryClient(); 31 | const fetcher = useCallback(createRemoveFilesFetcher(fileApi), [fileApi]); 32 | return useMutation({ 33 | mutationFn: async (paths) => { 34 | const res = await fetcher(paths); 35 | await mutatePaths(getBucketId(fileApi), paths, { 36 | cacheKeys: queryClient 37 | .getQueryCache() 38 | .getAll() 39 | .map((c) => c.queryKey), 40 | decode, 41 | mutate: async (key) => { 42 | await queryClient.invalidateQueries({ queryKey: key }); 43 | }, 44 | }); 45 | return res; 46 | }, 47 | ...config, 48 | }); 49 | } 50 | 51 | export { useRemoveFiles }; 52 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-directory-urls'; 2 | export * from './use-directory'; 3 | export * from './use-file-url'; 4 | -------------------------------------------------------------------------------- /packages/storage-react-query/src/query/use-directory.ts: -------------------------------------------------------------------------------- 1 | import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; 2 | import type { FileObject, StorageError } from '@supabase/storage-js'; 3 | import { 4 | type UseQueryOptions as UseReactQueryOptions, 5 | type UseQueryResult as UseReactQueryResult, 6 | useQuery as useReactQuery, 7 | } from '@tanstack/react-query'; 8 | 9 | import { type StorageFileApi, encode } from '../lib'; 10 | 11 | function buildDirectoryQueryOpts( 12 | fileApi: StorageFileApi, 13 | path: string, 14 | config?: Omit< 15 | UseReactQueryOptions, 16 | 'queryKey' | 'queryFn' 17 | >, 18 | ): UseReactQueryOptions { 19 | return { 20 | queryKey: encode([fileApi, path]), 21 | queryFn: () => fetchDirectory(fileApi, path), 22 | ...config, 23 | }; 24 | } 25 | 26 | /** 27 | * Convenience hook to fetch a directory from Supabase Storage using React Query. 28 | * 29 | * @param fileApi The StorageFileApi instance. 30 | * @param path The path to the directory. 31 | * @param config The React Query configuration. 32 | * @returns An UseQueryResult containing an array of FileObjects 33 | */ 34 | function useDirectory( 35 | fileApi: StorageFileApi, 36 | path: string, 37 | config?: Omit< 38 | UseReactQueryOptions, 39 | 'queryKey' | 'queryFn' 40 | >, 41 | ): UseReactQueryResult { 42 | return useReactQuery(buildDirectoryQueryOpts(fileApi, path, config)); 43 | } 44 | 45 | export { useDirectory }; 46 | -------------------------------------------------------------------------------- /packages/storage-react-query/tests/__fixtures__/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-react-query/tests/__fixtures__/1.jpg -------------------------------------------------------------------------------- /packages/storage-react-query/tests/__fixtures__/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-react-query/tests/__fixtures__/2.jpg -------------------------------------------------------------------------------- /packages/storage-react-query/tests/__fixtures__/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-react-query/tests/__fixtures__/3.jpg -------------------------------------------------------------------------------- /packages/storage-react-query/tests/__fixtures__/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-react-query/tests/__fixtures__/4.jpg -------------------------------------------------------------------------------- /packages/storage-react-query/tests/lib/decode.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { decode } from '../../src'; 3 | 4 | describe('decode', () => { 5 | it('should return null for invalid key', () => { 6 | expect(decode(['some', 'unrelated', 'key'])).toEqual(null); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/storage-react-query/tests/lib/key.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { assertStorageKeyInput } from '../../src/lib'; 3 | 4 | describe('key', () => { 5 | describe('assertStorageKeyInput', () => { 6 | it('should throw for invalid key', () => { 7 | expect(() => assertStorageKeyInput(['some', 'unrelated', 'key'])).toThrow( 8 | 'Invalid key', 9 | ); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/storage-react-query/tests/query/use-directory.spec.tsx: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { screen } from '@testing-library/react'; 3 | import { beforeAll, describe, it } from 'vitest'; 4 | 5 | import { useDirectory } from '../../src'; 6 | import { cleanup, renderWithConfig, upload } from '../utils'; 7 | 8 | const TEST_PREFIX = 'postgrest-storage-directory'; 9 | 10 | describe('useDirectory', () => { 11 | let client: SupabaseClient; 12 | let dirName: string; 13 | let privateFiles: string[]; 14 | let publicFiles: string[]; 15 | 16 | beforeAll(async () => { 17 | dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; 18 | client = createClient( 19 | process.env.SUPABASE_URL as string, 20 | process.env.SUPABASE_ANON_KEY as string, 21 | ); 22 | 23 | await Promise.all([ 24 | cleanup(client, 'public_contact_files', dirName), 25 | cleanup(client, 'private_contact_files', dirName), 26 | ]); 27 | 28 | privateFiles = await upload(client, 'private_contact_files', dirName); 29 | publicFiles = await upload(client, 'public_contact_files', dirName); 30 | }); 31 | 32 | it('should return files', async () => { 33 | function Page() { 34 | const { data: files } = useDirectory( 35 | client.storage.from('private_contact_files'), 36 | dirName, 37 | { 38 | refetchOnWindowFocus: false, 39 | }, 40 | ); 41 | return ( 42 |
43 | {(files ?? []).map((f) => ( 44 | {f.name} 45 | ))} 46 |
47 | ); 48 | } 49 | 50 | renderWithConfig(); 51 | await Promise.all( 52 | privateFiles.map( 53 | async (f) => await screen.findByText(f, {}, { timeout: 10000 }), 54 | ), 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/storage-react-query/tests/query/use-file-url.spec.tsx: -------------------------------------------------------------------------------- 1 | import { type SupabaseClient, createClient } from '@supabase/supabase-js'; 2 | import { screen } from '@testing-library/react'; 3 | import { beforeAll, describe, it } from 'vitest'; 4 | 5 | import { useFileUrl } from '../../src'; 6 | import { cleanup, renderWithConfig, upload } from '../utils'; 7 | 8 | const TEST_PREFIX = 'postgrest-storage-file-url'; 9 | 10 | describe('useFileUrl', () => { 11 | let client: SupabaseClient; 12 | let dirName: string; 13 | let privateFiles: string[]; 14 | let publicFiles: string[]; 15 | 16 | beforeAll(async () => { 17 | dirName = `${TEST_PREFIX}-${Math.floor(Math.random() * 100)}`; 18 | client = createClient( 19 | process.env.SUPABASE_URL as string, 20 | process.env.SUPABASE_ANON_KEY as string, 21 | ); 22 | 23 | await Promise.all([ 24 | cleanup(client, 'public_contact_files', dirName), 25 | cleanup(client, 'private_contact_files', dirName), 26 | ]); 27 | 28 | privateFiles = await upload(client, 'private_contact_files', dirName); 29 | publicFiles = await upload(client, 'public_contact_files', dirName); 30 | }); 31 | 32 | it('should return file url', async () => { 33 | function Page() { 34 | const { data: url } = useFileUrl( 35 | client.storage.from('public_contact_files'), 36 | `${dirName}/${publicFiles[0]}`, 37 | 'public', 38 | { 39 | ensureExistence: true, 40 | refetchOnWindowFocus: false, 41 | }, 42 | ); 43 | return
{`URL: ${url ? 'exists' : url}`}
; 44 | } 45 | 46 | renderWithConfig(); 47 | await screen.findByText('URL: exists', {}, { timeout: 10000 }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/storage-react-query/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/react-library.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-react-query/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/storage-react-query/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | environment: 'happy-dom', 7 | coverage: { 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/storage-swr/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | export * from './mutate'; 3 | export * from './query'; 4 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const KEY_PREFIX = 'storage'; 2 | export const KEY_SEPARATOR = '$'; 3 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/decode.ts: -------------------------------------------------------------------------------- 1 | import type { DecodedStorageKey } from '@supabase-cache-helpers/storage-core'; 2 | import type { Key } from 'swr'; 3 | 4 | import { KEY_PREFIX, KEY_SEPARATOR } from './constants'; 5 | 6 | export const decode = (key: Key): DecodedStorageKey | null => { 7 | if (typeof key !== 'string' || !key.startsWith(KEY_PREFIX)) return null; 8 | const [_, bucketId, path] = key.split(KEY_SEPARATOR); 9 | return { bucketId, path }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/encode.ts: -------------------------------------------------------------------------------- 1 | import type { Key } from 'swr'; 2 | 3 | import { KEY_PREFIX, KEY_SEPARATOR } from './constants'; 4 | import { getBucketId } from './get-bucket-id'; 5 | import { assertStorageKeyInput } from './key'; 6 | 7 | export const encode = (key: Key | null): Key => { 8 | if (key === null) return null; 9 | const [fileApi, path] = assertStorageKeyInput(key); 10 | return [KEY_PREFIX, getBucketId(fileApi), path].join(KEY_SEPARATOR); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/get-bucket-id.ts: -------------------------------------------------------------------------------- 1 | import type { StorageFileApi } from './types'; 2 | 3 | export const getBucketId = (fileApi: StorageFileApi) => 4 | fileApi['bucketId'] as string; 5 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './decode'; 3 | export * from './encode'; 4 | export * from './get-bucket-id'; 5 | export * from './middleware'; 6 | export * from './key'; 7 | export * from './truthy'; 8 | export * from './types'; 9 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/key.ts: -------------------------------------------------------------------------------- 1 | import type { Key } from 'swr'; 2 | 3 | import type { StorageFileApi } from './types'; 4 | 5 | export const isStorageKeyInput = (key: Key): key is StorageKeyInput => 6 | Array.isArray(key) && 7 | key.length === 2 && 8 | typeof key[1] === 'string' && 9 | Boolean(key[0]['bucketId']); 10 | 11 | export const assertStorageKeyInput = (key: Key): StorageKeyInput => { 12 | if (!isStorageKeyInput(key)) throw new Error('Invalid key'); 13 | return key; 14 | }; 15 | 16 | export type StorageKeyInput = [StorageFileApi, string]; 17 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { Middleware, SWRHook } from 'swr'; 2 | 3 | import { encode } from './encode'; 4 | 5 | export const middleware: Middleware = (useSWRNext: SWRHook) => { 6 | return (key, fetcher, config) => { 7 | if (!fetcher) throw new Error('No fetcher provided'); 8 | return useSWRNext(encode(key), () => fetcher(key), config); 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/truthy.ts: -------------------------------------------------------------------------------- 1 | type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash 2 | 3 | export function truthy(value: T): value is Truthy { 4 | return !!value; 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-swr/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { SupabaseClient } from '@supabase/supabase-js'; 2 | 3 | export type StorageFileApi = ReturnType; 4 | -------------------------------------------------------------------------------- /packages/storage-swr/src/mutate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-remove-directory'; 2 | export * from './use-remove-files'; 3 | export * from './use-upload'; 4 | -------------------------------------------------------------------------------- /packages/storage-swr/src/mutate/use-random-key.ts: -------------------------------------------------------------------------------- 1 | import { useId } from 'react'; 2 | 3 | import { KEY_SEPARATOR } from '../lib'; 4 | 5 | const PREFIX = 'random-mutation-key'; 6 | 7 | export const useRandomKey = () => { 8 | const key = useId(); 9 | 10 | return [PREFIX, key].join(KEY_SEPARATOR); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/storage-swr/src/mutate/use-remove-directory.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRemoveDirectoryFetcher, 3 | mutatePaths, 4 | } from '@supabase-cache-helpers/storage-core'; 5 | import type { FileObject, StorageError } from '@supabase/storage-js'; 6 | import { useCallback } from 'react'; 7 | import { type Key, useSWRConfig } from 'swr'; 8 | import useSWRMutation, { 9 | type SWRMutationResponse, 10 | type SWRMutationConfiguration, 11 | } from 'swr/mutation'; 12 | 13 | import { type StorageFileApi, decode, getBucketId } from '../lib'; 14 | import { useRandomKey } from './use-random-key'; 15 | 16 | /** 17 | * A hook that provides a mutation function to remove a directory and all its contents. 18 | * @param fileApi The `StorageFileApi` instance to use for the removal. 19 | * @param config Optional configuration options for the SWR mutation. 20 | * @returns An object containing the mutation function, loading state, and error state. 21 | */ 22 | function useRemoveDirectory( 23 | fileApi: StorageFileApi, 24 | config?: SWRMutationConfiguration, 25 | ): SWRMutationResponse { 26 | const key = useRandomKey(); 27 | const { cache, mutate } = useSWRConfig(); 28 | const fetcher = useCallback(createRemoveDirectoryFetcher(fileApi), [fileApi]); 29 | return useSWRMutation( 30 | key, 31 | async (_, { arg }) => { 32 | const result = fetcher(arg); 33 | await mutatePaths(getBucketId(fileApi), [arg], { 34 | cacheKeys: Array.from(cache.keys()), 35 | decode, 36 | mutate, 37 | }); 38 | return result; 39 | }, 40 | config, 41 | ); 42 | } 43 | 44 | export { useRemoveDirectory }; 45 | -------------------------------------------------------------------------------- /packages/storage-swr/src/mutate/use-remove-files.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRemoveFilesFetcher, 3 | mutatePaths, 4 | } from '@supabase-cache-helpers/storage-core'; 5 | import type { FileObject, StorageError } from '@supabase/storage-js'; 6 | import { useCallback } from 'react'; 7 | import { type Key, useSWRConfig } from 'swr'; 8 | import useSWRMutation, { 9 | type SWRMutationResponse, 10 | type SWRMutationConfiguration, 11 | } from 'swr/mutation'; 12 | 13 | import { type StorageFileApi, decode, getBucketId } from '../lib'; 14 | import { useRandomKey } from './use-random-key'; 15 | 16 | /** 17 | * Hook for removing files from storage using SWR mutation 18 | * @param {StorageFileApi} fileApi - The Supabase Storage API 19 | * @param {SWRMutationConfiguration} [config] - The SWR mutation configuration 20 | * @returns {SWRMutationResponse} - The SWR mutation response object 21 | */ 22 | function useRemoveFiles( 23 | fileApi: StorageFileApi, 24 | config?: SWRMutationConfiguration< 25 | FileObject[], 26 | StorageError, 27 | string, 28 | string[] 29 | >, 30 | ): SWRMutationResponse { 31 | const key = useRandomKey(); 32 | const { cache, mutate } = useSWRConfig(); 33 | const fetcher = useCallback(createRemoveFilesFetcher(fileApi), [fileApi]); 34 | return useSWRMutation( 35 | key, 36 | async (_, { arg: paths }) => { 37 | const res = await fetcher(paths); 38 | await mutatePaths(getBucketId(fileApi), paths, { 39 | cacheKeys: Array.from(cache.keys()), 40 | decode, 41 | mutate, 42 | }); 43 | return res; 44 | }, 45 | config, 46 | ); 47 | } 48 | 49 | export { useRemoveFiles }; 50 | -------------------------------------------------------------------------------- /packages/storage-swr/src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-directory-urls'; 2 | export * from './use-directory'; 3 | export * from './use-file-url'; 4 | -------------------------------------------------------------------------------- /packages/storage-swr/src/query/use-directory.ts: -------------------------------------------------------------------------------- 1 | import { fetchDirectory } from '@supabase-cache-helpers/storage-core'; 2 | import type { FileObject, StorageError } from '@supabase/storage-js'; 3 | import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr'; 4 | 5 | import { type StorageFileApi, type StorageKeyInput, middleware } from '../lib'; 6 | 7 | /** 8 | * Convenience hook to fetch a directory from Supabase Storage using SWR. 9 | * 10 | * @param fileApi The StorageFileApi instance. 11 | * @param path The path to the directory. 12 | * @param config The SWR configuration. 13 | * @returns An SWRResponse containing an array of FileObjects 14 | */ 15 | function useDirectory( 16 | fileApi: StorageFileApi, 17 | path: string | null, 18 | config?: SWRConfiguration, 19 | ): SWRResponse { 20 | return useSWR( 21 | path ? [fileApi, path] : null, 22 | ([fileApi, path]: StorageKeyInput) => fetchDirectory(fileApi, path), 23 | { 24 | ...config, 25 | use: [...(config?.use ?? []), middleware], 26 | }, 27 | ); 28 | } 29 | 30 | export { useDirectory }; 31 | -------------------------------------------------------------------------------- /packages/storage-swr/src/query/use-file-url.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type StoragePrivacy, 3 | type URLFetcherConfig, 4 | createUrlFetcher, 5 | } from '@supabase-cache-helpers/storage-core'; 6 | import type { StorageError } from '@supabase/storage-js'; 7 | import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr'; 8 | 9 | import { type StorageFileApi, type StorageKeyInput, middleware } from '../lib'; 10 | 11 | /** 12 | * A hook to fetch the URL for a file in the Storage. 13 | * 14 | * @param fileApi - the file API instance from the Supabase client. 15 | * @param path - the path of the file to fetch the URL for. 16 | * @param mode - the privacy mode of the bucket the file is in. 17 | * @param config - the SWR configuration options and URL fetcher configuration. 18 | * @returns the SWR response for the URL of the file 19 | */ 20 | function useFileUrl( 21 | fileApi: StorageFileApi, 22 | path: string | null, 23 | mode: StoragePrivacy, 24 | config?: SWRConfiguration & 25 | URLFetcherConfig, 26 | ): SWRResponse { 27 | return useSWR( 28 | path ? [fileApi, path] : null, 29 | ([fileApi, path]: StorageKeyInput) => 30 | createUrlFetcher(mode, config)(fileApi, path), 31 | { 32 | ...config, 33 | use: [...(config?.use ?? []), middleware], 34 | }, 35 | ); 36 | } 37 | 38 | export { useFileUrl }; 39 | -------------------------------------------------------------------------------- /packages/storage-swr/tests/__fixtures__/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-swr/tests/__fixtures__/1.jpg -------------------------------------------------------------------------------- /packages/storage-swr/tests/__fixtures__/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-swr/tests/__fixtures__/2.jpg -------------------------------------------------------------------------------- /packages/storage-swr/tests/__fixtures__/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-swr/tests/__fixtures__/3.jpg -------------------------------------------------------------------------------- /packages/storage-swr/tests/__fixtures__/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/psteinroe/supabase-cache-helpers/156275f576e76f05c6986862f537465c2fea2f27/packages/storage-swr/tests/__fixtures__/4.jpg -------------------------------------------------------------------------------- /packages/storage-swr/tests/lib/decode.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { decode } from '../../src'; 3 | 4 | describe('decode', () => { 5 | it('should return null for invalid key', () => { 6 | expect(decode('some unrelated key')).toEqual(null); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/storage-swr/tests/lib/key.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { assertStorageKeyInput } from '../../src/lib'; 3 | 4 | describe('key', () => { 5 | describe('assertStorageKeyInput', () => { 6 | it('should throw for invalid key', () => { 7 | expect(() => assertStorageKeyInput('some unrelated key')).toThrow( 8 | 'Invalid key', 9 | ); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/storage-swr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@supabase-cache-helpers/tsconfig/react-library.json", 3 | "include": ["**/*.ts", "**/*.tsx"], 4 | "exclude": ["node_modules"] 5 | } 6 | -------------------------------------------------------------------------------- /packages/storage-swr/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | export const tsup: Options = { 4 | dts: true, 5 | entryPoints: ['src/index.ts'], 6 | external: ['react', /^@supabase\//], 7 | format: ['cjs', 'esm'], 8 | // inject: ['src/react-shim.js'], 9 | // ! .cjs/.mjs doesn't work with Angular's webpack4 config by default! 10 | legacyOutput: false, 11 | sourcemap: true, 12 | splitting: false, 13 | bundle: true, 14 | clean: true, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/storage-swr/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { enabled: true }, 6 | environment: 'happy-dom', 7 | coverage: { 8 | provider: 'istanbul', 9 | }, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Base", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "downlevelIteration": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "inlineSources": false, 12 | "isolatedModules": true, 13 | "moduleResolution": "node", 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "preserveWatchOutput": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "lib": ["esnext"], 20 | "module": "ESNext", 21 | "target": "es6" 22 | }, 23 | "ts-node": { 24 | "esm": true, 25 | "experimentalSpecifierResolution": "node" 26 | }, 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/node.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Node", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["ESNext"], 7 | "module": "ESNext", 8 | "target": "ESNext", 9 | "strict": true, 10 | "esModuleInterop": false, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowSyntheticDefaultImports": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@supabase-cache-helpers/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": ["base.json", "nextjs.json", "react-library.json"], 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/psteinroe/supabase-cache-helpers.git", 10 | "directory": "packages/tsconfig" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./web.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig/web.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Web", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "lib": ["esnext", "dom", "dom.iterable"], 7 | "module": "ESNext", 8 | "target": "es6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "docs" 3 | - "examples/*" 4 | - "packages/*" 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"], 4 | "packageRules": [ 5 | { 6 | "matchPackagePatterns": ["*"], 7 | "matchUpdateTypes": ["minor", "pin", "digest"], 8 | "groupName": "all minor updates", 9 | "groupSlug": "all-minor" 10 | }, 11 | { 12 | "matchDatasources": ["npm"], 13 | "stabilityDays": 3 14 | }, 15 | { 16 | "matchUpdateTypes": ["minor", "patch", "pin", "digest"], 17 | "automerge": true 18 | }, 19 | { 20 | "matchPackagePatterns": ["pnpm", "merge-anything", "node"], 21 | "enabled": false 22 | }, 23 | { 24 | "updateTypes": ["patch"], 25 | "enabled": false 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | -------------------------------------------------------------------------------- /supabase/migrations/20230912094327_add_serial_pk_table.sql: -------------------------------------------------------------------------------- 1 | create table public.serial_key_table ( 2 | id serial primary key, 3 | value text 4 | ); 5 | -------------------------------------------------------------------------------- /supabase/migrations/20231114093521_add_multi_pk_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "multi_pk" ( 2 | "id_1" integer, 3 | "id_2" integer, 4 | "name" text, 5 | PRIMARY KEY ("id_1", "id_2") 6 | ); 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /supabase/migrations/20240229212226_add_many_to_many_test_tables.sql: -------------------------------------------------------------------------------- 1 | create table "public"."address_book" ( 2 | "id" uuid not null default gen_random_uuid(), 3 | "created_at" timestamp with time zone not null default now(), 4 | "name" character varying 5 | ); 6 | 7 | 8 | create table "public"."address_book_contact" ( 9 | "contact" uuid not null, 10 | "address_book" uuid not null 11 | ); 12 | 13 | CREATE UNIQUE INDEX address_book_contact_pkey ON public.address_book_contact USING btree (contact, address_book); 14 | 15 | CREATE UNIQUE INDEX address_book_pkey ON public.address_book USING btree (id); 16 | 17 | alter table "public"."address_book" add constraint "address_book_pkey" PRIMARY KEY using index "address_book_pkey"; 18 | 19 | alter table "public"."address_book_contact" add constraint "address_book_contact_pkey" PRIMARY KEY using index "address_book_contact_pkey"; 20 | 21 | alter table "public"."address_book_contact" add constraint "address_book_contact_address_book_fkey" FOREIGN KEY (address_book) REFERENCES address_book(id) on delete cascade not valid; 22 | 23 | alter table "public"."address_book_contact" validate constraint "address_book_contact_address_book_fkey"; 24 | 25 | alter table "public"."address_book_contact" add constraint "address_book_contact_contact_fkey" FOREIGN KEY (contact) REFERENCES contact(id) on delete cascade not valid; 26 | 27 | alter table "public"."address_book_contact" validate constraint "address_book_contact_contact_fkey"; 28 | -------------------------------------------------------------------------------- /supabase/migrations/20240311151146_add_multi_fkeys.sql: -------------------------------------------------------------------------------- 1 | alter table public.contact_note add column created_by_contact_id uuid references public.contact on delete set null; 2 | alter table public.contact_note add column updated_by_contact_id uuid references public.contact on delete set null; 3 | -------------------------------------------------------------------------------- /supabase/migrations/20240311151147_allow_aggregates.sql: -------------------------------------------------------------------------------- 1 | alter role authenticator set pgrst.db_aggregates_enabled to 'true'; 2 | 3 | 4 | -------------------------------------------------------------------------------- /supabase/migrations/20240311151148_add_rpcs.sql: -------------------------------------------------------------------------------- 1 | create or replace function public.contacts_offset( 2 | v_limit integer default 50, 3 | v_offset integer default 0, 4 | v_username_filter text default null 5 | ) returns setof public.contact 6 | as $$ 7 | select * from public.contact 8 | where (v_username_filter is null or username ilike v_username_filter) 9 | order by username, id 10 | limit v_limit offset v_offset; 11 | $$ language sql stable set search_path = ''; 12 | 13 | create or replace function public.contacts_cursor( 14 | v_username_cursor text default null, 15 | v_id_cursor uuid default null, 16 | v_username_filter text default null, 17 | v_limit integer default 50 18 | ) returns setof public.contact 19 | as $$ 20 | select * from public.contact 21 | where 22 | (v_username_cursor is null or (username > v_username_cursor or (username = v_username_cursor and id > v_id_cursor))) and 23 | (v_username_filter is null or username ilike v_username_filter) 24 | order by username, id 25 | limit v_limit; 26 | $$ language sql stable set search_path = ''; 27 | 28 | 29 | create or replace function public.contacts_cursor_id_only( 30 | v_id_cursor uuid default null, 31 | v_username_filter text default null, 32 | v_limit integer default 50 33 | ) returns setof public.contact 34 | as $$ 35 | select * from public.contact 36 | where 37 | (v_id_cursor is null or (id < v_id_cursor)) and 38 | (v_username_filter is null or username ilike v_username_filter) 39 | order by id desc 40 | limit v_limit; 41 | $$ language sql stable set search_path = ''; 42 | 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "packages/storage-fetcher" 6 | }, 7 | { 8 | "path": "packages/postgrest-fetcher" 9 | }, 10 | { 11 | "path": "packages/postgrest-filter" 12 | }, 13 | { 14 | "path": "packages/postgrest-mutate" 15 | }, 16 | { 17 | "path": "packages/postgrest-parser" 18 | }, 19 | { 20 | "path": "packages/postgrest-shared" 21 | }, 22 | { 23 | "path": "packages/postgrest-swr" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "globalEnv": ["SUPABASE_URL", "SUPABASE_ANON_KEY"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**"] 8 | }, 9 | "test": { 10 | "dependsOn": ["^build"], 11 | "outputs": ["coverage/**"] 12 | }, 13 | "clean": { 14 | "outputs": [] 15 | }, 16 | "typecheck": { 17 | "dependsOn": ["^build"], 18 | "outputs": [] 19 | }, 20 | "dev": { 21 | "cache": false 22 | } 23 | } 24 | } 25 | --------------------------------------------------------------------------------