├── .github └── workflows │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── docker-compose.yaml ├── hasura │ ├── config.yaml │ ├── metadata │ │ ├── actions.graphql │ │ ├── actions.yaml │ │ ├── allow_list.yaml │ │ ├── api_limits.yaml │ │ ├── cron_triggers.yaml │ │ ├── databases │ │ │ ├── databases.yaml │ │ │ └── default │ │ │ │ └── tables │ │ │ │ ├── public_address.yaml │ │ │ │ ├── public_order.yaml │ │ │ │ ├── public_order_product.yaml │ │ │ │ ├── public_order_status.yaml │ │ │ │ ├── public_product.yaml │ │ │ │ ├── public_product_category_enum.yaml │ │ │ │ ├── public_product_review.yaml │ │ │ │ ├── public_site_admin.yaml │ │ │ │ ├── public_user.yaml │ │ │ │ └── tables.yaml │ │ ├── graphql_schema_introspection.yaml │ │ ├── inherited_roles.yaml │ │ ├── network.yaml │ │ ├── query_collections.yaml │ │ ├── remote_schemas.yaml │ │ ├── rest_endpoints.yaml │ │ └── version.yaml │ ├── migrations │ │ └── default │ │ │ └── 1646834482402_init │ │ │ └── up.sql │ └── seeds │ │ └── default │ │ ├── 01_A_user_seeds.sql │ │ ├── 01_B_user_address_seeds.sql │ │ ├── 02_product_seeds_sanitized.sql │ │ ├── 03_A_order_seeds.sql │ │ ├── 03_B_order_product_seeds.sql │ │ └── 04_default_user_login_seeds.sql ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.css │ ├── App.tsx │ ├── address │ │ └── index.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── order │ │ └── index.tsx │ ├── order_product │ │ └── index.tsx │ ├── product │ │ └── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── user │ │ └── index.tsx └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── buildGqlQuery │ ├── buildArgs.ts │ ├── buildFields.ts │ └── index.ts ├── buildQuery │ └── index.ts ├── buildVariables │ ├── buildCreateVariables.ts │ ├── buildGetListVariables.ts │ ├── buildUpdateVariables.ts │ ├── index.ts │ ├── makeNestedTarget.ts │ └── typeAwareKeyValueReducer.ts ├── customDataProvider │ └── index.ts ├── getResponseParser │ ├── index.ts │ ├── sanitizeResource.test.ts │ └── sanitizeResource.ts ├── graphql-ast-types-browser │ ├── definitions │ │ ├── graphql.js │ │ ├── index.js │ │ └── init.js │ ├── index.d.ts │ └── index.js ├── helpers │ ├── fetchActions.ts │ ├── getArgType.ts │ ├── getFinalType.test.ts │ ├── getFinalType.ts │ ├── isList.test.ts │ ├── isList.ts │ ├── isRequired.test.ts │ └── isRequired.ts ├── index.ts └── types.ts └── tsconfig.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Install dependencies 13 | run: npm install 14 | - name: Run tests 15 | run: npm run test 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.org 3 | lib 4 | dist 5 | .vscode -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | webpack.config.js 3 | .prettierrc 4 | *.org 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.7.1 (March 6, 2025) 4 | 5 | - Fix a tree shaking bug, thanks to @Rhodanthe1116 6 | 7 | ## 0.7.0 (July 19, 2024) 8 | 9 | - Support react-admin v5, thanks to @quentin-decre 10 | 11 | ## 0.6.1 (April 16, 2024) 12 | 13 | - Add update by pk feature, thanks to @franzwilhelm 14 | 15 | ## 0.6.0 (May 29, 2023) 16 | 17 | **Breaking change** 18 | 19 | - Preserve array structure with array transformation, thanks to @nihauc12 20 | 21 | ## 0.5.6 (May 29, 2023) 22 | 23 | - Feature: Support Disabling Pagination, thanks to @alioguzhan 24 | - Feature: Support for JSONB filtering, thanks to @jbek7 25 | 26 | ## 0.5.5 (December 21, 2022) 27 | 28 | - Feature: Hasura raw query nested filtering support, thanks to @n3n 29 | - Feature: Ability to support sorting nulls_first and nulls_last, thanks to @n3n 30 | - Update: graphql v16 31 | 32 | ## 0.5.4 (November 22, 2022) 33 | 34 | - Update all dependencies 35 | - Fix: remove `graphql-ast-types-browser` dependency 36 | 37 | ## 0.5.3 (August 16, 2022) 38 | 39 | - Feature: distinct_on support (#124), thanks to @bharatkashyap 40 | - Feature: added support for empty operator in buildGetListVariables (#125), thanks to @ofilipowicz 41 | 42 | ## 0.5.2 (July 27, 2022) 43 | 44 | - Update to GraphQL 16 45 | - Fix: only filter out GraphQL reserved names (#116), thanks to @n3n 46 | - Feature: Support skipping count aggregate on hasura (#120), thanks to @mohammad-bolt 47 | 48 | ## 0.5.1 (June 14, 2022) 49 | 50 | - Fix dependency issue 51 | 52 | ## 0.5.0 (June 6, 2022) 53 | 54 | - Upgrade library and sample to React Admin v4, thanks to @LucaColonnello 55 | 56 | ## 0.4.2 (May 6, 2022) 57 | 58 | - Example: Add Example v3 App 59 | - Feature: \_nin operator (#89), thanks to @fkowal 60 | - Feature: Enable support for \_contains operator and nested path in jsonb joins, thanks to @fkowal 61 | - Feature: Support nested fields when sorting by multiple columns, thanks to @daa 62 | - Bug Fix: buildFields types (#91), thanks to @cpv123 63 | - Bug Fix: Keep null values when sanitizing resources (#97), thanks to @nselikoff 64 | 65 | ## 0.4.1 (April 7, 2022) 66 | 67 | - Bug Fix: Variables for mutations are not being populated, thanks to @nselikoff 68 | 69 | ## 0.4.0 (March 2, 2022) 70 | 71 | - Full Typescript rewrite thanks to Chris Vibert @cpv123 72 | 73 | ## 0.3.0 (March 2, 2022) 74 | 75 | - Bug Fix: Update only includes edited fields 76 | - Feature: Support sorting by multiple fields 77 | - Bug Fix: Return dataProvider object, not function 78 | - Bug fix: nested keys with array values 79 | 80 | ## 0.2.0 (June 30, 2021) 81 | 82 | - Feature: Update only permitted fields. 83 | - Feature: Add option for custom aggregate field names. 84 | - Feature: Reference a nested object in a reference field. 85 | - Bug Fix: Issue in sanitizing a null value in an array. 86 | - Feature: Add support for nested field filtering 87 | - Bug Fix: Fix issue with null / dates 88 | - Bug Fix: Fix error with react-admin 1.13.0 for date inputs 89 | 90 | ## 0.1.0 (January 19, 2021) 91 | 92 | - **Breaking change**: This release is a complete rewrite of the library, replacing the API with code from `ra-data-hasura-graphql` library. The `steams/ra-data-hasura-graphql` will henceforth be archived. Refer `README.md` for usage instructions. 93 | 94 | ## 0.0.8 (March 18, 2020) 95 | 96 | - Bug Fix: Translate id to primary key for custom primary keys. 97 | - Bug Fix: Respect primary key on order by. 98 | - Bug Fix: Fix typo in GET_MANY_REFERENCE. 99 | - Bug Fix: Set `asc` as default sorting order in GET_LIST. 100 | 101 | ## 0.0.7 (September 17, 2019) 102 | 103 | - Bug Fix: Re-build library to fix discrepancies. Pass `where` arguments to `count` query. 104 | - Feature: Add support for httpClient to pass in dynamic headers. Backwards compatibility maintained for static headers. 105 | - Update package dependencies. 106 | 107 | ## 0.0.6 (June 14, 2019) 108 | 109 | - Bug Fix: Fix sort order, fix primary key when response not an array and add filters to GET\_\* operations. 110 | 111 | ## 0.0.5 (May 16, 2019) 112 | 113 | - Feature: Support specifying primary keys other than id for tables using a config object 114 | Example: `const config = { 'primaryKey': {'author': 'name'} }` 115 | 116 | ## 0.0.4 (April 25, 2019) 117 | 118 | - Feature: Support multiple schemas using "." separator. 119 | Example: `` 120 | 121 | ## 0.0.3 (January 24, 2019) 122 | 123 | - Bug Fix: Fix count query to support UUID 124 | 125 | ## 0.0.2 (January 20, 2019) 126 | 127 | - Bug Fix: GET_MANY_REFERENCE definition 128 | 129 | ## 0.0.1 (January 19, 2019) 130 | 131 | - Add support for hasura data provider 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Radcliffe Robinson 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ra-data-hasura 2 | 3 | A GraphQL data provider for [react-admin v4](https://marmelab.com/react-admin) tailored to target [Hasura](https://hasura.io/) GraphQL endpoints. For React Admin v3 use v0.4.2 of this library. For React Admin v5 support, use >v0.7.0 of this library. 4 | 5 | - [ra-data-hasura](#ra-data-hasura) 6 | - [Benefits and Motivation](#benefits-and-motivation) 7 | - [Installation](#installation) 8 | - [Usage](#usage) 9 | - [How It Works](#how-it-works) 10 | - [Options](#options) 11 | - [Customize the Apollo client](#customize-the-apollo-client) 12 | - [Adding Authentication Headers](#adding-authentication-headers) 13 | - [Customize the introspection](#customize-the-introspection) 14 | - [Customize the Data Return](#customize-the-data-return) 15 | - [Customizing queries](#customizing-queries) 16 | - [Example: extending a query to include related entities](#example-extending-a-query-to-include-related-entities) 17 | - [Example: write a completely custom query](#example-write-a-completely-custom-query) 18 | - [Special Filter Feature](#special-filter-feature) 19 | - [Nested filtering](#nested-filtering) 20 | - [Jsonb filtering](#jsonb-filtering) 21 | - [Sorting lists by multiple columns](#sorting-lists-by-multiple-columns) 22 | - [Contributing](#contributing) 23 | - [Credits](#credits) 24 | 25 | Example applications demonstrating usage: 26 | 27 | - [react-admin-low-code](https://github.com/cpursley/react-admin-low-code) (basic usage) 28 | - [react-admin-hasura-queries](https://github.com/cpv123/react-admin-hasura-queries) (usage with custom queries) 29 | 30 | ## Benefits and Motivation 31 | 32 | This utility is built on top of [ra-data-graphql](https://github.com/vladimiregorov/react-admin/blob/master/packages/ra-data-graphql/README.md) and is a custom data provider for the current Hasura GraphQL API format. 33 | 34 | The existing ra-data-graphql-simple provider, requires that your GraphQL endpoint implement a specific grammar for the objects and methods exposed, which is different with Hasura because the exposed objects and methods are generated differently. 35 | 36 | This utility auto generates valid GraphQL queries based on the properties exposed by the Hasura API such as `object_bool_exp` and `object_set_input`. 37 | 38 | ## Installation 39 | 40 | Install with: 41 | 42 | ```sh 43 | npm install --save graphql ra-data-hasura 44 | ``` 45 | 46 | ## Usage 47 | 48 | The `ra-data-hasura` package exposes a single function with the following signature: 49 | 50 | ```js 51 | buildHasuraProvider( 52 | options?: Object, 53 | buildGqlQueryOverrides?: Object, 54 | customBuildVariables?: Function, 55 | customGetResponseParser?: Function, 56 | ) => Function 57 | ``` 58 | 59 | See the [Options](#options) and [Customizing queries](#customizing-queries) sections below for more details on these arguments. 60 | 61 | This function acts as a constructor for a `dataProvider` based on a Hasura GraphQL endpoint. When executed, this function calls the endpoint, running an [introspection](http://graphql.org/learn/introspection/) query to learn about the specific data models exposed by your Hasura endpoint. It uses the result of this query (the GraphQL schema) to automatically configure the `dataProvider` accordingly. 62 | 63 | ```jsx 64 | // Initialize the dataProvider before rendering react-admin resources. 65 | import React, { useState, useEffect } from 'react'; 66 | import buildHasuraProvider from 'ra-data-hasura'; 67 | import { Admin, Resource } from 'react-admin'; 68 | 69 | import { PostCreate, PostEdit, PostList } from './posts'; 70 | 71 | const App = () => { 72 | const [dataProvider, setDataProvider] = useState(null); 73 | 74 | useEffect(() => { 75 | const buildDataProvider = async () => { 76 | const dataProvider = await buildHasuraProvider({ 77 | clientOptions: { uri: 'http://localhost:8080/v1/graphql' }, 78 | }); 79 | setDataProvider(() => dataProvider); 80 | }; 81 | buildDataProvider(); 82 | }, []); 83 | 84 | if (!dataProvider) return

Loading...

; 85 | 86 | return ( 87 | 88 | 94 | 95 | ); 96 | }; 97 | 98 | export default App; 99 | ``` 100 | 101 | ## How It Works 102 | 103 | The data provider converts React Admin queries into the form expected by Hasura's GraphQL API. For example, a React Admin `GET_LIST` request for a person resource with the parameters : 104 | 105 | ```json 106 | { 107 | "pagination": { "page": 1, "perPage": 5 }, 108 | "sort": { "field": "name", "order": "DESC" }, 109 | "filter": { 110 | "ids": [101, 102] 111 | } 112 | } 113 | ``` 114 | 115 | will generate the following GraphQL request for Hasura : 116 | 117 | ``` 118 | query person($limit: Int, $offset: Int, $order_by: [person_order_by!]!, $where: person_bool_exp) { 119 | items: person(limit: $limit, offset: $offset, order_by: $order_by, where: $where) { 120 | id 121 | name 122 | address_id 123 | } 124 | total: person_aggregate(limit: $limit, offset: $offset, order_by: $order_by, where: $where) { 125 | aggregate { 126 | count 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | With the following variables to be passed alongside the query: 133 | 134 | ``` 135 | { 136 | limit: 5, 137 | offset: 0, 138 | order_by: { name: 'desc' }, 139 | where: { 140 | _and: [ 141 | { 142 | id: { 143 | _in: [101, 102] 144 | } 145 | } 146 | ] 147 | } 148 | } 149 | 150 | ``` 151 | 152 | React Admin sort and filter objects will be converted appropriately, for example sorting with dot notation: 153 | 154 | ```jsx 155 | export const PostList = (props) => ( 156 | 157 | ... 158 | 159 | ); 160 | ``` 161 | 162 | will generate the following GraphQL query variables: 163 | 164 | ```js 165 | { 166 | limit: 25, 167 | offset: 0, 168 | order_by: { user: { email: 'desc' } } 169 | } 170 | ``` 171 | 172 | and 173 | 174 | ```jsx 175 | export const AddressList = () => ( 176 | 180 | ... 181 | 182 | ); 183 | ``` 184 | 185 | will generate the following GraphQL query variables: 186 | 187 | ```json 188 | { 189 | // ... 190 | "order_by": { 191 | "city": "desc" 192 | }, 193 | "distinct_on": "city" 194 | } 195 | ``` 196 | 197 | Keep in mind that `distinct_on` must be used in conjunction with `order_by`, otherwise a `"distinct_on" columns must match initial "order_by" columns"` error will result. See more [here](https://hasura.io/docs/latest/queries/postgres/distinct-queries/#the-distinct_on-argument). 198 | 199 | ## Options 200 | 201 | ### Customize the Apollo client 202 | 203 | You can either supply just the client options: 204 | 205 | ```js 206 | buildGraphQLProvider({ 207 | clientOptions: { 208 | uri: 'http://localhost:8080/v1/graphql', 209 | ...otherApolloOptions, 210 | }, 211 | }); 212 | ``` 213 | 214 | or supply the client instance directly: 215 | 216 | ```js 217 | buildGraphQLProvider({ client: myClient }); 218 | ``` 219 | 220 | ### Adding Authentication Headers 221 | 222 | To send authentication headers, you'll need to supply the client instance directly with headers defined: 223 | 224 | ```js 225 | import { ApolloClient, InMemoryCache } from '@apollo/client'; 226 | 227 | const myClientWithAuth = new ApolloClient({ 228 | uri: 'http://localhost:8080/v1/graphql', 229 | cache: new InMemoryCache(), 230 | headers: { 231 | 'x-hasura-admin-secret': 'hasuraAdminSecret', 232 | // 'Authorization': `Bearer xxxx`, 233 | }, 234 | }); 235 | 236 | buildHasuraProvider({ client: myClientWithAuth }); 237 | ``` 238 | 239 |
240 | 241 | Adding headers using just client options 242 | 243 | You can also add headers using only client options rather than the client itself: 244 | 245 | ```js 246 | import { createHttpLink } from '@apollo/client'; 247 | import { setContext } from '@apollo/client/link/context'; 248 | 249 | const authLink = setContext((_, { headers }) => ({ 250 | headers: { 251 | ...headers, 252 | 'x-hasura-admin-secret': 'hasuraAdminSecret', 253 | // 'Authorization': `Bearer xxxx`, 254 | }, 255 | })); 256 | 257 | const httpLink = createHttpLink({ 258 | uri: 'http://localhost:8080/v1/graphql', 259 | }); 260 | 261 | const clientOptionsWithAuth = { 262 | link: authLink.concat(httpLink), 263 | }; 264 | 265 | buildHasuraProvider({ client: clientOptionsWithAuth }); 266 | ``` 267 | 268 |
269 | 270 | ### Customize the introspection 271 | 272 | These are the default options for introspection: 273 | 274 | ```js 275 | const introspectionOptions = { 276 | include: [], // Either an array of types to include or a function which will be called for every type discovered through introspection 277 | exclude: [], // Either an array of types to exclude or a function which will be called for every type discovered through introspection 278 | }; 279 | 280 | // Including types 281 | const introspectionOptions = { 282 | include: ['Post', 'Comment'], 283 | }; 284 | 285 | // Excluding types 286 | const introspectionOptions = { 287 | exclude: ['CommandItem'], 288 | }; 289 | 290 | // Including types with a function 291 | const introspectionOptions = { 292 | include: (type) => ['Post', 'Comment'].includes(type.name), 293 | }; 294 | 295 | // Including types with a function 296 | const introspectionOptions = { 297 | exclude: (type) => !['Post', 'Comment'].includes(type.name), 298 | }; 299 | ``` 300 | 301 | **Note**: `exclude` and `include` are mutually exclusives and `include` will take precendance. 302 | 303 | **Note**: When using functions, the `type` argument will be a type returned by the introspection query. Refer to the [introspection](http://graphql.org/learn/introspection/) documentation for more information. 304 | 305 | Pass the introspection options to the `buildApolloProvider` function: 306 | 307 | ```js 308 | buildApolloProvider({ introspection: introspectionOptions }); 309 | ``` 310 | 311 | ### Customize the Data Return 312 | 313 | Once the data is returned back from the provider, you can customize it by implementing the `DataProvider` interface. [An example is changing the ID key](https://marmelab.com/react-admin/FAQ.html#can-i-have-custom-identifiersprimary-keys-for-my-resources). 314 | 315 | ```typescript 316 | const [dataProvider, setDataProvider] = React.useState( 317 | null 318 | ); 319 | 320 | React.useEffect(() => { 321 | const buildDataProvider = async () => { 322 | const dataProviderHasura = await buildHasuraProvider({ 323 | clientOptions: { 324 | uri: 'http://localhost:8080/v1/graphql', 325 | }, 326 | }); 327 | const modifiedProvider: DataProvider = { 328 | getList: async (resource, params) => { 329 | let { data, ...metadata } = await dataProviderHasura.getList( 330 | resource, 331 | params 332 | ); 333 | 334 | if (resource === 'example_resource_name') { 335 | data = data.map( 336 | (val): Record => ({ 337 | ...val, 338 | id: val.region_id, 339 | }) 340 | ); 341 | } 342 | 343 | return { 344 | data: data as any[], 345 | ...metadata, 346 | }; 347 | }, 348 | getOne: (resource, params) => dataProviderHasura.getOne(resource, params), 349 | getMany: (resource, params) => 350 | dataProviderHasura.getMany(resource, params), 351 | getManyReference: (resource, params) => 352 | dataProviderHasura.getManyReference(resource, params), 353 | update: (resource, params) => dataProviderHasura.update(resource, params), 354 | updateMany: (resource, params) => 355 | dataProviderHasura.updateMany(resource, params), 356 | create: (resource, params) => dataProviderHasura.create(resource, params), 357 | delete: (resource, params) => dataProviderHasura.delete(resource, params), 358 | deleteMany: (resource, params) => 359 | dataProviderHasura.deleteMany(resource, params), 360 | }; 361 | setDataProvider(() => modifiedProvider); 362 | }; 363 | buildDataProvider(); 364 | }, []); 365 | ``` 366 | 367 | ## Customizing queries 368 | 369 | Queries built by this data provider are made up of 3 parts: 370 | 371 | 1. The set of fields requested 372 | 2. The variables defining the query constraints like `where, order_by, limit, offset` 373 | 3. The response format e.g. `{ data: {...}, total: 100 }` 374 | 375 | Each of these can be customized - functions overriding numbers 2 and 3 can be passed to directly to `buildDataProvider` as shown in [Usage](#usage), whilst number 1 can be customized in parts using the `buildGqlQueryOverrides` object argument: 376 | 377 | ```js 378 | { 379 | buildFields?: Function, 380 | buildMetaArgs?: Function, 381 | buildArgs?: Function, 382 | buildApolloArgs?: Function, 383 | } 384 | ``` 385 | 386 | A likely scenario is that you want to override only the `buildFields` part so that you can customize your GraphQL queries - requesting fewer fields, more fields, nested fields etc. 387 | 388 | This can be easily done, and importantly can be done using `gql` template literal tags, as shown in the examples below. Take a look at this [demo application](https://github.com/cpv123/react-admin-hasura-queries) to see it in action. 389 | 390 | ### Example: extending a query to include related entities 391 | 392 | By default, the data provider will generate queries that include all fields on a resource, but without any relationships to nested entities. If you would like to keep these base fields but extend the query to also include related entities, then you can write a custom `buildFields` like this: 393 | 394 | ```ts 395 | import buildDataProvider, { buildFields } from 'ra-data-hasura'; 396 | import type { BuildFields } from 'ra-data-hasura'; 397 | import gql from 'graphql-tag'; 398 | 399 | /** 400 | * Extracts just the fields from a GraphQL AST. 401 | * @param {GraphQL AST} queryAst 402 | */ 403 | const extractFieldsFromQuery = (queryAst) => { 404 | return queryAst.definitions[0].selectionSet.selections; 405 | }; 406 | 407 | // Define the additional fields that we want. 408 | const EXTENDED_GET_ONE_USER = gql` 409 | { 410 | todos_aggregate { 411 | aggregate { 412 | count 413 | } 414 | } 415 | } 416 | `; 417 | 418 | const customBuildFields: BuildFields = (type, fetchType) => { 419 | const resourceName = type.name; 420 | 421 | // First take the default fields (all, but no related or nested). 422 | const defaultFields = buildFields(type, fetchType); 423 | 424 | if (resourceName === 'users' && fetchType === 'GET_ONE') { 425 | const relatedEntities = extractFieldsFromQuery(EXTENDED_GET_ONE_USER); 426 | defaultFields.push(...relatedEntities); 427 | } 428 | 429 | // Extend other queries for other resources/fetchTypes here... 430 | 431 | return defaultFields; 432 | }; 433 | 434 | buildDataProvider(options, { buildFields: customBuildFields }); 435 | ``` 436 | 437 | ### Example: write a completely custom query 438 | 439 | If you want full control over the GraphQL query, then you can define the entire set of fields like this: 440 | 441 | ```ts 442 | import gql from 'graphql-tag'; 443 | import buildDataProvider, { buildFields } from 'ra-data-hasura'; 444 | import type { BuildFields } from 'ra-data-hasura'; 445 | 446 | /** 447 | * Extracts just the fields from a GraphQL AST. 448 | * @param {GraphQL AST} queryAst 449 | */ 450 | const extractFieldsFromQuery = (queryAst) => { 451 | return queryAst.definitions[0].selectionSet.selections; 452 | }; 453 | 454 | const GET_ONE_USER = gql` 455 | { 456 | id 457 | name 458 | todos( 459 | where: { is_completed: { _eq: false } } 460 | order_by: { created_at: asc } 461 | ) { 462 | title 463 | } 464 | todos_aggregate { 465 | aggregate { 466 | count 467 | } 468 | } 469 | } 470 | `; 471 | 472 | const customBuildFields: BuildFields = (type, fetchType) => { 473 | const resourceName = type.name; 474 | 475 | if (resourceName === 'users' && fetchType === 'GET_ONE') { 476 | return extractFieldsFromQuery(GET_ONE_USER); 477 | } 478 | 479 | // No custom query defined, so use the default query fields (all, but no related/nested). 480 | return buildFields(type, fetchType); 481 | }; 482 | 483 | buildDataProvider(options, { buildFields: customBuildFields }); 484 | ``` 485 | 486 | Note that when using this approach in particular, it is possible that you will come across [this issue](https://github.com/cpv123/react-admin-hasura-queries#troubleshooting). 487 | 488 | ## Special Filter Feature 489 | 490 | This adapter allows filtering several columns at a time with using specific comparators, e.g. `ilike`, `like`, `eq`, etc. 491 | 492 | ```tsx 493 | 494 | 499 | 500 | ``` 501 | 502 | It will generate the following filter payload 503 | 504 | ```json 505 | { 506 | "variables": { 507 | "where": { 508 | "_and": [], 509 | "_or": [ 510 | { 511 | "email": { 512 | "_ilike": "%edu%" 513 | } 514 | }, 515 | { 516 | "first_name": { 517 | "_eq": "edu" 518 | } 519 | }, 520 | { 521 | "last_name": { 522 | "_like": "%edu%" 523 | } 524 | } 525 | ] 526 | }, 527 | "limit": 10, 528 | "offset": 0, 529 | "order_by": { 530 | "id": "asc" 531 | } 532 | } 533 | } 534 | ``` 535 | 536 | The adapter assigns default comparator depends on the data type if it is not provided. 537 | For string data types, it assumes as text search and uses `ilike` otherwise it uses `eq`. 538 | For string data types that uses `like` or `ilike` it automatically transform the filter `value` as `%value%`. 539 | 540 | ### Nested filtering 541 | 542 | Nested filtering is supported using # as a field separator. 543 | 544 | ```tsx 545 | 550 | ``` 551 | 552 | Will produce the following payload: 553 | 554 | ```json 555 | { 556 | "where": { 557 | "_and": [], 558 | "_or": [ 559 | { 560 | "indication": { 561 | "name": { 562 | "_ilike": "%TEXT%" 563 | } 564 | } 565 | }, 566 | { 567 | "drug": { 568 | "name": { 569 | "_ilike": "%TEXT%" 570 | } 571 | } 572 | }, 573 | { 574 | "sponsor": { 575 | "name": { 576 | "_ilike": "%TEXT%" 577 | } 578 | } 579 | } 580 | ] 581 | }, 582 | "limit": 10, 583 | "offset": 0, 584 | "order_by": { 585 | "id": "asc" 586 | } 587 | } 588 | ``` 589 | 590 | ## Jsonb filtering 591 | 592 | ```jsx 593 | 594 | ``` 595 | 596 | Will produce payload: 597 | 598 | ```json 599 | { 600 | "where": { 601 | "_and": [ 602 | { 603 | "users": { 604 | "preferences": { 605 | "_contains": { 606 | "ux": { 607 | "theme": "%TEXT" 608 | } 609 | } 610 | } 611 | } 612 | } 613 | ] 614 | }, 615 | "limit": 10, 616 | "offset": 0, 617 | "order_by": { 618 | "id": "asc" 619 | } 620 | } 621 | ``` 622 | 623 | Fetch data matching a jsonb `_contains` operation 624 | 625 | ```jsx 626 | 632 | 633 | ... 634 | 635 | 636 | } /> 637 | ``` 638 | 639 | Will produce payload: 640 | 641 | ```json 642 | { 643 | "where": { 644 | "_and": [ 645 | { 646 | "payments": { 647 | "details": { 648 | "_contains": { 649 | "processor": { 650 | "%{rec.processor}_id": "%{rec.id}" 651 | } 652 | } 653 | } 654 | } 655 | } 656 | ] 657 | } 658 | } 659 | ``` 660 | 661 | ## Sorting lists by multiple columns 662 | 663 | Hasura support [sorting by multiple fields](https://hasura.io/docs/latest/graphql/core/databases/postgres/queries/sorting.html#sorting-by-multiple-fields) but React Admin itself doesn't allow the `List` component to receive an array as the `sort` prop. So to achieve sorting by multiple fields, separate the field and order values using a comma. 664 | 665 | For example, a list like 666 | 667 | ```jsx 668 | const TodoList = (props) => ( 669 | 670 | ... 671 | 672 | ); 673 | ``` 674 | 675 | will generate a query with an `order_by` variable like 676 | 677 | ``` 678 | order_by: [{ title: "asc" }, { is_completed: "desc" }] 679 | ``` 680 | 681 | Fields may contain dots to specify sorting by nested object properties similarly to React Admin `source` property. 682 | 683 | ## Contributing 684 | 685 | To modify, extend and test this package locally, 686 | 687 | ``` 688 | $ cd ra-data-hasura 689 | $ npm link 690 | ``` 691 | 692 | Now use this local package in your react app for testing 693 | 694 | ``` 695 | $ cd my-react-app 696 | $ npm link ra-data-hasura 697 | ``` 698 | 699 | Build the library by running `npm run build` and it will generate the transpiled version of the library under `lib` folder. 700 | 701 | ## Credits 702 | 703 | We would like to thank [Steams](https://github.com/Steams) and all the contributors to this library for porting this adapter to support GraphQL spec, since all the releases till v0.0.8 were based off the REST API spec. 704 | -------------------------------------------------------------------------------- /example/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # ra-data-hasura Example 2 | 3 | A sample React Admin app using ra-data-hasura and the [Hasura Super App schema](https://github.com/hasura/hasura-ecommerce). 4 | 5 | In the future, login functionality will be added. Meanwhile, a JWT representing a site-admin role has been pre-generated. 6 | 7 | You should be able to view, edit, and delete different schema items as well as see their relationships. 8 | 9 | To run: 10 | 11 | ```bash 12 | docker compose up -d 13 | 14 | hasura seed apply --database-name default --project hasura 15 | 16 | npm i 17 | 18 | npm run start 19 | ``` 20 | 21 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 22 | -------------------------------------------------------------------------------- /example/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15 4 | restart: always 5 | volumes: 6 | - db_data:/var/lib/postgresql/data 7 | ports: 8 | # Expose the port for tooling (SQL language server in IDE, connecting with GUI's etc) 9 | - 5432:5432 10 | environment: 11 | POSTGRES_PASSWORD: postgrespassword 12 | 13 | graphql-engine: 14 | image: hasura/graphql-engine:v2.15.2.cli-migrations-v3 15 | volumes: 16 | - ./hasura/migrations:/hasura-migrations 17 | - ./hasura/metadata:/hasura-metadata 18 | ports: 19 | - 8080:8080 20 | depends_on: 21 | - 'postgres' 22 | restart: always 23 | environment: 24 | NEXTJS_SERVER_URL: http://nextjs:3000 25 | HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres 26 | PG_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres 27 | ## enable the console served by server 28 | HASURA_GRAPHQL_ENABLE_CONSOLE: 'true' # set to "false" to disable console 29 | ## enable debugging mode. It is recommended to disable this in production 30 | HASURA_GRAPHQL_DEV_MODE: 'true' 31 | HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log 32 | HASURA_GRAPHQL_ADMIN_SECRET: my-secret 33 | HASURA_GRAPHQL_JWT_SECRET: '{ "type": "HS256", "key": "this-is-a-generic-HS256-secret-key-and-you-should-really-change-it" }' 34 | HASURA_GRAPHQL_UNAUTHORIZED_ROLE: anonymous 35 | 36 | volumes: 37 | db_data: 38 | -------------------------------------------------------------------------------- /example/hasura/config.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | endpoint: http://localhost:8080 3 | admin_secret: my-secret 4 | metadata_directory: metadata 5 | actions: 6 | kind: synchronous 7 | handler_webhook_baseurl: http://localhost:3000 8 | -------------------------------------------------------------------------------- /example/hasura/metadata/actions.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | adminLogin(params: AdminLoginInput!): JWT 3 | } 4 | 5 | type Mutation { 6 | adminSignup(params: AdminSignupInput!): JWT 7 | } 8 | 9 | type Mutation { 10 | createPaymentIntent( 11 | params: CreatePaymentIntentInput! 12 | ): PaymentIntentClientSecret 13 | } 14 | 15 | type Mutation { 16 | login(params: LoginInput!): JWT 17 | } 18 | 19 | type Query { 20 | refreshToken(params: RefreshTokenInput!): RefreshTokenJWT 21 | } 22 | 23 | type Mutation { 24 | signup(params: SignupInput!): JWT 25 | } 26 | 27 | input SignupInput { 28 | name: String! 29 | email: String! 30 | password: String! 31 | } 32 | 33 | input LoginInput { 34 | email: String! 35 | password: String! 36 | } 37 | 38 | input AdminLoginInput { 39 | email: String! 40 | password: String! 41 | } 42 | 43 | input AdminSignupInput { 44 | name: String! 45 | email: String! 46 | password: String! 47 | } 48 | 49 | input CreatePaymentIntentInput { 50 | paymentAmount: Float! 51 | } 52 | 53 | input RefreshTokenInput { 54 | refreshToken: String! 55 | } 56 | 57 | type PaymentIntentClientSecret { 58 | clientSecret: String! 59 | } 60 | 61 | type JWT { 62 | name: String! 63 | email: String! 64 | token: String! 65 | refreshToken: String! 66 | } 67 | 68 | type RefreshTokenJWT { 69 | token: String! 70 | } 71 | -------------------------------------------------------------------------------- /example/hasura/metadata/actions.yaml: -------------------------------------------------------------------------------- 1 | actions: 2 | - name: adminLogin 3 | definition: 4 | kind: '' 5 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/admin-login' 6 | permissions: 7 | - role: anonymous 8 | - name: adminSignup 9 | definition: 10 | kind: synchronous 11 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/admin-signup' 12 | permissions: 13 | - role: site-admin 14 | - name: createPaymentIntent 15 | definition: 16 | kind: synchronous 17 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/create-payment-intent' 18 | permissions: 19 | - role: anonymous 20 | - role: site-admin 21 | - role: user 22 | - name: login 23 | definition: 24 | kind: synchronous 25 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/login' 26 | permissions: 27 | - role: anonymous 28 | - name: refreshToken 29 | definition: 30 | kind: '' 31 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/refresh-token' 32 | permissions: 33 | - role: anonymous 34 | - role: site-admin 35 | - role: user 36 | - name: signup 37 | definition: 38 | kind: synchronous 39 | handler: '{{NEXTJS_SERVER_URL}}/api/actions/signup' 40 | permissions: 41 | - role: anonymous 42 | custom_types: 43 | enums: [] 44 | input_objects: 45 | - name: SignupInput 46 | - name: LoginInput 47 | - name: AdminLoginInput 48 | - name: AdminSignupInput 49 | - name: CreatePaymentIntentInput 50 | - name: RefreshTokenInput 51 | objects: 52 | - name: PaymentIntentClientSecret 53 | - name: JWT 54 | - name: RefreshTokenJWT 55 | scalars: [] 56 | -------------------------------------------------------------------------------- /example/hasura/metadata/allow_list.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/api_limits.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/cron_triggers.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/databases.yaml: -------------------------------------------------------------------------------- 1 | - name: default 2 | kind: postgres 3 | configuration: 4 | connection_info: 5 | use_prepared_statements: false 6 | database_url: 7 | from_env: PG_DATABASE_URL 8 | isolation_level: read-committed 9 | tables: '!include default/tables/tables.yaml' 10 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_address.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: address 4 | object_relationships: 5 | - name: user 6 | using: 7 | foreign_key_constraint_on: user_id 8 | array_relationships: 9 | - name: orders_with_billing_address 10 | using: 11 | foreign_key_constraint_on: 12 | column: billing_address_id 13 | table: 14 | schema: public 15 | name: order 16 | - name: orders_with_shipping_address 17 | using: 18 | foreign_key_constraint_on: 19 | column: shipping_address_id 20 | table: 21 | schema: public 22 | name: order 23 | insert_permissions: 24 | - role: site-admin 25 | permission: 26 | check: {} 27 | columns: '*' 28 | - role: user 29 | permission: 30 | check: 31 | user_id: 32 | _eq: X-Hasura-User-Id 33 | columns: '*' 34 | select_permissions: 35 | - role: site-admin 36 | permission: 37 | columns: '*' 38 | filter: {} 39 | allow_aggregations: true 40 | - role: user 41 | permission: 42 | columns: '*' 43 | filter: 44 | user_id: 45 | _eq: X-Hasura-User-Id 46 | update_permissions: 47 | - role: site-admin 48 | permission: 49 | columns: '*' 50 | filter: {} 51 | check: null 52 | - role: user 53 | permission: 54 | columns: '*' 55 | filter: 56 | user_id: 57 | _eq: X-Hasura-User-Id 58 | check: null 59 | delete_permissions: 60 | - role: site-admin 61 | permission: 62 | filter: {} 63 | - role: user 64 | permission: 65 | filter: 66 | user_id: 67 | _eq: X-Hasura-User-Id 68 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_order.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: order 4 | object_relationships: 5 | - name: billing_address 6 | using: 7 | foreign_key_constraint_on: billing_address_id 8 | - name: order_status 9 | using: 10 | foreign_key_constraint_on: status 11 | - name: shipping_address 12 | using: 13 | foreign_key_constraint_on: shipping_address_id 14 | - name: user 15 | using: 16 | foreign_key_constraint_on: user_id 17 | array_relationships: 18 | - name: products 19 | using: 20 | foreign_key_constraint_on: 21 | column: order_id 22 | table: 23 | schema: public 24 | name: order_product 25 | insert_permissions: 26 | - role: site-admin 27 | permission: 28 | check: {} 29 | columns: '*' 30 | - role: user 31 | permission: 32 | check: 33 | user_id: 34 | _eq: X-Hasura-User-Id 35 | columns: '*' 36 | select_permissions: 37 | - role: site-admin 38 | permission: 39 | columns: '*' 40 | filter: {} 41 | allow_aggregations: true 42 | - role: user 43 | permission: 44 | columns: '*' 45 | filter: 46 | user_id: 47 | _eq: X-Hasura-User-Id 48 | update_permissions: 49 | - role: site-admin 50 | permission: 51 | columns: '*' 52 | filter: {} 53 | check: null 54 | delete_permissions: 55 | - role: site-admin 56 | permission: 57 | filter: {} 58 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_order_product.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: order_product 4 | object_relationships: 5 | - name: order 6 | using: 7 | foreign_key_constraint_on: order_id 8 | - name: product 9 | using: 10 | foreign_key_constraint_on: product_id 11 | insert_permissions: 12 | - role: site-admin 13 | permission: 14 | check: {} 15 | columns: '*' 16 | - role: user 17 | permission: 18 | check: 19 | order: 20 | user_id: 21 | _eq: X-Hasura-User-Id 22 | columns: '*' 23 | select_permissions: 24 | - role: site-admin 25 | permission: 26 | columns: '*' 27 | filter: {} 28 | allow_aggregations: true 29 | - role: user 30 | permission: 31 | columns: '*' 32 | filter: 33 | order: 34 | user_id: 35 | _eq: X-Hasura-User-Id 36 | update_permissions: 37 | - role: site-admin 38 | permission: 39 | columns: '*' 40 | filter: {} 41 | check: null 42 | delete_permissions: 43 | - role: site-admin 44 | permission: 45 | filter: {} 46 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_order_status.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: order_status 4 | is_enum: true 5 | array_relationships: 6 | - name: orders 7 | using: 8 | foreign_key_constraint_on: 9 | column: status 10 | table: 11 | schema: public 12 | name: order 13 | insert_permissions: 14 | - role: site-admin 15 | permission: 16 | check: {} 17 | columns: '*' 18 | select_permissions: 19 | - role: site-admin 20 | permission: 21 | columns: '*' 22 | filter: {} 23 | - role: user 24 | permission: 25 | columns: '*' 26 | filter: {} 27 | update_permissions: 28 | - role: site-admin 29 | permission: 30 | columns: '*' 31 | filter: {} 32 | check: null 33 | delete_permissions: 34 | - role: site-admin 35 | permission: 36 | filter: {} 37 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_product.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: product 4 | object_relationships: 5 | - name: category 6 | using: 7 | foreign_key_constraint_on: category_display_name 8 | array_relationships: 9 | - name: orders 10 | using: 11 | foreign_key_constraint_on: 12 | column: product_id 13 | table: 14 | schema: public 15 | name: order_product 16 | - name: product_reviews 17 | using: 18 | foreign_key_constraint_on: 19 | column: product_id 20 | table: 21 | schema: public 22 | name: product_review 23 | insert_permissions: 24 | - role: site-admin 25 | permission: 26 | check: {} 27 | columns: '*' 28 | select_permissions: 29 | - role: anonymous 30 | permission: 31 | columns: '*' 32 | filter: {} 33 | allow_aggregations: true 34 | - role: site-admin 35 | permission: 36 | columns: '*' 37 | filter: {} 38 | allow_aggregations: true 39 | - role: user 40 | permission: 41 | columns: '*' 42 | filter: {} 43 | update_permissions: 44 | - role: site-admin 45 | permission: 46 | columns: '*' 47 | filter: {} 48 | check: null 49 | delete_permissions: 50 | - role: site-admin 51 | permission: 52 | filter: {} 53 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_product_category_enum.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: product_category_enum 4 | is_enum: true 5 | array_relationships: 6 | - name: products 7 | using: 8 | foreign_key_constraint_on: 9 | column: category_display_name 10 | table: 11 | schema: public 12 | name: product 13 | insert_permissions: 14 | - role: site-admin 15 | permission: 16 | check: {} 17 | columns: '*' 18 | select_permissions: 19 | - role: anonymous 20 | permission: 21 | columns: '*' 22 | filter: {} 23 | - role: site-admin 24 | permission: 25 | columns: '*' 26 | filter: {} 27 | - role: user 28 | permission: 29 | columns: '*' 30 | filter: {} 31 | update_permissions: 32 | - role: site-admin 33 | permission: 34 | columns: '*' 35 | filter: {} 36 | check: null 37 | delete_permissions: 38 | - role: site-admin 39 | permission: 40 | filter: {} 41 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_product_review.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: product_review 4 | object_relationships: 5 | - name: product 6 | using: 7 | foreign_key_constraint_on: product_id 8 | - name: user 9 | using: 10 | foreign_key_constraint_on: user_id 11 | insert_permissions: 12 | - role: site-admin 13 | permission: 14 | check: {} 15 | columns: '*' 16 | - role: user 17 | permission: 18 | check: 19 | user: 20 | id: 21 | _eq: X-Hasura-User-Id 22 | orders: 23 | products: 24 | id: 25 | _ceq: product_id 26 | set: 27 | user_id: X-Hasura-User-Id 28 | columns: 29 | - product_id 30 | - rating 31 | - comment 32 | select_permissions: 33 | - role: anonymous 34 | permission: 35 | columns: '*' 36 | filter: {} 37 | - role: site-admin 38 | permission: 39 | columns: '*' 40 | filter: {} 41 | allow_aggregations: true 42 | - role: user 43 | permission: 44 | columns: '*' 45 | filter: {} 46 | update_permissions: 47 | - role: site-admin 48 | permission: 49 | columns: '*' 50 | filter: {} 51 | check: null 52 | - role: user 53 | permission: 54 | columns: 55 | - product_id 56 | - rating 57 | - comment 58 | filter: 59 | user: 60 | id: 61 | _eq: X-Hasura-User-Id 62 | orders: 63 | products: 64 | id: 65 | _ceq: product_id 66 | check: null 67 | set: 68 | user_id: X-Hasura-User-Id 69 | delete_permissions: 70 | - role: site-admin 71 | permission: 72 | filter: {} 73 | - role: user 74 | permission: 75 | filter: 76 | user: 77 | id: 78 | _eq: X-Hasura-User-Id 79 | orders: 80 | products: 81 | id: 82 | _ceq: product_id 83 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_site_admin.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: site_admin 4 | insert_permissions: 5 | - role: site-admin 6 | permission: 7 | check: {} 8 | columns: '*' 9 | select_permissions: 10 | - role: site-admin 11 | permission: 12 | columns: '*' 13 | filter: {} 14 | update_permissions: 15 | - role: site-admin 16 | permission: 17 | columns: '*' 18 | filter: {} 19 | check: null 20 | delete_permissions: 21 | - role: site-admin 22 | permission: 23 | filter: {} 24 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/public_user.yaml: -------------------------------------------------------------------------------- 1 | table: 2 | schema: public 3 | name: user 4 | array_relationships: 5 | - name: addresses 6 | using: 7 | foreign_key_constraint_on: 8 | column: user_id 9 | table: 10 | schema: public 11 | name: address 12 | - name: orders 13 | using: 14 | foreign_key_constraint_on: 15 | column: user_id 16 | table: 17 | schema: public 18 | name: order 19 | - name: product_reviews 20 | using: 21 | foreign_key_constraint_on: 22 | column: user_id 23 | table: 24 | schema: public 25 | name: product_review 26 | insert_permissions: 27 | - role: site-admin 28 | permission: 29 | check: {} 30 | columns: '*' 31 | select_permissions: 32 | - role: site-admin 33 | permission: 34 | columns: '*' 35 | filter: {} 36 | allow_aggregations: true 37 | - role: user 38 | permission: 39 | columns: '*' 40 | filter: 41 | id: 42 | _eq: X-Hasura-User-Id 43 | update_permissions: 44 | - role: site-admin 45 | permission: 46 | columns: '*' 47 | filter: {} 48 | check: null 49 | delete_permissions: 50 | - role: site-admin 51 | permission: 52 | filter: {} 53 | -------------------------------------------------------------------------------- /example/hasura/metadata/databases/default/tables/tables.yaml: -------------------------------------------------------------------------------- 1 | - '!include public_address.yaml' 2 | - '!include public_order.yaml' 3 | - '!include public_order_product.yaml' 4 | - '!include public_order_status.yaml' 5 | - '!include public_product.yaml' 6 | - '!include public_product_category_enum.yaml' 7 | - '!include public_product_review.yaml' 8 | - '!include public_site_admin.yaml' 9 | - '!include public_user.yaml' 10 | -------------------------------------------------------------------------------- /example/hasura/metadata/graphql_schema_introspection.yaml: -------------------------------------------------------------------------------- 1 | disabled_for_roles: [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/inherited_roles.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/network.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/query_collections.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/remote_schemas.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/rest_endpoints.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /example/hasura/metadata/version.yaml: -------------------------------------------------------------------------------- 1 | version: 3 2 | -------------------------------------------------------------------------------- /example/hasura/migrations/default/1646834482402_init/up.sql: -------------------------------------------------------------------------------- 1 | CREATE FUNCTION public.set_current_timestamp_updated_at () 2 | RETURNS TRIGGER 3 | LANGUAGE plpgsql 4 | AS $$ 5 | DECLARE 6 | _new record; 7 | BEGIN 8 | _new := NEW; 9 | _new. "updated_at" = NOW(); 10 | RETURN _new; 11 | END; 12 | $$; 13 | 14 | -- Create an "url" Postgres type that is an alias for "text" 15 | -- Which validates the input is an URL 16 | CREATE DOMAIN url AS text CHECK (VALUE ~ 'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#()?&//=]*)'); 17 | 18 | COMMENT ON DOMAIN url IS 'Match URLs (http or https)'; 19 | 20 | 21 | CREATE TABLE "address" ( 22 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 23 | name text, 24 | created_at timestamptz DEFAULT now() NOT NULL, 25 | updated_at timestamptz DEFAULT now() NOT NULL, 26 | user_id int NOT NULL, 27 | city text NOT NULL, 28 | state text NOT NULL, 29 | -- Use text for zipcode to handle ZIP+4 extended zipcodes 30 | zipcode text NOT NULL, 31 | address_line_one text NOT NULL, 32 | address_line_two text 33 | ); 34 | 35 | COMMENT ON TABLE address IS 'A physical billing/shipping address, attached to a user account'; 36 | 37 | 38 | CREATE TABLE "site_admin" ( 39 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 40 | created_at timestamptz DEFAULT now() NOT NULL, 41 | updated_at timestamptz DEFAULT now() NOT NULL, 42 | name text NOT NULL, 43 | email text NOT NULL UNIQUE, 44 | password text NOT NULL, 45 | refresh_token text UNIQUE 46 | ); 47 | 48 | COMMENT ON TABLE "site_admin" IS 'Someone administrative capabilities on the site'; 49 | 50 | COMMENT ON COLUMN "site_admin"."password" IS 'A bcrypt-hashed version of the admin password, compared against securely in the JWT Auth API handler for sign-in'; 51 | 52 | 53 | CREATE TABLE "order" ( 54 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 55 | created_at timestamptz DEFAULT now() NOT NULL, 56 | updated_at timestamptz DEFAULT now() NOT NULL, 57 | user_id int NOT NULL, 58 | billing_address_id int NOT NULL, 59 | shipping_address_id int NOT NULL, 60 | is_shipped boolean DEFAULT FALSE NOT NULL, 61 | order_total numeric, 62 | status text DEFAULT 'CREATED' NOT NULL 63 | ); 64 | 65 | COMMENT ON TABLE "order" IS 'An order from a customer, containing one or more products and quantities'; 66 | 67 | 68 | CREATE TABLE "order_product" ( 69 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 70 | created_at timestamptz DEFAULT now() NOT NULL, 71 | updated_at timestamptz DEFAULT now() NOT NULL, 72 | order_id int NOT NULL, 73 | product_id int NOT NULL, 74 | quantity int NOT NULL 75 | ); 76 | 77 | COMMENT ON TABLE "order_product" IS 'A product belonging to a customer order, along with a quantity'; 78 | 79 | 80 | CREATE TABLE order_status ( 81 | status text PRIMARY KEY 82 | ); 83 | 84 | INSERT INTO order_status ( 85 | status) 86 | VALUES ( 87 | 'CREATED'), 88 | ( 89 | 'PAID'), 90 | ( 91 | 'SHIPPED'), 92 | ( 93 | 'DELIVERED'), 94 | ( 95 | 'CANCELLED'), 96 | ( 97 | 'REFUNDED'); 98 | CREATE TABLE product ( 99 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 100 | created_at timestamptz DEFAULT now() NOT NULL, 101 | updated_at timestamptz DEFAULT now() NOT NULL, 102 | name text NOT NULL, 103 | -- Do we want description to be mandatory? 104 | description text, 105 | category_display_name text NOT NULL, 106 | brand text, 107 | price numeric NOT NULL, 108 | -- TODO: Make "product_image_url" table and one-to-many relationship from product -> product_image_url 109 | -- The Kaggle data just came with it in this format so we can hackily (ab)use JSONB for shitty relationships 110 | image_urls jsonb 111 | ); 112 | 113 | 114 | -- Hasura Enum table containing product categories 115 | -- Arguably, making categories an Enum might not be a best practice 116 | -- 117 | -- For a few reasons: 118 | -- 119 | -- Primarily that, because each change to the Enum table (insert/update/delete) modifies the GQL schema types 120 | -- any sort of Client SDK containing the schema types has to be regenerated to stay up-to-date after any changes 121 | -- 122 | -- And also because the metadata needs to be reloaded after modifications to the enum, since this can't be detected and reflected automatically yet 123 | -- 124 | -- But, this is a great way to demo this feature and a semi-realistic usecase 125 | CREATE TABLE product_category_enum ( 126 | name text PRIMARY KEY, 127 | display_name text NOT NULL UNIQUE 128 | ); 129 | 130 | INSERT INTO product_category_enum ( 131 | display_name, 132 | name) 133 | VALUES ( 134 | 'Home Furnishing', 135 | 'home_furnishing'), 136 | ( 137 | 'Computers', 138 | 'computers'), 139 | ( 140 | 'Baby Care', 141 | 'baby_care'), 142 | ( 143 | 'Wearable Smart Devices', 144 | 'wearable_smart_devices'), 145 | ( 146 | 'Furniture', 147 | 'furniture'), 148 | ( 149 | 'Home Entertainment', 150 | 'home_entertainment'), 151 | ( 152 | 'Home & Kitchen', 153 | 'home_kitchen'), 154 | ( 155 | 'Clothing', 156 | 'clothing'), 157 | ( 158 | 'Beauty and Personal Care', 159 | 'beauty_and_personal_care'), 160 | ( 161 | 'Sunglasses', 162 | 'sunglasses'), 163 | ( 164 | 'Tools & Hardware', 165 | 'tools_hardware'), 166 | ( 167 | 'Household Supplies', 168 | 'household_supplies'), 169 | ( 170 | 'Home Improvement', 171 | 'home_improvement'), 172 | ( 173 | 'Footwear', 174 | 'footwear'), 175 | ( 176 | 'Gaming', 177 | 'gaming'), 178 | ( 179 | 'Mobiles & Accessories', 180 | 'mobiles_accessories'), 181 | ( 182 | 'Sports & Fitness', 183 | 'sports_fitness'), 184 | ( 185 | 'Health & Personal Care Appliances', 186 | 'health_personal_care_appliances'), 187 | ( 188 | 'Home Decor & Festive Needs', 189 | 'home_decor_festive_needs'), 190 | ( 191 | 'Pens & Stationery', 192 | 'pens_stationery'), 193 | ( 194 | 'Watches', 195 | 'watches'), 196 | ( 197 | 'Food & Nutrition', 198 | 'food_nutrition'), 199 | ( 200 | 'Kitchen & Dining', 201 | 'kitchen_dining'), 202 | ( 203 | 'Pet Supplies', 204 | 'pet_supplies'), 205 | ( 206 | 'Jewellery', 207 | 'jewellery'), 208 | ( 209 | 'Cameras & Accessories', 210 | 'cameras_accessories'), 211 | ( 212 | 'Automotive', 213 | 'automotive'), 214 | ( 215 | 'eBooks', 216 | 'e_books'), 217 | ( 218 | 'Toys & School Supplies', 219 | 'toys_school_supplies'), 220 | ( 221 | 'Eyewear', 222 | 'eyewear'), 223 | ( 224 | 'Automation & Robotics', 225 | 'automation_robotics'), 226 | ( 227 | 'Bags, Wallets & Belts', 228 | 'bags_wallets_belts'); 229 | 230 | 231 | CREATE TABLE "product_review" ( 232 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 233 | created_at timestamptz DEFAULT now() NOT NULL, 234 | updated_at timestamptz DEFAULT now() NOT NULL, 235 | user_id int NOT NULL, 236 | product_id int NOT NULL, 237 | rating int NOT NULL CHECK (rating >= 1 AND rating <= 5), 238 | -- 5,000 characters = 800 words or 1.5 pages single-spaced (based on 6-char word avg) 239 | comment text NOT NULL CHECK (char_length(comment) <= 5000), 240 | -- Only allow each person to leave one review per product they've purchased 241 | CONSTRAINT one_review_per_person_and_product UNIQUE (user_id, product_id) 242 | ); 243 | 244 | COMMENT ON TABLE "product_review" IS 'A review for a product which a customer has purchased before'; 245 | 246 | 247 | CREATE TABLE "user" ( 248 | id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 249 | created_at timestamptz DEFAULT now() NOT NULL, 250 | updated_at timestamptz DEFAULT now() NOT NULL, 251 | name text NOT NULL, 252 | email text NOT NULL UNIQUE, 253 | password text NOT NULL, 254 | -- refresh_token really should be NOT NULL but the seed data doesn't have them 255 | refresh_token text UNIQUE 256 | ); 257 | 258 | COMMENT ON TABLE "user" IS 'Someone with an account on the site, who uses it to make purchases'; 259 | 260 | COMMENT ON COLUMN "user"."password" IS 'A bcrypt-hashed version of the user password, compared against securely in the JWT Auth API handler for sign-in'; 261 | 262 | 263 | ALTER TABLE ONLY public.address 264 | ADD CONSTRAINT address_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user (id); 265 | 266 | 267 | ALTER TABLE ONLY public.order 268 | ADD CONSTRAINT order_billing_address_id_fkey FOREIGN KEY 269 | (billing_address_id) REFERENCES public.address (id); 270 | 271 | ALTER TABLE ONLY public.order 272 | ADD CONSTRAINT order_shipping_address_id_fkey FOREIGN KEY 273 | (shipping_address_id) REFERENCES public.address (id); 274 | 275 | ALTER TABLE ONLY public.order 276 | ADD CONSTRAINT order_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user (id); 277 | 278 | ALTER TABLE ONLY public.order 279 | ADD CONSTRAINT order_status_fkey FOREIGN KEY (status) REFERENCES 280 | public.order_status (status); 281 | 282 | 283 | /*Functions*/ 284 | CREATE OR REPLACE FUNCTION public.gen_order_total () 285 | RETURNS TRIGGER 286 | LANGUAGE plpgsql 287 | STABLE 288 | AS $function$ 289 | DECLARE 290 | sumtotal numeric; 291 | BEGIN 292 | SELECT 293 | TRUNC(SUM(p.price), 2) INTO STRICT sumtotal 294 | FROM 295 | public.order o 296 | INNER JOIN public.order_product op ON (o.id = op.order_id) 297 | INNER JOIN public.product p ON (op.product_id = p.id) 298 | WHERE 299 | o.id = OLD.id 300 | GROUP BY 301 | o.id; 302 | NEW.order_total := sumtotal; 303 | RETURN NEW; 304 | EXCEPTION 305 | WHEN no_data_found THEN 306 | RAISE NOTICE 'No products found for %', OLD.id; 307 | RETURN NEW; 308 | END; 309 | 310 | $function$; 311 | 312 | 313 | ALTER TABLE ONLY public.order_product 314 | ADD CONSTRAINT order_id_fkey FOREIGN KEY (order_id) REFERENCES public.order (id); 315 | 316 | ALTER TABLE ONLY public.order_product 317 | ADD CONSTRAINT product_id_fkey FOREIGN KEY (product_id) REFERENCES public.product (id); 318 | 319 | 320 | ALTER TABLE ONLY public.product 321 | ADD CONSTRAINT category_display_name_fk FOREIGN KEY (category_display_name) REFERENCES public.product_category_enum (display_name); 322 | 323 | 324 | ALTER TABLE ONLY public.product_review 325 | ADD CONSTRAINT user_id_fkey FOREIGN KEY (user_id) REFERENCES public.user (id); 326 | 327 | ALTER TABLE ONLY public.product_review 328 | ADD CONSTRAINT product_id_fkey FOREIGN KEY (product_id) REFERENCES public.product (id); 329 | 330 | 331 | CREATE TRIGGER set_address_updated_at 332 | BEFORE UPDATE ON public.address 333 | FOR EACH ROW 334 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 335 | CREATE TRIGGER set_site_admin_updated_at 336 | BEFORE UPDATE ON public.site_admin 337 | FOR EACH ROW 338 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 339 | CREATE TRIGGER set_order_updated_at 340 | BEFORE UPDATE ON public.order 341 | FOR EACH ROW 342 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 343 | 344 | CREATE TRIGGER sum_order 345 | BEFORE INSERT OR UPDATE ON public.order 346 | FOR EACH ROW 347 | EXECUTE PROCEDURE public.gen_order_total (); 348 | CREATE TRIGGER set_order_product_updated_at 349 | BEFORE UPDATE ON public.order_product 350 | FOR EACH ROW 351 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 352 | CREATE TRIGGER set_product_updated_at 353 | BEFORE UPDATE ON public.product 354 | FOR EACH ROW 355 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 356 | 357 | CREATE TRIGGER set_product_review_updated_at 358 | BEFORE UPDATE ON public.product_review 359 | FOR EACH ROW 360 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); 361 | CREATE TRIGGER set_user_updated_at 362 | BEFORE UPDATE ON public.user 363 | FOR EACH ROW 364 | EXECUTE FUNCTION public.set_current_timestamp_updated_at (); -------------------------------------------------------------------------------- /example/hasura/seeds/default/01_B_user_address_seeds.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | 3 | insert into public.address (user_id, city, state, zipcode, address_line_one) values 4 | (4, 'Springfield', 'Massachusetts', '01129', '119 Victoria Trail'), 5 | (5, 'Indianapolis', 'Indiana', '46216', '06 Holy Cross Lane'), 6 | (6, 'Athens', 'Georgia', '30610', '1287 Clyde Gallagher Terrace'), 7 | (7, 'Orlando', 'Florida', '32813', '02572 Forest Way'), 8 | (8, 'Reading', 'Pennsylvania', '19605', '83625 Dawn Park'), 9 | (9, 'Daytona Beach', 'Florida', '32128', '9202 Carpenter Park'), 10 | (10, 'El Paso', 'Texas', '88579', '509 Grayhawk Pass'), 11 | (11, 'Tacoma', 'Washington', '98411', '5 Troy Hill'), 12 | (12, 'New Castle', 'Pennsylvania', '16107', '7 Bay Center'), 13 | (13, 'El Paso', 'Texas', '79955', '5763 Mendota Point'), 14 | (14, 'Anaheim', 'California', '92825', '7 Mayer Road'), 15 | (15, 'Dallas', 'Texas', '75372', '31319 Maple Wood Terrace'), 16 | (16, 'Anchorage', 'Alaska', '99599', '111 Bashford Pass'), 17 | (17, 'Tulsa', 'Oklahoma', '74184', '77416 Birchwood Junction'), 18 | (18, 'Oklahoma City', 'Oklahoma', '73109', '74037 Comanche Park'), 19 | (19, 'Fullerton', 'California', '92640', '36599 Pennsylvania Circle'), 20 | (20, 'San Jose', 'California', '95113', '1 Hoffman Center'), 21 | (21, 'Albuquerque', 'New Mexico', '87110', '950 Thompson Crossing'), 22 | (22, 'Oakland', 'California', '94611', '2933 Bellgrove Parkway'), 23 | (23, 'San Diego', 'California', '92153', '8962 Katie Lane'), 24 | (24, 'Amarillo', 'Texas', '79105', '2 Gina Circle'), 25 | (25, 'Lansing', 'Michigan', '48912', '60 Jenna Junction'), 26 | (26, 'Sacramento', 'California', '95823', '2196 Corscot Court'), 27 | (27, 'Elizabeth', 'New Jersey', '07208', '1 Portage Parkway'), 28 | (28, 'Norfolk', 'Virginia', '23514', '9 Knutson Hill'), 29 | (29, 'Indianapolis', 'Indiana', '46266', '2394 Talisman Lane'), 30 | (30, 'Tucson', 'Arizona', '85732', '139 Sherman Point'), 31 | (31, 'El Paso', 'Texas', '79989', '7 Grover Avenue'), 32 | (32, 'Los Angeles', 'California', '90005', '1620 Ilene Place'), 33 | (33, 'Huntington', 'West Virginia', '25770', '93 Macpherson Avenue'), 34 | (34, 'Kansas City', 'Kansas', '66160', '02 Green Ridge Road'), 35 | (35, 'Cincinnati', 'Ohio', '45243', '6 Cascade Road'), 36 | (36, 'Fresno', 'California', '93721', '55 New Castle Trail'), 37 | (37, 'Tulsa', 'Oklahoma', '74156', '2483 Upham Pass'), 38 | (38, 'Miami', 'Florida', '33261', '2 Jackson Drive'), 39 | (39, 'Houston', 'Texas', '77075', '89 Service Parkway'), 40 | (40, 'Lexington', 'Kentucky', '40586', '61159 Onsgard Crossing'), 41 | (41, 'Saint Augustine', 'Florida', '32092', '69 Magdeline Parkway'), 42 | (42, 'San Angelo', 'Texas', '76905', '1043 Novick Court'), 43 | (43, 'Norfolk', 'Virginia', '23520', '62190 Jana Trail'), 44 | (44, 'Canton', 'Ohio', '44710', '0 Eggendart Circle'), 45 | (45, 'Helena', 'Montana', '59623', '2 Thackeray Junction'), 46 | (46, 'Montgomery', 'Alabama', '36177', '012 Dixon Lane'), 47 | (47, 'Troy', 'Michigan', '48098', '4 Bashford Trail'), 48 | (48, 'Amarillo', 'Texas', '79105', '4176 Clyde Gallagher Drive'), 49 | (49, 'Stamford', 'Connecticut', '06922', '5975 Armistice Place'), 50 | (50, 'Rochester', 'New York', '14652', '811 Tony Lane'), 51 | (51, 'Houston', 'Texas', '77095', '737 Steensland Avenue'), 52 | (52, 'Alexandria', 'Virginia', '22333', '4777 Lerdahl Plaza'), 53 | (53, 'Knoxville', 'Tennessee', '37914', '01294 Warbler Crossing'), 54 | (54, 'Valdosta', 'Georgia', '31605', '1 Ramsey Parkway'), 55 | (55, 'Wilmington', 'North Carolina', '28410', '48 Main Alley'), 56 | (56, 'Nashville', 'Tennessee', '37228', '076 Buhler Point'), 57 | (57, 'Pittsburgh', 'Pennsylvania', '15279', '5291 Haas Junction'), 58 | (58, 'Birmingham', 'Alabama', '35205', '94012 Bellgrove Crossing'), 59 | (59, 'San Jose', 'California', '95160', '86685 Rieder Circle'), 60 | (60, 'Dayton', 'Ohio', '45440', '527 Hintze Point'), 61 | (61, 'Saint Louis', 'Missouri', '63196', '1 Kensington Parkway'), 62 | (62, 'Boston', 'Massachusetts', '02208', '45321 Thackeray Way'), 63 | (63, 'Denver', 'Colorado', '80279', '54 Sloan Way'), 64 | (64, 'Albuquerque', 'New Mexico', '87190', '1 Thompson Crossing'), 65 | (65, 'Clearwater', 'Florida', '34629', '00356 Sugar Center'), 66 | (66, 'Knoxville', 'Tennessee', '37939', '69 Ryan Point'), 67 | (67, 'Shreveport', 'Louisiana', '71166', '10473 Brown Circle'), 68 | (68, 'Houston', 'Texas', '77260', '44 Warrior Way'), 69 | (69, 'Albuquerque', 'New Mexico', '87195', '55415 Waywood Crossing'), 70 | (70, 'Lansing', 'Michigan', '48930', '9830 Sycamore Parkway'), 71 | (71, 'Waltham', 'Massachusetts', '02453', '24648 Lakewood Crossing'), 72 | (72, 'Lexington', 'Kentucky', '40505', '30550 Macpherson Parkway'), 73 | (73, 'Phoenix', 'Arizona', '85053', '36184 Eggendart Court'), 74 | (74, 'Gastonia', 'North Carolina', '28055', '5839 Dapin Pass'), 75 | (75, 'Baton Rouge', 'Louisiana', '70883', '1646 Merry Way'), 76 | (76, 'Newark', 'Delaware', '19725', '26795 Swallow Lane'), 77 | (77, 'Ogden', 'Utah', '84409', '3329 Jana Crossing'), 78 | (78, 'Raleigh', 'North Carolina', '27610', '01950 Graedel Park'), 79 | (79, 'San Antonio', 'Texas', '78260', '08 Elmside Alley'), 80 | (80, 'New York City', 'New York', '10170', '0 Swallow Court'), 81 | (81, 'San Antonio', 'Texas', '78240', '4991 Menomonie Junction'), 82 | (82, 'Austin', 'Texas', '78721', '854 Waywood Point'), 83 | (83, 'San Angelo', 'Texas', '76905', '7 Lindbergh Way'), 84 | (84, 'Los Angeles', 'California', '90071', '99 Welch Pass'), 85 | (85, 'Richmond', 'Virginia', '23228', '7 Commercial Road'), 86 | (86, 'Oklahoma City', 'Oklahoma', '73157', '1090 Hoepker Court'), 87 | (87, 'Columbia', 'South Carolina', '29208', '894 Hoard Way'), 88 | (88, 'Roanoke', 'Virginia', '24048', '1584 Rowland Plaza'), 89 | (89, 'Lexington', 'Kentucky', '40510', '0983 Elmside Way'), 90 | (90, 'Portland', 'Oregon', '97232', '264 Eggendart Hill'), 91 | (91, 'Houston', 'Texas', '77070', '54 Debs Terrace'), 92 | (92, 'Corpus Christi', 'Texas', '78410', '65 5th Road'), 93 | (93, 'Arlington', 'Virginia', '22244', '7 Arrowood Trail'), 94 | (94, 'Anchorage', 'Alaska', '99599', '2 Pearson Place'), 95 | (95, 'Canton', 'Ohio', '44760', '5 Lawn Court'), 96 | (96, 'Arlington', 'Virginia', '22234', '0607 Rockefeller Point'), 97 | (97, 'Saginaw', 'Michigan', '48604', '68566 Mallard Crossing'), 98 | (98, 'Detroit', 'Michigan', '48242', '7513 Prairieview Trail'), 99 | (99, 'Tampa', 'Florida', '33686', '7928 Golf Lane'), 100 | (100, 'San Francisco', 'California', '94154', '69 Burrows Way'), 101 | (101, 'Saginaw', 'Michigan', '48604', '8 Mesta Parkway'), 102 | (102, 'Akron', 'Ohio', '44305', '0316 Nova Street'), 103 | (103, 'Fresno', 'California', '93715', '8 Onsgard Drive'), 104 | (104, 'Boston', 'Massachusetts', '02298', '7134 Lawn Alley'), 105 | (105, 'Frederick', 'Maryland', '21705', '6471 Muir Lane'), 106 | (106, 'Salt Lake City', 'Utah', '84115', '3384 Carberry Alley'), 107 | (107, 'Washington', 'District of Columbia', '20057', '013 Thompson Place'), 108 | (108, 'Cleveland', 'Ohio', '44191', '17 Bowman Parkway'), 109 | (109, 'Saint Cloud', 'Minnesota', '56372', '1 Del Sol Way'), 110 | (110, 'El Paso', 'Texas', '79994', '1919 Ludington Pass'), 111 | (111, 'Allentown', 'Pennsylvania', '18105', '17647 Shoshone Road'), 112 | (112, 'Bronx', 'New York', '10459', '0 Namekagon Lane'), 113 | (113, 'Corona', 'California', '92878', '11 Monterey Pass'), 114 | (114, 'Pittsburgh', 'Pennsylvania', '15286', '206 Novick Parkway'), 115 | (115, 'Washington', 'District of Columbia', '20099', '73 Anzinger Trail'), 116 | (116, 'Jamaica', 'New York', '11436', '66 Meadow Vale Point'), 117 | (117, 'Washington', 'District of Columbia', '20456', '4675 Menomonie Hill'), 118 | (118, 'Cincinnati', 'Ohio', '45203', '9 La Follette Crossing'), 119 | (119, 'Jacksonville', 'Florida', '32215', '996 South Parkway'), 120 | (120, 'Greenville', 'South Carolina', '29605', '0106 Myrtle Hill'), 121 | (121, 'Shawnee Mission', 'Kansas', '66225', '0083 Gerald Park'), 122 | (122, 'Washington', 'District of Columbia', '20041', '72954 Loftsgordon Trail'), 123 | (123, 'San Antonio', 'Texas', '78260', '45 Elmside Terrace'), 124 | (124, 'Bronx', 'New York', '10454', '8867 Namekagon Hill'), 125 | (125, 'Corona', 'California', '92878', '17 Vidon Avenue'), 126 | (126, 'Moreno Valley', 'California', '92555', '933 Clyde Gallagher Parkway'), 127 | (127, 'Memphis', 'Tennessee', '38114', '3660 Colorado Lane'), 128 | (128, 'Bakersfield', 'California', '93381', '1557 Granby Alley'), 129 | (129, 'Gulfport', 'Mississippi', '39505', '357 Spaight Way'), 130 | (130, 'Evansville', 'Indiana', '47737', '366 Manley Court'), 131 | (131, 'Amarillo', 'Texas', '79159', '565 Michigan Road'), 132 | (132, 'Richmond', 'Virginia', '23220', '5 Hovde Road'), 133 | (133, 'Las Cruces', 'New Mexico', '88006', '680 Graceland Parkway'), 134 | (134, 'Albuquerque', 'New Mexico', '87110', '26 Shelley Avenue'), 135 | (135, 'Henderson', 'Nevada', '89012', '4 Sunnyside Place'), 136 | (136, 'Saint Paul', 'Minnesota', '55123', '01 3rd Way'), 137 | (137, 'Huntsville', 'Texas', '77343', '04 Trailsway Crossing'), 138 | (138, 'Saint Paul', 'Minnesota', '55172', '454 Bayside Crossing'), 139 | (139, 'El Paso', 'Texas', '79940', '91257 Comanche Hill'), 140 | (140, 'Corpus Christi', 'Texas', '78465', '8884 Lukken Crossing'), 141 | (141, 'Kalamazoo', 'Michigan', '49006', '05191 Orin Pass'), 142 | (142, 'Kansas City', 'Missouri', '64125', '68977 Porter Crossing'), 143 | (143, 'Kansas City', 'Missouri', '64114', '5367 Fieldstone Crossing'), 144 | (144, 'Chicago', 'Illinois', '60663', '6997 Loomis Trail'), 145 | (145, 'Tacoma', 'Washington', '98481', '8688 Dryden Crossing'), 146 | (146, 'Fort Lauderdale', 'Florida', '33320', '7 Warrior Place'), 147 | (147, 'San Diego', 'California', '92115', '53367 Darwin Plaza'), 148 | (148, 'Washington', 'District of Columbia', '20414', '1 Fairfield Plaza'), 149 | (149, 'Metairie', 'Louisiana', '70005', '1 Maryland Parkway'), 150 | (150, 'New York City', 'New York', '10155', '53 Johnson Place'), 151 | (151, 'Midland', 'Michigan', '48670', '945 Gateway Point'), 152 | (152, 'Lakeland', 'Florida', '33811', '3554 Pennsylvania Alley'), 153 | (153, 'Fort Lauderdale', 'Florida', '33345', '2458 Forest Place'), 154 | (154, 'Raleigh', 'North Carolina', '27635', '26 Huxley Way'), 155 | (155, 'Washington', 'District of Columbia', '20310', '03 Sommers Lane'), 156 | (156, 'Richmond', 'Virginia', '23277', '24775 Mallard Place'), 157 | (157, 'Littleton', 'Colorado', '80126', '09132 Myrtle Trail'), 158 | (158, 'Los Angeles', 'California', '90071', '625 Warbler Street'), 159 | (159, 'Washington', 'District of Columbia', '20503', '8 Green Plaza'), 160 | (160, 'Palatine', 'Illinois', '60078', '349 5th Street'), 161 | (161, 'Philadelphia', 'Pennsylvania', '19184', '01905 Amoth Circle'), 162 | (162, 'Bradenton', 'Florida', '34205', '5200 John Wall Crossing'), 163 | (163, 'Carson City', 'Nevada', '89714', '2323 Florence Court'), 164 | (164, 'Irving', 'Texas', '75037', '466 Charing Cross Avenue'), 165 | (165, 'Waterbury', 'Connecticut', '06726', '993 Anniversary Junction'), 166 | (166, 'Richmond', 'Virginia', '23272', '90896 Stone Corner Lane'), 167 | (167, 'Atlanta', 'Georgia', '30375', '2 Armistice Lane'), 168 | (168, 'Mesquite', 'Texas', '75185', '0 Weeping Birch Court'), 169 | (169, 'Los Angeles', 'California', '90060', '916 Butterfield Terrace'), 170 | (170, 'Seattle', 'Washington', '98104', '82201 Clarendon Avenue'), 171 | (171, 'Detroit', 'Michigan', '48275', '62567 Walton Circle'), 172 | (172, 'Arlington', 'Texas', '76004', '897 Anniversary Place'), 173 | (173, 'Austin', 'Texas', '78737', '309 Fisk Road'), 174 | (174, 'Austin', 'Texas', '78744', '1 Burning Wood Circle'), 175 | (175, 'Miami', 'Florida', '33233', '30120 Truax Point'), 176 | (176, 'Sacramento', 'California', '94263', '47485 Sauthoff Junction'), 177 | (177, 'Denver', 'Colorado', '80209', '704 Morrow Park'), 178 | (178, 'Cumming', 'Georgia', '30130', '61 Homewood Plaza'), 179 | (179, 'Watertown', 'Massachusetts', '02472', '443 Bonner Park'), 180 | (180, 'Houston', 'Texas', '77035', '454 Sycamore Drive'), 181 | (181, 'Monticello', 'Minnesota', '55590', '3 Carpenter Crossing'), 182 | (182, 'Colorado Springs', 'Colorado', '80945', '8 Red Cloud Lane'), 183 | (183, 'Los Angeles', 'California', '90101', '750 Loftsgordon Crossing'), 184 | (184, 'Escondido', 'California', '92030', '93634 Farwell Center'), 185 | (185, 'Racine', 'Wisconsin', '53405', '45 Golf View Point'), 186 | (186, 'Oakland', 'California', '94616', '29 Northview Avenue'), 187 | (187, 'Washington', 'District of Columbia', '20205', '2 Sommers Place'), 188 | (188, 'Las Vegas', 'Nevada', '89120', '31088 Warbler Terrace'), 189 | (189, 'Kansas City', 'Missouri', '64149', '413 Nevada Street'), 190 | (190, 'Fort Worth', 'Texas', '76115', '20 Colorado Place'), 191 | (191, 'Fort Worth', 'Texas', '76178', '35 Duke Crossing'), 192 | (192, 'Southfield', 'Michigan', '48076', '694 Monica Alley'), 193 | (193, 'Tampa', 'Florida', '33686', '5 Katie Park'), 194 | (194, 'Santa Fe', 'New Mexico', '87505', '3 Porter Lane'), 195 | (195, 'Saint Petersburg', 'Florida', '33705', '4 Autumn Leaf Court'), 196 | (196, 'Fort Smith', 'Arkansas', '72916', '6489 Loomis Center'), 197 | (197, 'Galveston', 'Texas', '77554', '80 Hanson Alley'), 198 | (198, 'Oklahoma City', 'Oklahoma', '73135', '9 Mayer Junction'), 199 | (199, 'San Antonio', 'Texas', '78220', '7367 Merry Junction'), 200 | (200, 'Elmira', 'New York', '14905', '066 Anzinger Point'), 201 | (201, 'West Hartford', 'Connecticut', '06127', '8386 Dawn Parkway'), 202 | (202, 'Davenport', 'Iowa', '52804', '8 Melrose Parkway'), 203 | (203, 'Washington', 'District of Columbia', '20530', '336 Rockefeller Park'), 204 | (204, 'Garland', 'Texas', '75044', '52 Talmadge Center'), 205 | (205, 'Springfield', 'Illinois', '62756', '9785 Birchwood Crossing'), 206 | (206, 'Tucson', 'Arizona', '85754', '61256 Meadow Valley Junction'), 207 | (207, 'San Jose', 'California', '95113', '1 Magdeline Park'), 208 | (208, 'El Paso', 'Texas', '79950', '8 Gateway Court'), 209 | (209, 'Dallas', 'Texas', '75387', '125 Muir Parkway'), 210 | (210, 'Clearwater', 'Florida', '34620', '1 Thompson Junction'), 211 | (211, 'Staten Island', 'New York', '10310', '44953 Clemons Way'), 212 | (212, 'Simi Valley', 'California', '93094', '941 Sage Trail'), 213 | (213, 'Atlanta', 'Georgia', '30358', '831 Ohio Way'), 214 | (214, 'Washington', 'District of Columbia', '20310', '49614 Sherman Court'), 215 | (215, 'Jackson', 'Mississippi', '39204', '22747 Fremont Street'), 216 | (216, 'Birmingham', 'Alabama', '35210', '871 Russell Point'), 217 | (217, 'Irving', 'Texas', '75062', '6 Melvin Way'), 218 | (218, 'Charleston', 'West Virginia', '25313', '6836 Clyde Gallagher Plaza'), 219 | (219, 'Metairie', 'Louisiana', '70033', '6 Eagle Crest Center'), 220 | (220, 'Fort Lauderdale', 'Florida', '33345', '07240 Commercial Center'), 221 | (221, 'Lexington', 'Kentucky', '40586', '4 Mcguire Center'), 222 | (222, 'Portland', 'Oregon', '97216', '8 Amoth Place'), 223 | (223, 'Bridgeport', 'Connecticut', '06606', '44 Kedzie Terrace'), 224 | (224, 'New York City', 'New York', '10079', '16 Novick Alley'), 225 | (225, 'Orlando', 'Florida', '32819', '920 Alpine Point'), 226 | (226, 'Bozeman', 'Montana', '59771', '1470 Vermont Court'), 227 | (227, 'New Orleans', 'Louisiana', '70124', '97640 Ruskin Crossing'), 228 | (228, 'Boston', 'Massachusetts', '02283', '5092 Union Point'), 229 | (229, 'Colorado Springs', 'Colorado', '80940', '563 Dunning Trail'), 230 | (230, 'Fort Wayne', 'Indiana', '46852', '82 Elka Plaza'), 231 | (231, 'Baltimore', 'Maryland', '21290', '8018 Ludington Way'), 232 | (232, 'Birmingham', 'Alabama', '35236', '8 Grim Junction'), 233 | (233, 'New York City', 'New York', '10105', '1856 Anderson Way'), 234 | (234, 'Seattle', 'Washington', '98158', '89 Merry Circle'), 235 | (235, 'Chicago', 'Illinois', '60636', '5539 Starling Junction'), 236 | (236, 'Peoria', 'Illinois', '61656', '4373 Arapahoe Way'), 237 | (237, 'Visalia', 'California', '93291', '295 Stephen Trail'), 238 | (238, 'Pensacola', 'Florida', '32575', '15 Quincy Trail'), 239 | (239, 'El Paso', 'Texas', '79989', '83 Southridge Court'), 240 | (240, 'Detroit', 'Michigan', '48211', '729 Lakewood Gardens Road'), 241 | (241, 'El Paso', 'Texas', '79968', '97598 Continental Parkway'), 242 | (242, 'Silver Spring', 'Maryland', '20910', '66055 4th Point'), 243 | (243, 'Boston', 'Massachusetts', '02109', '7 Lunder Circle'), 244 | (244, 'Fort Worth', 'Texas', '76162', '14 Bunker Hill Way'), 245 | (245, 'Atlanta', 'Georgia', '30323', '06215 Hoepker Alley'), 246 | (246, 'Shreveport', 'Louisiana', '71166', '660 Logan Crossing'), 247 | (247, 'Birmingham', 'Alabama', '35220', '66 Browning Road'), 248 | (248, 'Springfield', 'Missouri', '65898', '2 Marquette Circle'), 249 | (249, 'El Paso', 'Texas', '88519', '2 Jay Circle'), 250 | (250, 'Wilmington', 'North Carolina', '28410', '5263 Hoepker Lane'), 251 | (251, 'Pensacola', 'Florida', '32505', '44544 Browning Drive'), 252 | (252, 'Escondido', 'California', '92030', '86933 Loomis Junction'), 253 | (253, 'Nashville', 'Tennessee', '37235', '8539 Caliangt Crossing'), 254 | (254, 'El Paso', 'Texas', '79955', '8 Luster Trail'), 255 | (255, 'Jamaica', 'New York', '11431', '627 Stephen Hill'), 256 | (256, 'New Orleans', 'Louisiana', '70142', '7 Hauk Lane'), 257 | (257, 'Bronx', 'New York', '10454', '4 Ramsey Place'), 258 | (258, 'Lincoln', 'Nebraska', '68505', '6 Morrow Hill'), 259 | (259, 'Tucson', 'Arizona', '85710', '6790 Shopko Pass'), 260 | (260, 'Washington', 'District of Columbia', '20067', '5879 Bartillon Park'), 261 | (261, 'Charleston', 'West Virginia', '25331', '8897 Blaine Crossing'), 262 | (262, 'Fresno', 'California', '93715', '04 6th Trail'), 263 | (263, 'Elmira', 'New York', '14905', '383 Roth Crossing'), 264 | (264, 'Saint Louis', 'Missouri', '63167', '12642 David Parkway'), 265 | (265, 'Las Vegas', 'Nevada', '89110', '9371 Reinke Center'), 266 | (266, 'Madison', 'Wisconsin', '53710', '801 Northview Circle'), 267 | (267, 'Los Angeles', 'California', '90025', '1 Macpherson Hill'), 268 | (268, 'Houston', 'Texas', '77281', '48 Prentice Road'), 269 | (269, 'Columbia', 'South Carolina', '29208', '789 Goodland Pass'), 270 | (270, 'Santa Barbara', 'California', '93111', '062 Lerdahl Way'), 271 | (271, 'Greensboro', 'North Carolina', '27499', '61 Beilfuss Terrace'), 272 | (272, 'Fullerton', 'California', '92835', '3 Forest Run Lane'), 273 | (273, 'Albany', 'New York', '12262', '317 Forest Run Crossing'), 274 | (274, 'Anchorage', 'Alaska', '99522', '5 Esch Street'), 275 | (275, 'Salt Lake City', 'Utah', '84199', '59 Glacier Hill Circle'), 276 | (276, 'Jackson', 'Mississippi', '39282', '2 Golf View Avenue'), 277 | (277, 'Austin', 'Texas', '78710', '16780 Rockefeller Point'), 278 | (278, 'Gainesville', 'Florida', '32605', '16 Homewood Pass'), 279 | (279, 'Washington', 'District of Columbia', '20425', '81271 Karstens Parkway'), 280 | (280, 'Saint Louis', 'Missouri', '63110', '99256 Bobwhite Parkway'), 281 | (281, 'Boston', 'Massachusetts', '02216', '6 Fairfield Place'), 282 | (282, 'Minneapolis', 'Minnesota', '55446', '2156 Truax Court'), 283 | (283, 'Indianapolis', 'Indiana', '46231', '6034 Thompson Avenue'), 284 | (284, 'Tucson', 'Arizona', '85710', '3 Sachs Terrace'), 285 | (285, 'Providence', 'Rhode Island', '02905', '47009 Maryland Court'), 286 | (286, 'Omaha', 'Nebraska', '68134', '1 Nova Alley'), 287 | (287, 'Visalia', 'California', '93291', '706 Moulton Drive'), 288 | (288, 'Danbury', 'Connecticut', '06816', '0806 Park Meadow Place'), 289 | (289, 'Minneapolis', 'Minnesota', '55428', '48 Kennedy Hill'), 290 | (290, 'Lexington', 'Kentucky', '40515', '431 Browning Hill'), 291 | (291, 'Lincoln', 'Nebraska', '68531', '898 Longview Hill'), 292 | (292, 'Mobile', 'Alabama', '36616', '800 Hazelcrest Center'), 293 | (293, 'Chicago', 'Illinois', '60609', '0 Crownhardt Road'), 294 | (294, 'New York City', 'New York', '10105', '0 Rieder Junction'), 295 | (295, 'Orlando', 'Florida', '32835', '1 Mallard Way'), 296 | (296, 'San Francisco', 'California', '94147', '251 Acker Circle'), 297 | (297, 'Erie', 'Pennsylvania', '16565', '985 Dryden Crossing'), 298 | (298, 'Cleveland', 'Ohio', '44125', '74 Fuller Parkway'), 299 | (299, 'Oakland', 'California', '94605', '450 Shelley Place'), 300 | (300, 'Phoenix', 'Arizona', '85015', '89 Barnett Crossing'), 301 | (301, 'Macon', 'Georgia', '31296', '0 Bayside Point'), 302 | (302, 'Pinellas Park', 'Florida', '34665', '53 Duke Avenue'), 303 | (303, 'Jacksonville', 'Florida', '32204', '9666 Riverside Hill'), 304 | (304, 'Fredericksburg', 'Virginia', '22405', '301 Cody Center'), 305 | (305, 'El Paso', 'Texas', '79940', '2218 Washington Parkway'), 306 | (306, 'New York City', 'New York', '10150', '974 Aberg Pass'), 307 | (307, 'San Jose', 'California', '95160', '08109 Mayer Alley'), 308 | (308, 'Arlington', 'Virginia', '22212', '0985 Clarendon Trail'), 309 | (309, 'Baton Rouge', 'Louisiana', '70836', '8 Buell Street'), 310 | (310, 'South Bend', 'Indiana', '46614', '965 Sullivan Avenue'), 311 | (311, 'Austin', 'Texas', '78769', '48226 Straubel Junction'), 312 | (312, 'Kansas City', 'Missouri', '64190', '71300 Oak Valley Point'), 313 | (313, 'Greensboro', 'North Carolina', '27404', '19687 Maywood Drive'), 314 | (314, 'Jacksonville', 'Florida', '32204', '9 Rusk Parkway'), 315 | (315, 'Evansville', 'Indiana', '47719', '29 Donald Street'), 316 | (316, 'Lincoln', 'Nebraska', '68510', '88 Talisman Crossing'), 317 | (317, 'Los Angeles', 'California', '90035', '1036 Chive Street'), 318 | (318, 'Temple', 'Texas', '76505', '8730 Aberg Terrace'), 319 | (319, 'Waterbury', 'Connecticut', '06726', '3 2nd Trail'), 320 | (320, 'Atlanta', 'Georgia', '30392', '5 Namekagon Plaza'), 321 | (321, 'Terre Haute', 'Indiana', '47812', '31 Ilene Parkway'), 322 | (322, 'Pensacola', 'Florida', '32505', '74910 Bowman Avenue'), 323 | (323, 'Los Angeles', 'California', '90087', '527 Beilfuss Terrace'), 324 | (324, 'San Francisco', 'California', '94121', '19 Debs Terrace'), 325 | (325, 'Saint Louis', 'Missouri', '63116', '2 Mallard Court'), 326 | (326, 'Aurora', 'Colorado', '80015', '80010 Homewood Street'), 327 | (327, 'Seattle', 'Washington', '98140', '30 Troy Center'), 328 | (328, 'Kansas City', 'Kansas', '66160', '725 Brown Way'), 329 | (329, 'San Diego', 'California', '92153', '32 Green Ridge Hill'), 330 | (330, 'Springfield', 'Illinois', '62711', '53161 Tony Alley'), 331 | (331, 'Colorado Springs', 'Colorado', '80925', '14636 Forster Lane'), 332 | (332, 'Atlanta', 'Georgia', '30386', '90746 Lindbergh Park'), 333 | (333, 'Riverside', 'California', '92519', '58 Prairieview Pass'), 334 | (334, 'Des Moines', 'Iowa', '50981', '24 Sugar Way'), 335 | (335, 'Durham', 'North Carolina', '27717', '670 American Ash Street'), 336 | (336, 'Topeka', 'Kansas', '66617', '4407 Hollow Ridge Court'), 337 | (337, 'Cincinnati', 'Ohio', '45238', '60 Fisk Circle'), 338 | (338, 'Evansville', 'Indiana', '47747', '10893 Cascade Center'), 339 | (339, 'Los Angeles', 'California', '90076', '013 Gerald Crossing'), 340 | (340, 'Houston', 'Texas', '77288', '131 Pennsylvania Plaza'), 341 | (341, 'Jamaica', 'New York', '11407', '084 Kensington Circle'), 342 | (342, 'Seattle', 'Washington', '98121', '6117 Center Drive'), 343 | (343, 'West Hartford', 'Connecticut', '06127', '4 Red Cloud Alley'), 344 | (344, 'Sioux Falls', 'South Dakota', '57105', '5 Mcbride Crossing'), 345 | (345, 'Buffalo', 'New York', '14210', '148 Mayer Crossing'), 346 | (346, 'Springfield', 'Missouri', '65805', '98 Westend Drive'), 347 | (347, 'New York City', 'New York', '10270', '9 Spohn Park'), 348 | (348, 'Arlington', 'Virginia', '22244', '3786 Farragut Circle'), 349 | (349, 'New Orleans', 'Louisiana', '70174', '8674 Kinsman Park'), 350 | (350, 'Buffalo', 'New York', '14210', '951 Washington Way'), 351 | (351, 'Baton Rouge', 'Louisiana', '70820', '07 Manufacturers Court'), 352 | (352, 'Nashville', 'Tennessee', '37210', '518 Butternut Point'), 353 | (353, 'Seattle', 'Washington', '98121', '47 Graedel Street'), 354 | (354, 'Nashville', 'Tennessee', '37235', '6 Dunning Avenue'), 355 | (355, 'Orlando', 'Florida', '32813', '310 Sutherland Drive'), 356 | (356, 'Cleveland', 'Ohio', '44105', '5 Badeau Parkway'), 357 | (357, 'Monroe', 'Louisiana', '71208', '2833 Manley Trail'), 358 | (358, 'Des Moines', 'Iowa', '50315', '132 Dovetail Plaza'), 359 | (359, 'Bethesda', 'Maryland', '20816', '10859 Mccormick Drive'), 360 | (360, 'New Haven', 'Connecticut', '06520', '593 Schurz Place'), 361 | (361, 'Terre Haute', 'Indiana', '47805', '459 Quincy Alley'), 362 | (362, 'Lakeland', 'Florida', '33811', '52 Goodland Way'), 363 | (363, 'Roanoke', 'Virginia', '24014', '518 Armistice Center'), 364 | (364, 'Tampa', 'Florida', '33673', '3722 Larry Plaza'), 365 | (365, 'Fresno', 'California', '93762', '36096 Shopko Road'), 366 | (366, 'Winston Salem', 'North Carolina', '27110', '98 Spenser Pass'), 367 | (367, 'Chicago', 'Illinois', '60657', '29 Lawn Hill'), 368 | (368, 'Kansas City', 'Missouri', '64199', '72339 Boyd Center'), 369 | (369, 'Canton', 'Ohio', '44720', '9 Butternut Drive'), 370 | (370, 'Lawrenceville', 'Georgia', '30245', '350 Commercial Trail'), 371 | (371, 'Humble', 'Texas', '77346', '64 Rutledge Point'), 372 | (372, 'Newton', 'Massachusetts', '02162', '44549 1st Street'), 373 | (373, 'El Paso', 'Texas', '88530', '28836 Northwestern Park'), 374 | (374, 'Inglewood', 'California', '90305', '0 Melby Park'), 375 | (375, 'El Paso', 'Texas', '88574', '161 Schurz Place'), 376 | (376, 'Salem', 'Oregon', '97312', '4370 Dixon Circle'), 377 | (377, 'Philadelphia', 'Pennsylvania', '19104', '5652 Katie Avenue'), 378 | (378, 'Johnson City', 'Tennessee', '37605', '01234 Goodland Trail'), 379 | (379, 'Iowa City', 'Iowa', '52245', '75 Nevada Pass'), 380 | (380, 'San Diego', 'California', '92145', '6662 Daystar Drive'), 381 | (381, 'Saint Paul', 'Minnesota', '55166', '423 Almo Center'), 382 | (382, 'Anchorage', 'Alaska', '99512', '5 Stang Street'), 383 | (383, 'Seattle', 'Washington', '98140', '4200 Shoshone Terrace'), 384 | (384, 'Austin', 'Texas', '78778', '3030 Jackson Park'), 385 | (385, 'Salt Lake City', 'Utah', '84170', '4 Vermont Alley'), 386 | (386, 'Lynchburg', 'Virginia', '24515', '671 Warbler Avenue'), 387 | (387, 'Reading', 'Pennsylvania', '19610', '8 Fairview Place'), 388 | (388, 'Omaha', 'Nebraska', '68117', '5361 Dottie Way'), 389 | (389, 'Washington', 'District of Columbia', '20057', '569 Vera Court'), 390 | (390, 'Houston', 'Texas', '77015', '12745 Veith Lane'), 391 | (391, 'Minneapolis', 'Minnesota', '55487', '08545 Kim Pass'), 392 | (392, 'Philadelphia', 'Pennsylvania', '19120', '579 Carioca Center'), 393 | (393, 'Longview', 'Texas', '75605', '4 Schlimgen Crossing'), 394 | (394, 'Las Vegas', 'Nevada', '89193', '963 Lunder Pass'), 395 | (395, 'Philadelphia', 'Pennsylvania', '19146', '56808 Bellgrove Parkway'), 396 | (396, 'Corpus Christi', 'Texas', '78475', '20111 Green Place'), 397 | (397, 'Worcester', 'Massachusetts', '01654', '61 Ridgeview Crossing'), 398 | (398, 'Tallahassee', 'Florida', '32309', '7 Scoville Terrace'), 399 | (399, 'Bonita Springs', 'Florida', '34135', '8294 Nova Avenue'), 400 | (400, 'Charlotte', 'North Carolina', '28225', '409 Anthes Court'), 401 | (401, 'Colorado Springs', 'Colorado', '80925', '1 Vernon Plaza'), 402 | (402, 'Chicago', 'Illinois', '60669', '38741 Iowa Alley'), 403 | (403, 'Colorado Springs', 'Colorado', '80910', '9 Kensington Place'), 404 | (404, 'York', 'Pennsylvania', '17405', '2 Meadow Vale Junction'), 405 | (405, 'Philadelphia', 'Pennsylvania', '19120', '88364 Transport Center'), 406 | (406, 'Port Washington', 'New York', '11054', '84 Jackson Road'), 407 | (407, 'Houston', 'Texas', '77065', '35541 Chive Court'), 408 | (408, 'Port Charlotte', 'Florida', '33954', '4377 Union Drive'), 409 | (409, 'Terre Haute', 'Indiana', '47812', '281 Redwing Lane'), 410 | (410, 'Cape Coral', 'Florida', '33915', '2439 Doe Crossing Parkway'), 411 | (411, 'Phoenix', 'Arizona', '85083', '5 Oak Valley Avenue'), 412 | (412, 'Harrisburg', 'Pennsylvania', '17105', '566 Eagle Crest Point'), 413 | (413, 'Topeka', 'Kansas', '66642', '002 Harbort Street'), 414 | (414, 'Harrisburg', 'Pennsylvania', '17105', '60129 Bultman Pass'), 415 | (415, 'Norfolk', 'Virginia', '23504', '73880 Kings Parkway'), 416 | (416, 'North Little Rock', 'Arkansas', '72118', '654 Prairieview Lane'), 417 | (417, 'Washington', 'District of Columbia', '20456', '78871 Hallows Court'), 418 | (418, 'San Jose', 'California', '95150', '00 Lillian Trail'), 419 | (419, 'Savannah', 'Georgia', '31410', '69881 Doe Crossing Place'), 420 | (420, 'Dearborn', 'Michigan', '48126', '8162 Sutherland Place'), 421 | (421, 'Akron', 'Ohio', '44315', '1 Summerview Trail'), 422 | (422, 'Sacramento', 'California', '94297', '85 Namekagon Road'), 423 | (423, 'Omaha', 'Nebraska', '68105', '1 Hagan Hill'), 424 | (424, 'Des Moines', 'Iowa', '50315', '41077 Muir Way'), 425 | (425, 'New York City', 'New York', '10275', '374 Warrior Crossing'), 426 | (426, 'Delray Beach', 'Florida', '33448', '5 Lotheville Center'), 427 | (427, 'Columbia', 'South Carolina', '29220', '27 Dottie Drive'), 428 | (428, 'Phoenix', 'Arizona', '85072', '877 Rigney Plaza'), 429 | (429, 'Wilmington', 'Delaware', '19805', '2 Crescent Oaks Avenue'), 430 | (430, 'Garland', 'Texas', '75044', '4434 Vernon Alley'), 431 | (431, 'Longview', 'Texas', '75605', '07509 Lunder Park'), 432 | (432, 'Richmond', 'Virginia', '23260', '35 Pennsylvania Court'), 433 | (433, 'El Paso', 'Texas', '79934', '31 Dawn Pass'), 434 | (434, 'Washington', 'District of Columbia', '20599', '334 Rowland Center'), 435 | (435, 'San Jose', 'California', '95160', '4133 Brown Hill'), 436 | (436, 'Philadelphia', 'Pennsylvania', '19172', '862 Shelley Junction'), 437 | (437, 'Plano', 'Texas', '75074', '858 Marquette Avenue'), 438 | (438, 'Greensboro', 'North Carolina', '27455', '1 Buell Trail'), 439 | (439, 'Peoria', 'Illinois', '61614', '06 Maryland Center'), 440 | (440, 'Richmond', 'Virginia', '23208', '135 Sunnyside Street'), 441 | (441, 'San Francisco', 'California', '94132', '44443 Texas Park'), 442 | (442, 'Houston', 'Texas', '77218', '5547 Jenifer Hill'), 443 | (443, 'Athens', 'Georgia', '30605', '057 Farmco Alley'), 444 | (444, 'Washington', 'District of Columbia', '20580', '39838 Londonderry Trail'), 445 | (445, 'New York City', 'New York', '10060', '767 Westerfield Street'), 446 | (446, 'Santa Fe', 'New Mexico', '87592', '97565 Nelson Court'), 447 | (447, 'Brockton', 'Massachusetts', '02405', '418 Bonner Park'), 448 | (448, 'Newark', 'New Jersey', '07112', '7868 Muir Hill'), 449 | (449, 'Columbia', 'Missouri', '65211', '61 Karstens Place'), 450 | (450, 'Washington', 'District of Columbia', '20299', '39431 Sloan Park'), 451 | (451, 'Orange', 'California', '92668', '17 Laurel Trail'), 452 | (452, 'Denver', 'Colorado', '80243', '15 Columbus Center'), 453 | (453, 'Seattle', 'Washington', '98166', '097 Brentwood Terrace'), 454 | (454, 'Saginaw', 'Michigan', '48604', '48315 Graedel Center'), 455 | (455, 'Houston', 'Texas', '77040', '273 Surrey Road'), 456 | (456, 'Whittier', 'California', '90605', '0698 Jenna Trail'), 457 | (457, 'Fairbanks', 'Alaska', '99790', '91 Anderson Pass'), 458 | (458, 'Springfield', 'Missouri', '65810', '20 Namekagon Road'), 459 | (459, 'Grand Forks', 'North Dakota', '58207', '3563 Karstens Park'), 460 | (460, 'Baltimore', 'Maryland', '21281', '71 Bartelt Way'), 461 | (461, 'Chicago', 'Illinois', '60663', '40 Mesta Plaza'), 462 | (462, 'Lincoln', 'Nebraska', '68517', '9 Hagan Point'), 463 | (463, 'Jacksonville', 'Florida', '32204', '8335 Novick Crossing'), 464 | (464, 'Durham', 'North Carolina', '27710', '09344 Stephen Pass'), 465 | (465, 'Orlando', 'Florida', '32868', '4 Susan Street'), 466 | (466, 'Houston', 'Texas', '77212', '173 Pearson Parkway'), 467 | (467, 'Midland', 'Texas', '79710', '7105 Lakewood Circle'), 468 | (468, 'Cleveland', 'Ohio', '44130', '82 High Crossing Center'), 469 | (469, 'Salt Lake City', 'Utah', '84120', '5973 1st Junction'), 470 | (470, 'Santa Clara', 'California', '95054', '6470 Sundown Crossing'), 471 | (471, 'Roanoke', 'Virginia', '24014', '87 Hoard Center'), 472 | (472, 'Rochester', 'Minnesota', '55905', '02920 Ronald Regan Place'), 473 | (473, 'Erie', 'Pennsylvania', '16565', '93 Cody Point'), 474 | (474, 'Tampa', 'Florida', '33686', '673 Kipling Street'), 475 | (475, 'Lancaster', 'Pennsylvania', '17622', '38290 Forest Run Lane'), 476 | (476, 'Saint Louis', 'Missouri', '63121', '6 Fallview Avenue'), 477 | (477, 'San Francisco', 'California', '94105', '2 Lighthouse Bay Point'), 478 | (478, 'Saint Louis', 'Missouri', '63158', '4 Waywood Court'), 479 | (479, 'San Diego', 'California', '92196', '5 Hooker Terrace'), 480 | (480, 'Jefferson City', 'Missouri', '65105', '52337 Nobel Plaza'), 481 | (481, 'Carson City', 'Nevada', '89706', '08232 Fairfield Circle'), 482 | (482, 'El Paso', 'Texas', '79994', '3602 Arapahoe Street'), 483 | (483, 'Norman', 'Oklahoma', '73071', '7303 Bonner Parkway'), 484 | (484, 'Seattle', 'Washington', '98166', '47914 Schurz Hill'), 485 | (485, 'Miami', 'Florida', '33147', '4864 Waxwing Alley'), 486 | (486, 'Pasadena', 'California', '91186', '7637 Northland Park'), 487 | (487, 'Salem', 'Oregon', '97312', '9637 Brown Circle'), 488 | (488, 'Columbia', 'Missouri', '65211', '3646 Bluejay Lane'), 489 | (489, 'Dallas', 'Texas', '75323', '52 Warrior Park'), 490 | (490, 'Buffalo', 'New York', '14276', '265 John Wall Court'), 491 | (491, 'San Jose', 'California', '95138', '6184 Russell Crossing'), 492 | (492, 'Laredo', 'Texas', '78044', '6900 Main Place'), 493 | (493, 'New York City', 'New York', '10270', '361 Springs Pass'), 494 | (494, 'Prescott', 'Arizona', '86305', '829 Schlimgen Junction'), 495 | (495, 'Sandy', 'Utah', '84093', '09898 Dexter Place'), 496 | (496, 'Washington', 'District of Columbia', '20005', '19346 Mallard Hill'), 497 | (497, 'Baltimore', 'Maryland', '21281', '5 Starling Drive'), 498 | (498, 'Colorado Springs', 'Colorado', '80940', '49563 Sutherland Junction'), 499 | (499, 'Midland', 'Texas', '79710', '487 Monument Place'), 500 | (500, 'Sacramento', 'California', '95828', '0 Coolidge Trail'); 501 | 502 | COMMIT; 503 | 504 | SELECT setval(pg_get_serial_sequence('address', 'id'), 505 | (select max(id) from "address")); -------------------------------------------------------------------------------- /example/hasura/seeds/default/04_default_user_login_seeds.sql: -------------------------------------------------------------------------------- 1 | -- "password" bcrypted = $2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK 2 | INSERT INTO public.user 3 | (name, email, password) 4 | VALUES 5 | ('Person', 'user@site.com', '$2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK'); 6 | 7 | INSERT INTO public.site_admin 8 | (name, email, password) 9 | VALUES 10 | ('Admin', 'admin@site.com', '$2y$10$4fLjiqiJ.Rh0F/qYfIfCeOy7a9nLJN49YzUEJbYnj2ZsiwVhGsOxK'); 11 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.9.5", 7 | "@testing-library/jest-dom": "^5.16.5", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^14.4.3", 10 | "@types/jest": "^29.2.4", 11 | "@types/node": "^18.11.17", 12 | "@types/react": "^18.0.26", 13 | "@types/react-dom": "^18.0.9", 14 | "graphql": "^16.6.0", 15 | "ra-data-hasura": "^0.5.4", 16 | "react": "^18.2.0", 17 | "react-admin": "^4.6.2", 18 | "react-dom": "^18.2.0", 19 | "react-scripts": "5.0.1", 20 | "typescript": "^4.9.4", 21 | "web-vitals": "^3.1.0" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": [ 31 | "react-app", 32 | "react-app/jest" 33 | ] 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.2%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/ra-data-hasura/20878f7c1352fd478bd82b86d9169ac9e5e4ea71/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/ra-data-hasura/20878f7c1352fd478bd82b86d9169ac9e5e4ea71/example/public/logo192.png -------------------------------------------------------------------------------- /example/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasura/ra-data-hasura/20878f7c1352fd478bd82b86d9169ac9e5e4ea71/example/public/logo512.png -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /example/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Admin, Resource, DataProvider } from 'react-admin'; 3 | import buildHasuraProvider from 'ra-data-hasura'; 4 | import { ProductEdit, ProductList } from './product'; 5 | import { createHttpLink } from '@apollo/client'; 6 | import { setContext } from '@apollo/client/link/context'; 7 | import { OrderEdit, OrderList } from './order'; 8 | import { OrderProductList } from './order_product'; 9 | import { UserEdit, UserList } from './user'; 10 | import { AddressEdit, AddressList } from './address'; 11 | 12 | const httpLink = createHttpLink({ 13 | uri: 'http://localhost:8080/v1/graphql', 14 | }); 15 | 16 | const authLink = setContext((_, { headers }) => { 17 | // return the headers to the context so httpLink can read them 18 | return { 19 | headers: { 20 | ...headers, 21 | authorization: 22 | // To generate your own JWT see Hasura Super App https://github.com/hasura/hasura-ecommerce 23 | 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsic2l0ZS1hZG1pbiIsInVzZXIiXSwieC1oYXN1cmEtZGVmYXVsdC1yb2xlIjoic2l0ZS1hZG1pbiIsIlgtSGFzdXJhLVNpdGUtQWRtaW4tSWQiOiIxIn0sImlhdCI6MTY1MTc4MTg1NH0.DsyJSVZYt7Ah8PIWFvnYbAvnpSDdMbhAkmjuLRE8Gas', 24 | }, 25 | }; 26 | }); 27 | 28 | const App = () => { 29 | const [dataProvider, setDataProvider] = React.useState( 30 | null 31 | ); 32 | 33 | React.useEffect(() => { 34 | const buildDataProvider = async () => { 35 | const dataProviderHasura = await buildHasuraProvider({ 36 | clientOptions: { 37 | link: authLink.concat(httpLink) as any, 38 | }, 39 | }); 40 | setDataProvider(() => dataProviderHasura); 41 | }; 42 | buildDataProvider(); 43 | }, []); 44 | 45 | if (!dataProvider) return

Loading...

; 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default App; 59 | -------------------------------------------------------------------------------- /example/src/address/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Datagrid, 3 | DateField, 4 | DateInput, 5 | Edit, 6 | List, 7 | ReferenceField, 8 | ReferenceInput, 9 | SelectInput, 10 | SimpleForm, 11 | TextField, 12 | TextInput, 13 | } from 'react-admin'; 14 | 15 | export const AddressList = () => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | export const AddressEdit = () => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container!); // createRoot(container!) if you use TypeScript 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /example/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/order/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanField, 3 | BooleanInput, 4 | Datagrid, 5 | DateField, 6 | DateInput, 7 | Edit, 8 | List, 9 | ReferenceField, 10 | ReferenceInput, 11 | SelectInput, 12 | SimpleForm, 13 | TextField, 14 | TextInput, 15 | } from 'react-admin'; 16 | 17 | export const OrderList = () => ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | export const OrderEdit = () => ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | -------------------------------------------------------------------------------- /example/src/order_product/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Datagrid, 3 | DateField, 4 | List, 5 | NumberField, 6 | ReferenceField, 7 | TextField, 8 | } from 'react-admin'; 9 | 10 | export const OrderProductList = () => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /example/src/product/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | BooleanInput, 3 | Datagrid, 4 | DateField, 5 | DateInput, 6 | Edit, 7 | List, 8 | NumberField, 9 | ReferenceInput, 10 | SelectInput, 11 | SimpleForm, 12 | TextField, 13 | TextInput, 14 | } from 'react-admin'; 15 | 16 | export const ProductList = () => ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export const ProductEdit = () => ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /example/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /example/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /example/src/user/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Datagrid, 3 | DateField, 4 | DateInput, 5 | Edit, 6 | EmailField, 7 | List, 8 | SimpleForm, 9 | TextField, 10 | TextInput, 11 | } from 'react-admin'; 12 | 13 | export const UserList = () => ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | export const UserEdit = () => ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.tsx?$': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ra-data-hasura", 3 | "version": "0.7.1", 4 | "description": "A data provider for connecting react-admin to a Hasura endpoint", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/hasura/ra-data-hasura.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/hasura/ra-data-hasura/issues" 14 | }, 15 | "homepage": "https://github.com/hasura/ra-data-hasura#readme", 16 | "authors": [ 17 | "Praveen Durairaju", 18 | "Radcliffe Robinson" 19 | ], 20 | "keywords": [ 21 | "reactjs", 22 | "react", 23 | "react-admin", 24 | "admin-on-rest", 25 | "rest", 26 | "graphql", 27 | "hasura" 28 | ], 29 | "license": "MIT", 30 | "scripts": { 31 | "build": "rimraf ./dist && tsc", 32 | "prepublishOnly": "npm run test && npm run build", 33 | "test": "jest", 34 | "prettier": "prettier --config ./.prettierrc --write '**/*.{js,jsx,md}'", 35 | "prepare": "husky install" 36 | }, 37 | "dependencies": { 38 | "lodash": "^4.17.21", 39 | "ra-data-graphql": "^4.6.0 || ^5.0.3", 40 | "graphql": "^16.6.0" 41 | }, 42 | "peerDependencies": { 43 | "ra-core": "^4.6.2 || ^5.0.0" 44 | }, 45 | "devDependencies": { 46 | "@types/jest": "^29.5.2", 47 | "@types/lodash": "^4.14.191", 48 | "husky": "^8.0.2", 49 | "jest": "^29.5.0", 50 | "prettier": "~3.2.5", 51 | "pretty-quick": "^4.0.0", 52 | "rimraf": "^3.0.2", 53 | "ts-jest": "^29.1.0", 54 | "typescript": "^5.1.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/buildGqlQuery/buildArgs.ts: -------------------------------------------------------------------------------- 1 | import * as gqlTypes from '../graphql-ast-types-browser'; 2 | import { 3 | IntrospectionField, 4 | ArgumentNode, 5 | VariableDefinitionNode, 6 | } from 'graphql'; 7 | import { 8 | GET_LIST, 9 | GET_MANY, 10 | GET_MANY_REFERENCE, 11 | } from '../helpers/fetchActions'; 12 | import getArgType from '../helpers/getArgType'; 13 | import { FetchType } from '../types'; 14 | 15 | export type BuildArgs = ( 16 | query: IntrospectionField, 17 | variables: any 18 | ) => ArgumentNode[]; 19 | 20 | export type BuildMetaArgs = ( 21 | query: IntrospectionField, 22 | variables: any, 23 | aorFetchType: FetchType 24 | ) => ArgumentNode[]; 25 | 26 | export type BuildApolloArgs = ( 27 | query: IntrospectionField, 28 | variables: any 29 | ) => VariableDefinitionNode[]; 30 | 31 | export const buildArgs: BuildArgs = (query, variables) => { 32 | if (query.args.length === 0) { 33 | return []; 34 | } 35 | 36 | const validVariables = Object.keys(variables).filter( 37 | (k) => typeof variables[k] !== 'undefined' 38 | ); 39 | 40 | return query.args 41 | .filter((a) => validVariables.includes(a.name)) 42 | .reduce( 43 | (acc, arg) => [ 44 | ...acc, 45 | gqlTypes.argument( 46 | gqlTypes.name(arg.name), 47 | gqlTypes.variable(gqlTypes.name(arg.name)) 48 | ), 49 | ], 50 | [] as ArgumentNode[] 51 | ); 52 | }; 53 | 54 | export const buildMetaArgs: BuildMetaArgs = ( 55 | query, 56 | variables, 57 | aorFetchType 58 | ) => { 59 | if (query.args.length === 0) { 60 | return []; 61 | } 62 | 63 | const validVariables = Object.keys(variables).filter((k) => { 64 | if ( 65 | aorFetchType === GET_LIST || 66 | aorFetchType === GET_MANY || 67 | aorFetchType === GET_MANY_REFERENCE 68 | ) { 69 | return ( 70 | typeof variables[k] !== 'undefined' && k !== 'limit' && k !== 'offset' 71 | ); 72 | } 73 | 74 | return typeof variables[k] !== 'undefined'; 75 | }); 76 | 77 | return query.args 78 | .filter((a) => validVariables.includes(a.name)) 79 | .reduce( 80 | (acc, arg) => [ 81 | ...acc, 82 | gqlTypes.argument( 83 | gqlTypes.name(arg.name), 84 | gqlTypes.variable(gqlTypes.name(arg.name)) 85 | ), 86 | ], 87 | [] as ArgumentNode[] 88 | ); 89 | }; 90 | 91 | export const buildApolloArgs: BuildApolloArgs = (query, variables) => { 92 | if (query.args.length === 0) { 93 | return []; 94 | } 95 | 96 | const validVariables = Object.keys(variables).filter( 97 | (k) => typeof variables[k] !== 'undefined' 98 | ); 99 | 100 | return query.args 101 | .filter((a) => validVariables.includes(a.name)) 102 | .reduce((acc, arg) => { 103 | return [ 104 | ...acc, 105 | gqlTypes.variableDefinition( 106 | gqlTypes.variable(gqlTypes.name(arg.name)), 107 | getArgType(arg) 108 | ), 109 | ]; 110 | }, [] as VariableDefinitionNode[]); 111 | }; 112 | -------------------------------------------------------------------------------- /src/buildGqlQuery/buildFields.ts: -------------------------------------------------------------------------------- 1 | import { TypeKind, IntrospectionObjectType, FieldNode } from 'graphql'; 2 | import * as gqlTypes from '../graphql-ast-types-browser'; 3 | import getFinalType from '../helpers/getFinalType'; 4 | import { FetchType } from '../types'; 5 | 6 | export type BuildFields = ( 7 | type: IntrospectionObjectType, 8 | aorFetchType?: FetchType 9 | ) => FieldNode[]; 10 | 11 | export const buildFields: BuildFields = (type) => 12 | type.fields.reduce((acc, field) => { 13 | const type = getFinalType(field.type); 14 | 15 | if (type.kind !== TypeKind.OBJECT && type.kind !== TypeKind.INTERFACE) { 16 | return [...acc, gqlTypes.field(gqlTypes.name(field.name))]; 17 | } 18 | 19 | return acc; 20 | }, [] as FieldNode[]); 21 | -------------------------------------------------------------------------------- /src/buildGqlQuery/index.ts: -------------------------------------------------------------------------------- 1 | import * as gqlTypes from '../graphql-ast-types-browser'; 2 | import { IntrospectionField, OperationTypeNode } from 'graphql'; 3 | import { 4 | GET_LIST, 5 | GET_MANY, 6 | GET_MANY_REFERENCE, 7 | DELETE, 8 | CREATE, 9 | UPDATE, 10 | UPDATE_MANY, 11 | DELETE_MANY, 12 | } from '../helpers/fetchActions'; 13 | import { buildFields, BuildFields } from './buildFields'; 14 | import { 15 | buildArgs, 16 | buildApolloArgs, 17 | buildMetaArgs, 18 | BuildArgs, 19 | BuildMetaArgs, 20 | BuildApolloArgs, 21 | } from './buildArgs'; 22 | import { FetchType, IntrospectionResult } from '../types'; 23 | 24 | export type BuildGqlQuery = ( 25 | introspectionResults: IntrospectionResult, 26 | buildFields: BuildFields, 27 | buildMetaArgs: BuildMetaArgs, 28 | buildArgs: BuildArgs, 29 | buildApolloArgs: BuildApolloArgs, 30 | aggregateFieldName: (resourceName: string) => string 31 | ) => ( 32 | resource: any, 33 | aorFetchType: FetchType, 34 | queryType: IntrospectionField, 35 | variables: any 36 | ) => any; 37 | 38 | export type BuildGqlQueryFactory = ( 39 | introspectionResults: IntrospectionResult 40 | ) => ReturnType; 41 | 42 | export const buildGqlQuery: BuildGqlQuery = 43 | ( 44 | _, 45 | buildFields, 46 | buildMetaArgs, 47 | buildArgs, 48 | buildApolloArgs, 49 | aggregateFieldName 50 | ) => 51 | (resource, aorFetchType, queryType, variables) => { 52 | const { sortField, sortOrder, ...metaVariables } = variables; 53 | const apolloArgs = buildApolloArgs(queryType, variables); 54 | const args = buildArgs(queryType, variables); 55 | const metaArgs = buildMetaArgs(queryType, metaVariables, aorFetchType); 56 | const fields = buildFields(resource.type, aorFetchType); 57 | if ( 58 | aorFetchType === GET_LIST || 59 | aorFetchType === GET_MANY || 60 | aorFetchType === GET_MANY_REFERENCE 61 | ) { 62 | let gqlArray = [ 63 | gqlTypes.field( 64 | gqlTypes.name(queryType.name), 65 | gqlTypes.name('items'), 66 | args, 67 | null, 68 | gqlTypes.selectionSet(fields) 69 | ), 70 | ]; 71 | // Skip aggregate calls when provided aggregateFieldName function returns NO_COUNT. 72 | // This is useful to avoid expensive count queries. 73 | if (aggregateFieldName(queryType.name) !== 'NO_COUNT') { 74 | gqlArray.push( 75 | gqlTypes.field( 76 | gqlTypes.name(aggregateFieldName(queryType.name)), 77 | gqlTypes.name('total'), 78 | metaArgs, 79 | null, 80 | gqlTypes.selectionSet([ 81 | gqlTypes.field( 82 | gqlTypes.name('aggregate'), 83 | null, 84 | null, 85 | null, 86 | gqlTypes.selectionSet([gqlTypes.field(gqlTypes.name('count'))]) 87 | ), 88 | ]) 89 | ) 90 | ); 91 | } 92 | return gqlTypes.document([ 93 | gqlTypes.operationDefinition( 94 | OperationTypeNode.QUERY, 95 | gqlTypes.selectionSet(gqlArray), 96 | gqlTypes.name(queryType.name), 97 | apolloArgs 98 | ), 99 | ]); 100 | } 101 | 102 | if (aorFetchType === UPDATE) { 103 | return gqlTypes.document([ 104 | gqlTypes.operationDefinition( 105 | OperationTypeNode.MUTATION, 106 | gqlTypes.selectionSet([ 107 | gqlTypes.field( 108 | gqlTypes.name(queryType.name), 109 | null, 110 | args, 111 | null, 112 | gqlTypes.selectionSet(fields) 113 | ), 114 | ]), 115 | gqlTypes.name(queryType.name), 116 | apolloArgs 117 | ), 118 | ]); 119 | } 120 | 121 | if ( 122 | aorFetchType === CREATE || 123 | aorFetchType === UPDATE_MANY || 124 | aorFetchType === DELETE || 125 | aorFetchType === DELETE_MANY 126 | ) { 127 | return gqlTypes.document([ 128 | gqlTypes.operationDefinition( 129 | OperationTypeNode.MUTATION, 130 | gqlTypes.selectionSet([ 131 | gqlTypes.field( 132 | gqlTypes.name(queryType.name), 133 | gqlTypes.name('data'), 134 | args, 135 | null, 136 | gqlTypes.selectionSet([ 137 | gqlTypes.field( 138 | gqlTypes.name('returning'), 139 | null, 140 | null, 141 | null, 142 | gqlTypes.selectionSet(fields) 143 | ), 144 | ]) 145 | ), 146 | ]), 147 | gqlTypes.name(queryType.name), 148 | apolloArgs 149 | ), 150 | ]); 151 | } 152 | 153 | return gqlTypes.document([ 154 | gqlTypes.operationDefinition( 155 | OperationTypeNode.QUERY, 156 | gqlTypes.selectionSet([ 157 | gqlTypes.field( 158 | gqlTypes.name(queryType.name), 159 | gqlTypes.name('returning'), 160 | args, 161 | null, 162 | gqlTypes.selectionSet(fields) 163 | ), 164 | ]), 165 | gqlTypes.name(queryType.name), 166 | apolloArgs 167 | ), 168 | ]); 169 | }; 170 | 171 | const buildGqlQueryFactory: BuildGqlQueryFactory = (introspectionResults) => 172 | buildGqlQuery( 173 | introspectionResults, 174 | buildFields, 175 | buildMetaArgs, 176 | buildArgs, 177 | buildApolloArgs, 178 | (resourceName) => `${resourceName}_aggregate` 179 | ); 180 | 181 | export default buildGqlQueryFactory; 182 | -------------------------------------------------------------------------------- /src/buildQuery/index.ts: -------------------------------------------------------------------------------- 1 | import { buildVariables } from '../buildVariables'; 2 | import buildGqlQuery, { BuildGqlQueryFactory } from '../buildGqlQuery'; 3 | import { getResponseParser, GetResponseParser } from '../getResponseParser'; 4 | import type { FetchType, IntrospectionResult } from '../types'; 5 | 6 | export type QueryResponse = { 7 | data: any; 8 | total?: number; 9 | pageInfo?: { 10 | hasNextPage?: boolean; 11 | hasPreviousPage?: boolean; 12 | }; 13 | }; 14 | 15 | export type BuildQuery = (introspectionResults: IntrospectionResult) => ( 16 | aorFetchType: FetchType, 17 | resourceName: string, 18 | params: any 19 | ) => { 20 | query: any; 21 | variables: any; 22 | parseResponse: ({ data }: any) => QueryResponse; 23 | }; 24 | 25 | export type BuildQueryFactory = ( 26 | buildVariablesImpl: any, 27 | buildGqlQueryImpl: BuildGqlQueryFactory, 28 | getResponseParserImpl: GetResponseParser 29 | ) => BuildQuery; 30 | 31 | export const buildQueryFactory: BuildQueryFactory = 32 | (buildVariablesImpl, buildGqlQueryImpl, getResponseParserImpl) => 33 | (introspectionResults) => { 34 | const knownResources = introspectionResults.resources.map( 35 | (r) => r.type.name 36 | ); 37 | 38 | return (aorFetchType, resourceName, params) => { 39 | const resource = introspectionResults.resources.find( 40 | (r) => r.type.name === resourceName 41 | ); 42 | 43 | if (!resource) { 44 | if (knownResources.length) { 45 | throw new Error( 46 | `Unknown resource ${resourceName}. Make sure it has been declared on your server side schema. Known resources are ${knownResources.join( 47 | ', ' 48 | )}` 49 | ); 50 | } else { 51 | throw new Error( 52 | `Unknown resource ${resourceName}. No resources were found. Make sure it has been declared on your server side schema and check if your Authorization header is properly set up.` 53 | ); 54 | } 55 | } 56 | 57 | const queryType = resource[aorFetchType]; 58 | 59 | if (!queryType) { 60 | throw new Error( 61 | `No query or mutation matching fetch type ${aorFetchType} could be found for resource ${resource.type.name}` 62 | ); 63 | } 64 | 65 | const variables = buildVariablesImpl(introspectionResults)( 66 | resource, 67 | aorFetchType, 68 | params, 69 | queryType 70 | ); 71 | const query = buildGqlQueryImpl(introspectionResults)( 72 | resource, 73 | aorFetchType, 74 | queryType, 75 | variables 76 | ); 77 | const parseResponse = getResponseParserImpl(introspectionResults)( 78 | aorFetchType, 79 | resource 80 | ); 81 | 82 | return { 83 | query, 84 | variables, 85 | parseResponse, 86 | }; 87 | }; 88 | }; 89 | 90 | export default buildQueryFactory( 91 | buildVariables, 92 | buildGqlQuery, 93 | getResponseParser 94 | ); 95 | -------------------------------------------------------------------------------- /src/buildVariables/buildCreateVariables.ts: -------------------------------------------------------------------------------- 1 | import { typeAwareKeyValueReducer } from './typeAwareKeyValueReducer'; 2 | import { FetchType, IntrospectedResource, IntrospectionResult } from '../types'; 3 | 4 | type BuildCreateVariables = ( 5 | introspectionResults: IntrospectionResult 6 | ) => ( 7 | resource: IntrospectedResource, 8 | aorFetchType: FetchType, 9 | params: any, 10 | queryType: any 11 | ) => any; 12 | 13 | export const buildCreateVariables: BuildCreateVariables = 14 | (introspectionResults) => (resource, _, params, __) => { 15 | const reducer = typeAwareKeyValueReducer( 16 | introspectionResults, 17 | resource, 18 | params 19 | ); 20 | return Object.keys(params.data).reduce(reducer, {}); 21 | }; 22 | -------------------------------------------------------------------------------- /src/buildVariables/buildGetListVariables.ts: -------------------------------------------------------------------------------- 1 | import set from 'lodash/set'; 2 | import omit from 'lodash/omit'; 3 | import getFinalType from '../helpers/getFinalType'; 4 | import type { 5 | FetchType, 6 | IntrospectionResult, 7 | IntrospectedResource, 8 | } from '../types'; 9 | 10 | type BuildGetListVariables = ( 11 | introspectionResults: IntrospectionResult 12 | ) => ( 13 | resource: IntrospectedResource, 14 | aorFetchType: FetchType, 15 | params: any 16 | ) => any; 17 | 18 | const SPLIT_TOKEN = '#'; 19 | const MULTI_SORT_TOKEN = ','; 20 | const SPLIT_OPERATION = '@'; 21 | 22 | export const buildGetListVariables: BuildGetListVariables = 23 | () => (resource, _, params) => { 24 | const result: any = {}; 25 | let { filter: filterObj = {} } = params; 26 | const { customFilters = [] } = params; 27 | 28 | const distinctOnField = 'distinct_on'; 29 | /** Setting "distinct_on" to be the `filters` object attribute to be used inside RA 30 | * and setting to a `distinct_on` variable 31 | * and removing from the filter object 32 | */ 33 | const { distinct_on = '' } = filterObj; 34 | filterObj = omit(filterObj, [distinctOnField]); 35 | 36 | /** 37 | * Nested entities are parsed by CRA, which returns a nested object 38 | * { 'level1': {'level2': 'test'}} 39 | * instead of { 'level1.level2': 'test'} 40 | * That's why we use a HASH for properties, when we declared nested stuff at CRA: 41 | * level1#level2@_ilike 42 | */ 43 | 44 | /** 45 | keys with comma separated values 46 | { 47 | 'title@ilike,body@like,authors@similar': 'test', 48 | 'col1@like,col2@like': 'val' 49 | } 50 | */ 51 | const orFilterKeys = Object.keys(filterObj).filter((e) => e.includes(',')); 52 | 53 | /** 54 | format filters 55 | { 56 | 'title@ilike': 'test', 57 | 'body@like': 'test', 58 | 'authors@similar': 'test', 59 | 'col1@like': 'val', 60 | 'col2@like': 'val' 61 | } 62 | */ 63 | const orFilterObj = orFilterKeys.reduce((acc, commaSeparatedKey) => { 64 | const keys = commaSeparatedKey.split(','); 65 | return { 66 | ...acc, 67 | ...keys.reduce((acc2, key) => { 68 | return { 69 | ...acc2, 70 | [key]: filterObj[commaSeparatedKey], 71 | }; 72 | }, {}), 73 | }; 74 | }, {}); 75 | filterObj = omit(filterObj, orFilterKeys); 76 | 77 | const makeNestedFilter = (obj: any, operation: string): any => { 78 | if (Object.keys(obj).length === 1) { 79 | const [key] = Object.keys(obj); 80 | return { [key]: makeNestedFilter(obj[key], operation) }; 81 | } else { 82 | return { [operation]: obj }; 83 | } 84 | }; 85 | 86 | const filterReducer = (obj: any) => (acc: any, key: any) => { 87 | let filter; 88 | if (key === 'ids') { 89 | filter = { id: { _in: obj['ids'] } }; 90 | } else if (Array.isArray(obj[key])) { 91 | let [keyName, operation = '_in', opPath] = key.split(SPLIT_OPERATION); 92 | let value = opPath 93 | ? set({}, opPath.split(SPLIT_TOKEN), obj[key]) 94 | : obj[key]; 95 | filter = set({}, keyName.split(SPLIT_TOKEN), { [operation]: value }); 96 | } else if (obj[key] && obj[key].format === 'hasura-raw-query') { 97 | filter = set({}, key.split(SPLIT_TOKEN), obj[key].value || {}); 98 | } else { 99 | let [keyName, operation = ''] = key.split(SPLIT_OPERATION); 100 | let operator; 101 | if (operation === '{}') operator = {}; 102 | const field = resource.type.fields.find((f) => f.name === keyName); 103 | if (field) { 104 | switch (getFinalType(field.type).name) { 105 | case 'String': 106 | operation = operation || '_ilike'; 107 | if (!operator) 108 | operator = { 109 | [operation]: operation.includes('like') 110 | ? `%${obj[key]}%` 111 | : obj[key], 112 | }; 113 | break; 114 | case 'jsonb': 115 | try { 116 | const parsedJSONQuery = JSON.parse(obj[key]); 117 | if (parsedJSONQuery) { 118 | operator = { 119 | [operation || '_contains']: parsedJSONQuery 120 | }; 121 | } 122 | } catch (ex) {} 123 | break; 124 | default: 125 | if (!operator) 126 | operator = { 127 | [operation || '_eq']: operation.includes('like') 128 | ? `%${obj[key]}%` 129 | : obj[key], 130 | }; 131 | } 132 | } else { 133 | // Else block runs when the field is not found in Graphql schema. 134 | // Most likely it's nested. If it's not, it's better to let 135 | // Hasura fail with a message than silently fail/ignore it 136 | if (!operator) 137 | operator = { 138 | [operation || '_eq']: operation.includes('like') 139 | ? `%${obj[key]}%` 140 | : obj[key], 141 | }; 142 | } 143 | filter = set({}, keyName.split(SPLIT_TOKEN), operator); 144 | } 145 | return [...acc, filter]; 146 | }; 147 | const andFilters = Object.keys(filterObj) 148 | .reduce(filterReducer(filterObj), customFilters) 149 | .filter(Boolean); 150 | const orFilters = Object.keys(orFilterObj) 151 | .reduce(filterReducer(orFilterObj), []) 152 | .filter(Boolean); 153 | 154 | result['where'] = { 155 | _and: andFilters, 156 | ...(orFilters.length && { _or: orFilters }), 157 | }; 158 | 159 | if (params.pagination && params.pagination.perPage > -1) { 160 | result['limit'] = parseInt(params.pagination.perPage, 10); 161 | result['offset'] = 162 | (params.pagination.page - 1) * params.pagination.perPage; 163 | } 164 | 165 | if (params.sort) { 166 | const { field, order } = params.sort; 167 | const hasMultiSort = 168 | field.includes(MULTI_SORT_TOKEN) || order.includes(MULTI_SORT_TOKEN); 169 | if (hasMultiSort) { 170 | const fields = field.split(MULTI_SORT_TOKEN); 171 | const orders = order 172 | .split(MULTI_SORT_TOKEN) 173 | .map((order: string) => order.toLowerCase()); 174 | 175 | if (fields.length !== orders.length) { 176 | throw new Error( 177 | `The ${ 178 | resource.type.name 179 | } list must have an order value for each sort field. Sort fields are "${fields.join( 180 | ',' 181 | )}" but sort orders are "${orders.join(',')}"` 182 | ); 183 | } 184 | 185 | const multiSort = fields.map((field: any, index: number) => 186 | makeSort(field, orders[index]) 187 | ); 188 | result['order_by'] = multiSort; 189 | } else { 190 | result['order_by'] = makeSort(field, order); 191 | } 192 | } 193 | 194 | if (distinct_on) { 195 | result['distinct_on'] = distinct_on; 196 | } 197 | 198 | return result; 199 | }; 200 | 201 | /** 202 | * if the field contains a SPLIT_OPERATION, it means it's column ordering option. 203 | * 204 | * @example 205 | * ``` 206 | * makeSort('title', 'ASC') => { title: 'asc' } 207 | * ``` 208 | * @example 209 | * ``` 210 | * makeSort('title@nulls_last', 'ASC') => { title: 'asc_nulls_last' } 211 | * ``` 212 | * @example 213 | * ``` 214 | * makeSort('title@nulls_first', 'ASC') => { title: 'asc_nulls_first' } 215 | * ``` 216 | * 217 | */ 218 | const makeSort = (field: string, sort: 'ASC' | 'DESC') => { 219 | const [fieldName, operation] = field.split(SPLIT_OPERATION); 220 | const fieldSort = operation ? `${sort}_${operation}` : sort; 221 | return set({}, fieldName, fieldSort.toLowerCase()); 222 | }; 223 | -------------------------------------------------------------------------------- /src/buildVariables/buildUpdateVariables.ts: -------------------------------------------------------------------------------- 1 | import isEqual from 'lodash/isEqual'; 2 | import { IntrospectionInputObjectType } from 'graphql'; 3 | import { FetchType, IntrospectedResource, IntrospectionResult } from '../types'; 4 | import { typeAwareKeyValueReducer } from './typeAwareKeyValueReducer'; 5 | 6 | type BuildUpdateVariables = ( 7 | introspectionResults: IntrospectionResult 8 | ) => ( 9 | resource: IntrospectedResource, 10 | aorFetchType: FetchType, 11 | params: any, 12 | queryType: any 13 | ) => any; 14 | 15 | export const buildUpdateVariables: BuildUpdateVariables = 16 | (introspectionResults) => (resource, _, params, __) => { 17 | const reducer = typeAwareKeyValueReducer( 18 | introspectionResults, 19 | resource, 20 | params 21 | ); 22 | let permitted_fields: any = null; 23 | const resource_name = resource.type.name; 24 | if (resource_name) { 25 | let inputType = introspectionResults.types.find( 26 | (obj) => obj.name === `${resource_name}_set_input` 27 | ); 28 | if (inputType) { 29 | let inputTypeFields = (inputType as IntrospectionInputObjectType) 30 | .inputFields; 31 | if (inputTypeFields) { 32 | permitted_fields = inputTypeFields.map((obj) => obj.name); 33 | } 34 | } 35 | } 36 | return Object.keys(params.data).reduce((acc, key) => { 37 | // If hasura permissions do not allow a field to be updated like (id), 38 | // we are not allowed to put it inside the variables 39 | // RA passes the whole previous Object here 40 | // https://github.com/marmelab/react-admin/issues/2414#issuecomment-428945402 41 | 42 | // Fetch permitted fields from *_set_input INPUT_OBJECT and filter out any key 43 | // not present inside it 44 | if (permitted_fields && !permitted_fields.includes(key)) return acc; 45 | 46 | if ( 47 | params.previousData && 48 | isEqual(params.data[key], params.previousData[key]) 49 | ) { 50 | return acc; 51 | } 52 | return reducer(acc, key); 53 | }, {}); 54 | }; 55 | -------------------------------------------------------------------------------- /src/buildVariables/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_ONE, 3 | GET_LIST, 4 | GET_MANY, 5 | GET_MANY_REFERENCE, 6 | DELETE, 7 | CREATE, 8 | UPDATE, 9 | UPDATE_MANY, 10 | DELETE_MANY, 11 | } from '../helpers/fetchActions'; 12 | import { buildGetListVariables } from './buildGetListVariables'; 13 | import { buildUpdateVariables } from './buildUpdateVariables'; 14 | import { buildCreateVariables } from './buildCreateVariables'; 15 | import { makeNestedTarget } from './makeNestedTarget'; 16 | import { FetchType, IntrospectedResource, IntrospectionResult } from '../types'; 17 | 18 | export type BuildVariables = ( 19 | introspectionResults: IntrospectionResult 20 | ) => ( 21 | resource: IntrospectedResource, 22 | aorFetchType: FetchType, 23 | params: any, 24 | queryType: any 25 | ) => any; 26 | 27 | export const buildVariables: BuildVariables = 28 | (introspectionResults) => (resource, aorFetchType, params, queryType) => { 29 | switch (aorFetchType) { 30 | case GET_LIST: 31 | return buildGetListVariables(introspectionResults)( 32 | resource, 33 | aorFetchType, 34 | params 35 | ); 36 | case GET_MANY_REFERENCE: { 37 | var built = buildGetListVariables(introspectionResults)( 38 | resource, 39 | aorFetchType, 40 | params 41 | ); 42 | if (params.filter) { 43 | return { 44 | ...built, 45 | where: { 46 | _and: [ 47 | ...built['where']['_and'], 48 | makeNestedTarget(params.target, params.id), 49 | ], 50 | }, 51 | }; 52 | } 53 | return { 54 | ...built, 55 | where: makeNestedTarget(params.target, params.id), 56 | }; 57 | } 58 | case GET_MANY: 59 | case DELETE_MANY: 60 | return { 61 | where: { id: { _in: params.ids } }, 62 | }; 63 | 64 | case GET_ONE: 65 | return { 66 | where: { id: { _eq: params.id } }, 67 | limit: 1, 68 | }; 69 | 70 | case DELETE: 71 | return { 72 | where: { id: { _eq: params.id } }, 73 | }; 74 | case CREATE: 75 | return { 76 | objects: buildCreateVariables(introspectionResults)( 77 | resource, 78 | aorFetchType, 79 | params, 80 | queryType 81 | ), 82 | }; 83 | 84 | case UPDATE: 85 | return { 86 | _set: buildUpdateVariables(introspectionResults)( 87 | resource, 88 | aorFetchType, 89 | params, 90 | queryType 91 | ), 92 | pk_columns: { id: params.id }, 93 | }; 94 | 95 | case UPDATE_MANY: 96 | return { 97 | _set: buildUpdateVariables(introspectionResults)( 98 | resource, 99 | aorFetchType, 100 | params, 101 | queryType 102 | ), 103 | where: { id: { _in: params.ids } }, 104 | }; 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/buildVariables/makeNestedTarget.ts: -------------------------------------------------------------------------------- 1 | import set from 'lodash/set'; 2 | 3 | type TargetEquals = { 4 | _eq: any; 5 | }; 6 | 7 | type NestedTarget = 8 | | K 9 | | { 10 | [key: string]: K | NestedTarget; 11 | }; 12 | 13 | export const makeNestedTarget = ( 14 | target: string, 15 | id: string | number 16 | ): NestedTarget => { 17 | // This simple example should make clear what this function does 18 | // makeNestedTarget("a.b", 42) 19 | // makeNestedTarget("a#b", 42) 20 | // => { a: { b: { _eq: 42 } } } 21 | // makeNestedTarget("a#b@_contains@c#d", id) 22 | // => { a: { b: { _contains: { c: { d: 42 } } } } } 23 | // . -> # to make nested filtering support the same separator/standard 24 | 25 | let [path, operation = '_eq', oppath] = target.split('@'); 26 | let value = oppath ? set({}, oppath 27 | .split(".").join("#") // nested filtering support the same standard 28 | .split('#'), id) : id; 29 | 30 | return set({}, path.split('.').join("#").split("#"), { 31 | [operation]: value, 32 | }) as NestedTarget; 33 | }; 34 | -------------------------------------------------------------------------------- /src/buildVariables/typeAwareKeyValueReducer.ts: -------------------------------------------------------------------------------- 1 | import type { IntrospectionField } from 'graphql'; 2 | import { IntrospectionResult, IntrospectedResource } from '../types'; 3 | 4 | /** 5 | * Returns a reducer that converts the react-admin key-values to hasura-acceptable values 6 | * 7 | * Currently that means that dates should never be an empty string, but in the future that can be extended 8 | * See https://github.com/marmelab/react-admin/pull/6199 9 | * 10 | */ 11 | 12 | type TypeAwareKeyValueReducer = ( 13 | introspectionResults: IntrospectionResult, 14 | resource: IntrospectedResource, 15 | params: any 16 | ) => (acc: any, key: any) => any; 17 | 18 | export const typeAwareKeyValueReducer: TypeAwareKeyValueReducer = 19 | (introspectionResults, resource, params) => (acc, key) => { 20 | const type = introspectionResults.types.find( 21 | (t) => t.name === resource.type.name 22 | ); 23 | 24 | let value = params.data[key]; 25 | if (type) { 26 | const field = (type as any)?.fields?.find( 27 | (t: IntrospectionField) => t.name === key 28 | ); 29 | if (field?.type?.name === 'date' && params.data[key] === '') { 30 | value = null; 31 | } 32 | } 33 | return resource.type.fields.some((f) => f.name === key) 34 | ? { 35 | ...acc, 36 | [key]: value, 37 | } 38 | : acc; 39 | }; 40 | -------------------------------------------------------------------------------- /src/customDataProvider/index.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | import buildDataProvider, { Options } from 'ra-data-graphql'; 3 | import { 4 | GET_ONE, 5 | GET_LIST, 6 | GET_MANY, 7 | GET_MANY_REFERENCE, 8 | DELETE, 9 | CREATE, 10 | UPDATE, 11 | UPDATE_MANY, 12 | DELETE_MANY, 13 | } from '../helpers/fetchActions'; 14 | import { 15 | buildVariables as defaultBuildVariables, 16 | BuildVariables, 17 | } from '../buildVariables'; 18 | import { 19 | getResponseParser as defaultGetResponseParser, 20 | GetResponseParser, 21 | } from '../getResponseParser'; 22 | import { buildGqlQuery } from '../buildGqlQuery'; 23 | import { 24 | buildMetaArgs, 25 | buildArgs, 26 | buildApolloArgs, 27 | BuildMetaArgs, 28 | BuildArgs, 29 | BuildApolloArgs, 30 | } from '../buildGqlQuery/buildArgs'; 31 | import { buildFields, BuildFields } from '../buildGqlQuery/buildFields'; 32 | import { buildQueryFactory } from '../buildQuery'; 33 | import type { IntrospectionResult } from '../types'; 34 | 35 | const defaultOptions: Partial = { 36 | introspection: { 37 | operationNames: { 38 | [GET_LIST]: (resource) => `${resource.name}`, 39 | [GET_ONE]: (resource) => `${resource.name}`, 40 | [GET_MANY]: (resource) => `${resource.name}`, 41 | [GET_MANY_REFERENCE]: (resource) => `${resource.name}`, 42 | [CREATE]: (resource) => `insert_${resource.name}`, 43 | [UPDATE]: (resource) => `update_${resource.name}_by_pk`, 44 | [UPDATE_MANY]: (resource) => `update_${resource.name}`, 45 | [DELETE]: (resource) => `delete_${resource.name}`, 46 | [DELETE_MANY]: (resource) => `delete_${resource.name}`, 47 | }, 48 | }, 49 | }; 50 | 51 | const buildGqlQueryDefaults = { 52 | buildFields, 53 | buildMetaArgs, 54 | buildArgs, 55 | buildApolloArgs, 56 | aggregateFieldName: (resourceName: string) => `${resourceName}_aggregate`, 57 | }; 58 | 59 | export type BuildCustomDataProvider = ( 60 | options: Partial, 61 | buildGqlQueryOverrides?: { 62 | buildFields?: BuildFields; 63 | buildMetaArgs?: BuildMetaArgs; 64 | buildArgs?: BuildArgs; 65 | buildApolloArgs?: BuildApolloArgs; 66 | aggregateFieldName?: (resourceName: string) => string; 67 | }, 68 | customBuildVariables?: BuildVariables, 69 | customGetResponseParser?: GetResponseParser 70 | ) => ReturnType; 71 | 72 | export const buildCustomDataProvider: BuildCustomDataProvider = ( 73 | options = {}, 74 | buildGqlQueryOverrides = {}, 75 | customBuildVariables = defaultBuildVariables, 76 | customGetResponseParser = defaultGetResponseParser 77 | ) => { 78 | const buildGqlQueryOptions = { 79 | ...buildGqlQueryDefaults, 80 | ...buildGqlQueryOverrides, 81 | }; 82 | 83 | const customBuildGqlQuery = (introspectionResults: IntrospectionResult) => 84 | buildGqlQuery( 85 | introspectionResults, 86 | buildGqlQueryOptions.buildFields, 87 | buildGqlQueryOptions.buildMetaArgs, 88 | buildGqlQueryOptions.buildArgs, 89 | buildGqlQueryOptions.buildApolloArgs, 90 | buildGqlQueryOptions.aggregateFieldName 91 | ); 92 | 93 | const buildQuery = buildQueryFactory( 94 | customBuildVariables, 95 | customBuildGqlQuery, 96 | customGetResponseParser 97 | ); 98 | 99 | return buildDataProvider(merge({}, defaultOptions, { buildQuery }, options)); 100 | }; 101 | -------------------------------------------------------------------------------- /src/getResponseParser/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GET_LIST, 3 | GET_MANY, 4 | GET_MANY_REFERENCE, 5 | GET_ONE, 6 | CREATE, 7 | UPDATE, 8 | DELETE, 9 | UPDATE_MANY, 10 | DELETE_MANY, 11 | } from '../helpers/fetchActions'; 12 | import { IntrospectionResult, IntrospectedResource, FetchType } from '../types'; 13 | import { QueryResponse } from '../buildQuery'; 14 | import { sanitizeResource } from './sanitizeResource'; 15 | 16 | export type GetResponseParser = ( 17 | introspectionResults: IntrospectionResult 18 | ) => ( 19 | aorFetchType: FetchType, 20 | resource?: IntrospectedResource 21 | ) => (res: { data: any }) => QueryResponse; 22 | 23 | export const getResponseParser: GetResponseParser = 24 | () => (aorFetchType, resource) => (res) => { 25 | const response = res.data; 26 | 27 | switch (aorFetchType) { 28 | case GET_MANY_REFERENCE: 29 | case GET_LIST: 30 | let output: QueryResponse = { 31 | data: response.items.map(sanitizeResource), 32 | }; 33 | if (typeof response.total !== 'undefined') { 34 | output.total = response.total.aggregate.count; 35 | } else { 36 | // TODO: behave smarter and set hasNextPage=false when no more records exist. 37 | output.pageInfo = { 38 | hasPreviousPage: true, 39 | hasNextPage: true, 40 | }; 41 | } 42 | return output; 43 | 44 | case GET_MANY: 45 | return { data: response.items.map(sanitizeResource) }; 46 | 47 | case GET_ONE: 48 | return { data: sanitizeResource(response.returning[0]) }; 49 | 50 | case CREATE: 51 | case DELETE: 52 | return { data: sanitizeResource(response.data.returning[0]) }; 53 | 54 | case UPDATE: 55 | return { data: sanitizeResource(response[resource!.UPDATE.name]) }; 56 | case UPDATE_MANY: 57 | case DELETE_MANY: 58 | return { data: response.data.returning.map((x: { id: any }) => x.id) }; 59 | 60 | default: 61 | throw Error(`Expected a proper fetchType, got: ${aorFetchType}`); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/getResponseParser/sanitizeResource.test.ts: -------------------------------------------------------------------------------- 1 | import { sanitizeResource } from './sanitizeResource'; 2 | 3 | describe('sanitizeResource', () => { 4 | it('should properly parse array values at any depth', () => { 5 | expect(sanitizeResource([0, 1, 2, 3])).toEqual([0, 1, 2, 3]); 6 | expect(sanitizeResource([[0, 1, 2, 3]])).toEqual([[0, 1, 2, 3]]); 7 | expect(sanitizeResource({ data: [[0, 1, 2, 3]] })).toEqual({ 8 | data: [[0, 1, 2, 3]], 9 | }); 10 | }); 11 | 12 | it('should add keyIds for arrays of objects with id', () => { 13 | expect( 14 | sanitizeResource({ 15 | testValue: [ 16 | { id: 1, name: 'Test 1' }, 17 | { id: 2, name: 'Test 2' }, 18 | ], 19 | }) 20 | ).toEqual({ 21 | testValue: [ 22 | { id: 1, name: 'Test 1' }, 23 | { id: 2, name: 'Test 2' }, 24 | ], 25 | testValueIds: [1, 2], 26 | }); 27 | }); 28 | 29 | it('should add key.id value for objects with id', () => { 30 | expect( 31 | sanitizeResource({ 32 | testValue: { id: 1, name: 'Test 1' }, 33 | }) 34 | ).toEqual({ 35 | testValue: { id: 1, name: 'Test 1' }, 36 | 'testValue.id': 1, 37 | }); 38 | }); 39 | 40 | it('It keeps null values', () => { 41 | expect( 42 | sanitizeResource([ 43 | { name: 'vendor', value: 'Test Vendor' }, 44 | { name: 'brand', value: null }, 45 | ]) 46 | ).toEqual([ 47 | { name: 'vendor', value: 'Test Vendor' }, 48 | { name: 'brand', value: null }, 49 | ]); 50 | }); 51 | 52 | it('Should skip key with two underscores', () => { 53 | expect( 54 | sanitizeResource([ 55 | { 56 | name: 'vendor', 57 | value: 'Test Vendor', 58 | metadata: { 59 | _key: 'value', 60 | fields: [{ _type: 'string', name: 'title' }], 61 | }, 62 | __typename: 'name', 63 | }, 64 | ]) 65 | ).toEqual([ 66 | { 67 | name: 'vendor', 68 | value: 'Test Vendor', 69 | metadata: { 70 | _key: 'value', 71 | fields: [{ _type: 'string', name: 'title' }], 72 | }, 73 | }, 74 | ]); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/getResponseParser/sanitizeResource.ts: -------------------------------------------------------------------------------- 1 | function isPrimitive(test: any) { 2 | return test !== Object(test); 3 | } 4 | export const sanitizeResource = (data: any): any => { 5 | // primitive no transformation needed (catches null, undefined, string, number, boolean) 6 | if (isPrimitive(data)) { 7 | return data; 8 | } 9 | 10 | // array, apply sanitizeResource to each element 11 | if (Array.isArray(data)) { 12 | return data.map(sanitizeResource); 13 | } 14 | 15 | // default object, check each (key, value) pair 16 | return Object.entries(data).reduce((acc, [key, value]) => { 17 | // intend to remove the following reserved names https://spec.graphql.org/draft/#sec-Names.Reserved-Names 18 | if (key.startsWith('__')) { 19 | return acc; 20 | } 21 | 22 | const newAcc: Record = { ...acc }; 23 | 24 | // if it's an array of objects, we want to create a new key with the list of ids 25 | if (Array.isArray(value) && value?.[0]?.id && value?.[0]?.id !== null) { 26 | newAcc[`${key}Ids`] = value.map((d) => d.id); 27 | } 28 | 29 | // if it's an object with an id, we want to create a new key with the id 30 | if ((value as any)?.id) { 31 | newAcc[`${key}.id`] = (value as any).id; 32 | } 33 | 34 | return { ...newAcc, [key]: sanitizeResource(value) }; 35 | }, {} as Record); 36 | }; 37 | -------------------------------------------------------------------------------- /src/graphql-ast-types-browser/definitions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true, 5 | }); 6 | 7 | var _typeof = 8 | typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' 9 | ? function (obj) { 10 | return typeof obj; 11 | } 12 | : function (obj) { 13 | return obj && 14 | typeof Symbol === 'function' && 15 | obj.constructor === Symbol && 16 | obj !== Symbol.prototype 17 | ? 'symbol' 18 | : typeof obj; 19 | }; 20 | 21 | exports.default = defineType; 22 | exports.chain = chain; 23 | exports.assertEach = assertEach; 24 | exports.assertOneOf = assertOneOf; 25 | exports.assertNodeType = assertNodeType; 26 | exports.assertNodeOrValueType = assertNodeOrValueType; 27 | exports.assertValueType = assertValueType; 28 | exports.assertArrayOf = assertArrayOf; 29 | var t = require('../index'); 30 | 31 | var BUILDER_KEYS = (exports.BUILDER_KEYS = {}); 32 | var NODE_FIELDS = (exports.NODE_FIELDS = {}); 33 | var ALIAS_KEYS = (exports.ALIAS_KEYS = {}); 34 | 35 | /** 36 | * Used to define an AST node. 37 | * @param {String} type The AST node name 38 | * @param {Object} opts Type definition object 39 | * @returns {void} 40 | */ 41 | function defineType(type) { 42 | var _ref = 43 | arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 44 | _ref$fields = _ref.fields, 45 | fields = _ref$fields === undefined ? {} : _ref$fields, 46 | _ref$aliases = _ref.aliases, 47 | aliases = _ref$aliases === undefined ? [] : _ref$aliases, 48 | _ref$builder = _ref.builder, 49 | builder = _ref$builder === undefined ? [] : _ref$builder; 50 | 51 | for (var key in fields) { 52 | var field = fields[key]; 53 | 54 | // Sets field as optional if builder exist but validator does not. 55 | if (builder.indexOf(key) === -1) { 56 | field.optional = true; 57 | } 58 | } 59 | 60 | BUILDER_KEYS[type] = builder; 61 | NODE_FIELDS[type] = fields; 62 | ALIAS_KEYS[type] = aliases; 63 | } 64 | 65 | function getType(val) { 66 | if (Array.isArray(val)) { 67 | return 'array'; 68 | } else if (val === null) { 69 | return 'null'; 70 | } else if (val === undefined) { 71 | return 'undefined'; 72 | } else { 73 | return typeof val === 'undefined' ? 'undefined' : _typeof(val); 74 | } 75 | } 76 | 77 | // Validation helpers 78 | 79 | function chain() { 80 | for ( 81 | var _len = arguments.length, fns = Array(_len), _key = 0; 82 | _key < _len; 83 | _key++ 84 | ) { 85 | fns[_key] = arguments[_key]; 86 | } 87 | 88 | return function validate() { 89 | for ( 90 | var _len2 = arguments.length, args = Array(_len2), _key2 = 0; 91 | _key2 < _len2; 92 | _key2++ 93 | ) { 94 | args[_key2] = arguments[_key2]; 95 | } 96 | 97 | fns.forEach(function (fn) { 98 | return fn.apply(undefined, args); 99 | }); 100 | }; 101 | } 102 | 103 | function assertEach(callback) { 104 | function validator(node, key, val) { 105 | if (!Array.isArray(val)) { 106 | return; 107 | } 108 | 109 | val.forEach(function (it, i) { 110 | return callback(node, key + '[' + i + ']', it); 111 | }); 112 | } 113 | return validator; 114 | } 115 | 116 | function assertOneOf() { 117 | for ( 118 | var _len3 = arguments.length, vals = Array(_len3), _key3 = 0; 119 | _key3 < _len3; 120 | _key3++ 121 | ) { 122 | vals[_key3] = arguments[_key3]; 123 | } 124 | 125 | function validate(node, key, val) { 126 | if (vals.indexOf(val.kind) < 0) { 127 | throw new TypeError( 128 | 'Property ' + 129 | key + 130 | ' expected value to be one of ' + 131 | JSON.stringify(vals) + 132 | ' but got ' + 133 | JSON.stringify(val) 134 | ); 135 | } 136 | } 137 | 138 | return validate; 139 | } 140 | 141 | function assertNodeType() { 142 | for ( 143 | var _len4 = arguments.length, types = Array(_len4), _key4 = 0; 144 | _key4 < _len4; 145 | _key4++ 146 | ) { 147 | types[_key4] = arguments[_key4]; 148 | } 149 | 150 | function validate(node, key, val) { 151 | var valid = types.every(function (type) { 152 | return t.is(type, val); 153 | }); 154 | 155 | if (!valid) { 156 | throw new TypeError( 157 | 'Property ' + 158 | key + 159 | ' of ' + 160 | node.type + 161 | ' expected node to be of a type ' + 162 | JSON.stringify(types) + 163 | ' ' + 164 | ('but instead got ' + JSON.stringify(val && val.type)) 165 | ); 166 | } 167 | } 168 | 169 | return validate; 170 | } 171 | 172 | function assertNodeOrValueType() { 173 | for ( 174 | var _len5 = arguments.length, types = Array(_len5), _key5 = 0; 175 | _key5 < _len5; 176 | _key5++ 177 | ) { 178 | types[_key5] = arguments[_key5]; 179 | } 180 | 181 | function validate(node, key, val) { 182 | var valid = types.every(function (type) { 183 | return getType(val) === type || t.is(type, val); 184 | }); 185 | 186 | if (!valid) { 187 | throw new TypeError( 188 | 'Property ' + 189 | key + 190 | ' of ' + 191 | node.type + 192 | ' expected node to be of a type ' + 193 | JSON.stringify(types) + 194 | ' ' + 195 | ('but instead got ' + JSON.stringify(val && val.type)) 196 | ); 197 | } 198 | } 199 | 200 | return validate; 201 | } 202 | 203 | function assertValueType(type) { 204 | function validate(node, key, val) { 205 | var valid = getType(val) === type; 206 | 207 | if (!valid) { 208 | throw new TypeError( 209 | 'Property ' + 210 | key + 211 | ' expected type of ' + 212 | type + 213 | ' but got ' + 214 | getType(val) 215 | ); 216 | } 217 | } 218 | 219 | return validate; 220 | } 221 | 222 | function assertArrayOf(cb) { 223 | return chain(assertValueType('array'), assertEach(cb)); 224 | } 225 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kZWZpbml0aW9ucy9pbmRleC5qcyJdLCJuYW1lcyI6WyJkZWZpbmVUeXBlIiwiY2hhaW4iLCJhc3NlcnRFYWNoIiwiYXNzZXJ0T25lT2YiLCJhc3NlcnROb2RlVHlwZSIsImFzc2VydE5vZGVPclZhbHVlVHlwZSIsImFzc2VydFZhbHVlVHlwZSIsImFzc2VydEFycmF5T2YiLCJ0IiwicmVxdWlyZSIsIkJVSUxERVJfS0VZUyIsIk5PREVfRklFTERTIiwiQUxJQVNfS0VZUyIsInR5cGUiLCJmaWVsZHMiLCJhbGlhc2VzIiwiYnVpbGRlciIsImtleSIsImZpZWxkIiwiaW5kZXhPZiIsIm9wdGlvbmFsIiwiZ2V0VHlwZSIsInZhbCIsIkFycmF5IiwiaXNBcnJheSIsInVuZGVmaW5lZCIsImZucyIsInZhbGlkYXRlIiwiYXJncyIsImZvckVhY2giLCJmbiIsImNhbGxiYWNrIiwidmFsaWRhdG9yIiwibm9kZSIsIml0IiwiaSIsInZhbHMiLCJraW5kIiwiVHlwZUVycm9yIiwiSlNPTiIsInN0cmluZ2lmeSIsInR5cGVzIiwidmFsaWQiLCJldmVyeSIsImlzIiwiY2IiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7O2tCQTBCd0JBLFU7UUFnQ1JDLEssR0FBQUEsSztRQU1BQyxVLEdBQUFBLFU7UUFXQUMsVyxHQUFBQSxXO1FBY0FDLGMsR0FBQUEsYztRQWdCQUMscUIsR0FBQUEscUI7UUFnQkFDLGUsR0FBQUEsZTtRQWNBQyxhLEdBQUFBLGE7QUF0SWhCLElBQU1DLElBQUlDLFFBQVEsVUFBUixDQUFWOztBQU9PLElBQU1DLHNDQUFrRCxFQUF4RDtBQUNBLElBQU1DLG9DQUVULEVBRkc7QUFHQSxJQUFNQyxrQ0FBZ0QsRUFBdEQ7O0FBUVA7Ozs7OztBQU1lLFNBQVNaLFVBQVQsQ0FDYmEsSUFEYSxFQUdiO0FBQUEsaUZBRHNELEVBQ3REO0FBQUEseUJBREVDLE1BQ0Y7QUFBQSxNQURFQSxNQUNGLCtCQURXLEVBQ1g7QUFBQSwwQkFEZUMsT0FDZjtBQUFBLE1BRGVBLE9BQ2YsZ0NBRHlCLEVBQ3pCO0FBQUEsMEJBRDZCQyxPQUM3QjtBQUFBLE1BRDZCQSxPQUM3QixnQ0FEdUMsRUFDdkM7O0FBQ0EsT0FBSyxJQUFNQyxHQUFYLElBQWtCSCxNQUFsQixFQUEwQjtBQUN4QixRQUFNSSxRQUFRSixPQUFPRyxHQUFQLENBQWQ7O0FBRUE7QUFDQSxRQUFJRCxRQUFRRyxPQUFSLENBQWdCRixHQUFoQixNQUF5QixDQUFDLENBQTlCLEVBQWlDO0FBQy9CQyxZQUFNRSxRQUFOLEdBQWlCLElBQWpCO0FBQ0Q7QUFDRjs7QUFFRFYsZUFBYUcsSUFBYixJQUFxQkcsT0FBckI7QUFDQUwsY0FBWUUsSUFBWixJQUFvQkMsTUFBcEI7QUFDQUYsYUFBV0MsSUFBWCxJQUFtQkUsT0FBbkI7QUFDRDs7QUFFRCxTQUFTTSxPQUFULENBQWlCQyxHQUFqQixFQUFzQjtBQUNwQixNQUFJQyxNQUFNQyxPQUFOLENBQWNGLEdBQWQsQ0FBSixFQUF3QjtBQUN0QixXQUFPLE9BQVA7QUFDRCxHQUZELE1BRU8sSUFBSUEsUUFBUSxJQUFaLEVBQWtCO0FBQ3ZCLFdBQU8sTUFBUDtBQUNELEdBRk0sTUFFQSxJQUFJQSxRQUFRRyxTQUFaLEVBQXVCO0FBQzVCLFdBQU8sV0FBUDtBQUNELEdBRk0sTUFFQTtBQUNMLGtCQUFjSCxHQUFkLHlDQUFjQSxHQUFkO0FBQ0Q7QUFDRjs7QUFFRDs7QUFFTyxTQUFTckIsS0FBVCxHQUFrRDtBQUFBLG9DQUFoQ3lCLEdBQWdDO0FBQWhDQSxPQUFnQztBQUFBOztBQUN2RCxTQUFPLFNBQVNDLFFBQVQsR0FBMkI7QUFBQSx1Q0FBTkMsSUFBTTtBQUFOQSxVQUFNO0FBQUE7O0FBQ2hDRixRQUFJRyxPQUFKLENBQVk7QUFBQSxhQUFNQyxvQkFBTUYsSUFBTixDQUFOO0FBQUEsS0FBWjtBQUNELEdBRkQ7QUFHRDs7QUFFTSxTQUFTMUIsVUFBVCxDQUFvQjZCLFFBQXBCLEVBQWtEO0FBQ3ZELFdBQVNDLFNBQVQsQ0FBbUJDLElBQW5CLEVBQXlCaEIsR0FBekIsRUFBOEJLLEdBQTlCLEVBQW1DO0FBQ2pDLFFBQUksQ0FBQ0MsTUFBTUMsT0FBTixDQUFjRixHQUFkLENBQUwsRUFBeUI7QUFDdkI7QUFDRDs7QUFFREEsUUFBSU8sT0FBSixDQUFZLFVBQUNLLEVBQUQsRUFBS0MsQ0FBTDtBQUFBLGFBQVdKLFNBQVNFLElBQVQsRUFBa0JoQixHQUFsQixTQUF5QmtCLENBQXpCLFFBQStCRCxFQUEvQixDQUFYO0FBQUEsS0FBWjtBQUNEO0FBQ0QsU0FBT0YsU0FBUDtBQUNEOztBQUVNLFNBQVM3QixXQUFULEdBQXVEO0FBQUEscUNBQS9CaUMsSUFBK0I7QUFBL0JBLFFBQStCO0FBQUE7O0FBQzVELFdBQVNULFFBQVQsQ0FBa0JNLElBQWxCLEVBQXdCaEIsR0FBeEIsRUFBNkJLLEdBQTdCLEVBQWtDO0FBQ2hDLFFBQUljLEtBQUtqQixPQUFMLENBQWFHLElBQUllLElBQWpCLElBQXlCLENBQTdCLEVBQWdDO0FBQzlCLFlBQU0sSUFBSUMsU0FBSixlQUNRckIsR0FEUixxQ0FDMkNzQixLQUFLQyxTQUFMLENBQzdDSixJQUQ2QyxDQUQzQyxpQkFHU0csS0FBS0MsU0FBTCxDQUFlbEIsR0FBZixDQUhULENBQU47QUFLRDtBQUNGOztBQUVELFNBQU9LLFFBQVA7QUFDRDs7QUFFTSxTQUFTdkIsY0FBVCxHQUEyRDtBQUFBLHFDQUFoQ3FDLEtBQWdDO0FBQWhDQSxTQUFnQztBQUFBOztBQUNoRSxXQUFTZCxRQUFULENBQWtCTSxJQUFsQixFQUF3QmhCLEdBQXhCLEVBQTZCSyxHQUE3QixFQUFrQztBQUNoQyxRQUFNb0IsUUFBUUQsTUFBTUUsS0FBTixDQUFZO0FBQUEsYUFBUW5DLEVBQUVvQyxFQUFGLENBQUsvQixJQUFMLEVBQVdTLEdBQVgsQ0FBUjtBQUFBLEtBQVosQ0FBZDs7QUFFQSxRQUFJLENBQUNvQixLQUFMLEVBQVk7QUFDVixZQUFNLElBQUlKLFNBQUosQ0FDSixjQUFZckIsR0FBWixZQUFzQmdCLEtBQUtwQixJQUEzQix1Q0FBaUUwQixLQUFLQyxTQUFMLENBQy9EQyxLQUQrRCxDQUFqRSwrQkFFMEJGLEtBQUtDLFNBQUwsQ0FBZWxCLE9BQU9BLElBQUlULElBQTFCLENBRjFCLENBREksQ0FBTjtBQUtEO0FBQ0Y7O0FBRUQsU0FBT2MsUUFBUDtBQUNEOztBQUVNLFNBQVN0QixxQkFBVCxHQUFrRTtBQUFBLHFDQUFoQ29DLEtBQWdDO0FBQWhDQSxTQUFnQztBQUFBOztBQUN2RSxXQUFTZCxRQUFULENBQWtCTSxJQUFsQixFQUF3QmhCLEdBQXhCLEVBQTZCSyxHQUE3QixFQUFrQztBQUNoQyxRQUFNb0IsUUFBUUQsTUFBTUUsS0FBTixDQUFZO0FBQUEsYUFBUXRCLFFBQVFDLEdBQVIsTUFBaUJULElBQWpCLElBQXlCTCxFQUFFb0MsRUFBRixDQUFLL0IsSUFBTCxFQUFXUyxHQUFYLENBQWpDO0FBQUEsS0FBWixDQUFkOztBQUVBLFFBQUksQ0FBQ29CLEtBQUwsRUFBWTtBQUNWLFlBQU0sSUFBSUosU0FBSixDQUNKLGNBQVlyQixHQUFaLFlBQXNCZ0IsS0FBS3BCLElBQTNCLHVDQUFpRTBCLEtBQUtDLFNBQUwsQ0FDL0RDLEtBRCtELENBQWpFLCtCQUUwQkYsS0FBS0MsU0FBTCxDQUFlbEIsT0FBT0EsSUFBSVQsSUFBMUIsQ0FGMUIsQ0FESSxDQUFOO0FBS0Q7QUFDRjs7QUFFRCxTQUFPYyxRQUFQO0FBQ0Q7O0FBRU0sU0FBU3JCLGVBQVQsQ0FBeUJPLElBQXpCLEVBQWlEO0FBQ3RELFdBQVNjLFFBQVQsQ0FBa0JNLElBQWxCLEVBQXdCaEIsR0FBeEIsRUFBNkJLLEdBQTdCLEVBQWtDO0FBQ2hDLFFBQU1vQixRQUFRckIsUUFBUUMsR0FBUixNQUFpQlQsSUFBL0I7O0FBRUEsUUFBSSxDQUFDNkIsS0FBTCxFQUFZO0FBQ1YsWUFBTSxJQUFJSixTQUFKLGVBQ1FyQixHQURSLDBCQUNnQ0osSUFEaEMsaUJBQ2dEUSxRQUFRQyxHQUFSLENBRGhELENBQU47QUFHRDtBQUNGOztBQUVELFNBQU9LLFFBQVA7QUFDRDs7QUFFTSxTQUFTcEIsYUFBVCxDQUF1QnNDLEVBQXZCLEVBQStDO0FBQ3BELFNBQU81QyxNQUFNSyxnQkFBZ0IsT0FBaEIsQ0FBTixFQUFnQ0osV0FBVzJDLEVBQVgsQ0FBaEMsQ0FBUDtBQUNEIiwiZmlsZSI6ImluZGV4LmpzIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQGZsb3dcbmNvbnN0IHQgPSByZXF1aXJlKFwiLi4vaW5kZXhcIik7XG5cbnR5cGUgVmFsaWRhdG9yID0ge1xuICB2YWxpZGF0ZTogRnVuY3Rpb24sXG4gIG9wdGlvbmFsPzogYm9vbGVhblxufTtcblxuZXhwb3J0IGNvbnN0IEJVSUxERVJfS0VZUzogeyBbdHlwZTogc3RyaW5nXTogQXJyYXk8c3RyaW5nPiB9ID0ge307XG5leHBvcnQgY29uc3QgTk9ERV9GSUVMRFM6IHtcbiAgW3R5cGU6IHN0cmluZ106IHsgW2ZpZWxkS2V5OiBzdHJpbmddOiBWYWxpZGF0b3IgfVxufSA9IHt9O1xuZXhwb3J0IGNvbnN0IEFMSUFTX0tFWVM6IHsgW3R5cGU6IHN0cmluZ106IEFycmF5PHN0cmluZz4gfSA9IHt9O1xuXG50eXBlIE9wdGlvbiA9IHtcbiAgZmllbGRzPzogeyBbZmllbGRLZXk6IHN0cmluZ106IFZhbGlkYXRvciB9LFxuICBhbGlhc2VzPzogQXJyYXk8c3RyaW5nPixcbiAgYnVpbGRlcj86IEFycmF5PHN0cmluZz4gLy8gTm9kZSBwcm9wZXJ0aWVzIHRvIGJlIHRyYW5zZm9ybWVkIGludG8gcGFyYW1zXG59O1xuXG4vKipcbiAqIFVzZWQgdG8gZGVmaW5lIGFuIEFTVCBub2RlLlxuICogQHBhcmFtIHtTdHJpbmd9IHR5cGUgVGhlIEFTVCBub2RlIG5hbWVcbiAqIEBwYXJhbSB7T2JqZWN0fSBvcHRzIFR5cGUgZGVmaW5pdGlvbiBvYmplY3RcbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBkZWZpbmVUeXBlKFxuICB0eXBlOiBzdHJpbmcsXG4gIHsgZmllbGRzID0ge30sIGFsaWFzZXMgPSBbXSwgYnVpbGRlciA9IFtdIH06IE9wdGlvbiA9IHt9XG4pIHtcbiAgZm9yIChjb25zdCBrZXkgaW4gZmllbGRzKSB7XG4gICAgY29uc3QgZmllbGQgPSBmaWVsZHNba2V5XTtcblxuICAgIC8vIFNldHMgZmllbGQgYXMgb3B0aW9uYWwgaWYgYnVpbGRlciBleGlzdCBidXQgdmFsaWRhdG9yIGRvZXMgbm90LlxuICAgIGlmIChidWlsZGVyLmluZGV4T2Yoa2V5KSA9PT0gLTEpIHtcbiAgICAgIGZpZWxkLm9wdGlvbmFsID0gdHJ1ZTtcbiAgICB9XG4gIH1cblxuICBCVUlMREVSX0tFWVNbdHlwZV0gPSBidWlsZGVyO1xuICBOT0RFX0ZJRUxEU1t0eXBlXSA9IGZpZWxkcztcbiAgQUxJQVNfS0VZU1t0eXBlXSA9IGFsaWFzZXM7XG59XG5cbmZ1bmN0aW9uIGdldFR5cGUodmFsKSB7XG4gIGlmIChBcnJheS5pc0FycmF5KHZhbCkpIHtcbiAgICByZXR1cm4gXCJhcnJheVwiO1xuICB9IGVsc2UgaWYgKHZhbCA9PT0gbnVsbCkge1xuICAgIHJldHVybiBcIm51bGxcIjtcbiAgfSBlbHNlIGlmICh2YWwgPT09IHVuZGVmaW5lZCkge1xuICAgIHJldHVybiBcInVuZGVmaW5lZFwiO1xuICB9IGVsc2Uge1xuICAgIHJldHVybiB0eXBlb2YgdmFsO1xuICB9XG59XG5cbi8vIFZhbGlkYXRpb24gaGVscGVyc1xuXG5leHBvcnQgZnVuY3Rpb24gY2hhaW4oLi4uZm5zOiBBcnJheTxGdW5jdGlvbj4pOiBGdW5jdGlvbiB7XG4gIHJldHVybiBmdW5jdGlvbiB2YWxpZGF0ZSguLi5hcmdzKSB7XG4gICAgZm5zLmZvckVhY2goZm4gPT4gZm4oLi4uYXJncykpO1xuICB9O1xufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0RWFjaChjYWxsYmFjazogRnVuY3Rpb24pOiBGdW5jdGlvbiB7XG4gIGZ1bmN0aW9uIHZhbGlkYXRvcihub2RlLCBrZXksIHZhbCkge1xuICAgIGlmICghQXJyYXkuaXNBcnJheSh2YWwpKSB7XG4gICAgICByZXR1cm47XG4gICAgfVxuXG4gICAgdmFsLmZvckVhY2goKGl0LCBpKSA9PiBjYWxsYmFjayhub2RlLCBgJHtrZXl9WyR7aX1dYCwgaXQpKTtcbiAgfVxuICByZXR1cm4gdmFsaWRhdG9yO1xufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0T25lT2YoLi4udmFsczogQXJyYXk8c3RyaW5nPik6IEZ1bmN0aW9uIHtcbiAgZnVuY3Rpb24gdmFsaWRhdGUobm9kZSwga2V5LCB2YWwpIHtcbiAgICBpZiAodmFscy5pbmRleE9mKHZhbC5raW5kKSA8IDApIHtcbiAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXG4gICAgICAgIGBQcm9wZXJ0eSAke2tleX0gZXhwZWN0ZWQgdmFsdWUgdG8gYmUgb25lIG9mICR7SlNPTi5zdHJpbmdpZnkoXG4gICAgICAgICAgdmFsc1xuICAgICAgICApfSBidXQgZ290ICR7SlNPTi5zdHJpbmdpZnkodmFsKX1gXG4gICAgICApO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB2YWxpZGF0ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydE5vZGVUeXBlKC4uLnR5cGVzOiBBcnJheTxzdHJpbmc+KTogRnVuY3Rpb24ge1xuICBmdW5jdGlvbiB2YWxpZGF0ZShub2RlLCBrZXksIHZhbCkge1xuICAgIGNvbnN0IHZhbGlkID0gdHlwZXMuZXZlcnkodHlwZSA9PiB0LmlzKHR5cGUsIHZhbCkpO1xuXG4gICAgaWYgKCF2YWxpZCkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcbiAgICAgICAgYFByb3BlcnR5ICR7a2V5fSBvZiAke25vZGUudHlwZX0gZXhwZWN0ZWQgbm9kZSB0byBiZSBvZiBhIHR5cGUgJHtKU09OLnN0cmluZ2lmeShcbiAgICAgICAgICB0eXBlc1xuICAgICAgICApfSBgICsgYGJ1dCBpbnN0ZWFkIGdvdCAke0pTT04uc3RyaW5naWZ5KHZhbCAmJiB2YWwudHlwZSl9YFxuICAgICAgKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gdmFsaWRhdGU7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnROb2RlT3JWYWx1ZVR5cGUoLi4udHlwZXM6IEFycmF5PHN0cmluZz4pOiBGdW5jdGlvbiB7XG4gIGZ1bmN0aW9uIHZhbGlkYXRlKG5vZGUsIGtleSwgdmFsKSB7XG4gICAgY29uc3QgdmFsaWQgPSB0eXBlcy5ldmVyeSh0eXBlID0+IGdldFR5cGUodmFsKSA9PT0gdHlwZSB8fCB0LmlzKHR5cGUsIHZhbCkpO1xuXG4gICAgaWYgKCF2YWxpZCkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcbiAgICAgICAgYFByb3BlcnR5ICR7a2V5fSBvZiAke25vZGUudHlwZX0gZXhwZWN0ZWQgbm9kZSB0byBiZSBvZiBhIHR5cGUgJHtKU09OLnN0cmluZ2lmeShcbiAgICAgICAgICB0eXBlc1xuICAgICAgICApfSBgICsgYGJ1dCBpbnN0ZWFkIGdvdCAke0pTT04uc3RyaW5naWZ5KHZhbCAmJiB2YWwudHlwZSl9YFxuICAgICAgKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gdmFsaWRhdGU7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRWYWx1ZVR5cGUodHlwZTogc3RyaW5nKTogRnVuY3Rpb24ge1xuICBmdW5jdGlvbiB2YWxpZGF0ZShub2RlLCBrZXksIHZhbCkge1xuICAgIGNvbnN0IHZhbGlkID0gZ2V0VHlwZSh2YWwpID09PSB0eXBlO1xuXG4gICAgaWYgKCF2YWxpZCkge1xuICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcbiAgICAgICAgYFByb3BlcnR5ICR7a2V5fSBleHBlY3RlZCB0eXBlIG9mICR7dHlwZX0gYnV0IGdvdCAke2dldFR5cGUodmFsKX1gXG4gICAgICApO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB2YWxpZGF0ZTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydEFycmF5T2YoY2I6IEZ1bmN0aW9uKTogRnVuY3Rpb24ge1xuICByZXR1cm4gY2hhaW4oYXNzZXJ0VmFsdWVUeXBlKFwiYXJyYXlcIiksIGFzc2VydEVhY2goY2IpKTtcbn1cbiJdfQ== 226 | -------------------------------------------------------------------------------- /src/graphql-ast-types-browser/definitions/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = (function () { 4 | function sliceIterator(arr, i) { 5 | var _arr = []; 6 | var _n = true; 7 | var _d = false; 8 | var _e = undefined; 9 | try { 10 | for ( 11 | var _i = arr[Symbol.iterator](), _s; 12 | !(_n = (_s = _i.next()).done); 13 | _n = true 14 | ) { 15 | _arr.push(_s.value); 16 | if (i && _arr.length === i) break; 17 | } 18 | } catch (err) { 19 | _d = true; 20 | _e = err; 21 | } finally { 22 | try { 23 | if (!_n && _i['return']) _i['return'](); 24 | } finally { 25 | if (_d) throw _e; 26 | } 27 | } 28 | return _arr; 29 | } 30 | return function (arr, i) { 31 | if (Array.isArray(arr)) { 32 | return arr; 33 | } else if (Symbol.iterator in Object(arr)) { 34 | return sliceIterator(arr, i); 35 | } else { 36 | throw new TypeError( 37 | 'Invalid attempt to destructure non-iterable instance' 38 | ); 39 | } 40 | }; 41 | })(); 42 | 43 | var _index = require('./index'); 44 | 45 | var _index2 = _interopRequireDefault(_index); 46 | 47 | var _graphql = require('./graphql'); 48 | 49 | var _graphql2 = _interopRequireDefault(_graphql); 50 | 51 | function _interopRequireDefault(obj) { 52 | return obj && obj.__esModule ? obj : { default: obj }; 53 | } 54 | 55 | (0, _graphql2.default)().forEach(function (_ref) { 56 | var _ref2 = _slicedToArray(_ref, 2), 57 | name = _ref2[0], 58 | params = _ref2[1]; 59 | 60 | return (0, _index2.default)(name, params); 61 | }); 62 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kZWZpbml0aW9ucy9pbml0LmpzIl0sIm5hbWVzIjpbImZvckVhY2giLCJuYW1lIiwicGFyYW1zIl0sIm1hcHBpbmdzIjoiOzs7O0FBQUE7Ozs7QUFDQTs7Ozs7O0FBRUEseUJBQWFBLE9BQWIsQ0FBcUI7QUFBQTtBQUFBLE1BQUVDLElBQUY7QUFBQSxNQUFRQyxNQUFSOztBQUFBLFNBQW9CLHFCQUFXRCxJQUFYLEVBQWlCQyxNQUFqQixDQUFwQjtBQUFBLENBQXJCIiwiZmlsZSI6ImluaXQuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgZGVmaW5lVHlwZSBmcm9tIFwiLi9pbmRleFwiO1xuaW1wb3J0IGdyYXBocWxEZWYgZnJvbSBcIi4vZ3JhcGhxbFwiO1xuXG5ncmFwaHFsRGVmKCkuZm9yRWFjaCgoW25hbWUsIHBhcmFtc10pID0+IGRlZmluZVR5cGUobmFtZSwgcGFyYW1zKSk7XG4iXX0= 63 | -------------------------------------------------------------------------------- /src/graphql-ast-types-browser/index.d.ts: -------------------------------------------------------------------------------- 1 | // From the npm library 'graphql-ast-types-browser' 2 | import type { 3 | NameNode, 4 | DocumentNode, 5 | OperationDefinitionNode, 6 | VariableDefinitionNode, 7 | VariableNode, 8 | SelectionSetNode, 9 | FieldNode, 10 | ArgumentNode, 11 | FragmentSpreadNode, 12 | InlineFragmentNode, 13 | FragmentDefinitionNode, 14 | IntValueNode, 15 | FloatValueNode, 16 | StringValueNode, 17 | BooleanValueNode, 18 | NullValueNode, 19 | EnumValueNode, 20 | ListValueNode, 21 | ObjectValueNode, 22 | ObjectFieldNode, 23 | DirectiveNode, 24 | NamedTypeNode, 25 | ListTypeNode, 26 | NonNullTypeNode, 27 | SchemaDefinitionNode, 28 | OperationTypeDefinitionNode, 29 | ScalarTypeDefinitionNode, 30 | ObjectTypeDefinitionNode, 31 | FieldDefinitionNode, 32 | InputValueDefinitionNode, 33 | InterfaceTypeDefinitionNode, 34 | UnionTypeDefinitionNode, 35 | EnumTypeDefinitionNode, 36 | EnumValueDefinitionNode, 37 | InputObjectTypeDefinitionNode, 38 | TypeExtensionDefinitionNode, 39 | DirectiveDefinitionNode, 40 | OperationTypeNode, 41 | ASTNode, 42 | DefinitionNode, 43 | ValueNode, 44 | SelectionNode, 45 | TypeNode, 46 | TypeSystemDefinitionNode, 47 | TypeDefinitionNode, 48 | } from 'graphql/language/ast'; 49 | declare function name(value: string): NameNode; 50 | declare function isName(node: any): boolean; 51 | declare function assertName(node: any): boolean; 52 | declare function document(definitions: Array): DocumentNode; 53 | declare function isDocument(node: any): boolean; 54 | declare function assertDocument(node: any): boolean; 55 | declare function operationDefinition( 56 | operation: OperationTypeNode, 57 | selectionSet: SelectionSetNode, 58 | name?: NameNode, 59 | variableDefinitions?: Array, 60 | directives?: Array 61 | ): OperationDefinitionNode; 62 | declare function isOperationDefinition(node: any): boolean; 63 | declare function assertOperationDefinition(node: any): boolean; 64 | declare function variableDefinition( 65 | variable: VariableNode, 66 | type: TypeNode, 67 | defaultValue?: ValueNode 68 | ): VariableDefinitionNode; 69 | declare function isVariableDefinition(node: any): boolean; 70 | declare function assertVariableDefinition(node: any): boolean; 71 | declare function variable(name: NameNode): VariableNode; 72 | declare function isVariable(node: any): boolean; 73 | declare function assertVariable(node: any): boolean; 74 | declare function selectionSet( 75 | selections: Array 76 | ): SelectionSetNode; 77 | declare function isSelectionSet(node: any): boolean; 78 | declare function assertSelectionSet(node: any): boolean; 79 | declare function field( 80 | name: NameNode, 81 | alias?: NameNode | null, 82 | arguments?: Array | null, 83 | directives?: Array | null, 84 | selectionSet?: SelectionSetNode | null 85 | ): FieldNode; 86 | declare function isField(node: any): boolean; 87 | declare function assertField(node: any): boolean; 88 | declare function argument(name: NameNode, value: ValueNode): ArgumentNode; 89 | declare function isArgument(node: any): boolean; 90 | declare function assertArgument(node: any): boolean; 91 | declare function fragmentSpread( 92 | name: NameNode, 93 | directives?: Array 94 | ): FragmentSpreadNode; 95 | declare function isFragmentSpread(node: any): boolean; 96 | declare function assertFragmentSpread(node: any): boolean; 97 | declare function inlineFragment( 98 | selectionSet: SelectionSetNode, 99 | typeCondition?: NamedTypeNode, 100 | directives?: Array 101 | ): InlineFragmentNode; 102 | declare function isInlineFragment(node: any): boolean; 103 | declare function assertInlineFragment(node: any): boolean; 104 | declare function fragmentDefinition( 105 | name: NameNode, 106 | typeCondition: NamedTypeNode, 107 | selectionSet: SelectionSetNode, 108 | directives?: Array 109 | ): FragmentDefinitionNode; 110 | declare function isFragmentDefinition(node: any): boolean; 111 | declare function assertFragmentDefinition(node: any): boolean; 112 | declare function intValue(value: string): IntValueNode; 113 | declare function isIntValue(node: any): boolean; 114 | declare function assertIntValue(node: any): boolean; 115 | declare function floatValue(value: string): FloatValueNode; 116 | declare function isFloatValue(node: any): boolean; 117 | declare function assertFloatValue(node: any): boolean; 118 | declare function stringValue(value: string): StringValueNode; 119 | declare function isStringValue(node: any): boolean; 120 | declare function assertStringValue(node: any): boolean; 121 | declare function booleanValue(value: boolean): BooleanValueNode; 122 | declare function isBooleanValue(node: any): boolean; 123 | declare function assertBooleanValue(node: any): boolean; 124 | declare function nullValue(): NullValueNode; 125 | declare function isNullValue(node: any): boolean; 126 | declare function assertNullValue(node: any): boolean; 127 | declare function enumValue(value: string): EnumValueNode; 128 | declare function isEnumValue(node: any): boolean; 129 | declare function assertEnumValue(node: any): boolean; 130 | declare function listValue(values: Array): ListValueNode; 131 | declare function isListValue(node: any): boolean; 132 | declare function assertListValue(node: any): boolean; 133 | declare function objectValue(fields: Array): ObjectValueNode; 134 | declare function isObjectValue(node: any): boolean; 135 | declare function assertObjectValue(node: any): boolean; 136 | declare function objectField(name: NameNode, value: ValueNode): ObjectFieldNode; 137 | declare function isObjectField(node: any): boolean; 138 | declare function assertObjectField(node: any): boolean; 139 | declare function directive( 140 | name: NameNode, 141 | arguments?: Array 142 | ): DirectiveNode; 143 | declare function isDirective(node: any): boolean; 144 | declare function assertDirective(node: any): boolean; 145 | declare function namedType(name: NameNode): NamedTypeNode; 146 | declare function isNamedType(node: any): boolean; 147 | declare function assertNamedType(node: any): boolean; 148 | declare function listType(type: TypeNode): ListTypeNode; 149 | declare function isListType(node: any): boolean; 150 | declare function assertListType(node: any): boolean; 151 | declare function nonNullType( 152 | type: NamedTypeNode | ListTypeNode 153 | ): NonNullTypeNode; 154 | declare function isNonNullType(node: any): boolean; 155 | declare function assertNonNullType(node: any): boolean; 156 | declare function schemaDefinition( 157 | directives: Array, 158 | operationTypes: Array 159 | ): SchemaDefinitionNode; 160 | declare function isSchemaDefinition(node: any): boolean; 161 | declare function assertSchemaDefinition(node: any): boolean; 162 | declare function operationTypeDefinition( 163 | operation: OperationTypeNode, 164 | type: NamedTypeNode 165 | ): OperationTypeDefinitionNode; 166 | declare function isOperationTypeDefinition(node: any): boolean; 167 | declare function assertOperationTypeDefinition(node: any): boolean; 168 | declare function scalarTypeDefinition( 169 | name: NameNode, 170 | directives?: Array 171 | ): ScalarTypeDefinitionNode; 172 | declare function isScalarTypeDefinition(node: any): boolean; 173 | declare function assertScalarTypeDefinition(node: any): boolean; 174 | declare function objectTypeDefinition( 175 | name: NameNode, 176 | fields: Array, 177 | interfaces?: Array, 178 | directives?: Array 179 | ): ObjectTypeDefinitionNode; 180 | declare function isObjectTypeDefinition(node: any): boolean; 181 | declare function assertObjectTypeDefinition(node: any): boolean; 182 | declare function fieldDefinition( 183 | name: NameNode, 184 | arguments: Array, 185 | type: TypeNode, 186 | directives?: Array 187 | ): FieldDefinitionNode; 188 | declare function isFieldDefinition(node: any): boolean; 189 | declare function assertFieldDefinition(node: any): boolean; 190 | declare function inputValueDefinition( 191 | name: NameNode, 192 | type: TypeNode, 193 | defaultValue?: ValueNode, 194 | directives?: Array 195 | ): InputValueDefinitionNode; 196 | declare function isInputValueDefinition(node: any): boolean; 197 | declare function assertInputValueDefinition(node: any): boolean; 198 | declare function interfaceTypeDefinition( 199 | name: NameNode, 200 | fields: Array, 201 | directives?: Array 202 | ): InterfaceTypeDefinitionNode; 203 | declare function isInterfaceTypeDefinition(node: any): boolean; 204 | declare function assertInterfaceTypeDefinition(node: any): boolean; 205 | declare function unionTypeDefinition( 206 | name: NameNode, 207 | types: Array, 208 | directives?: Array 209 | ): UnionTypeDefinitionNode; 210 | declare function isUnionTypeDefinition(node: any): boolean; 211 | declare function assertUnionTypeDefinition(node: any): boolean; 212 | declare function enumTypeDefinition( 213 | name: NameNode, 214 | values: Array, 215 | directives?: Array 216 | ): EnumTypeDefinitionNode; 217 | declare function isEnumTypeDefinition(node: any): boolean; 218 | declare function assertEnumTypeDefinition(node: any): boolean; 219 | declare function enumValueDefinition( 220 | name: NameNode, 221 | directives?: Array 222 | ): EnumValueDefinitionNode; 223 | declare function isEnumValueDefinition(node: any): boolean; 224 | declare function assertEnumValueDefinition(node: any): boolean; 225 | declare function inputObjectTypeDefinition( 226 | name: NameNode, 227 | fields: Array, 228 | directives?: Array 229 | ): InputObjectTypeDefinitionNode; 230 | declare function isInputObjectTypeDefinition(node: any): boolean; 231 | declare function assertInputObjectTypeDefinition(node: any): boolean; 232 | declare function typeExtensionDefinition( 233 | definition: ObjectTypeDefinitionNode 234 | ): TypeExtensionDefinitionNode; 235 | declare function isTypeExtensionDefinition(node: any): boolean; 236 | declare function assertTypeExtensionDefinition(node: any): boolean; 237 | declare function directiveDefinition( 238 | name: NameNode, 239 | locations: Array, 240 | arguments?: Array 241 | ): DirectiveDefinitionNode; 242 | declare function isDirectiveDefinition(node: any): boolean; 243 | declare function assertDirectiveDefinition(node: any): boolean; 244 | 245 | declare function is(nodeName: string, node: any): boolean; 246 | -------------------------------------------------------------------------------- /src/graphql-ast-types-browser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true, 5 | }); 6 | 7 | var _typeof = 8 | typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' 9 | ? function (obj) { 10 | return typeof obj; 11 | } 12 | : function (obj) { 13 | return obj && 14 | typeof Symbol === 'function' && 15 | obj.constructor === Symbol && 16 | obj !== Symbol.prototype 17 | ? 'symbol' 18 | : typeof obj; 19 | }; 20 | 21 | exports.is = is; 22 | exports.isType = isType; 23 | exports.validate = validate; 24 | exports.shallowEqual = shallowEqual; 25 | 26 | var _require = require('./definitions/init'); 27 | 28 | var _require = require('./definitions'), 29 | ALIAS_KEYS = _require.ALIAS_KEYS, 30 | NODE_FIELDS = _require.NODE_FIELDS, 31 | BUILDER_KEYS = _require.BUILDER_KEYS; 32 | 33 | var t = exports; // Maps all exports to t 34 | 35 | /** 36 | * Registers `is[Type]` and `assert[Type]` generated functions for a given `type`. 37 | * Pass `skipAliasCheck` to force it to directly compare `node.type` with `type`. 38 | */ 39 | 40 | function registerType(type) { 41 | var key = 'is' + type; 42 | 43 | var _isType = 44 | t[key] !== undefined 45 | ? t[key] 46 | : (t[key] = function (node, opts) { 47 | return t.is(type, node, opts); 48 | }); 49 | 50 | t['assert' + type] = function (node) { 51 | var opts = 52 | arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 53 | 54 | if (!_isType(node, opts)) { 55 | throw new Error( 56 | 'Expected type "' + type + '" with option ' + JSON.stringify(opts) 57 | ); 58 | } 59 | }; 60 | } 61 | 62 | exports.ALIAS_KEYS = ALIAS_KEYS; 63 | exports.NODE_FIELDS = NODE_FIELDS; 64 | exports.BUILDER_KEYS = BUILDER_KEYS; 65 | 66 | /** 67 | * Registers `is[Type]` and `assert[Type]` for all types. 68 | */ 69 | 70 | for (var type in t.NODE_FIELDS) { 71 | registerType(type); 72 | } 73 | 74 | /** 75 | * Flip `ALIAS_KEYS` for faster access in the reverse direction. 76 | */ 77 | 78 | var TYPES = (exports.TYPES = []); 79 | 80 | t.FLIPPED_ALIAS_KEYS = Object.keys(t.ALIAS_KEYS).reduce(function (acc, type) { 81 | var aliasKeys = t.ALIAS_KEYS[type]; 82 | 83 | aliasKeys.forEach(function (alias) { 84 | if (acc[alias] === undefined) { 85 | TYPES.push(alias); // Populate `TYPES` with FLIPPED_ALIAS_KEY(S) 86 | 87 | // Registers `is[Alias]` and `assert[Alias]` functions for all aliases. 88 | t[alias.toUpperCase() + '_TYPES'] = acc[alias]; 89 | registerType(alias); 90 | 91 | acc[alias] = []; 92 | } 93 | 94 | acc[alias].push(type); 95 | }); 96 | 97 | return acc; 98 | }, {}); 99 | 100 | /** 101 | * Returns whether `node` is of given `type`. 102 | * 103 | * For better performance, use this instead of `is[Type]` when `type` is unknown. 104 | * Optionally, pass `skipAliasCheck` to directly compare `node.type` with `type`. 105 | */ 106 | 107 | function is(type, node, opts) { 108 | if ( 109 | node === null || 110 | (typeof node === 'undefined' ? 'undefined' : _typeof(node)) !== 'object' 111 | ) { 112 | return false; 113 | } 114 | 115 | var matches = isType(node.kind, type); 116 | if (!matches) { 117 | return false; 118 | } 119 | 120 | if (typeof opts === 'undefined') { 121 | return true; 122 | } else { 123 | return t.shallowEqual(node, opts); 124 | } 125 | } 126 | 127 | /** 128 | * Test if a `nodeType` is a `targetType` or if `targetType` is an alias of `nodeType`. 129 | */ 130 | 131 | function isType(nodeType, targetType) { 132 | if (nodeType === targetType) { 133 | return true; 134 | } 135 | 136 | // This is a fast-path. If the test above failed, but an alias key is found, then the 137 | // targetType was a primary node type, so there's no need to check the aliases. 138 | if (t.ALIAS_KEYS[targetType]) { 139 | return false; 140 | } 141 | 142 | var aliases = t.FLIPPED_ALIAS_KEYS[targetType]; 143 | if (aliases) { 144 | if (aliases[0] === nodeType) { 145 | return true; 146 | } 147 | 148 | var _iteratorNormalCompletion = true; 149 | var _didIteratorError = false; 150 | var _iteratorError = undefined; 151 | 152 | try { 153 | for ( 154 | var _iterator = aliases[Symbol.iterator](), _step; 155 | !(_iteratorNormalCompletion = (_step = _iterator.next()).done); 156 | _iteratorNormalCompletion = true 157 | ) { 158 | var alias = _step.value; 159 | 160 | if (nodeType === alias) { 161 | return true; 162 | } 163 | } 164 | } catch (err) { 165 | _didIteratorError = true; 166 | _iteratorError = err; 167 | } finally { 168 | try { 169 | if (!_iteratorNormalCompletion && _iterator.return) { 170 | _iterator.return(); 171 | } 172 | } finally { 173 | if (_didIteratorError) { 174 | throw _iteratorError; 175 | } 176 | } 177 | } 178 | } 179 | 180 | return false; 181 | } 182 | 183 | /** 184 | * For each call of #defineType, the following expression evalutates and generates 185 | * a builder function that validates incoming arguments and returns a valid AST node. 186 | */ 187 | 188 | var _loop = function _loop(_type) { 189 | var keys = t.BUILDER_KEYS[_type]; 190 | var fields = t.NODE_FIELDS[_type]; 191 | 192 | function builder() { 193 | for ( 194 | var _len = arguments.length, args = Array(_len), _key = 0; 195 | _key < _len; 196 | _key++ 197 | ) { 198 | args[_key] = arguments[_key]; 199 | } 200 | 201 | if (args.length > keys.length) { 202 | throw new Error( 203 | 't.' + 204 | _type + 205 | ': Too many arguments passed. Received ' + 206 | args.length + 207 | ' but can receive ' + 208 | ('no more than ' + keys.length) 209 | ); 210 | } 211 | 212 | var node = keys.reduce( 213 | function (node, key, i) { 214 | node[key] = args[i] === undefined ? fields[key].default : args[i]; 215 | return node; 216 | }, 217 | { kind: _type } 218 | ); 219 | 220 | for (var key in node) { 221 | validate(node, key, node[key]); 222 | } 223 | 224 | return node; 225 | } 226 | 227 | t[_type[0].toLowerCase() + _type.slice(1)] = builder; 228 | }; 229 | 230 | for (var _type in t.BUILDER_KEYS) { 231 | _loop(_type); 232 | } 233 | 234 | /** 235 | * Executes the field validators for a given node 236 | */ 237 | 238 | function validate(node, key, val) { 239 | if ( 240 | node === null || 241 | (typeof node === 'undefined' ? 'undefined' : _typeof(node)) !== 'object' 242 | ) { 243 | return; 244 | } 245 | 246 | var fields = t.NODE_FIELDS[node.kind]; 247 | if (fields === undefined) { 248 | return; 249 | } 250 | 251 | var field = fields[key]; 252 | if (field === undefined || field.validate === undefined) { 253 | return; 254 | } 255 | 256 | if (field.optional && (val === undefined || val === null)) { 257 | return; 258 | } 259 | 260 | field.validate(node, key, val); 261 | } 262 | 263 | /** 264 | * Test if an object is shallowly equal. 265 | */ 266 | 267 | function shallowEqual(actual, expected) { 268 | for (var key in expected) { 269 | if (expected.hasOwnProperty(key) && actual[key] !== expected[key]) { 270 | return false; 271 | } 272 | } 273 | 274 | return true; 275 | } 276 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC5qcyJdLCJuYW1lcyI6WyJpcyIsImlzVHlwZSIsInZhbGlkYXRlIiwic2hhbGxvd0VxdWFsIiwicmVxdWlyZSIsIkFMSUFTX0tFWVMiLCJOT0RFX0ZJRUxEUyIsIkJVSUxERVJfS0VZUyIsInQiLCJleHBvcnRzIiwicmVnaXN0ZXJUeXBlIiwidHlwZSIsImtleSIsIl9pc1R5cGUiLCJ1bmRlZmluZWQiLCJub2RlIiwib3B0cyIsIkVycm9yIiwiSlNPTiIsInN0cmluZ2lmeSIsIlRZUEVTIiwiRkxJUFBFRF9BTElBU19LRVlTIiwiT2JqZWN0Iiwia2V5cyIsInJlZHVjZSIsImFjYyIsImFsaWFzS2V5cyIsImZvckVhY2giLCJhbGlhcyIsInB1c2giLCJ0b1VwcGVyQ2FzZSIsIm1hdGNoZXMiLCJraW5kIiwibm9kZVR5cGUiLCJ0YXJnZXRUeXBlIiwiYWxpYXNlcyIsImZpZWxkcyIsImJ1aWxkZXIiLCJhcmdzIiwibGVuZ3RoIiwiaSIsImRlZmF1bHQiLCJ0b0xvd2VyQ2FzZSIsInNsaWNlIiwidmFsIiwiZmllbGQiLCJvcHRpb25hbCIsImFjdHVhbCIsImV4cGVjdGVkIiwiaGFzT3duUHJvcGVydHkiXSwibWFwcGluZ3MiOiI7Ozs7Ozs7O1FBc0VnQkEsRSxHQUFBQSxFO1FBcUJBQyxNLEdBQUFBLE07UUFrRUFDLFEsR0FBQUEsUTtRQTBCQUMsWSxHQUFBQSxZO0FBckxoQkMsUUFBUSxvQkFBUjs7ZUFFa0RBLFFBQVEsZUFBUixDO0lBQTFDQyxVLFlBQUFBLFU7SUFBWUMsVyxZQUFBQSxXO0lBQWFDLFksWUFBQUEsWTs7QUFFakMsSUFBTUMsSUFBSUMsT0FBVixDLENBQW1COztBQUVuQjs7Ozs7QUFLQSxTQUFTQyxZQUFULENBQXNCQyxJQUF0QixFQUFvQztBQUNsQyxNQUFNQyxhQUFXRCxJQUFqQjs7QUFFQSxNQUFNRSxVQUFVTCxFQUFFSSxHQUFGLE1BQVdFLFNBQVgsR0FDWk4sRUFBRUksR0FBRixDQURZLEdBRVpKLEVBQUVJLEdBQUYsSUFBUyxVQUFDRyxJQUFELEVBQU9DLElBQVA7QUFBQSxXQUFnQlIsRUFBRVIsRUFBRixDQUFLVyxJQUFMLEVBQVdJLElBQVgsRUFBaUJDLElBQWpCLENBQWhCO0FBQUEsR0FGYjs7QUFJQVIsZUFBV0csSUFBWCxJQUFxQixVQUFDSSxJQUFELEVBQXFCO0FBQUEsUUFBZEMsSUFBYyx1RUFBUCxFQUFPOztBQUN4QyxRQUFJLENBQUNILFFBQVFFLElBQVIsRUFBY0MsSUFBZCxDQUFMLEVBQTBCO0FBQ3hCLFlBQU0sSUFBSUMsS0FBSixxQkFBNEJOLElBQTVCLHNCQUFpRE8sS0FBS0MsU0FBTCxDQUFlSCxJQUFmLENBQWpELENBQU47QUFDRDtBQUNGLEdBSkQ7QUFLRDs7UUFFUVgsVSxHQUFBQSxVO1FBQVlDLFcsR0FBQUEsVztRQUFhQyxZLEdBQUFBLFk7O0FBRWxDOzs7O0FBSUEsS0FBSyxJQUFNSSxJQUFYLElBQW1CSCxFQUFFRixXQUFyQixFQUFrQztBQUNoQ0ksZUFBYUMsSUFBYjtBQUNEOztBQUVEOzs7O0FBSU8sSUFBTVMsd0JBQVEsRUFBZDs7QUFFUFosRUFBRWEsa0JBQUYsR0FBdUJDLE9BQU9DLElBQVAsQ0FBWWYsRUFBRUgsVUFBZCxFQUEwQm1CLE1BQTFCLENBQWlDLFVBQUNDLEdBQUQsRUFBTWQsSUFBTixFQUFlO0FBQ3JFLE1BQU1lLFlBQVlsQixFQUFFSCxVQUFGLENBQWFNLElBQWIsQ0FBbEI7O0FBRUFlLFlBQVVDLE9BQVYsQ0FBa0IsaUJBQVM7QUFDekIsUUFBSUYsSUFBSUcsS0FBSixNQUFlZCxTQUFuQixFQUE4QjtBQUM1Qk0sWUFBTVMsSUFBTixDQUFXRCxLQUFYLEVBRDRCLENBQ1Q7O0FBRW5CO0FBQ0FwQixRQUFLb0IsTUFBTUUsV0FBTixFQUFMLGVBQW9DTCxJQUFJRyxLQUFKLENBQXBDO0FBQ0FsQixtQkFBYWtCLEtBQWI7O0FBRUFILFVBQUlHLEtBQUosSUFBYSxFQUFiO0FBQ0Q7O0FBRURILFFBQUlHLEtBQUosRUFBV0MsSUFBWCxDQUFnQmxCLElBQWhCO0FBQ0QsR0FaRDs7QUFjQSxTQUFPYyxHQUFQO0FBQ0QsQ0FsQnNCLEVBa0JwQixFQWxCb0IsQ0FBdkI7O0FBb0JBOzs7Ozs7O0FBT08sU0FBU3pCLEVBQVQsQ0FBWVcsSUFBWixFQUEwQkksSUFBMUIsRUFBd0NDLElBQXhDLEVBQWdFO0FBQ3JFLE1BQUlELFNBQVMsSUFBVCxJQUFpQixRQUFPQSxJQUFQLHlDQUFPQSxJQUFQLE9BQWdCLFFBQXJDLEVBQStDO0FBQzdDLFdBQU8sS0FBUDtBQUNEOztBQUVELE1BQU1nQixVQUFVOUIsT0FBT2MsS0FBS2lCLElBQVosRUFBa0JyQixJQUFsQixDQUFoQjtBQUNBLE1BQUksQ0FBQ29CLE9BQUwsRUFBYztBQUNaLFdBQU8sS0FBUDtBQUNEOztBQUVELE1BQUksT0FBT2YsSUFBUCxLQUFnQixXQUFwQixFQUFpQztBQUMvQixXQUFPLElBQVA7QUFDRCxHQUZELE1BRU87QUFDTCxXQUFPUixFQUFFTCxZQUFGLENBQWVZLElBQWYsRUFBcUJDLElBQXJCLENBQVA7QUFDRDtBQUNGOztBQUVEOzs7O0FBSU8sU0FBU2YsTUFBVCxDQUFnQmdDLFFBQWhCLEVBQWtDQyxVQUFsQyxFQUErRDtBQUNwRSxNQUFJRCxhQUFhQyxVQUFqQixFQUE2QjtBQUMzQixXQUFPLElBQVA7QUFDRDs7QUFFRDtBQUNBO0FBQ0EsTUFBSTFCLEVBQUVILFVBQUYsQ0FBYTZCLFVBQWIsQ0FBSixFQUE4QjtBQUM1QixXQUFPLEtBQVA7QUFDRDs7QUFFRCxNQUFNQyxVQUEwQjNCLEVBQUVhLGtCQUFGLENBQXFCYSxVQUFyQixDQUFoQztBQUNBLE1BQUlDLE9BQUosRUFBYTtBQUNYLFFBQUlBLFFBQVEsQ0FBUixNQUFlRixRQUFuQixFQUE2QjtBQUMzQixhQUFPLElBQVA7QUFDRDs7QUFIVTtBQUFBO0FBQUE7O0FBQUE7QUFLWCwyQkFBb0JFLE9BQXBCLDhIQUE2QjtBQUFBLFlBQWxCUCxLQUFrQjs7QUFDM0IsWUFBSUssYUFBYUwsS0FBakIsRUFBd0I7QUFDdEIsaUJBQU8sSUFBUDtBQUNEO0FBQ0Y7QUFUVTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBVVo7O0FBRUQsU0FBTyxLQUFQO0FBQ0Q7O0FBRUQ7Ozs7OzJCQUtXakIsSztBQUNULE1BQU1ZLE9BQU9mLEVBQUVELFlBQUYsQ0FBZUksS0FBZixDQUFiO0FBQ0EsTUFBTXlCLFNBQVM1QixFQUFFRixXQUFGLENBQWNLLEtBQWQsQ0FBZjs7QUFFQSxXQUFTMEIsT0FBVCxHQUEwQjtBQUFBLHNDQUFOQyxJQUFNO0FBQU5BLFVBQU07QUFBQTs7QUFDeEIsUUFBSUEsS0FBS0MsTUFBTCxHQUFjaEIsS0FBS2dCLE1BQXZCLEVBQStCO0FBQzdCLFlBQU0sSUFBSXRCLEtBQUosQ0FDSixPQUFLTixLQUFMLDhDQUFrRDJCLEtBQUtDLE1BQXZELDRDQUNrQmhCLEtBQUtnQixNQUR2QixDQURJLENBQU47QUFJRDs7QUFFRCxRQUFNeEIsT0FBT1EsS0FBS0MsTUFBTCxDQUNYLFVBQUNULElBQUQsRUFBT0gsR0FBUCxFQUFZNEIsQ0FBWixFQUFrQjtBQUNoQnpCLFdBQUtILEdBQUwsSUFBYTBCLEtBQUtFLENBQUwsTUFBWTFCLFNBQVosR0FBd0JzQixPQUFPeEIsR0FBUCxFQUFZNkIsT0FBcEMsR0FBOENILEtBQUtFLENBQUwsQ0FBM0Q7QUFDQSxhQUFPekIsSUFBUDtBQUNELEtBSlUsRUFLWCxFQUFFaUIsTUFBTXJCLEtBQVIsRUFMVyxDQUFiOztBQVFBLFNBQUssSUFBTUMsR0FBWCxJQUFrQkcsSUFBbEIsRUFBd0I7QUFDdEJiLGVBQVNhLElBQVQsRUFBZUgsR0FBZixFQUFvQkcsS0FBS0gsR0FBTCxDQUFwQjtBQUNEOztBQUVELFdBQU9HLElBQVA7QUFDRDs7QUFFRFAsSUFBRUcsTUFBSyxDQUFMLEVBQVErQixXQUFSLEtBQXdCL0IsTUFBS2dDLEtBQUwsQ0FBVyxDQUFYLENBQTFCLElBQTJDTixPQUEzQzs7O0FBM0JGLEtBQUssSUFBTTFCLEtBQVgsSUFBbUJILEVBQUVELFlBQXJCLEVBQW1DO0FBQUEsUUFBeEJJLEtBQXdCO0FBNEJsQzs7QUFFRDs7OztBQUlPLFNBQVNULFFBQVQsQ0FBa0JhLElBQWxCLEVBQWlDSCxHQUFqQyxFQUE4Q2dDLEdBQTlDLEVBQXdEO0FBQzdELE1BQUk3QixTQUFTLElBQVQsSUFBaUIsUUFBT0EsSUFBUCx5Q0FBT0EsSUFBUCxPQUFnQixRQUFyQyxFQUErQztBQUM3QztBQUNEOztBQUVELE1BQU1xQixTQUFTNUIsRUFBRUYsV0FBRixDQUFjUyxLQUFLaUIsSUFBbkIsQ0FBZjtBQUNBLE1BQUlJLFdBQVd0QixTQUFmLEVBQTBCO0FBQ3hCO0FBQ0Q7O0FBRUQsTUFBTStCLFFBQVFULE9BQU94QixHQUFQLENBQWQ7QUFDQSxNQUFJaUMsVUFBVS9CLFNBQVYsSUFBdUIrQixNQUFNM0MsUUFBTixLQUFtQlksU0FBOUMsRUFBeUQ7QUFDdkQ7QUFDRDs7QUFFRCxNQUFJK0IsTUFBTUMsUUFBTixLQUFtQkYsUUFBUTlCLFNBQVIsSUFBcUI4QixRQUFRLElBQWhELENBQUosRUFBMkQ7QUFDekQ7QUFDRDs7QUFFREMsUUFBTTNDLFFBQU4sQ0FBZWEsSUFBZixFQUFxQkgsR0FBckIsRUFBMEJnQyxHQUExQjtBQUNEOztBQUVEOzs7O0FBSU8sU0FBU3pDLFlBQVQsQ0FBc0I0QyxNQUF0QixFQUFzQ0MsUUFBdEMsRUFBaUU7QUFDdEUsT0FBSyxJQUFNcEMsR0FBWCxJQUFrQm9DLFFBQWxCLEVBQTRCO0FBQzFCLFFBQUlBLFNBQVNDLGNBQVQsQ0FBd0JyQyxHQUF4QixLQUFnQ21DLE9BQU9uQyxHQUFQLE1BQWdCb0MsU0FBU3BDLEdBQVQsQ0FBcEQsRUFBbUU7QUFDakUsYUFBTyxLQUFQO0FBQ0Q7QUFDRjs7QUFFRCxTQUFPLElBQVA7QUFDRCIsImZpbGUiOiJpbmRleC5qcyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEBmbG93XG5cbnJlcXVpcmUoJy4vZGVmaW5pdGlvbnMvaW5pdCcpO1xuXG5jb25zdCB7IEFMSUFTX0tFWVMsIE5PREVfRklFTERTLCBCVUlMREVSX0tFWVMgfSA9IHJlcXVpcmUoJy4vZGVmaW5pdGlvbnMnKTtcblxuY29uc3QgdCA9IGV4cG9ydHM7IC8vIE1hcHMgYWxsIGV4cG9ydHMgdG8gdFxuXG4vKipcbiAqIFJlZ2lzdGVycyBgaXNbVHlwZV1gIGFuZCBgYXNzZXJ0W1R5cGVdYCBnZW5lcmF0ZWQgZnVuY3Rpb25zIGZvciBhIGdpdmVuIGB0eXBlYC5cbiAqIFBhc3MgYHNraXBBbGlhc0NoZWNrYCB0byBmb3JjZSBpdCB0byBkaXJlY3RseSBjb21wYXJlIGBub2RlLnR5cGVgIHdpdGggYHR5cGVgLlxuICovXG5cbmZ1bmN0aW9uIHJlZ2lzdGVyVHlwZSh0eXBlOiBzdHJpbmcpIHtcbiAgY29uc3Qga2V5ID0gYGlzJHt0eXBlfWA7XG5cbiAgY29uc3QgX2lzVHlwZSA9IHRba2V5XSAhPT0gdW5kZWZpbmVkXG4gICAgPyB0W2tleV1cbiAgICA6IHRba2V5XSA9IChub2RlLCBvcHRzKSA9PiB0LmlzKHR5cGUsIG5vZGUsIG9wdHMpO1xuXG4gIHRbYGFzc2VydCR7dHlwZX1gXSA9IChub2RlLCBvcHRzID0ge30pID0+IHtcbiAgICBpZiAoIV9pc1R5cGUobm9kZSwgb3B0cykpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgRXhwZWN0ZWQgdHlwZSBcIiR7dHlwZX1cIiB3aXRoIG9wdGlvbiAke0pTT04uc3RyaW5naWZ5KG9wdHMpfWApO1xuICAgIH1cbiAgfTtcbn1cblxuZXhwb3J0IHsgQUxJQVNfS0VZUywgTk9ERV9GSUVMRFMsIEJVSUxERVJfS0VZUyB9O1xuXG4vKipcbiAqIFJlZ2lzdGVycyBgaXNbVHlwZV1gIGFuZCBgYXNzZXJ0W1R5cGVdYCBmb3IgYWxsIHR5cGVzLlxuICovXG5cbmZvciAoY29uc3QgdHlwZSBpbiB0Lk5PREVfRklFTERTKSB7XG4gIHJlZ2lzdGVyVHlwZSh0eXBlKTtcbn1cblxuLyoqXG4gKiBGbGlwIGBBTElBU19LRVlTYCBmb3IgZmFzdGVyIGFjY2VzcyBpbiB0aGUgcmV2ZXJzZSBkaXJlY3Rpb24uXG4gKi9cblxuZXhwb3J0IGNvbnN0IFRZUEVTID0gW107XG5cbnQuRkxJUFBFRF9BTElBU19LRVlTID0gT2JqZWN0LmtleXModC5BTElBU19LRVlTKS5yZWR1Y2UoKGFjYywgdHlwZSkgPT4ge1xuICBjb25zdCBhbGlhc0tleXMgPSB0LkFMSUFTX0tFWVNbdHlwZV07XG5cbiAgYWxpYXNLZXlzLmZvckVhY2goYWxpYXMgPT4ge1xuICAgIGlmIChhY2NbYWxpYXNdID09PSB1bmRlZmluZWQpIHtcbiAgICAgIFRZUEVTLnB1c2goYWxpYXMpOyAvLyBQb3B1bGF0ZSBgVFlQRVNgIHdpdGggRkxJUFBFRF9BTElBU19LRVkoUylcblxuICAgICAgLy8gUmVnaXN0ZXJzIGBpc1tBbGlhc11gIGFuZCBgYXNzZXJ0W0FsaWFzXWAgZnVuY3Rpb25zIGZvciBhbGwgYWxpYXNlcy5cbiAgICAgIHRbYCR7YWxpYXMudG9VcHBlckNhc2UoKX1fVFlQRVNgXSA9IGFjY1thbGlhc107XG4gICAgICByZWdpc3RlclR5cGUoYWxpYXMpO1xuXG4gICAgICBhY2NbYWxpYXNdID0gW107XG4gICAgfVxuXG4gICAgYWNjW2FsaWFzXS5wdXNoKHR5cGUpO1xuICB9KTtcblxuICByZXR1cm4gYWNjO1xufSwge30pO1xuXG4vKipcbiAqIFJldHVybnMgd2hldGhlciBgbm9kZWAgaXMgb2YgZ2l2ZW4gYHR5cGVgLlxuICpcbiAqIEZvciBiZXR0ZXIgcGVyZm9ybWFuY2UsIHVzZSB0aGlzIGluc3RlYWQgb2YgYGlzW1R5cGVdYCB3aGVuIGB0eXBlYCBpcyB1bmtub3duLlxuICogT3B0aW9uYWxseSwgcGFzcyBgc2tpcEFsaWFzQ2hlY2tgIHRvIGRpcmVjdGx5IGNvbXBhcmUgYG5vZGUudHlwZWAgd2l0aCBgdHlwZWAuXG4gKi9cblxuZXhwb3J0IGZ1bmN0aW9uIGlzKHR5cGU6IHN0cmluZywgbm9kZTogT2JqZWN0LCBvcHRzPzogT2JqZWN0KTogYm9vbGVhbiB7XG4gIGlmIChub2RlID09PSBudWxsIHx8IHR5cGVvZiBub2RlICE9PSAnb2JqZWN0Jykge1xuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIGNvbnN0IG1hdGNoZXMgPSBpc1R5cGUobm9kZS5raW5kLCB0eXBlKTtcbiAgaWYgKCFtYXRjaGVzKSB7XG4gICAgcmV0dXJuIGZhbHNlO1xuICB9XG5cbiAgaWYgKHR5cGVvZiBvcHRzID09PSAndW5kZWZpbmVkJykge1xuICAgIHJldHVybiB0cnVlO1xuICB9IGVsc2Uge1xuICAgIHJldHVybiB0LnNoYWxsb3dFcXVhbChub2RlLCBvcHRzKTtcbiAgfVxufVxuXG4vKipcbiAqIFRlc3QgaWYgYSBgbm9kZVR5cGVgIGlzIGEgYHRhcmdldFR5cGVgIG9yIGlmIGB0YXJnZXRUeXBlYCBpcyBhbiBhbGlhcyBvZiBgbm9kZVR5cGVgLlxuICovXG5cbmV4cG9ydCBmdW5jdGlvbiBpc1R5cGUobm9kZVR5cGU6IHN0cmluZywgdGFyZ2V0VHlwZTogc3RyaW5nKTogYm9vbGVhbiB7XG4gIGlmIChub2RlVHlwZSA9PT0gdGFyZ2V0VHlwZSkge1xuICAgIHJldHVybiB0cnVlO1xuICB9XG5cbiAgLy8gVGhpcyBpcyBhIGZhc3QtcGF0aC4gSWYgdGhlIHRlc3QgYWJvdmUgZmFpbGVkLCBidXQgYW4gYWxpYXMga2V5IGlzIGZvdW5kLCB0aGVuIHRoZVxuICAvLyB0YXJnZXRUeXBlIHdhcyBhIHByaW1hcnkgbm9kZSB0eXBlLCBzbyB0aGVyZSdzIG5vIG5lZWQgdG8gY2hlY2sgdGhlIGFsaWFzZXMuXG4gIGlmICh0LkFMSUFTX0tFWVNbdGFyZ2V0VHlwZV0pIHtcbiAgICByZXR1cm4gZmFsc2U7XG4gIH1cblxuICBjb25zdCBhbGlhc2VzOiA/QXJyYXk8c3RyaW5nPiA9IHQuRkxJUFBFRF9BTElBU19LRVlTW3RhcmdldFR5cGVdO1xuICBpZiAoYWxpYXNlcykge1xuICAgIGlmIChhbGlhc2VzWzBdID09PSBub2RlVHlwZSkge1xuICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuXG4gICAgZm9yIChjb25zdCBhbGlhcyBvZiBhbGlhc2VzKSB7XG4gICAgICBpZiAobm9kZVR5cGUgPT09IGFsaWFzKSB7XG4gICAgICAgIHJldHVybiB0cnVlO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiBmYWxzZTtcbn1cblxuLyoqXG4gKiBGb3IgZWFjaCBjYWxsIG9mICNkZWZpbmVUeXBlLCB0aGUgZm9sbG93aW5nIGV4cHJlc3Npb24gZXZhbHV0YXRlcyBhbmQgZ2VuZXJhdGVzXG4gKiBhIGJ1aWxkZXIgZnVuY3Rpb24gdGhhdCB2YWxpZGF0ZXMgaW5jb21pbmcgYXJndW1lbnRzIGFuZCByZXR1cm5zIGEgdmFsaWQgQVNUIG5vZGUuXG4gKi9cblxuZm9yIChjb25zdCB0eXBlIGluIHQuQlVJTERFUl9LRVlTKSB7XG4gIGNvbnN0IGtleXMgPSB0LkJVSUxERVJfS0VZU1t0eXBlXTtcbiAgY29uc3QgZmllbGRzID0gdC5OT0RFX0ZJRUxEU1t0eXBlXTtcblxuICBmdW5jdGlvbiBidWlsZGVyKC4uLmFyZ3MpIHtcbiAgICBpZiAoYXJncy5sZW5ndGggPiBrZXlzLmxlbmd0aCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKFxuICAgICAgICBgdC4ke3R5cGV9OiBUb28gbWFueSBhcmd1bWVudHMgcGFzc2VkLiBSZWNlaXZlZCAke2FyZ3MubGVuZ3RofSBidXQgY2FuIHJlY2VpdmUgYCArXG4gICAgICAgICAgYG5vIG1vcmUgdGhhbiAke2tleXMubGVuZ3RofWBcbiAgICAgICk7XG4gICAgfVxuXG4gICAgY29uc3Qgbm9kZSA9IGtleXMucmVkdWNlKFxuICAgICAgKG5vZGUsIGtleSwgaSkgPT4ge1xuICAgICAgICBub2RlW2tleV0gPSAoYXJnc1tpXSA9PT0gdW5kZWZpbmVkID8gZmllbGRzW2tleV0uZGVmYXVsdCA6IGFyZ3NbaV0pO1xuICAgICAgICByZXR1cm4gbm9kZTtcbiAgICAgIH0sXG4gICAgICB7IGtpbmQ6IHR5cGUgfVxuICAgICk7XG5cbiAgICBmb3IgKGNvbnN0IGtleSBpbiBub2RlKSB7XG4gICAgICB2YWxpZGF0ZShub2RlLCBrZXksIG5vZGVba2V5XSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIG5vZGU7XG4gIH1cblxuICB0W3R5cGVbMF0udG9Mb3dlckNhc2UoKSArIHR5cGUuc2xpY2UoMSldID0gYnVpbGRlcjtcbn1cblxuLyoqXG4gKiBFeGVjdXRlcyB0aGUgZmllbGQgdmFsaWRhdG9ycyBmb3IgYSBnaXZlbiBub2RlXG4gKi9cblxuZXhwb3J0IGZ1bmN0aW9uIHZhbGlkYXRlKG5vZGU/OiBPYmplY3QsIGtleTogc3RyaW5nLCB2YWw6IGFueSkge1xuICBpZiAobm9kZSA9PT0gbnVsbCB8fCB0eXBlb2Ygbm9kZSAhPT0gJ29iamVjdCcpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBjb25zdCBmaWVsZHMgPSB0Lk5PREVfRklFTERTW25vZGUua2luZF07XG4gIGlmIChmaWVsZHMgPT09IHVuZGVmaW5lZCkge1xuICAgIHJldHVybjtcbiAgfVxuXG4gIGNvbnN0IGZpZWxkID0gZmllbGRzW2tleV07XG4gIGlmIChmaWVsZCA9PT0gdW5kZWZpbmVkIHx8IGZpZWxkLnZhbGlkYXRlID09PSB1bmRlZmluZWQpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBpZiAoZmllbGQub3B0aW9uYWwgJiYgKHZhbCA9PT0gdW5kZWZpbmVkIHx8IHZhbCA9PT0gbnVsbCkpIHtcbiAgICByZXR1cm47XG4gIH1cblxuICBmaWVsZC52YWxpZGF0ZShub2RlLCBrZXksIHZhbCk7XG59XG5cbi8qKlxuICogVGVzdCBpZiBhbiBvYmplY3QgaXMgc2hhbGxvd2x5IGVxdWFsLlxuICovXG5cbmV4cG9ydCBmdW5jdGlvbiBzaGFsbG93RXF1YWwoYWN0dWFsOiBPYmplY3QsIGV4cGVjdGVkOiBPYmplY3QpOiBib29sZWFuIHtcbiAgZm9yIChjb25zdCBrZXkgaW4gZXhwZWN0ZWQpIHtcbiAgICBpZiAoZXhwZWN0ZWQuaGFzT3duUHJvcGVydHkoa2V5KSAmJiBhY3R1YWxba2V5XSAhPT0gZXhwZWN0ZWRba2V5XSkge1xuICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiB0cnVlO1xufVxuIl19 277 | -------------------------------------------------------------------------------- /src/helpers/fetchActions.ts: -------------------------------------------------------------------------------- 1 | import { FetchType } from '../types'; 2 | 3 | export const GET_LIST = 'GET_LIST'; 4 | export const GET_ONE = 'GET_ONE'; 5 | export const GET_MANY = 'GET_MANY'; 6 | export const GET_MANY_REFERENCE = 'GET_MANY_REFERENCE'; 7 | export const CREATE = 'CREATE'; 8 | export const UPDATE = 'UPDATE'; 9 | export const UPDATE_MANY = 'UPDATE_MANY'; 10 | export const DELETE = 'DELETE'; 11 | export const DELETE_MANY = 'DELETE_MANY'; 12 | 13 | export const fetchActionsWithRecordResponse = [GET_ONE, CREATE, UPDATE]; 14 | export const fetchActionsWithArrayOfIdentifiedRecordsResponse = [ 15 | GET_LIST, 16 | GET_MANY, 17 | GET_MANY_REFERENCE, 18 | ]; 19 | export const fetchActionsWithArrayOfRecordsResponse = [ 20 | ...fetchActionsWithArrayOfIdentifiedRecordsResponse, 21 | UPDATE_MANY, 22 | DELETE_MANY, 23 | ]; 24 | export const fetchActionsWithTotalResponse = [GET_LIST, GET_MANY_REFERENCE]; 25 | 26 | export const sanitizeFetchType = (fetchType: FetchType) => { 27 | switch (fetchType) { 28 | case GET_LIST: 29 | return 'getList'; 30 | case GET_ONE: 31 | return 'getOne'; 32 | case GET_MANY: 33 | return 'getMany'; 34 | case GET_MANY_REFERENCE: 35 | return 'getManyReference'; 36 | case CREATE: 37 | return 'create'; 38 | case UPDATE: 39 | return 'update'; 40 | case UPDATE_MANY: 41 | return 'updateMany'; 42 | case DELETE: 43 | return 'delete'; 44 | case DELETE_MANY: 45 | return 'deleteMany'; 46 | default: 47 | return fetchType; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/helpers/getArgType.ts: -------------------------------------------------------------------------------- 1 | import * as gqlTypes from '../graphql-ast-types-browser'; 2 | import getFinalType from './getFinalType'; 3 | import isRequired from './isRequired'; 4 | import isList from './isList'; 5 | 6 | const getArgType = (arg: any) => { 7 | const type = getFinalType(arg.type); 8 | const required = isRequired(arg.type); 9 | const list = isList(arg.type); 10 | 11 | if (required) { 12 | if (list) { 13 | return gqlTypes.nonNullType( 14 | gqlTypes.listType( 15 | gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name))) 16 | ) 17 | ); 18 | } 19 | 20 | return gqlTypes.nonNullType(gqlTypes.namedType(gqlTypes.name(type.name))); 21 | } 22 | 23 | if (list) { 24 | return gqlTypes.listType(gqlTypes.namedType(gqlTypes.name(type.name))); 25 | } 26 | 27 | return gqlTypes.namedType(gqlTypes.name(type.name)); 28 | }; 29 | 30 | export default getArgType; 31 | -------------------------------------------------------------------------------- /src/helpers/getFinalType.test.ts: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | import getFinalType from './getFinalType'; 3 | 4 | describe('getFinalType', () => { 5 | it('returns the correct type for SCALAR types', () => { 6 | expect(getFinalType({ name: 'foo', kind: TypeKind.SCALAR })).toEqual({ 7 | name: 'foo', 8 | kind: TypeKind.SCALAR, 9 | }); 10 | }); 11 | it('returns the correct type for NON_NULL types', () => { 12 | expect( 13 | getFinalType({ 14 | kind: TypeKind.NON_NULL, 15 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 16 | }) 17 | ).toEqual({ 18 | name: 'foo', 19 | kind: TypeKind.SCALAR, 20 | }); 21 | }); 22 | it('returns the correct type for LIST types', () => { 23 | expect( 24 | getFinalType({ 25 | kind: TypeKind.LIST, 26 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 27 | }) 28 | ).toEqual({ 29 | name: 'foo', 30 | kind: TypeKind.SCALAR, 31 | }); 32 | }); 33 | it('returns the correct type for NON_NULL LIST types', () => { 34 | expect( 35 | getFinalType({ 36 | kind: TypeKind.NON_NULL, 37 | ofType: { 38 | kind: TypeKind.LIST, 39 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 40 | }, 41 | }) 42 | ).toEqual({ name: 'foo', kind: TypeKind.SCALAR }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/helpers/getFinalType.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionType, 3 | IntrospectionTypeRef, 4 | IntrospectionNonNullTypeRef, 5 | TypeKind, 6 | } from 'graphql'; 7 | 8 | type GraphQLTypes = 9 | | IntrospectionType 10 | | IntrospectionNonNullTypeRef 11 | | IntrospectionTypeRef; 12 | 13 | /** 14 | * Ensure we get the real type even if the root type is NON_NULL or LIST 15 | * @param {GraphQLType} type 16 | */ 17 | const getFinalType = (type: GraphQLTypes): IntrospectionType => { 18 | if (type.kind === TypeKind.NON_NULL || type.kind === TypeKind.LIST) { 19 | return getFinalType(type.ofType); 20 | } 21 | 22 | return type as IntrospectionType; 23 | }; 24 | 25 | export default getFinalType; 26 | -------------------------------------------------------------------------------- /src/helpers/isList.test.ts: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | import isList from './isList'; 3 | 4 | describe('isList', () => { 5 | it('returns the correct type for SCALAR types', () => { 6 | expect(isList({ name: 'foo', kind: TypeKind.SCALAR })).toEqual(false); 7 | }); 8 | it('returns the correct type for NON_NULL types', () => { 9 | expect( 10 | isList({ 11 | kind: TypeKind.NON_NULL, 12 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 13 | }) 14 | ).toEqual(false); 15 | }); 16 | it('returns the correct type for LIST types', () => { 17 | expect( 18 | isList({ 19 | kind: TypeKind.LIST, 20 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 21 | }) 22 | ).toEqual(true); 23 | }); 24 | it('returns the correct type for NON_NULL LIST types', () => { 25 | expect( 26 | isList({ 27 | kind: TypeKind.NON_NULL, 28 | ofType: { 29 | kind: TypeKind.LIST, 30 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 31 | }, 32 | }) 33 | ).toEqual(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/helpers/isList.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionType, 3 | IntrospectionTypeRef, 4 | IntrospectionNonNullTypeRef, 5 | TypeKind, 6 | } from 'graphql'; 7 | 8 | const isList = ( 9 | type: IntrospectionType | IntrospectionNonNullTypeRef | IntrospectionTypeRef 10 | ): boolean => { 11 | if (type.kind === TypeKind.NON_NULL) { 12 | return isList(type.ofType); 13 | } 14 | 15 | return type.kind === TypeKind.LIST; 16 | }; 17 | 18 | export default isList; 19 | -------------------------------------------------------------------------------- /src/helpers/isRequired.test.ts: -------------------------------------------------------------------------------- 1 | import { TypeKind } from 'graphql'; 2 | import isRequired from './isRequired'; 3 | 4 | describe('isRequired', () => { 5 | it('returns the correct type for SCALAR types', () => { 6 | expect(isRequired({ name: 'foo', kind: TypeKind.SCALAR })).toEqual(false); 7 | }); 8 | it('returns the correct type for NON_NULL types', () => { 9 | expect( 10 | isRequired({ 11 | kind: TypeKind.NON_NULL, 12 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 13 | }) 14 | ).toEqual(true); 15 | }); 16 | it('returns the correct type for LIST types', () => { 17 | expect( 18 | isRequired({ 19 | kind: TypeKind.LIST, 20 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 21 | }) 22 | ).toEqual(false); 23 | }); 24 | it('returns the correct type for NON_NULL LIST types', () => { 25 | expect( 26 | isRequired({ 27 | kind: TypeKind.NON_NULL, 28 | ofType: { 29 | kind: TypeKind.LIST, 30 | ofType: { name: 'foo', kind: TypeKind.SCALAR }, 31 | }, 32 | }) 33 | ).toEqual(true); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/helpers/isRequired.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionType, 3 | IntrospectionListTypeRef, 4 | IntrospectionTypeRef, 5 | TypeKind, 6 | } from 'graphql'; 7 | 8 | const isRequired = ( 9 | type: IntrospectionType | IntrospectionListTypeRef | IntrospectionTypeRef 10 | ): boolean => { 11 | if (type.kind === TypeKind.LIST) { 12 | return isRequired(type.ofType); 13 | } 14 | 15 | return type.kind === TypeKind.NON_NULL; 16 | }; 17 | 18 | export default isRequired; 19 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { buildFields } from './buildGqlQuery/buildFields'; 2 | export type { BuildFields } from './buildGqlQuery/buildFields'; 3 | 4 | export { 5 | buildArgs, 6 | buildMetaArgs, 7 | buildApolloArgs, 8 | } from './buildGqlQuery/buildArgs'; 9 | export type { 10 | BuildArgs, 11 | BuildMetaArgs, 12 | BuildApolloArgs, 13 | } from './buildGqlQuery/buildArgs'; 14 | 15 | export { buildGqlQuery } from './buildGqlQuery'; 16 | export type { BuildGqlQuery } from './buildGqlQuery'; 17 | 18 | export { getResponseParser } from './getResponseParser'; 19 | export type { GetResponseParser } from './getResponseParser'; 20 | 21 | import buildQuery from './buildQuery'; 22 | export { buildQuery }; 23 | export type { BuildQuery, BuildQueryFactory } from './buildQuery'; 24 | 25 | export { buildVariables, BuildVariables } from './buildVariables'; 26 | 27 | export { buildCustomDataProvider as default } from './customDataProvider'; 28 | export type { BuildCustomDataProvider } from './customDataProvider'; 29 | 30 | export { FetchType } from './types'; 31 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IntrospectionObjectType, 3 | IntrospectionSchema, 4 | IntrospectionType, 5 | IntrospectionField, 6 | } from 'graphql'; 7 | 8 | export enum FetchType { 9 | GET_LIST = 'GET_LIST', 10 | GET_ONE = 'GET_ONE', 11 | GET_MANY = 'GET_MANY', 12 | GET_MANY_REFERENCE = 'GET_MANY_REFERENCE', 13 | CREATE = 'CREATE', 14 | UPDATE = 'UPDATE', 15 | UPDATE_MANY = 'UPDATE_MANY', 16 | DELETE = 'DELETE', 17 | DELETE_MANY = 'DELETE_MANY', 18 | } 19 | 20 | export type IntrospectedResource = { 21 | type: IntrospectionObjectType; 22 | } & { 23 | [fetchType in FetchType]: IntrospectionField; 24 | }; 25 | 26 | export type IntrospectionResult = { 27 | types: IntrospectionType[]; 28 | queries: IntrospectionObjectType[]; 29 | resources: IntrospectedResource[]; 30 | schema: IntrospectionSchema; 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | "target": "es2016" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | "allowJs": true /* Allow javascript files to be compiled. */, 9 | "declaration": true /* Generates corresponding '.d.ts' file. */, 10 | "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 11 | "outDir": "./dist" /* Redirect output structure to the directory. */, 12 | 13 | /* Strict Type-Checking Options */ 14 | "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Module Resolution Options */ 17 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 18 | 19 | /* Advanced Options */ 20 | "skipLibCheck": true /* Skip type checking of declaration files. */, 21 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 22 | }, 23 | "include": ["src/**/*"], 24 | "exclude": ["node_modules", "**/*.test.ts"] 25 | } 26 | --------------------------------------------------------------------------------