├── .changeset └── config.json ├── .eslintrc ├── .github └── workflows │ ├── release.yml │ ├── static-analysis.yml │ └── unit-test.yml ├── .gitignore ├── .npmrc ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── example │ └── src │ │ └── sanity-client-example.ts └── playground-example │ ├── .env.example │ ├── .gitignore │ ├── CHANGELOG.md │ ├── package.json │ ├── sanity.cli.ts │ └── sanity.config.ts ├── package.json ├── packages ├── groqd-legacy │ ├── CHANGELOG.md │ ├── LEGACY-WORKFLOW.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── addDeprecationMessage.ts │ │ ├── baseQuery.ts │ │ ├── builder.test.ts │ │ ├── builder.ts │ │ ├── contentBlock.test.ts │ │ ├── contentBlock.ts │ │ ├── grab.ts │ │ ├── index.ts │ │ ├── makeSafeQueryRunner.test.ts │ │ ├── makeSafeQueryRunner.ts │ │ ├── nullToUndefined.test.ts │ │ ├── nullToUndefined.ts │ │ ├── pipe.test.ts │ │ ├── sanityImage.test.ts │ │ ├── sanityImage.ts │ │ ├── schemas.test.ts │ │ ├── schemas.ts │ │ ├── select.test.ts │ │ ├── select.ts │ │ ├── selectionUtils.ts │ │ ├── typeGuards.ts │ │ ├── types.test.ts │ │ └── types.ts │ ├── test-utils │ │ ├── pokemon.ts │ │ ├── runQuery.ts │ │ ├── sampleContentBlocks.ts │ │ └── users.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts ├── groqd-playground │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── PlaygroundWrapper.tsx │ │ ├── components │ │ │ ├── JSONExplorer.styled.tsx │ │ │ ├── JSONExplorer.tsx │ │ │ ├── Playground.styled.tsx │ │ │ ├── Playground.tsx │ │ │ └── ShareUrlField.tsx │ │ ├── consts.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── util │ │ │ ├── copyDataToClipboard.ts │ │ │ ├── messaging.ts │ │ │ └── useDatasets.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── groqd │ ├── .eslintrc │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── as.ts │ │ │ ├── asCombined.test.ts │ │ │ ├── asCombined.ts │ │ │ ├── deref.test.ts │ │ │ ├── deref.ts │ │ │ ├── filterBy.test.ts │ │ │ ├── filterBy.ts │ │ │ ├── filterByType.test.ts │ │ │ ├── filterByType.ts │ │ │ ├── grab-deprecated.test.ts │ │ │ ├── grab-deprecated.ts │ │ │ ├── index.ts │ │ │ ├── notNull.test.ts │ │ │ ├── notNull.ts │ │ │ ├── nullable.test.ts │ │ │ ├── nullable.ts │ │ │ ├── order.test.ts │ │ │ ├── order.ts │ │ │ ├── project.test.ts │ │ │ ├── project.ts │ │ │ ├── projectField.test.ts │ │ │ ├── projectField.ts │ │ │ ├── raw.test.ts │ │ │ ├── raw.ts │ │ │ ├── root │ │ │ │ ├── coalesce-types.ts │ │ │ │ ├── coalesce.test.ts │ │ │ │ ├── coalesce.ts │ │ │ │ ├── count.test.ts │ │ │ │ ├── count.ts │ │ │ │ ├── fragment.test.ts │ │ │ │ ├── fragment.ts │ │ │ │ ├── parameters.test.ts │ │ │ │ ├── parameters.ts │ │ │ │ ├── star.test.ts │ │ │ │ ├── star.ts │ │ │ │ └── value.ts │ │ │ ├── score.test.ts │ │ │ ├── score.ts │ │ │ ├── slice.test.ts │ │ │ ├── slice.ts │ │ │ ├── subquery │ │ │ │ ├── conditional-types.ts │ │ │ │ ├── conditional.test.ts │ │ │ │ ├── conditional.ts │ │ │ │ ├── conditionalByType.test.ts │ │ │ │ ├── conditionalByType.ts │ │ │ │ ├── select-types.ts │ │ │ │ ├── select.test.ts │ │ │ │ ├── select.ts │ │ │ │ ├── selectByType.test.ts │ │ │ │ └── selectByType.ts │ │ │ ├── validate-utils.ts │ │ │ ├── validate.test.ts │ │ │ └── validate.ts │ │ ├── createGroqBuilder.ts │ │ ├── createGroqBuilderWithZod.ts │ │ ├── groq-builder │ │ │ ├── groq-builder-types.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── makeSafeQueryRunner.test.ts │ │ ├── makeSafeQueryRunner.ts │ │ ├── tests │ │ │ ├── any-tests.test.ts │ │ │ ├── getSubquery.ts │ │ │ ├── legacy-query.test.ts │ │ │ ├── mocks │ │ │ │ ├── executeQuery.ts │ │ │ │ └── nextjs-sanity-fe-mocks.ts │ │ │ ├── schemas │ │ │ │ ├── nextjs-sanity-fe.sanity-typegen.ts │ │ │ │ └── nextjs-sanity-fe.ts │ │ │ └── utils.ts │ │ ├── types │ │ │ ├── compatible-types.test.ts │ │ │ ├── compatible-types.ts │ │ │ ├── deep-required.ts │ │ │ ├── document-types.ts │ │ │ ├── fragment-types.ts │ │ │ ├── groq-expressions.test.ts │ │ │ ├── groq-expressions.ts │ │ │ ├── invalid-query-error.ts │ │ │ ├── parameter-types.ts │ │ │ ├── parser-types.ts │ │ │ ├── projection-paths.test.ts │ │ │ ├── projection-paths.ts │ │ │ ├── projection-types.test.ts │ │ │ ├── projection-types.ts │ │ │ ├── query-config.test.ts │ │ │ ├── query-config.ts │ │ │ ├── ref-types.ts │ │ │ ├── result-types.test.ts │ │ │ ├── result-types.ts │ │ │ ├── type-mismatch-error.test.ts │ │ │ ├── type-mismatch-error.ts │ │ │ ├── union-to-intersection.test.ts │ │ │ ├── union-to-intersection.ts │ │ │ ├── utils.test.ts │ │ │ ├── utils.ts │ │ │ └── zod-like.ts │ │ └── validation │ │ │ ├── simple-validation.ts │ │ │ ├── validation-errors.ts │ │ │ ├── zod.test.ts │ │ │ └── zod.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts └── playground-editor │ ├── .babelrc │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── public │ └── index.html │ ├── scripts │ └── gather-types.js │ ├── src │ ├── App.tsx │ ├── index.tsx │ └── messaging.ts │ ├── tsconfig.json │ └── webpack.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── shared └── util │ ├── formatErrorPath.ts │ ├── jsonExplorerUtils.ts │ ├── makeDarkModeHook.ts │ └── twoslashInlays.ts ├── tsconfig.json ├── tsconfig.shared.json └── website ├── .gitignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── docs ├── API │ ├── _category_.json │ ├── conditionals.md │ ├── filters.md │ ├── fragments.md │ ├── functions.md │ ├── overview.md │ ├── parameters.md │ ├── projections.md │ └── validation.md ├── configuration.md ├── groqd-playground.mdx ├── img │ └── groqd-playground-sample.png ├── installation.mdx ├── legacy │ ├── _category_.json │ ├── faq.mdx │ ├── groqd-playground.mdx │ ├── img │ │ └── groqd-playground-sample.png │ ├── installation.mdx │ ├── introduction.md │ ├── query-building.md │ ├── schema-types.md │ ├── utility-methods.md │ └── utility-types.md ├── migration.md └── overview.md ├── docusaurus.config.ts ├── package.json ├── scripts ├── datasets │ ├── pokemon.js │ ├── todo-list-draft.js │ └── todo-list.js ├── gather-types.js └── generate-dataset-presets.js ├── sidebars.ts ├── src ├── arcade │ ├── .gitignore │ ├── Arcade.tsx │ ├── ArcadeActionList.tsx │ ├── ArcadeDatasetEditor.tsx │ ├── ArcadeEditor.tsx │ ├── ArcadeExampleSelector.tsx │ ├── ArcadeHeader.tsx │ ├── ArcadeLoadingIndicator.tsx │ ├── ArcadeQueryDisplay.tsx │ ├── ArcadeResponseView.tsx │ ├── ArcadeSection.tsx │ ├── JSONExplorer.tsx │ ├── abacus.ts │ ├── consts.ts │ ├── editorShortcuts.ts │ ├── eventEmitters.ts │ ├── examples.ts │ ├── models.ts │ ├── playground │ │ ├── README.md │ │ ├── pokemon │ │ │ ├── groqd-client.ts │ │ │ └── pokemon.sanity.types.ts │ │ ├── run-query.ts │ │ ├── todo-list │ │ │ ├── groqd-client.ts │ │ │ └── todo-list.sanity.types.ts │ │ └── tsconfig.json │ ├── share.ts │ ├── state.ts │ └── useIsDarkMode.ts ├── components │ └── landing │ │ ├── divider.tsx │ │ ├── landing-banner.tsx │ │ ├── landing-featured-projects.tsx │ │ ├── landing-features.tsx │ │ ├── landing-hero.tsx │ │ ├── landing-images.tsx │ │ └── nf-link-button.tsx ├── css │ └── custom.css ├── datasets.json ├── metadata.ts ├── pages │ ├── .gitkeep │ ├── arcade.tsx │ └── index.tsx └── react-app-env.d.ts ├── static ├── .nojekyll ├── font │ ├── InterBold.woff2 │ ├── InterMedium.woff2 │ └── InterRegular.woff2 ├── img │ ├── background-banner.png │ ├── favicon.ico │ ├── feature-1.png │ ├── feature-2.png │ ├── feature-3.png │ ├── formidable-icon.svg │ ├── github.svg │ ├── groqd-logo.png │ ├── groqd-social.png │ ├── nearform-icon-white.svg │ ├── nearform-icon.svg │ ├── nearform-logo-white.svg │ └── nearform-logo.svg └── open-graph.png ├── tailwind.config.ts └── tsconfig.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@svitejs/changesets-changelog-github-compact", 5 | { 6 | "repo": "FormidableLabs/groqd" 7 | } 8 | ], 9 | "access": "public", 10 | "baseBranch": "main" 11 | } 12 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint", 5 | "prettier" 6 | ], 7 | "extends": [ 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 12, 13 | "sourceType": "module" 14 | }, 15 | "env": { 16 | "node": true, 17 | "browser": true 18 | }, 19 | "rules": { 20 | "@typescript-eslint/no-unused-vars": "error", 21 | "@typescript-eslint/ban-types": "warn", 22 | "@typescript-eslint/no-var-requires": "off" 23 | }, 24 | "ignorePatterns": [ 25 | "**/dist/**/*", 26 | "**/build/**/*", 27 | "**/node_modules/**/*", 28 | "**/public/**/*", 29 | "**/.docusaurus/**/*", 30 | "**/*.d.ts" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: groqd Release Workflow 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | id-token: write 14 | issues: write 15 | repository-projects: write 16 | deployments: write 17 | packages: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: pnpm/action-setup@v3 23 | with: 24 | version: 9 25 | - uses: actions/setup-node@v4 26 | with: 27 | cache: "pnpm" 28 | node-version: 18 29 | - name: Install dependencies 30 | run: pnpm install --frozen-lockfile 31 | 32 | - name: Build packages 33 | run: pnpm run build:packages 34 | - name: PR or Publish 35 | id: changesets 36 | uses: changesets/action@v1 37 | with: 38 | version: pnpm run version 39 | publish: pnpm run changeset publish 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/static-analysis.yml: -------------------------------------------------------------------------------- 1 | name: Static Analysis 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | static-analysis: 12 | name: "Lint and Type-check" 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v3 17 | with: 18 | version: 9 19 | - uses: actions/setup-node@v4 20 | with: 21 | cache: "pnpm" 22 | node-version: 18 23 | - name: Install dependencies 24 | run: pnpm install --frozen-lockfile 25 | 26 | - name: Build library 27 | run: pnpm run build:lib 28 | - name: Type Check 29 | run: pnpm run typecheck 30 | - name: Lint 31 | run: pnpm run lint 32 | -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | unit-test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: pnpm/action-setup@v3 16 | with: 17 | version: 9 18 | - uses: actions/setup-node@v4 19 | with: 20 | cache: "pnpm" 21 | node-version: 18 22 | - name: Install dependencies 23 | run: pnpm install --frozen-lockfile 24 | 25 | - name: Unit Test 26 | run: pnpm run test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.tgz 4 | .idea 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | strict-peer-dependencies=false 2 | prefer-workspace-packages=true 3 | auto-install-peers=true 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2024 Formidable Labs, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GROQD](https://oss.nearform.com/api/banner?badge=groqd&bg=c99f46)](https://commerce.nearform.com/open-source/groqd) 2 | 3 | **[Check out the official documentation.](https://commerce.nearform.com/open-source/groqd)** 4 | 5 | `groqd` is a schema-unaware, runtime-safe query builder for [GROQ](https://www.sanity.io/docs/groq). **The goal of `groqd` is to give you (most of) the flexibility of GROQ, with the runtime/type safety of [Zod](https://github.com/colinhacks/zod) and TypeScript.** 6 | 7 | `groqd` works by accepting a series of GROQ operations, and generating a query to be used by GROQ and a Zod schema to be used for parsing the associated GROQ response. 8 | 9 | An illustrative example: 10 | 11 | ```ts 12 | import { q } from "groqd"; 13 | 14 | // Get all of the Pokemon types, and the Pokemon associated to each type. 15 | const { query, schema } = q("*") 16 | .filter("_type == 'poketype'") 17 | .grab({ 18 | name: q.string(), 19 | pokemons: q("*") 20 | .filter("_type == 'pokemon' && references(^._id)") 21 | .grab({ name: q.string() }), 22 | }); 23 | 24 | // Use the schema and the query as you see fit, for example: 25 | const response = schema.parse(await sanityClient.fetch(query)); 26 | 27 | // At this point, response has a type of: 28 | // { name: string, pokemons: { name: string }[] }[] 29 | // 👆👆 30 | ``` 31 | 32 | ## Support 33 | 34 | Have a question about Groqd? Submit an issue in this repository using the 35 | ["Question" template](https://github.com/FormidableLabs/groqd/issues/new?template=question.md). 36 | 37 | Notice something inaccurate or confusing? Feel free to [open an issue](https://github.com/FormidableLabs/groqd/issues/new/choose) or [make a pull request](https://github.com/FormidableLabs/groqd/pulls) to help improve the documentation for everyone! 38 | 39 | The source for our docs site lives in this repo in the [`docs`](https://github.com/FormidableLabs/groqd/blob/main/website/docs) folder. 40 | 41 | ## Contributing 42 | 43 | Please see our [contributing guide](CONTRIBUTING.md). 44 | 45 | ## Maintenance Status 46 | 47 | **Active:** Nearform is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 48 | -------------------------------------------------------------------------------- /examples/example/src/sanity-client-example.ts: -------------------------------------------------------------------------------- 1 | import sanityClient from "@sanity/client"; 2 | import { makeSafeQueryRunner } from "groqd/src"; 3 | 4 | const client = sanityClient({ 5 | projectId: "your-project-id", 6 | apiVersion: "2021-03-25", 7 | }); 8 | 9 | export const runQuery = makeSafeQueryRunner( 10 | (query: string, params: Record = {}) => 11 | client.fetch(query, params) 12 | ); 13 | -------------------------------------------------------------------------------- /examples/playground-example/.env.example: -------------------------------------------------------------------------------- 1 | SANITY_STUDIO_PROJECT_ID= 2 | SANITY_STUDIO_GROQD_PLAYGROUND_ENV=development 3 | -------------------------------------------------------------------------------- /examples/playground-example/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .sanity 3 | -------------------------------------------------------------------------------- /examples/playground-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # playground-example 2 | 3 | ## 0.0.2 4 | 5 | ### Patch Changes 6 | 7 | - bump Sanity to 3.15.0. bump vitest to 1.3.1 ([#278](https://github.com/FormidableLabs/groqd/pull/278)) 8 | 9 | - security update: bump sanity to 3.14.0 ([#277](https://github.com/FormidableLabs/groqd/pull/277)) 10 | 11 | - Updated dependencies [[`96804f1`](https://github.com/FormidableLabs/groqd/commit/96804f17b04c445852dd209f099a4ce3b4e3360e), [`38f3f23`](https://github.com/FormidableLabs/groqd/commit/38f3f233398cf360a4be2f76ec908857946be64c), [`3fa5b46`](https://github.com/FormidableLabs/groqd/commit/3fa5b46209be3b2db5399e994b5674514e2077ce)]: 12 | - groqd-playground@0.0.19 13 | -------------------------------------------------------------------------------- /examples/playground-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-example", 3 | "version": "0.0.2", 4 | "private": true, 5 | "description": "sample sanity project to test out the groqd-playground package", 6 | "scripts": { 7 | "dev": "sanity dev" 8 | }, 9 | "dependencies": { 10 | "@sanity/vision": "^3.0.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "react-is": "^18.2.0", 14 | "sanity": "^3.15.0", 15 | "styled-components": "^5.2.0", 16 | "groqd-playground": "*" 17 | }, 18 | "devDependencies": { 19 | "@sanity/eslint-config-studio": "^2.0.1", 20 | "@types/react": "^18.0.25", 21 | "@types/styled-components": "^5.1.26", 22 | "eslint": "^8.6.0", 23 | "prettier": "^2.8.7", 24 | "typescript": "^4.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/playground-example/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import { defineCliConfig } from "sanity/cli"; 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: process.env.SANITY_STUDIO_PROJECT_ID || "", 6 | dataset: process.env.SANITY_STUDIO_DATASET || "production", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/playground-example/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "sanity"; 2 | import { deskTool } from "sanity/desk"; 3 | import { visionTool } from "@sanity/vision"; 4 | import { groqdPlaygroundTool } from "groqd-playground"; 5 | 6 | export default defineConfig({ 7 | name: "default", 8 | title: "formidable.com", 9 | 10 | projectId: process.env.SANITY_STUDIO_PROJECT_ID || "", 11 | dataset: process.env.SANITY_STUDIO_DATASET || "production", 12 | 13 | plugins: [deskTool(), visionTool(), groqdPlaygroundTool({})], 14 | 15 | schema: { 16 | types: [], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groqd-workspace", 3 | "private": "true", 4 | "scripts": { 5 | "lint": "eslint shared packages website --quiet", 6 | "test": "pnpm run -r test", 7 | "test:watch": "pnpm run -r test:watch", 8 | "typecheck:shared": "tsc --project tsconfig.shared.json --noEmit", 9 | "typecheck": "pnpm run typecheck:shared && pnpm run -r typecheck", 10 | "check:ci": "pnpm run typecheck && pnpm run lint && pnpm run test", 11 | "build:lib": "pnpm run --filter groqd --filter groqd-legacy build", 12 | "build:packages": "pnpm run --filter groqd --filter groqd-legacy --filter groqd-playground build", 13 | "build": "pnpm run -r build", 14 | "changeset": "changeset", 15 | "version": "pnpm changeset version && pnpm install --no-frozen-lockfile", 16 | "dev:docs": "pnpm run --filter website dev", 17 | "build:docs": "pnpm run build:lib && pnpm run --filter website build", 18 | "build:docs:vercel": "pnpm run build:lib && pnpm run --filter website build:vercel", 19 | "dev:playground": "concurrently \"pnpm run --filter groqd-legacy dev\" \"pnpm run --filter groqd-playground-editor dev\" \"pnpm run --filter groqd-playground dev\" \"pnpm run --filter playground-example dev\"", 20 | "dev:editor": "pnpm run --filter playground-editor dev" 21 | }, 22 | "devDependencies": { 23 | "@changesets/cli": "^2.26.0", 24 | "@svitejs/changesets-changelog-github-compact": "^1.1.0", 25 | "@typescript-eslint/eslint-plugin": "^5.52.0", 26 | "@typescript-eslint/parser": "^5.52.0", 27 | "concurrently": "^8.0.1", 28 | "eslint": "^8.34.0", 29 | "eslint-config-prettier": "^8.6.0", 30 | "eslint-plugin-prettier": "^4.2.1", 31 | "prettier": "^2.8.4", 32 | "typescript": "^4.9.5", 33 | "monaco-editor": "^0.50.0" 34 | }, 35 | "engines": { 36 | "node": ">=18.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/groqd-legacy/LEGACY-WORKFLOW.md: -------------------------------------------------------------------------------- 1 | # `groqd v0.x` Workflow 2 | 3 | This `groqd-legacy` package is the source for the `v0.x` releases of `groqd`. 4 | 5 | If we need to publish a release for `groqd @ 0.x`, please use the following workflow: 6 | 7 | # TL;DR: 8 | 9 | - Merge a normal PR into `main` 10 | - Merge `main` into `groqd-legacy-publish` 11 | - Merge the auto-generated PR (**Version Packages (legacy)**) 12 | 13 | # Step 1: a normal PR 14 | 15 | Create a normal PR: 16 | 17 | - Branch from `main` 18 | - Make changes to `groqd-legacy` code 19 | - Create PR into `main` 20 | - Add a Changeset file to this PR with a description of the change 21 | - Run `npm run changeset` 22 | - or when the PR is created, click the automated "create changeset" comment 23 | - Merge PR into `main` 24 | 25 | # Step 2: merge into `groqd-legacy-publish` branch 26 | 27 | - Merge `main` into the `groqd-legacy-publish` branch. 28 | - This will create an automated PR called **Version Packages (legacy)**. 29 | - ✨ **IMPORTANT STEP** In this PR, the version will be bumped with a "prerelease" tag, like `v0.15.15-legacy.0` 30 | - Manually edit `package.json`, and remove the `-legacy.0` part. Then commit it to the PR. 31 | - This ensures we don't release as a "prerelease". 32 | - Approve and merge this PR into `groqd-legacy-publish`. 33 | -------------------------------------------------------------------------------- /packages/groqd-legacy/README.md: -------------------------------------------------------------------------------- 1 | [![GROQD](https://oss.nearform.com/api/banner?badge=groqd&bg=c99f46)](https://commerce.nearform.com/open-source/groqd) 2 | 3 | **[Check out the official documentation.](https://commerce.nearform.com/open-source/groqd)** 4 | 5 | `groqd` is a schema-unaware, runtime-safe query builder for [GROQ](https://www.sanity.io/docs/groq). **The goal of `groqd` is to give you (most of) the flexibility of GROQ, with the runtime/type safety of [Zod](https://github.com/colinhacks/zod) and TypeScript.** 6 | 7 | `groqd` works by accepting a series of GROQ operations, and generating a query to be used by GROQ and a Zod schema to be used for parsing the associated GROQ response. 8 | 9 | An illustrative example: 10 | 11 | ```ts 12 | import { q } from "groqd"; 13 | 14 | // Get all of the Pokemon types, and the Pokemon associated to each type. 15 | const { query, schema } = q("*") 16 | .filter("_type == 'poketype'") 17 | .grab({ 18 | name: q.string(), 19 | pokemons: q("*") 20 | .filter("_type == 'pokemon' && references(^._id)") 21 | .grab({ name: q.string() }), 22 | }); 23 | 24 | // Use the schema and the query as you see fit, for example: 25 | const response = schema.parse(await sanityClient.fetch(query)); 26 | 27 | // At this point, response has a type of: 28 | // { name: string, pokemons: { name: string }[] }[] 29 | // 👆👆 30 | ``` 31 | 32 | ## Support 33 | 34 | Have a question about Groqd? Submit an issue in this repository using the 35 | ["Question" template](https://github.com/FormidableLabs/groqd/issues/new?template=question.md). 36 | 37 | Notice something inaccurate or confusing? Feel free to [open an issue](https://github.com/FormidableLabs/groqd/issues/new/choose) or [make a pull request](https://github.com/FormidableLabs/groqd/pulls) to help improve the documentation for everyone! 38 | 39 | The source for our docs site lives in this repo in the [`docs`](https://github.com/FormidableLabs/groqd/blob/main/website/docs) folder. 40 | 41 | ## Contributing 42 | 43 | Please see our [contributing guide](CONTRIBUTING.md). 44 | 45 | ## Maintenance Status 46 | 47 | **Active:** Nearform is actively working on this project, and we expect to continue for work for the foreseeable future. Bug reports, feature requests and pull requests are welcome. 48 | -------------------------------------------------------------------------------- /packages/groqd-legacy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groqd-legacy", 3 | "private": true, 4 | "license": "MIT", 5 | "version": "0.15.13", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/FormidableLabs/groqd" 13 | }, 14 | "homepage": "https://github.com/formidablelabs/groqd", 15 | "keywords": [ 16 | "sanity", 17 | "groq", 18 | "query", 19 | "typescript" 20 | ], 21 | "main": "dist/index.js", 22 | "module": "dist/index.mjs", 23 | "types": "dist/index.d.ts", 24 | "exports": { 25 | ".": [ 26 | { 27 | "import": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "default": "./dist/index.js" 30 | }, 31 | "./dist/index.js" 32 | ], 33 | "./package.json": "./package.json" 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "scripts": { 39 | "test:watch": "vitest", 40 | "test": "vitest run", 41 | "typecheck": "tsc --noEmit", 42 | "build": "tsup", 43 | "build:watch": "tsup --watch", 44 | "dev": "pnpm run build:watch" 45 | }, 46 | "devDependencies": { 47 | "@sanity/client": "^6.24.1", 48 | "groq-js": "^1.1.1", 49 | "tiny-invariant": "^1.3.1", 50 | "tsup": "^6.3.0", 51 | "typescript": "^5.7.2", 52 | "vitest": "^2.1.9" 53 | }, 54 | "dependencies": { 55 | "zod": "3.22.4" 56 | }, 57 | "engines": { 58 | "node": ">= 14" 59 | }, 60 | "publishConfig": { 61 | "provenance": true 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/addDeprecationMessage.ts: -------------------------------------------------------------------------------- 1 | export function addDeprecationMessage any>( 2 | fn: Fn, 3 | message: string 4 | ): Fn { 5 | return function (...args: any[]) { 6 | console.warn(message); 7 | return fn(...args); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/baseQuery.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export type Query = string; 4 | export type Payload = { schema: T; query: Query }; 5 | 6 | export class BaseQuery { 7 | query: string; 8 | schema: T; 9 | 10 | constructor({ query, schema }: Payload) { 11 | this.query = query; 12 | this.schema = schema; 13 | } 14 | 15 | public value(): Payload { 16 | return { schema: this.schema, query: this.query }; 17 | } 18 | 19 | nullable() { 20 | return new NullableBaseQuery({ 21 | query: this.query, 22 | schema: this.schema.nullable(), 23 | }); 24 | } 25 | } 26 | 27 | export class NullableBaseQuery extends BaseQuery< 28 | z.ZodNullable 29 | > { 30 | constructor({ schema, query }: Payload) { 31 | super({ schema: schema.nullable(), query }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/contentBlock.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | /** 4 | * Content block schema for standard content blocks. 5 | */ 6 | export function contentBlock(): ReturnType< 7 | typeof makeContentBlockQuery 8 | >; 9 | export function contentBlock(args: { 10 | markDefs: T; 11 | }): ReturnType>; 12 | export function contentBlock({ markDefs }: { markDefs?: z.ZodType } = {}) { 13 | return makeContentBlockQuery(markDefs || baseMarkdefsType); 14 | } 15 | 16 | export function contentBlocks(): z.ZodArray< 17 | ReturnType> 18 | >; 19 | export function contentBlocks(args: { 20 | markDefs: T; 21 | }): z.ZodArray>>; 22 | export function contentBlocks({ markDefs }: { markDefs?: z.ZodType } = {}) { 23 | return z.array(makeContentBlockQuery(markDefs || baseMarkdefsType)); 24 | } 25 | 26 | function makeContentBlockQuery(markDefs: T) { 27 | return z.object({ 28 | _type: z.string(), 29 | _key: z.string().optional(), 30 | children: z.array( 31 | z.object({ 32 | _key: z.string(), 33 | _type: z.string(), 34 | text: z.string(), 35 | marks: z.array(z.string()).optional().default([]), 36 | }) 37 | ), 38 | markDefs: z.array(markDefs).optional(), 39 | style: z.string().optional(), 40 | listItem: z.string().optional(), 41 | level: z.number().optional(), 42 | }); 43 | } 44 | 45 | const baseMarkdefsType = z 46 | .object({ 47 | _type: z.string(), 48 | _key: z.string(), 49 | }) 50 | .catchall(z.unknown()); 51 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/grab.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { ValueOf, FromSelection, Selection } from "./types"; 3 | import { ArrayQuery, EntityQuery } from "./builder"; 4 | import { 5 | getProjectionEntriesFromSelection, 6 | getSchemaFromSelection, 7 | } from "./selectionUtils"; 8 | 9 | export const grab = < 10 | T extends z.ZodTypeAny | z.ZodArray, 11 | S extends Selection, 12 | CondSelections extends Record | undefined 13 | >( 14 | query: string, 15 | schema: T, 16 | selection: S, 17 | conditionalSelections?: CondSelections 18 | ) => { 19 | type AllSelection = undefined extends CondSelections 20 | ? FromSelection 21 | : z.ZodUnion< 22 | [ 23 | ValueOf<{ 24 | [K in keyof CondSelections]: FromSelection; 25 | }>, 26 | FromSelection 27 | ] 28 | >; 29 | 30 | type NewType = T extends z.ZodArray 31 | ? ArrayQuery 32 | : EntityQuery; 33 | 34 | const projections = getProjectionEntriesFromSelection(selection); 35 | if (conditionalSelections) { 36 | const condProjections = Object.entries(conditionalSelections).reduce< 37 | string[] 38 | >((acc, [key, val]) => { 39 | acc.push( 40 | `${key} => { ${getProjectionEntriesFromSelection(val).join(", ")} }` 41 | ); 42 | return acc; 43 | }, []); 44 | 45 | projections.push(`...select(${condProjections.join(", ")})`); 46 | } 47 | 48 | const newSchema = (() => { 49 | // Split base and conditional fields 50 | const conditionalFields = Object.values( 51 | conditionalSelections || {} 52 | ) as Selection[]; 53 | 54 | const baseSchema = getSchemaFromSelection(selection); 55 | const conditionalFieldSchemas = conditionalFields.map((field) => 56 | baseSchema.merge(getSchemaFromSelection(field)) 57 | ); 58 | 59 | const unionEls = [...conditionalFieldSchemas, baseSchema]; 60 | const s = 61 | unionEls.length > 1 62 | ? z.union([unionEls[0], unionEls[1], ...unionEls.slice(2)]) 63 | : unionEls[0]; 64 | 65 | return schema instanceof z.ZodArray ? z.array(s) : s; 66 | })(); 67 | 68 | const res = (newSchema instanceof z.ZodArray 69 | ? new ArrayQuery({ 70 | query: query + `{${projections.join(", ")}}`, 71 | schema: newSchema, 72 | }) 73 | : new EntityQuery({ 74 | query: query + `{${projections.join(", ")}}`, 75 | schema: newSchema, 76 | })) as unknown as NewType; 77 | 78 | return res; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { UnknownArrayQuery, UnknownQuery } from "./builder"; 3 | import { sanityImage } from "./sanityImage"; 4 | import { schemas } from "./schemas"; 5 | import { select } from "./select"; 6 | import { addDeprecationMessage } from "./addDeprecationMessage"; 7 | 8 | export type { InferType, TypeFromSelection, Selection } from "./types"; 9 | export { makeSafeQueryRunner, GroqdParseError } from "./makeSafeQueryRunner"; 10 | export { nullToUndefined } from "./nullToUndefined"; 11 | export { BaseQuery } from "./baseQuery"; 12 | 13 | export function pipe(filter: string): UnknownQuery; 14 | export function pipe( 15 | filter: string, 16 | opts: { isArray: IsArray } 17 | ): IsArray extends true ? UnknownArrayQuery : UnknownQuery; 18 | export function pipe( 19 | filter: string, 20 | { isArray = false }: { isArray?: boolean } = {} 21 | ) { 22 | return isArray 23 | ? new UnknownArrayQuery({ query: filter }) 24 | : new UnknownQuery({ query: filter }); 25 | } 26 | 27 | pipe.sanityImage = addDeprecationMessage( 28 | sanityImage, 29 | "`q.sanityImage` has been deprecated in favor of importing `sanityImage` directly from `groqd`. `q.sanityImage` will be removed in future versions." 30 | ); 31 | pipe.select = select; 32 | 33 | // Add schemas 34 | Object.assign(pipe, schemas); 35 | type Pipe = typeof pipe & typeof schemas; 36 | 37 | // Our main export is the pipe, renamed as q 38 | export const q = pipe as Pipe; 39 | 40 | /** 41 | * Export zod for convenience 42 | */ 43 | export { z, sanityImage }; 44 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/makeSafeQueryRunner.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseQuery } from "./baseQuery"; 3 | 4 | /** 5 | * Utility to create a "query runner" that consumes the result of the `q` function. 6 | */ 7 | export const makeSafeQueryRunner = 8 | Promise>(fn: Fn) => 9 | ( 10 | { query, schema }: BaseQuery, 11 | ...rest: ButFirst> 12 | ): Promise> => 13 | fn(query, ...rest).then((res) => { 14 | try { 15 | return schema.parse(res); 16 | } catch (e) { 17 | if (e instanceof z.ZodError) throw new GroqdParseError(e, res); 18 | throw e; 19 | } 20 | }); 21 | 22 | export class GroqdParseError extends Error { 23 | readonly zodError: z.ZodError; 24 | readonly rawResponse: unknown; 25 | 26 | // zodError: z.ZodError; 27 | constructor(public readonly err: z.ZodError, rawResponse: unknown) { 28 | const errorMessages = err.errors.map( 29 | (e) => 30 | `\t\`result${e.path.reduce((acc, el) => { 31 | if (typeof el === "string") { 32 | return `${acc}.${el}`; 33 | } 34 | return `${acc}[${el}]`; 35 | }, "")}\`: ${e.message}` 36 | ); 37 | 38 | super(`Error parsing:\n${errorMessages.join("\n")}`); 39 | this.zodError = err; 40 | this.rawResponse = rawResponse; 41 | } 42 | } 43 | 44 | type BaseType = z.ZodType; 45 | type ButFirst = T extends [unknown, ...infer U] 46 | ? U 47 | : never; 48 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/nullToUndefined.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, it } from "vitest"; 2 | import { runPokemonQuery } from "../test-utils/runQuery"; 3 | import { nullToUndefined, q } from "./index"; 4 | import invariant from "tiny-invariant"; 5 | 6 | describe("nullToUndefined", () => { 7 | it("casts for single fields", async () => { 8 | const { data, query } = await runPokemonQuery( 9 | q("*") 10 | .filter("_type == 'pokemon'") 11 | .grab({ 12 | name: q.string(), 13 | foo: nullToUndefined(q.string().optional()), 14 | bar: nullToUndefined(q.string().optional().default("baz")), 15 | }) 16 | .slice(0) 17 | ); 18 | 19 | expect(query).toBe(`*[_type == 'pokemon']{name, foo, bar}[0]`); 20 | 21 | invariant(data); 22 | expectTypeOf(data).toEqualTypeOf<{ 23 | name: string; 24 | foo?: string | undefined; 25 | bar: string; 26 | }>(); 27 | expect(data).toEqual({ name: "Bulbasaur", foo: undefined, bar: "baz" }); 28 | }); 29 | 30 | it("casts for whole selection", async () => { 31 | const { data } = await runPokemonQuery( 32 | q("*") 33 | .filter("_type == 'pokemon'") 34 | .slice(0) 35 | .grab( 36 | nullToUndefined({ 37 | name: q.string(), 38 | foo: q.string().optional(), 39 | bar: q.string().optional().default("baz"), 40 | }) 41 | ) 42 | ); 43 | 44 | invariant(data); 45 | expectTypeOf(data).toEqualTypeOf<{ 46 | name: string; 47 | foo?: string | undefined; 48 | bar: string; 49 | }>(); 50 | expect(data).toEqual({ name: "Bulbasaur", foo: undefined, bar: "baz" }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/nullToUndefined.ts: -------------------------------------------------------------------------------- 1 | import { preprocess, z } from "zod"; 2 | import { Selection } from "./types"; 3 | 4 | export const _nullToUndefined = (schema: T) => { 5 | return preprocess((arg) => (arg === null ? undefined : arg), schema); 6 | }; 7 | 8 | export function nullToUndefined( 9 | schema: T 10 | ): z.ZodEffects; 11 | export function nullToUndefined( 12 | selection: T 13 | ): NullToUndefinedSelection; 14 | export function nullToUndefined(schemaOrSelection: z.ZodType | Selection) { 15 | if (schemaOrSelection instanceof z.ZodType) 16 | return _nullToUndefined(schemaOrSelection); 17 | 18 | return Object.entries(schemaOrSelection).reduce< 19 | NullToUndefinedSelection 20 | >((acc, [key, value]) => { 21 | if (value instanceof z.ZodType) acc[key] = _nullToUndefined(value); 22 | else if (Array.isArray(value)) 23 | acc[key] = [value[0], _nullToUndefined(value[1])]; 24 | else acc[key] = value; 25 | return acc; 26 | }, {}); 27 | } 28 | 29 | export function nullToUndefinedOnConditionalSelection( 30 | conditionalSelection?: undefined 31 | ): undefined; 32 | export function nullToUndefinedOnConditionalSelection< 33 | T extends Record 34 | >(conditionalSelection: T): { [K in keyof T]: NullToUndefinedSelection }; 35 | export function nullToUndefinedOnConditionalSelection( 36 | conditionalSelection?: Record | undefined 37 | ) { 38 | if (!conditionalSelection) return conditionalSelection; 39 | 40 | return Object.entries(conditionalSelection).reduce< 41 | Record> 42 | >((acc, [key, value]) => { 43 | acc[key] = nullToUndefined(value); 44 | return acc; 45 | }, {}); 46 | } 47 | 48 | type NullToUndefinedSelection = { 49 | [K in keyof Sel]: Sel[K] extends z.ZodType 50 | ? z.ZodEffects 51 | : Sel[K] extends [infer R, infer ZT] 52 | ? ZT extends z.ZodType 53 | ? [R, z.ZodEffects] 54 | : never 55 | : Sel[K]; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, expectTypeOf, it } from "vitest"; 2 | import { UnknownArrayQuery, UnknownQuery } from "./builder"; 3 | import { q } from "./index"; 4 | import { runPokemonQuery } from "../test-utils/runQuery"; 5 | import invariant from "tiny-invariant"; 6 | 7 | describe("pipe", () => { 8 | it("returns instance of UnknownQuery with query set to first arg", () => { 9 | const result = q("foo"); 10 | expect(result).toBeInstanceOf(UnknownQuery); 11 | expect(result.query).toBe("foo"); 12 | }); 13 | 14 | it("can return instance of UnknownArrayQuery when isArray is true", () => { 15 | const result = q("foo", { isArray: true }); 16 | expect(result).toBeInstanceOf(UnknownArrayQuery); 17 | expect(result.query).toBe("foo"); 18 | }); 19 | 20 | it("can chain .grab$ after q() if isArray = true", async () => { 21 | const { data, query } = await runPokemonQuery( 22 | q("*[_type == 'pokemon'][0..2]", { isArray: true }).grab$({ 23 | name: q.string(), 24 | }) 25 | ); 26 | 27 | expect(query).toBe("*[_type == 'pokemon'][0..2]{name}"); 28 | invariant(data); 29 | expectTypeOf(data).toEqualTypeOf<{ name: string }[]>(); 30 | expect(data[0].name).toBe("Bulbasaur"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { contentBlock, contentBlocks } from "./contentBlock"; 3 | 4 | /** 5 | * Custom date schema that will parse date strings to Date objects 6 | */ 7 | const dateSchema = () => 8 | z.preprocess((arg) => { 9 | if (typeof arg == "string" || arg instanceof Date) return new Date(arg); 10 | }, z.date()); 11 | 12 | const slug = (fieldName: string) => 13 | [`${fieldName}.current`, z.string()] as [string, z.ZodString]; 14 | 15 | export const schemas = { 16 | string: z.string, 17 | number: z.number, 18 | boolean: z.boolean, 19 | unknown: z.unknown, 20 | null: z.null, 21 | undefined: z.undefined, 22 | date: dateSchema, 23 | literal: z.literal, 24 | union: z.union, 25 | array: z.array, 26 | object: z.object, 27 | slug, 28 | contentBlock, 29 | contentBlocks, 30 | }; 31 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/selectionUtils.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { Selection } from "./types"; 3 | import { extendsBaseQuery, isQuerySchemaTuple } from "./typeGuards"; 4 | 5 | // Recursively define projections to pick up nested conditionals 6 | export function getProjectionEntriesFromSelection(selection: Selection) { 7 | return Object.entries(selection).reduce((acc, [key, val]) => { 8 | let toPush = ""; 9 | if (extendsBaseQuery(val)) { 10 | toPush = `"${key}": ${val.query}`; 11 | } else if (isQuerySchemaTuple(val)) { 12 | toPush = `"${key}": ${val[0]}`; 13 | } else { 14 | toPush = key; 15 | } 16 | 17 | if (toPush) acc.push(toPush); 18 | 19 | return acc; 20 | }, []); 21 | } 22 | 23 | export function getSchemaFromSelection(selection: Selection): z.ZodObject { 24 | return z.object( 25 | Object.entries(selection).reduce((acc, [key, value]) => { 26 | if (extendsBaseQuery(value)) { 27 | acc[key] = value.schema; 28 | } else if (isQuerySchemaTuple(value)) { 29 | acc[key] = value[1]; 30 | } else if (value instanceof z.ZodType) { 31 | acc[key] = value; 32 | } 33 | return acc; 34 | }, {}) 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/typeGuards.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseQuery } from "./baseQuery"; 3 | 4 | export function extendsBaseQuery( 5 | v: T 6 | ): v is T extends BaseQuery ? T : never { 7 | return v instanceof BaseQuery; 8 | } 9 | 10 | export function isQuerySchemaTuple( 11 | v: T 12 | ): v is T extends [string, z.ZodType] ? T : never { 13 | return Array.isArray(v); 14 | } 15 | 16 | export function isReturnType BaseQuery>( 17 | v: Parameters[0] | ReturnType 18 | ): v is ReturnType { 19 | return v instanceof BaseQuery; 20 | } 21 | -------------------------------------------------------------------------------- /packages/groqd-legacy/src/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { BaseQuery } from "./baseQuery"; 3 | 4 | export type ValueOf = T[keyof T]; 5 | 6 | export type InferType

= P extends BaseQuery 7 | ? T extends z.ZodType 8 | ? z.infer 9 | : never 10 | : P extends z.ZodType 11 | ? z.infer

12 | : never; 13 | 14 | /** 15 | * Type from selection, useful if defining conditional selections in a separate file 16 | * and you want the type that you'll get from that. 17 | */ 18 | export type TypeFromSelection = z.infer< 19 | FromSelection 20 | >; 21 | 22 | /** 23 | * Helper to determine if list of values includes a value, 24 | * used in sanityImage 25 | */ 26 | export type ListIncludes = T extends any[] 27 | ? ArrayToObj extends FieldToObj 28 | ? true 29 | : false 30 | : false; 31 | 32 | type ArrayToObj = { 33 | [K in T[number]]: true; 34 | }; 35 | type FieldToObj = { 36 | [K in T & string]: { [Key in K]: true }; 37 | }[T & string]; 38 | 39 | /** 40 | * Misc internal utils 41 | */ 42 | 43 | type Field = T; 44 | type FromField = T extends Field 45 | ? R 46 | : T extends [string, infer R] 47 | ? R 48 | : never; 49 | export type FromSelection = z.ZodObject<{ 50 | [K in keyof Sel]: Sel[K] extends BaseQuery 51 | ? Sel[K]["schema"] 52 | : FromField; 53 | }>; 54 | 55 | export type Selection = Record< 56 | string, 57 | BaseQuery | z.ZodType | [string, z.ZodType] 58 | >; 59 | -------------------------------------------------------------------------------- /packages/groqd-legacy/test-utils/runQuery.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { evaluate, parse } from "groq-js"; 3 | import { pokemonDataset } from "./pokemon"; 4 | import { BaseQuery } from "../src/baseQuery"; 5 | import { userDataset } from "./users"; 6 | import { GroqdParseError, makeSafeQueryRunner } from "../src"; 7 | 8 | const _makeRunner = 9 | (dataset: any[]) => 10 | async ( 11 | pipeVal: BaseQuery 12 | ): Promise<{ 13 | schema: T; 14 | query: string; 15 | data?: z.infer; 16 | error?: Error; 17 | }> => { 18 | const runner = makeSafeQueryRunner(async (query: string) => { 19 | const tree = parse(query); 20 | const _ = await evaluate(tree, { dataset }); 21 | const rawRes = await _.get(); 22 | 23 | return rawRes; 24 | }); 25 | 26 | try { 27 | const data = await runner(pipeVal); 28 | 29 | return { data, query: pipeVal.query, schema: pipeVal.schema }; 30 | } catch (err) { 31 | return { 32 | query: pipeVal.query, 33 | schema: pipeVal.schema, 34 | error: err as GroqdParseError, 35 | }; 36 | } 37 | }; 38 | 39 | export const runPokemonQuery = _makeRunner(pokemonDataset); 40 | 41 | export const runUserQuery = _makeRunner(userDataset); 42 | -------------------------------------------------------------------------------- /packages/groqd-legacy/test-utils/sampleContentBlocks.ts: -------------------------------------------------------------------------------- 1 | export const sampleContentBlocks = [ 2 | { 3 | _key: "4a8123d672e4_deduped_2", 4 | _type: "block", 5 | children: [ 6 | { 7 | _key: "97d6b0fb822b", 8 | _type: "span", 9 | marks: [], 10 | text: "Hello ", 11 | }, 12 | { 13 | _key: "0d916eef35d5", 14 | _type: "span", 15 | marks: ["strong"], 16 | text: "world", 17 | }, 18 | { 19 | _key: "08d4552319d6", 20 | _type: "span", 21 | marks: [], 22 | text: ", it ", 23 | }, 24 | { 25 | _key: "efac7033ea07", 26 | _type: "span", 27 | marks: ["37ac693910cb"], 28 | text: "is", 29 | }, 30 | { 31 | _key: "a7935aa58cd7", 32 | _type: "span", 33 | marks: [], 34 | text: " Pikachu.", 35 | }, 36 | ], 37 | markDefs: [ 38 | { 39 | _key: "37ac693910cb", 40 | _type: "link", 41 | href: "https://google.com", 42 | }, 43 | ], 44 | style: "normal", 45 | }, 46 | { 47 | _key: "694615b411df", 48 | _type: "block", 49 | children: [ 50 | { 51 | _key: "1d83c5becd42", 52 | _type: "span", 53 | marks: [], 54 | text: "and some more", 55 | }, 56 | ], 57 | markDefs: [], 58 | style: "h2", 59 | }, 60 | { 61 | _key: "7852cd508368", 62 | _type: "block", 63 | children: [ 64 | { 65 | _key: "792a72f72a57", 66 | _type: "span", 67 | marks: [], 68 | text: "and some more", 69 | }, 70 | ], 71 | markDefs: [], 72 | style: "normal", 73 | }, 74 | ]; 75 | 76 | export const sampleContentBlocksWithoutMarks = [ 77 | { 78 | _key: "62dc4f6c6236", 79 | _type: "block", 80 | children: [ 81 | { 82 | _key: "7ad29d2c3069", 83 | _type: "span", 84 | text: "Hello world.", 85 | }, 86 | { 87 | _key: "8d6df66fd3e3", 88 | _type: "span", 89 | text: "Hello world.", 90 | }, 91 | ], 92 | style: "normal", 93 | }, 94 | ]; 95 | -------------------------------------------------------------------------------- /packages/groqd-legacy/test-utils/users.ts: -------------------------------------------------------------------------------- 1 | import { 2 | sampleContentBlocks, 3 | sampleContentBlocksWithoutMarks, 4 | } from "./sampleContentBlocks"; 5 | 6 | const userData: { 7 | slug: { current: string }; 8 | name: string; 9 | age: number; 10 | role: RoleType; 11 | nicknames?: string[]; 12 | bio?: unknown; 13 | }[] = [ 14 | { 15 | slug: { current: "john" }, 16 | name: "John", 17 | age: 20, 18 | role: "guest", 19 | nicknames: ["Johnny", "J Boi", "Dat Boi Doe"], 20 | }, 21 | { slug: { current: "jane" }, name: "Jane", age: 30, role: "admin" }, 22 | { 23 | slug: { current: "matas" }, 24 | name: "Matas Buzelis", 25 | age: 20, 26 | role: "guest", 27 | bio: sampleContentBlocksWithoutMarks, 28 | }, 29 | ]; 30 | 31 | const users = userData.map((user) => ({ 32 | _type: "user", 33 | _id: `user.${user.name}`, 34 | ...user, 35 | role: { 36 | _type: "reference", 37 | _ref: `role.${user.role}`, 38 | }, 39 | bio: user.bio ?? sampleContentBlocks, 40 | })); 41 | 42 | type RoleType = "guest" | "admin"; 43 | const roles: { _type: "role"; title: string; _id: `role.${RoleType}` }[] = [ 44 | { _type: "role", title: "guest", _id: "role.guest" }, 45 | { _type: "role", title: "admin", _id: "role.admin" }, 46 | ]; 47 | 48 | export const userDataset = [...users, ...roles]; 49 | -------------------------------------------------------------------------------- /packages/groqd-legacy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/groqd-legacy/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | sourcemap: true, 6 | clean: true, 7 | dts: true, 8 | format: ["cjs", "esm"], 9 | target: "es6", 10 | }); 11 | -------------------------------------------------------------------------------- /packages/groqd-legacy/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { configDefaults, defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | typecheck: { 6 | enabled: true, 7 | checker: "tsc", 8 | allowJs: false, 9 | include: ["**.test.ts"], 10 | }, 11 | exclude: [...configDefaults.exclude], 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/groqd-playground/README.md: -------------------------------------------------------------------------------- 1 | ## Groqd Playground 2 | 3 | Groqd Playground is a plugin for Sanity Studio for testing [groqd](https://commerce.nearform.com/open-source/groqd/) queries, featuring: 4 | 5 | - a TypeScript editor experience with syntax/type highlighting; 6 | - parsed response and error messages for when responses fail to pass Zod validation; 7 | - dataset/api version switchers. 8 | 9 | ![Screenshot of Groqd Playground in action](https://raw.githubusercontent.com/FormidableLabs/groqd/main/docs/img/groqd-playground-sample.png) 10 | 11 | To setup, just `yarn add groqd-playground` and then add the `groqdPlaygroundTool` plugin to your sanity config: 12 | 13 | ```ts 14 | import { defineConfig } from "sanity"; 15 | import { groqdPlaygroundTool } from "groqd-playground"; 16 | 17 | export default defineConfig({ 18 | /* ... */ 19 | plugins: [groqdPlaygroundTool()], 20 | }); 21 | ``` 22 | 23 | ### [See the docs for more information!](https://commerce.nearform.com/open-source/groqd/groqd-playground) 24 | -------------------------------------------------------------------------------- /packages/groqd-playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groqd-playground", 3 | "private": false, 4 | "license": "MIT", 5 | "version": "0.0.20", 6 | "author": { 7 | "name": "Formidable", 8 | "url": "https://formidable.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/FormidableLabs/groqd" 13 | }, 14 | "homepage": "https://github.com/formidablelabs/groqd", 15 | "keywords": [ 16 | "sanity", 17 | "groq", 18 | "query", 19 | "typescript" 20 | ], 21 | "main": "dist/index.js", 22 | "module": "dist/index.mjs", 23 | "types": "dist/index.d.ts", 24 | "exports": { 25 | ".": [ 26 | { 27 | "import": "./dist/index.mjs", 28 | "types": "./dist/index.d.ts", 29 | "default": "./dist/index.js" 30 | }, 31 | "./dist/index.js" 32 | ], 33 | "./package.json": "./package.json" 34 | }, 35 | "files": [ 36 | "dist" 37 | ], 38 | "scripts": { 39 | "build": "tsup", 40 | "build:watch": "tsup --watch", 41 | "typecheck": "tsc --noEmit", 42 | "dev": "pnpm run build:watch" 43 | }, 44 | "devDependencies": { 45 | "@types/lodash.has": "^4.5.7", 46 | "@types/react": "^18.0.35", 47 | "@types/styled-components": "^5.1.26", 48 | "tsup": "^6.7.0", 49 | "typescript": "^5.7.2" 50 | }, 51 | "peerDependencies": { 52 | "@sanity/icons": "^2.3.1", 53 | "@sanity/ui": "^2", 54 | "react": "^18.2.0", 55 | "react-dom": "^18.2.0", 56 | "react-is": "^18.2.0", 57 | "sanity": "^3.15.0", 58 | "styled-components": "^5.2 || ^6" 59 | }, 60 | "dependencies": { 61 | "@uiw/react-split": "^5.8.10", 62 | "groqd": "workspace:groqd-legacy@*", 63 | "lodash.has": "^4.5.2", 64 | "zod": "^3.22.4" 65 | }, 66 | "publishConfig": { 67 | "provenance": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/groqd-playground/src/PlaygroundWrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { GroqdPlaygroundProps } from "./types"; 3 | import { ToastProvider } from "@sanity/ui"; 4 | import GroqdPlayground from "./components/Playground"; 5 | 6 | export default function GroqdPlaygroundWrapper(props: GroqdPlaygroundProps) { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/groqd-playground/src/components/JSONExplorer.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Box, Stack } from "@sanity/ui"; 3 | 4 | export const Root = styled(Box)` 5 | font-family: Menlo, monospace; 6 | font-size: 0.9em; 7 | position: relative; 8 | height: 100%; 9 | 10 | --border-radius: 4px; 11 | --error-bg-color: #ffe5ea; 12 | --item-hover-color: #e7e7e7; 13 | 14 | @media (prefers-color-scheme: dark) { 15 | --error-bg-color: #470417; 16 | --item-hover-color: #505050; 17 | } 18 | `; 19 | 20 | export const Label = styled.span` 21 | color: #9d1fcd; 22 | @media (prefers-color-scheme: dark) { 23 | color: #d05afc; 24 | } 25 | `; 26 | 27 | export const Key = styled.span` 28 | color: #1e61cd; 29 | @media (prefers-color-scheme: dark) { 30 | color: #5998fc; 31 | } 32 | `; 33 | export const Value = styled.span` 34 | color: #967e1c; 35 | @media (prefers-color-scheme: dark) { 36 | color: #dbb931; 37 | } 38 | `; 39 | 40 | export const LineItem = styled(Box)<{ 41 | depth: number; 42 | pointer?: boolean; 43 | hasError?: boolean; 44 | }>` 45 | padding-left: ${({ depth }) => depth * DEPTH_SC}px; 46 | border-radius: var(--border-radius); 47 | cursor: ${({ pointer }) => (pointer ? "pointer" : "initial")}; 48 | 49 | background-color: ${({ hasError }) => 50 | hasError ? "var(--error-bg-color)" : "initial"}; 51 | 52 | &:hover { 53 | background-color: ${({ hasError }) => 54 | hasError ? undefined : "var(--item-hover-color)"}; 55 | } 56 | `; 57 | 58 | export const CollapsibleContainer = styled(Stack)<{ hasError?: boolean }>` 59 | border-radius: var(--border-radius); 60 | 61 | background-color: ${({ hasError }) => 62 | hasError ? "var(--error-bg-color)" : "initial"}; 63 | `; 64 | 65 | export const ErrorMessageText = styled.div` 66 | font-weight: 400; 67 | `; 68 | 69 | const DEPTH_SC = 15; 70 | -------------------------------------------------------------------------------- /packages/groqd-playground/src/components/Playground.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Box } from "@sanity/ui"; 3 | 4 | export const ErrorLineItem = styled(Box)` 5 | border-radius: 4px; 6 | cursor: pointer; 7 | 8 | &:hover { 9 | background-color: #e7e7e7; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | &:hover { 14 | background-color: #505050; 15 | } 16 | } 17 | `; 18 | 19 | export const CopyQueryButton = styled.button` 20 | all: unset; 21 | cursor: pointer; 22 | &:focus { 23 | box-shadow: inset 0 0 0 1px var(--card-border-color), 0 0 0 1px #fff, 24 | 0 0 0 3px var(--card-focus-ring-color); 25 | border-radius: 0.1875rem; 26 | } 27 | `; 28 | -------------------------------------------------------------------------------- /packages/groqd-playground/src/components/ShareUrlField.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Box, 4 | Button, 5 | Card, 6 | Flex, 7 | Label, 8 | Stack, 9 | Text, 10 | TextInput, 11 | Tooltip, 12 | } from "@sanity/ui"; 13 | import { CopyIcon } from "@sanity/icons"; 14 | import { useCopyDataAndNotify } from "../util/copyDataToClipboard"; 15 | 16 | type ShareUrlFieldProps = { 17 | url: string; 18 | title: string; 19 | column?: number; 20 | notificationMessage?: string; 21 | }; 22 | 23 | export const ShareUrlField = ({ 24 | title, 25 | url, 26 | column = 4, 27 | notificationMessage = "Copied URL to clipboard!", 28 | }: ShareUrlFieldProps) => { 29 | const copyUrl = useCopyDataAndNotify(notificationMessage); 30 | const handleCopyUrl = () => copyUrl(url); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | Copy to clipboard 46 | 47 | } 48 | > 49 |