├── .npmignore ├── .gitignore ├── src ├── utils │ ├── generateSort.ts │ ├── generateSelect.ts │ └── generateFilter.ts └── index.ts ├── .changeset ├── config.json └── README.md ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── scorecard.yml ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | # build files 4 | dist -------------------------------------------------------------------------------- /src/utils/generateSort.ts: -------------------------------------------------------------------------------- 1 | import { CrudSorting } from "@refinedev/core"; 2 | 3 | export const generateSort = (sorters: CrudSorting): string[] => { 4 | return sorters.map(sorter => `${sorter.field} ${sorter.order}`); 5 | }; -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "hirenf14/refine-sanity" }], 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "esnext", 15 | "rootDir": "./src", 16 | "outDir": "./dist", 17 | }, 18 | "include": ["src"] 19 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/utils/generateSelect.ts: -------------------------------------------------------------------------------- 1 | import { MetaQuery } from "@refinedev/core"; 2 | 3 | 4 | export const generateSelect = (fields: MetaQuery["fields"]): string => { 5 | if(!fields) { 6 | return ''; 7 | } 8 | if(typeof fields === 'string') return fields; 9 | if(!Array.isArray(fields)) { 10 | console.log(fields); 11 | } 12 | const selected: string[] = fields.map(f => { 13 | if(typeof f === 'string') return f; 14 | const field = f as any; 15 | if(field.operation || field.fields) { 16 | // TODO: Doesn't support nested yet, have to add it. 17 | throw new Error("Sorry, nested fields are not implemented yet"); 18 | } 19 | return Object.keys(field).map((key: string) => `"${key}": ${generateSelect(field[key])}`).join(", "); 20 | }); 21 | return `{${selected.join(", ")}}`; 22 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | concurrency: ${{ github.workflow }}-${{ github.ref }} 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: "18.x" 16 | 17 | - name: Install Dependencies & Build 18 | run: | 19 | npm ci 20 | npm run prepublish 21 | 22 | - name: Create Release Pull Request 23 | uses: changesets/action@v1 24 | with: 25 | version: npx changeset version 26 | publish: npx changeset publish 27 | commit: "ci(changesets): version packages" 28 | title: "ci(changesets): version packages" 29 | env: 30 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "refine-sanity", 3 | "version": "1.1.1", 4 | "description": "Data provider for refine with sanity. refine is a React-based framework for building internal tools, rapidly. Sanity is headless CMS.", 5 | "type": "module", 6 | "author": { 7 | "name": "Hiren F", 8 | "email": "hirenf14@gmail.com", 9 | "url": "https://github.com/hirenf14" 10 | }, 11 | "types": "./dist/index.d.ts", 12 | "exports": "./dist/index.js", 13 | "homepage": "https://github.com/hirenf14/refine-sanity#readme", 14 | "private": false, 15 | "keywords": [ 16 | "refine", 17 | "refinedev", 18 | "sanity", 19 | "data provider", 20 | "react" 21 | ], 22 | "scripts": { 23 | "build": "tsc", 24 | "prepublish": "tsc" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/hirenf14/refine-sanity.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/hirenf14/refine-sanity/issues" 32 | }, 33 | "peerDependencies": { 34 | "@refinedev/core": "^4.28.2", 35 | "@sanity/client": "^6.4.12" 36 | }, 37 | "devDependencies": { 38 | "@changesets/changelog-github": "^0.5.0", 39 | "@changesets/cli": "^2.26.2", 40 | "typescript": "^5.2.2" 41 | }, 42 | "dependencies": { 43 | "groqd": "^0.15.9" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/generateFilter.ts: -------------------------------------------------------------------------------- 1 | import { CrudFilters, CrudOperators } from "@refinedev/core"; 2 | 3 | const mapOperator = (operator: CrudOperators) => { 4 | switch (operator) { 5 | case "eq": 6 | return "=="; 7 | case "ne": 8 | return "!="; 9 | case "lt": 10 | return "<"; 11 | case "gt": 12 | return ">"; 13 | case "lte": 14 | return "<="; 15 | case "gte": 16 | return ">="; 17 | case "in": 18 | case "nin": 19 | return "in"; 20 | case "contains": 21 | case "ncontains": 22 | return "match"; 23 | default: 24 | throw new Error(`Does't support Operator ${operator} yet.`); 25 | } 26 | } 27 | const negativeFilters = ["nin", "ncontains"]; 28 | export const generateFilter = (filters?: CrudFilters) => { 29 | const queryFilters = filters 30 | ?.map((filter) => { 31 | if (Array.isArray(filter?.value) && filter.value?.length === 0) { 32 | return undefined; 33 | } 34 | 35 | if ("field" in filter) { 36 | const { field, operator, value } = filter; 37 | const mappedOperator = mapOperator(operator); 38 | const isNegative = negativeFilters.includes(operator); 39 | const filterStr = `${field} ${mappedOperator} "${value}"`; 40 | 41 | return isNegative ? `!(${filterStr})` : filterStr; 42 | } 43 | return undefined; 44 | }) 45 | .filter((v) => v); 46 | 47 | return queryFilters?.join(" && "); 48 | }; 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # refine-sanity 2 | 3 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | [![NPM Version](https://img.shields.io/npm/v/refine-sanity.svg)](https://www.npmjs.com/package/refine-sanity) 5 | [![NPM Downloads](https://img.shields.io/npm/dt/refine-sanity.svg)](https://www.npmjs.com/package/refine-sanity) 6 | ![GitHub Repo stars](https://img.shields.io/github/stars/hirenf14/refine-sanity) 7 | ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/hirenf14/refine-sanity) 8 | 9 | 10 | ![npm peer dependency version (scoped)](https://img.shields.io/npm/dependency-version/refine-sanity/peer/%40refinedev%2Fcore) 11 | ![npm peer dependency version (scoped)](https://img.shields.io/npm/dependency-version/refine-sanity/peer/%40sanity%2Fclient) 12 | ![npm (prod) dependency version (scoped)](https://img.shields.io/npm/dependency-version/refine-sanity/groqd) 13 | ![npm type definitions](https://img.shields.io/npm/types/refine-sanity) 14 | 15 | 16 | 17 | `refine-sanity` is a data provider for [Refine](https://refine.dev/) that enables seamless integration with [Sanity](https://www.sanity.io/). It simplifies the management of your Sanity data within Refine-powered React applications. 18 | 19 | ## Installation 20 | 21 | Install `refine-sanity` via npm or yarn: 22 | 23 | ```bash 24 | npm install @sanity/client refine-sanity 25 | # or 26 | yarn add @sanity/client refine-sanity 27 | ``` 28 | 29 | ## Usage 30 | 31 | ```tsx 32 | import dataProvider from "refine-sanity"; 33 | import { createClient } from "@sanity/client"; 34 | 35 | const client = createClient({ 36 | token: "EDITOR_SANITY_ACCESS_TOKEN", 37 | projectId: "SANITY_PROJECT_ID", 38 | dataset: "SANITY_DATASET" 39 | }); 40 | 41 | const App = () => { 42 | return ( 43 | 47 | {/* ... */} 48 | 49 | ); 50 | }; 51 | 52 | ``` 53 | 54 | 55 | ## Documentation 56 | - For more detailed information and usage, refer to [the refine data provider documentation](https://refine.dev/docs/api-reference/core/providers/data-provider/). 57 | - [Refer to documentation for more info about refine](https://refine.dev/docs/) 58 | - [Step up to refine tutorials.](https://refine.dev/docs/tutorial/introduction/index/) -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '25 0 * * 5' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard. 69 | - name: "Upload to code-scanning" 70 | uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 71 | with: 72 | sarif_file: results.sarif 73 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { SanityClient } from "@sanity/client"; 2 | 3 | import { 4 | DataProvider, 5 | } from "@refinedev/core"; 6 | 7 | import { q } from 'groqd'; 8 | import { generateFilter } from "./utils/generateFilter"; 9 | import { generateSelect } from "./utils/generateSelect"; 10 | import { generateSort } from "./utils/generateSort"; 11 | 12 | 13 | export const dataProvider = (client: SanityClient): DataProvider => { 14 | return { 15 | async getList({ resource, pagination, sorters, filters, meta }: Parameters[0]) { 16 | const { 17 | current = 1, 18 | pageSize = 10, 19 | } = pagination ?? {}; 20 | const start = (current - 1) * pageSize; 21 | const end = start + pageSize - 1; 22 | let dataQuery: any = q("*").filterByType(resource); 23 | const filterStr = generateFilter(filters); 24 | if (filterStr) { 25 | dataQuery = dataQuery.filter(filterStr); // Apply filters if any result's achieved 26 | } 27 | const totalQuery = dataQuery.query; // Separate query to avoid sliced total 28 | if (sorters?.length) { 29 | dataQuery = dataQuery.order(...generateSort(sorters)); 30 | } 31 | dataQuery = dataQuery.slice(start, end); 32 | const paginatedQuery = q(`{ 33 | "data": ${dataQuery.query}${generateSelect(meta?.fields)}, 34 | "total": count(${totalQuery}._id) 35 | }`); 36 | const response = await client.fetch(paginatedQuery.query); 37 | return { 38 | data: response.data, 39 | total: response.total 40 | }; 41 | }, 42 | 43 | async getOne({ resource, id, meta }: Parameters[0]) { 44 | const { query } = q("*").filterByType(resource).filter(`_id == "${id}"`).slice(0); 45 | const dataQuery = q(`{ 46 | "data": ${query}${generateSelect(meta?.fields)} 47 | }`); 48 | // const data = await client.getDocument(id as string); 49 | const response = await client.fetch(dataQuery.query); 50 | return { 51 | data: response.data 52 | } 53 | }, 54 | 55 | async create({ resource, variables }: Parameters[0]) { 56 | const response = await client.create( 57 | { 58 | _type: resource, 59 | ...(variables as any) 60 | }, 61 | { 62 | autoGenerateArrayKeys: true 63 | }); 64 | return { 65 | data: { 66 | ...response, 67 | id: response._id 68 | } as any 69 | } 70 | }, 71 | 72 | async update({ id, variables }: Parameters[0]) { 73 | const response = await client 74 | .patch(id as string) 75 | .set({ 76 | _id: id, 77 | ...(variables as any), 78 | }) 79 | .commit({ autoGenerateArrayKeys: true }); 80 | return { 81 | data: { 82 | ...response, 83 | id: response._id, 84 | } as any, 85 | } 86 | }, 87 | 88 | async deleteOne({ id }: Parameters[0]) { 89 | const response = await client.delete(id as string); 90 | return { 91 | data: response as any 92 | } 93 | }, 94 | 95 | async deleteMany({ ids, resource, meta }) { 96 | const idsStr = ids.map((id) => `"${id}"`).join(", "); 97 | const { query } = q( 98 | `*[_id in [${idsStr}]]${generateSelect(meta?.fields)}`, 99 | ).filterByType(resource); 100 | 101 | const response = await client.delete({ query }); 102 | 103 | return { 104 | data: response as any, 105 | } 106 | }, 107 | 108 | getApiUrl(): string { 109 | throw Error("Not implemented on refine-sanity data provider."); 110 | }, 111 | }; 112 | } 113 | export default dataProvider; --------------------------------------------------------------------------------