├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── base-tsconfig.json ├── jest.config.js ├── package.json ├── performance-test ├── performance-test.ts └── test-with-big-amount-of-data.ts ├── src ├── denormalize.ts ├── functions.ts ├── index.ts ├── norm-map.ts ├── normalize.ts ├── normalized-object.ts ├── tsconfig.json └── types.ts ├── test ├── denormalize-test-data.ts ├── denormalize-test-def.ts ├── denormalize-tests │ ├── with-complete.ts │ ├── with-incomplete.ts │ ├── with-multiple-subtree-same-query-node.ts │ └── with-scalar-incomplete.ts ├── denormalize.test.ts ├── merge.test.ts ├── normalize.test.ts ├── shared-data │ ├── standard-norm-map.ts │ └── standard-response.ts ├── shared-test-data.ts ├── shared-test-def.ts ├── shared-tests │ ├── query-with-array-of-string.ts │ ├── same-entity-twice-different-fields.ts │ ├── simple.ts │ ├── with-alias.ts │ ├── with-array-of-string.ts │ ├── with-deep-null-values.ts │ ├── with-include-literal-false.ts │ ├── with-include-literal-true.ts │ ├── with-include-variable-false.ts │ ├── with-include-variable-true.ts │ ├── with-inline-fragments.ts │ ├── with-missing-id.ts │ ├── with-named-fragments.ts │ ├── with-nested-array-of-entities.ts │ ├── with-nested-array-of-strings.ts │ ├── with-null-object.ts │ ├── with-null-values.ts │ ├── with-object-and-null-in-array.ts │ ├── with-reserved-words.ts │ ├── with-skip-literal-false.ts │ ├── with-skip-literal-true.ts │ ├── with-skip-variable-false.ts │ ├── with-skip-variable-true.ts │ ├── with-union-type-fragment-spread.ts │ ├── with-union-type-inline-fragments.ts │ ├── with-value-object-array.ts │ ├── with-value-object-nested-parents.ts │ ├── with-value-object-nested.ts │ ├── with-value-object-no-parents.ts │ ├── with-value-object-parent-with-variables.ts │ ├── with-value-object.ts │ ├── with-variables-simple-boolean-external-2.ts │ ├── with-variables-simple-boolean-external.ts │ ├── with-variables-simple-boolean.ts │ ├── with-variables-simple-int.ts │ ├── with-variables-simple-list.ts │ ├── with-variables-simple-nested-object.ts │ └── with-variables-simple-object.ts ├── test-data-utils.ts └── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", 3 | plugins: ["@typescript-eslint", "functional"], 4 | extends: ["plugin:@typescript-eslint/recommended", "prettier"], 5 | parserOptions: { 6 | ecmaVersion: 2018, 7 | sourceType: "module" 8 | }, 9 | rules: { 10 | "functional/prefer-readonly-type": ["error", { allowLocalMutation: true }], 11 | "@typescript-eslint/indent": "off", 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-non-null-assertion": "off", 14 | "@typescript-eslint/no-object-literal-type-assertion": "off", 15 | "@typescript-eslint/no-empty-interface": "off", 16 | "@typescript-eslint/array-type": ["error", { default: "generic" }], 17 | "@typescript-eslint/no-unused-vars": [ 18 | "error", 19 | { 20 | vars: "all", 21 | args: "after-used", 22 | ignoreRestSiblings: false, 23 | argsIgnorePattern: "^_" 24 | } 25 | ], 26 | "@typescript-eslint/explicit-function-return-type": [ 27 | "error", 28 | { allowExpressions: true, allowTypedFunctionExpressions: true } 29 | ] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | thumbs.db 3 | *.log 4 | node_modules/ 5 | coverage/ 6 | .nyc_output 7 | lib/ 8 | lib-test/ 9 | .rpt2_cache/ 10 | dist/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | # - "lts/*" 9 | - "12" 10 | # before_install: 11 | # before_script: 12 | script: 13 | - yarn verify 14 | after_success: 15 | - yarn report-coverage 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Test Current Test File", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", 12 | "args": [ 13 | "--runInBand", 14 | "--no-coverage", 15 | // needs the '' around it so that the () are properly handled 16 | "'test/(.+/)?${fileBasenameNoExtension}'" 17 | ], 18 | "sourceMaps": true, 19 | "console": "integratedTerminal", 20 | "internalConsoleOptions": "neverOpen" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "typescript"], 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | // "eslint.codeAction.disableRuleComment": { 5 | // "enable": true, 6 | // "location": "sameLine" 7 | // } 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased](https://github.com/dividab/graphql-norm/compare/v1.3.6...master) 9 | 10 | ## [1.3.6](https://github.com/dividab/graphql-norm/compare/v1.3.5...v1.3.6) - 2020-01-07 11 | 12 | ### Fixed 13 | 14 | - Type `NormalizedObject` should support plain objects, see issue [#55](https://github.com/dividab/graphql-norm/issues/55). 15 | 16 | ## [1.3.5](https://github.com/dividab/graphql-norm/compare/v1.3.0...v1.3.5) - 2019-10-15 17 | 18 | ### Added 19 | 20 | - Include typescript source from `src/` in published npm package. See PR [#52](https://github.com/dividab/graphql-norm/pull/52) for more info. 21 | - Versions 1.3.1 to 1.3.4 are unsuccessful attememtps to publish and have been unpublished. 22 | 23 | ## [1.3.0](https://github.com/dividab/graphql-norm/compare/v1.2.0...v1.3.0) - 2019-10-15 24 | 25 | - If denormalize() could not be fulfill a query, data will be `undefined` and `fields` will contain the first field that could not be resolved. 26 | 27 | ## [1.2.0](https://github.com/dividab/graphql-norm/compare/v1.1.0...v1.2.0) - 2019-09-20 28 | 29 | ### Added 30 | 31 | - Upgrade peer deps. The graphql package now has built-in types so no peer dependency is required for the @types/graphql package. 32 | 33 | ## [1.1.0](https://github.com/dividab/graphql-norm/compare/v1.0.0...v1.1.0) - 2019-08-20 34 | 35 | ### Added 36 | 37 | - Fix for union types. Introduces new optional argument `typeResolver` to `normalize()` and `denormalize()` functions. See issue [#49](https://github.com/dividab/graphql-norm/issues/49) and PR [#50](https://github.com/dividab/graphql-norm/pull/50). 38 | 39 | ## [1.0.0](https://github.com/dividab/graphql-norm/compare/v0.14.0...v1.0.0) - 2019-07-21 40 | 41 | ### Changed 42 | 43 | - Removed the `partial` property from the `denormalize()` return object. Partial results was not implemented and instead we can check if the `data` is undefined instead. See issue [#47](https://github.com/dividab/graphql-norm/issues/47) and PR [#48](https://github.com/dividab/graphql-norm/pull/48). 44 | 45 | ## [0.14.0](https://github.com/dividab/graphql-norm/compare/v0.13.0...v0.14.0) - 2019-07-21 46 | 47 | ### Added 48 | 49 | - Include an UMD bundle. See issue [#37](https://github.com/dividab/graphql-norm/issues/37) and PR [#46](https://github.com/dividab/graphql-norm/pull/46). 50 | 51 | ## [0.13.0](https://github.com/dividab/graphql-norm/compare/v0.12.2...v0.13.0) - 2019-07-18 52 | 53 | ### Changed 54 | 55 | - Use colon as default ID separator. See issue [#42](https://github.com/dividab/graphql-norm/issues/42) and PR [#43](https://github.com/dividab/graphql-norm/pull/43). 56 | - Genearate fallback IDs using the nearest parent as base. See issue [#27](https://github.com/dividab/graphql-norm/issues/27) and PR [#41](https://github.com/dividab/graphql-norm/pull/41). 57 | 58 | ### Removed 59 | 60 | - Removed staleness checking from denormalize(). See issue [#38](https://github.com/dividab/graphql-norm/issues/38) and PR [#40](https://github.com/dividab/graphql-norm/pull/40). Staleness checking is now available in the external package [graphql-norm-stale](https://github.com/dividab/graphql-norm-stale). 61 | 62 | ## [0.12.2](https://github.com/dividab/graphql-norm/compare/v0.12.1...v0.12.2) - 2019-07-16 63 | 64 | ### Fixed 65 | 66 | - Remove invalid dependency. 67 | 68 | ## [0.12.1](https://github.com/dividab/graphql-norm/compare/v0.12.0...v0.12.1) - 2019-07-16 69 | 70 | ### Fixed 71 | 72 | - Add missing export for type `FieldsMap`. 73 | 74 | ## [0.12.0](https://github.com/dividab/graphql-norm/compare/v0.11.0...v0.12.0) - 2019-07-16 75 | 76 | ### Changes 77 | 78 | - Return used fields per key of normalized object used during denormalization in `fields` props in denormalize() result. See [#30](https://github.com/dividab/graphql-norm/pull/30), PR [#36](https://github.com/dividab/graphql-norm/pull/36) and additional work in PR [#39](https://github.com/dividab/graphql-norm/pull/39) 79 | - Rename `EntityCache` to `NormMap`, `Entity` to `NormObj`, `EntityFieldVlue` to `NormFieldVlue`, and associated renames. See [#16](https://github.com/dividab/graphql-norm/issues/16) and PR [#35](https://github.com/dividab/graphql-norm/pull/35). 80 | - `normalize` directly take the `data` from a GraphQL response as argument instead of an object with a `data` property. See [#34](https://github.com/dividab/graphql-norm/pull/29). 81 | - `denormalize` returns `data` directly instead of an object with a `data` property. See [#34](https://github.com/dividab/graphql-norm/pull/29). 82 | 83 | ## [0.11.0](https://github.com/dividab/graphql-norm/compare/v0.10.1...v0.11.0) - 2019-07-06 84 | 85 | ### Fixed 86 | 87 | - Update peer dependencies. 88 | 89 | ## [0.10.1](https://github.com/dividab/graphql-norm/compare/v0.10.0...v0.10.1) - 2019-07-06 90 | 91 | ### Fixed 92 | 93 | - Update readme with new package name. 94 | 95 | ## [0.10.0](https://github.com/dividab/graphql-norm/compare/v0.9.2...v0.10.0) - 2019-07-06 96 | 97 | ### Changed 98 | 99 | - Renamed the package from `gql-cache` to `graphql-norm` to indicate that it focus is on normalization only, not providing a full caching solution. 100 | 101 | - Renamed mergeEntityCache function to merge, see See [#29](https://github.com/dividab/graphql-norm/pull/29). Thanks to [@drejohnson](https://github.com/drejohnson) for this PR. 102 | 103 | - Consolidated and cleaned up readme documentation. 104 | 105 | ## [0.9.2](https://github.com/dividab/graphql-norm/compare/v0.9.1...v0.9.2) - 2019-01-16 106 | 107 | ### Fixed 108 | 109 | - Remove console.log. 110 | 111 | ## [0.9.1](https://github.com/dividab/graphql-norm/compare/v0.9.0...v0.9.1) - 2019-01-16 112 | 113 | ### Fixed 114 | 115 | - Fix null in result arrays. See [#25](https://github.com/dividab/graphql-norm/pull/25) 116 | 117 | ## [0.9.0](https://github.com/dividab/graphql-norm/compare/v0.8.0...v0.9.0) - 2018-09-28 118 | 119 | ### Fixed 120 | 121 | - Trees with array. See [#23](https://github.com/dividab/graphql-norm/pull/23) 122 | - With reserved words See [#24](https://github.com/dividab/graphql-norm/pull/24) 123 | 124 | ## [0.8.0](https://github.com/dividab/graphql-norm/compare/v0.7.0...v0.8.0) - 2018-09-27 125 | 126 | ### Fixed 127 | 128 | - Invalid response when a query contains multiple subtrees of same node 129 | 130 | ## [0.7.0](https://github.com/dividab/graphql-norm/compare/v0.6.0...v0.7.0) - 2018-09-14 131 | 132 | ### Fixed 133 | 134 | - Fix partial bug when only scalar is missing in cache. #19 135 | 136 | ## [0.6.0](https://github.com/dividab/graphql-norm/compare/v0.5.0...v0.6.0) - 2018-08-26 137 | 138 | ### Changed 139 | 140 | - Make the `graphql` package a peer dependency. See [#18](https://github.com/dividab/graphql-norm/pull/18). 141 | 142 | ## [0.5.0](https://github.com/dividab/graphql-norm/compare/v0.4.0...v0.5.0) - 2018-08-24 143 | 144 | ### Changed 145 | 146 | - Rename `updateStaleEntities()` to `updateStale()`. 147 | - Fix bug in `updateStaleEntities()` which caused the inputs to be mutated. 148 | 149 | ## [0.4.0](https://github.com/dividab/graphql-norm/compare/v0.3.0...v0.4.0) - 2018-08-23 150 | 151 | ### Added 152 | 153 | - Support for `@skip` and `@include` directives. See [#14](https://github.com/dividab/graphql-norm/issues/14). 154 | 155 | ## [0.3.0](https://github.com/dividab/graphql-norm/compare/v0.2.0...v0.3.0) - 2018-08-17 156 | 157 | ### Added 158 | 159 | - The `graphql` and `@types/graphql` packages are now a regular dependencies. See [#12](https://github.com/dividab/graphql-norm/issues/12). 160 | 161 | ## [0.2.0](https://github.com/dividab/graphql-norm/compare/v0.1.3...v0.2.0) - 2018-08-08 162 | 163 | ### Added 164 | 165 | - Support for fallback id when `GetObjectToId` returns undefined. `GetObjectToId` can now return `string | undefined` 166 | 167 | ## [0.1.3](https://github.com/dividab/graphql-norm/compare/v0.1.2...v0.1.3) - 2018-06-28 168 | 169 | ### Fixed 170 | 171 | - Add missing exports for types `Variables`, `GraphQLResponse`. 172 | 173 | ## [0.1.2](https://github.com/dividab/graphql-norm/compare/v0.1.1...v0.1.2) - 2018-06-27 174 | 175 | ### Added 176 | 177 | - Export `DenormalizationResult`, see PR [#4](https://github.com/dividab/graphql-norm/pull/4). 178 | 179 | ### Fixed 180 | 181 | - Remove redundant typing, see PR [#1](https://github.com/dividab/graphql-norm/pull/1). Thanks to [@Jontem](https://github.com/Jontem) for this fix! 182 | 183 | ## [0.1.1](https://github.com/dividab/graphql-norm/compare/v0.1.0...v0.1.1) - 2018-06-08 184 | 185 | ### Added 186 | 187 | - Initial version. 188 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Divid AB 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # graphql-norm 2 | 3 | [![npm version][version-image]][version-url] 4 | [![travis build][travis-image]][travis-url] 5 | [![Coverage Status][codecov-image]][codecov-url] 6 | [![code style: prettier][prettier-image]][prettier-url] 7 | [![types][types-image]][types-url] 8 | [![MIT license][license-image]][license-url] 9 | 10 | Normalization and denormalization of GraphQL responses 11 | 12 | ## How to install 13 | 14 | ``` 15 | npm install graphql-norm --save 16 | ``` 17 | 18 | ## Introduction 19 | 20 | Responses from graphql servers may contain the same logical object several times. Consider for example a response from a blog server that contains a person object both as an author and a commenter. Both person objects have the same ID and are of the same GraphQL type so they are logically the same object. However, since they appear in two different parts of the response they need to be duplicated. When we want to store several GraphQL responsese the problem of duplication amplifies, as many respones may contain the same object. When we later want to update an object, it can be difficult to find all the places where the update needs to happen because there are multiple copies of the same logical object. This package solves these problems by using normalization and denormalization. 21 | 22 | A basic description of normalization (in this context) is that it takes a tree and flattens it to a map where each object will be assigned an unique ID which is used as the key in the map. Any references that an object holds to other objects will be exhanged to an ID instead of an object reference. The process of denormalizaton goes the other way around, starting with a map and producing a tree. The [normalizr](https://www.npmjs.com/package/normalizr) library does a good job of explaining this. In fact, this package is very similar to normalizr, but it was specifically designed to work with GraphQL so it does not require hand-coded normalization schemas. Instead it uses GraphQL queries to determine how to normalize and denormalize the data. 23 | 24 | Normalization and denormalization is useful for a number of scenarios but the main usage is probably to store and update a client-side GraphQL cache without any duplication problems. For example, [Relay](https://facebook.github.io/relay/) and [Apollo](https://www.apollographql.com/) use this approach for their caches. So the main use-case for this library is probably to build your own client-side cache where you get full control of the caching without loosing the benefit of normalization. 25 | 26 | ## Goal 27 | 28 | The goal of the package is only to perform normalization and denormalization of graphql responses. Providing a complete caching solution is an explicit non-goal of this package. However this package can be a building block in a normalized GraphQL caching solution. 29 | 30 | ## Features 31 | 32 | - Full GraphQL syntax support (including variables, alias, @skip, @include, union types etc.) 33 | - Turn any graphql response into a flat (normalized) object map 34 | - Build a response for any grapqhl query from the normalized object map (denormalize) 35 | - Merge normalized object maps to build a larger map (eg. a cache) 36 | - Optimized for run-time speed 37 | 38 | ## Example usage 39 | 40 | You can also run the below example [live at stackblitz](https://stackblitz.com/edit/typescript-pbmen8). 41 | 42 | ```js 43 | import { normalize, denormalize, merge } from "graphql-norm"; 44 | import { request } from "graphql-request"; 45 | import { parse } from "graphql"; 46 | 47 | // A plain JS object to hold the normalized responses 48 | let cache = {}; 49 | 50 | // This query will be fetched from the server 51 | const query = ` 52 | query GetCountry($code: String!) { 53 | country(code: $code) { 54 | __typename code name 55 | continent {__typename code name} 56 | languages {__typename code name} 57 | } 58 | }`; 59 | const queryDoc = parse(query); 60 | const queryVars = { code: "SE" }; 61 | request("https://countries.trevorblades.com/graphql", query, queryVars).then( 62 | data => { 63 | console.log("data", JSON.stringify(data)); 64 | /* 65 | { 66 | "country": { 67 | "__typename": "Country", 68 | "code": "SE", 69 | "name": "Sweden", 70 | "continent": {"__typename": "Continent", "code": "EU", "name": "Europe"}, 71 | "languages": [{"__typename": "Language", "code": "sv", "name": "Swedish"}] 72 | } 73 | } 74 | */ 75 | 76 | // Function to find normalized key for each object in response data 77 | const getKey = obj => 78 | obj.code && obj.__typename && `${obj.__typename}:${obj.code}`; 79 | 80 | // Normalize the response data 81 | const normMap = normalize(queryDoc, queryVars, data, getKey); 82 | 83 | // In the normalized data, an ID was assigned to each object. 84 | // References between objects are now using these IDs. 85 | console.log("normMap", JSON.stringify(normMap)); 86 | /* 87 | { 88 | "ROOT_QUERY": {"country({\"code\":\"SE\"})": "Country:SE"}, 89 | "Country:SE": { 90 | "__typename": "Country", 91 | "code": "SE", 92 | "name": "Sweden", 93 | "languages": ["Language:sv"], 94 | "continent": "Continent:EU" 95 | }, 96 | "Language:sv": {"__typename": "Language", "code": "sv", "name": "Swedish"}, 97 | "Continent:EU": {"__typename": "Continent", "code": "EU", "name": "Europe"} 98 | } 99 | */ 100 | 101 | // Merge the normalized response into the cache 102 | cache = merge(cache, normMap); 103 | 104 | // Now we can now use denormalize to read a query from the cache 105 | const query2 = ` 106 | query GetCountry2($code: String!) { 107 | country(code: $code) {__typename code name} 108 | }`; 109 | const query2Doc = parse(query2); 110 | const denormResult = denormalize(query2Doc, { code: "SE" }, cache); 111 | 112 | const setToJSON = (k, v) => (v instanceof Set ? Array.from(v) : v); 113 | console.log("denormResult", JSON.stringify(denormResult, setToJSON)); 114 | /* 115 | { 116 | "data": {"country": {"__typename": "Country","code": "SE","name": "Sweden"}}, 117 | "fields": { 118 | "ROOT_QUERY": ["country({\"code\":\"SE\"})"], 119 | "Country:SE": ["__typename", "code", "name"] 120 | } 121 | } 122 | */ 123 | } 124 | ); 125 | ``` 126 | 127 | ## API 128 | 129 | ### normalize() 130 | 131 | ```ts 132 | const normMap = normalize(query, variables, data, getObjectId, resolveType); 133 | ``` 134 | 135 | The normalize() function takes a GraphQL query with associated variables, and data from a GraphQL response. From those inputs it produces a normalized object map which is returned as a plain JS object. Each field in the query becomes a field in the normalized version of the object. If the field has variables they are included in the field name to make them unique. If the object has nested child objects they are exhanged for the ID of the nested object, and the nested objects becomes part of the normalized object map. This happens recursively until there are no nested objects left. 136 | 137 | #### Parameters 138 | 139 | - **query**: Graphql query parsed into an AST. 140 | - **variables**: Variables associated with the query. This is the exact same object that was used when querying the graphql server. 141 | - **data**: Data returned by a GraphQL server (the data property of the raw response). 142 | - **getObjectId**: An optional callback function that is called each time an object is normalized. It is passed the object as a single parameter and should return the ID of that object. If this parameter is omitted, a default function that looks for `__typename` and `id` and combines them into a `__typename:id` string will be used. If this function returns a falsy value (eg. undefined), a fallback ID will be used. Some objects may be value objects that have no ID and in that case it is OK to return falsy. The fallback ID will use the closest parent with an ID as a base (or ROOT_QUERY if there is no parent with ID). 143 | - **resolveType**: An optional callback function that is called each time a fragment is encountered. To check if a fragment should apply, we need to know the type of the object. For example an inline fragment `... on Bar` should only apply to objects of type `Bar`. This becomes extra useful when graphql union types are used. The default implementation will look for `__typename` of the object. 144 | 145 | #### Return value 146 | 147 | This function returns an object that is a map of keys and normalized objects. 148 | 149 | ### denormalize() 150 | 151 | ```ts 152 | const denormResult = denormalize(query, variables, normMap, resolveType); 153 | ``` 154 | 155 | The denormalize() function takes a GraphQL query with associated variables, and a normalized object map (as returned by normalize()). From those inputs it produces the data for a GraphQL JSON response. Note that the GraphQL query can be any query, it does not have to be one that was previously normalized. If the response cannot be fully created from the normalized object map then `undefined` will be returned. 156 | 157 | #### Parameters 158 | 159 | - **query**: Graphql query parsed into an AST. 160 | - **variables**: Variables associated with the query. This is the exact same object that was used when querying the graphql server. 161 | - **normMap**: The map of normalized objects as returned by `normalize()` and/or `merge()`. 162 | - **resolveType**: See parameter with same name on `normalize()` above. 163 | 164 | #### Return value 165 | 166 | This function returns an object with information about the denormalization result. The following properties are available on the returned object: 167 | 168 | - **data**: This is the data for the query as it would have been returned from a GraphQL serverl, or `undefined` is the query could not be completely fulfilled from the data in `normMap`. 169 | - **fields**: An object where each property is an normlized object key, and the value is a `Set` of used fields. This can be useful for tracking which key/fields will affect this query data. If an tuple of this object and the data is stored, each time a new normalized result is merged a cache we can check if the new normalized data being merged contains any of the keys/fields of this query then it is affected by the merge, otherwise not. This is similar to the approach used [by relay](https://relay.dev/docs/en/thinking-in-graphql#achieving-view-consistency) for tracking changes. If the query could not be fulfilled from cache, data will be `undefined` and `fields` will contain the first field that could not be resolved. 170 | 171 | ### merge() 172 | 173 | ```ts 174 | const normMap = merge(normMap, newNormMap); 175 | ``` 176 | 177 | When you normalize the response of a query you probably want to merge the resulting normalized object map into a another, large normalized object map that is held by your application. Since the normalized object map is just a JS object you can do this merge any way you want but the `merge()` function is provided an optimized convenience to do the merging. 178 | 179 | #### Parameters 180 | 181 | - **normMap**: The normalized map to merge into. This object is not mutated. 182 | - **newNormMap**: The normalized map to merge into the first map. Any overlapping keys in the first map is overwritten by this map. This object is not mutated. 183 | 184 | #### Return value 185 | 186 | This function returns an object which is the merged normalized map. It has the same structure as the passed in objects but the keys/values from both of them. 187 | 188 | ## Related packages 189 | 190 | - [graphql-add-remove-fields](https://www.npmjs.com/package/graphql-add-remove-fields) 191 | - [graphql-norm-patch](https://www.npmjs.com/package/graphql-norm-patch) 192 | - [graphql-norm-stale](https://www.npmjs.com/package/graphql-norm-stale) 193 | 194 | ## Typescript support 195 | 196 | This project is developed using typescript and typescript types are distributed with the package. 197 | 198 | ## How to develop 199 | 200 | Node version >=12.6.0 is needed for development. 201 | 202 | To execute the tests run `yarn test`. 203 | 204 | ## How to publish 205 | 206 | ``` 207 | yarn version --patch 208 | yarn version --minor 209 | yarn version --major 210 | ``` 211 | 212 | [version-image]: https://img.shields.io/npm/v/graphql-norm.svg?style=flat 213 | [version-url]: https://www.npmjs.com/package/graphql-norm 214 | [travis-image]: https://travis-ci.com/dividab/graphql-norm.svg?branch=master&style=flat 215 | [travis-url]: https://travis-ci.com/dividab/graphql-norm 216 | [codecov-image]: https://codecov.io/gh/dividab/graphql-norm/branch/master/graph/badge.svg 217 | [codecov-url]: https://codecov.io/gh/dividab/graphql-norm 218 | [prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat 219 | [prettier-url]: https://github.com/prettier/prettier 220 | [types-image]: https://img.shields.io/npm/types/scrub-js.svg 221 | [types-url]: https://www.typescriptlang.org/ 222 | [license-image]: https://img.shields.io/github/license/dividab/graphql-norm.svg?style=flat 223 | [license-url]: https://opensource.org/licenses/MIT 224 | -------------------------------------------------------------------------------- /base-tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "forceConsistentCasingInFileNames": true, 5 | "newLine": "LF", 6 | "noEmitOnError": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitAny": true, 9 | "noImplicitReturns": true, 10 | "noImplicitThis": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strictNullChecks": true, 14 | "downlevelIteration": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testRegex: "./test/.+\\.test\\.ts$", 5 | collectCoverage: true, 6 | collectCoverageFrom: ["src/**/*.ts"], 7 | moduleFileExtensions: ["ts", "js", "json", "node"], 8 | coverageReporters: ["text-summary", "lcov"], 9 | globals: { 10 | "ts-jest": { 11 | tsConfig: "/test/tsconfig.json" 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-norm", 3 | "version": "1.3.6", 4 | "description": "Normalization and denormalization of GraphQL responses", 5 | "main": "dist/umd.js", 6 | "module": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/dividab/graphql-norm.git" 11 | }, 12 | "keywords": [ 13 | "graphql", 14 | "normalization", 15 | "denormalization" 16 | ], 17 | "author": "Jonas Kello ", 18 | "license": "MIT", 19 | "files": [ 20 | "/lib", 21 | "/dist", 22 | "/src", 23 | "package.json", 24 | "CHANGELOG.md", 25 | "LICENSE", 26 | "README.md" 27 | ], 28 | "peerDependencies": { 29 | "graphql": "^14.5.8" 30 | }, 31 | "devDependencies": { 32 | "@types/benchmark": "^1.0.31", 33 | "@types/jest": "^24.0.15", 34 | "@types/node": "^12.0.12", 35 | "@typescript-eslint/eslint-plugin": "^2.15.0", 36 | "@typescript-eslint/parser": "^2.15.0", 37 | "benchmark": "^2.1.4", 38 | "codecov": "^3.5.0", 39 | "eslint": "^6.8.0", 40 | "eslint-config-prettier": "^6.9.0", 41 | "eslint-plugin-functional": "^2.0.0", 42 | "graphql": "^14.5.8", 43 | "graphql-tag": "^2.10.1", 44 | "husky": "^2.4.1", 45 | "jest": "^24.8.0", 46 | "lint-staged": "^9.1.0", 47 | "prettier": "^1.18.2", 48 | "rimraf": "^2.6.3", 49 | "rollup": "^1.17.0", 50 | "ts-jest": "^24.0.2", 51 | "ts-node": "^8.3.0", 52 | "typescript": "^3.5.2" 53 | }, 54 | "scripts": { 55 | "lint": "eslint './{src,tests}/**/*.ts' --ext .ts -f visualstudio ", 56 | "dist": "yarn build && rimraf dist && rollup lib/index.js --file dist/umd.js --format umd --name GraphqlNorm", 57 | "build": "rimraf lib && tsc -p src", 58 | "build-test": "rimraf lib-test && tsc -p test", 59 | "performance": "ts-node performance-test/performance-test.ts", 60 | "test-coverage": "jest", 61 | "test": "jest --no-coverage", 62 | "test:work": "jest --no-coverage ./test/denormalize.test.ts", 63 | "verify": "yarn lint && yarn test-coverage && yarn dist", 64 | "report-coverage": "codecov -f coverage/lcov.info", 65 | "preversion": "yarn verify", 66 | "postversion": "git push --tags && yarn publish --new-version $npm_package_version && git push --follow-tags && echo \"Successfully released version $npm_package_version!\"" 67 | }, 68 | "lint-staged": { 69 | "*.{ts,tsx}": "eslint --ext .ts -f visualstudio", 70 | "*.{ts,tsx,json,css}": [ 71 | "prettier --write", 72 | "git add" 73 | ] 74 | }, 75 | "husky": { 76 | "hooks": { 77 | "pre-commit": "lint-staged" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /performance-test/performance-test.ts: -------------------------------------------------------------------------------- 1 | import * as BenchMark from "benchmark"; 2 | import { normalize } from "../src/normalize"; 3 | import { denormalize } from "../src/denormalize"; 4 | import * as BigAmountOfData from "./test-with-big-amount-of-data"; 5 | 6 | const normalizeSuite = new BenchMark.Suite(); 7 | 8 | normalizeSuite 9 | .add("Normalize BigAmountOfData", function() { 10 | const { query, variables, data } = BigAmountOfData.test; 11 | normalize(query, variables, data); 12 | }) 13 | .add("Denormalize BigAmountOfData", function() { 14 | const { query, variables, data } = BigAmountOfData.test; 15 | denormalize(query, variables, data); 16 | }) 17 | .on("complete", function(this: any) { 18 | // tslint:disable-next-line:no-invalid-this 19 | for (const b of this) { 20 | console.log(b.toString()); 21 | } 22 | 23 | console.log(); 24 | console.log(); 25 | }) 26 | .run(/* { async: true } */); 27 | -------------------------------------------------------------------------------- /performance-test/test-with-big-amount-of-data.ts: -------------------------------------------------------------------------------- 1 | import { OneTest } from "../test/shared-test-def"; 2 | import gql from "graphql-tag"; 3 | import { normalize } from "../src/normalize"; 4 | 5 | function generateTestData(): OneTest { 6 | const query = gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments(a: { b: 1, c: "asd" }) { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `; 29 | const posts: Array = []; 30 | for (let i = 0; i < 10000; i++) { 31 | const post = { 32 | id: `post-${i}`, 33 | __typename: "Post", 34 | author: { 35 | id: `author-${i}`, 36 | __typename: "Author", 37 | name: "Paul" 38 | }, 39 | title: `My awesome blog post ${i}`, 40 | comments: [ 41 | { 42 | id: `comment-${i}`, 43 | __typename: "Comment", 44 | commenter: { 45 | id: `commenter-${i}`, 46 | __typename: "Author", 47 | name: "Nicole" 48 | } 49 | } 50 | ] 51 | }; 52 | 53 | posts.push(post); 54 | } 55 | 56 | return { 57 | normMap: normalize(query, undefined, posts), 58 | name: "GeneratedTestData", 59 | query, 60 | data: posts 61 | }; 62 | } 63 | 64 | export const test: OneTest = generateTestData(); 65 | -------------------------------------------------------------------------------- /src/denormalize.ts: -------------------------------------------------------------------------------- 1 | import * as GraphQL from "graphql"; 2 | import { 3 | DenormalizationResult, 4 | FieldNodeWithSelectionSet, 5 | Variables, 6 | ResponseObject, 7 | ResponseObject2, 8 | ResponseObjectArray, 9 | RootFields, 10 | ResolveType 11 | } from "./types"; 12 | import { 13 | expandFragments, 14 | getDocumentDefinitions, 15 | fieldNameWithArguments, 16 | shouldIncludeField, 17 | defaultResolveType 18 | } from "./functions"; 19 | import { NormMap, NormKey } from "./norm-map"; 20 | 21 | type Mutable = { -readonly [P in keyof T]: T[P] }; // Remove readonly 22 | 23 | type MutableResponseObject = Mutable; 24 | // eslint-disable-next-line functional/prefer-readonly-type 25 | type MutableResponseObjectArray = Array; 26 | type ParentResponseObjectOrArray = 27 | | Mutable 28 | | ResponseObjectArray; 29 | type ParentResponseKey = string | number | undefined; 30 | type StackWorkItem = readonly [ 31 | FieldNodeWithSelectionSet, 32 | NormKey | ReadonlyArray, 33 | ParentResponseObjectOrArray, 34 | ParentResponseKey, 35 | NormKey, 36 | string 37 | ]; 38 | 39 | /** 40 | * Creates a graphql response by denormalizing. 41 | * @param query The graphql query document 42 | * @param variables The graphql query variables 43 | * @param normMap The map of normalized objects 44 | * @param resolveType Function get get typeName from an object 45 | */ 46 | export function denormalize( 47 | query: GraphQL.DocumentNode, 48 | variables: Variables | undefined, 49 | normMap: NormMap, 50 | resolveType: ResolveType = defaultResolveType 51 | ): DenormalizationResult { 52 | const [fragmentMap, rootFieldNode] = getDocumentDefinitions( 53 | query.definitions 54 | ); 55 | 56 | const stack: Array = []; 57 | const response = {}; 58 | const usedFieldsMap: { 59 | // eslint-disable-next-line 60 | [key: string]: Set; 61 | } = {}; 62 | stack.push([ 63 | rootFieldNode, 64 | "ROOT_QUERY", 65 | response, 66 | undefined, 67 | "ROOT_QUERY", 68 | "ROOT_QUERY" 69 | ]); 70 | while (stack.length > 0) { 71 | const [ 72 | fieldNode, 73 | idOrIdArray, 74 | parentObjectOrArray, 75 | parentResponseKey, 76 | parentNormKey, 77 | fieldNameInParent 78 | ] = stack.pop()!; 79 | 80 | // The stack has work items, depending on the work item we have four different cases to handle: 81 | // field + id + parentObject = denormalize(ID) => [responseObject, workitems] and parentObject[field] = responseObject 82 | // field + id + parentArray = denormalize(ID) => [responseObject, workitems] and parentArray.push(responseObject) 83 | // field + idArray + parentObject = stack.push(workItemsFrom(idArray)) and parentObject[field] = new Array() 84 | // field + idArray + parentArray = stack.push(workItemsFrom(idArray)) and parentArray.push(new Array()) 85 | 86 | let responseObjectOrNewParentArray: 87 | | MutableResponseObject 88 | | MutableResponseObjectArray 89 | | null; 90 | 91 | if (idOrIdArray === null) { 92 | responseObjectOrNewParentArray = null; 93 | } else if (!Array.isArray(idOrIdArray)) { 94 | const key: NormKey = idOrIdArray as NormKey; 95 | 96 | const normObj = normMap[key]; 97 | 98 | // Does not exist in normalized map. We can't fully resolve query 99 | if (normObj === undefined) { 100 | return { 101 | data: undefined, 102 | fields: { [parentNormKey]: new Set([fieldNameInParent]) } 103 | }; 104 | } 105 | 106 | let usedFields = usedFieldsMap[key]; 107 | if (usedFields === undefined) { 108 | usedFields = new Set(); 109 | usedFieldsMap[key] = usedFields; 110 | } 111 | 112 | // If we've been here before we need to use the previously created response object 113 | if (Array.isArray(parentObjectOrArray)) { 114 | responseObjectOrNewParentArray = 115 | (parentObjectOrArray as MutableResponseObjectArray)[ 116 | parentResponseKey as number 117 | ] || Object.create(null); 118 | } else { 119 | responseObjectOrNewParentArray = 120 | (parentObjectOrArray as MutableResponseObject)[ 121 | parentResponseKey as string 122 | ] || Object.create(null); 123 | } 124 | 125 | // Expand fragments and loop all fields 126 | const expandedSelections = expandFragments( 127 | resolveType, 128 | normObj, 129 | fieldNode.selectionSet.selections, 130 | fragmentMap 131 | ); 132 | for (const field of expandedSelections) { 133 | // Check if this field should be skipped according to @skip and @include directives 134 | const include = field.directives 135 | ? shouldIncludeField(field.directives, variables) 136 | : true; 137 | if (include) { 138 | // Build key according to any arguments 139 | const fieldName = 140 | field.arguments && field.arguments.length > 0 141 | ? fieldNameWithArguments(field, variables) 142 | : field.name.value; 143 | // Add this to used fields 144 | usedFields.add(fieldName); 145 | const normObjValue = normObj[fieldName]; 146 | if (normObjValue !== null && field.selectionSet) { 147 | // Put a work-item on the stack to build this field and set it on the response object 148 | stack.push([ 149 | field as FieldNodeWithSelectionSet, 150 | normObjValue as any, 151 | responseObjectOrNewParentArray as 152 | | MutableResponseObject 153 | | MutableResponseObjectArray, 154 | (field.alias && field.alias.value) || field.name.value, 155 | key, 156 | fieldName 157 | ]); 158 | } else { 159 | // This field is a primitive (not a array or object) 160 | if (normObjValue !== undefined) { 161 | (responseObjectOrNewParentArray as MutableResponseObject)[ 162 | (field.alias && field.alias.value) || field.name.value 163 | ] = normObjValue; 164 | } else { 165 | return { 166 | data: undefined, 167 | fields: { [key]: new Set([fieldName]) } 168 | }; 169 | } 170 | } 171 | } 172 | } 173 | } else { 174 | const idArray: ReadonlyArray = idOrIdArray; 175 | responseObjectOrNewParentArray = 176 | (parentObjectOrArray as MutableResponseObject)[ 177 | parentResponseKey as string 178 | ] || []; 179 | for (let i = 0; i < idArray.length; i++) { 180 | const idArrayItem = idArray[i]; 181 | stack.push([ 182 | fieldNode, 183 | idArrayItem, 184 | responseObjectOrNewParentArray as 185 | | MutableResponseObject 186 | | MutableResponseObjectArray, 187 | i, 188 | parentNormKey, 189 | fieldNameInParent 190 | ]); 191 | } 192 | } 193 | 194 | // Add to the parent, either field or an array 195 | if (Array.isArray(parentObjectOrArray)) { 196 | const parentArray: MutableResponseObjectArray = parentObjectOrArray; 197 | parentArray[ 198 | parentResponseKey as number 199 | ] = responseObjectOrNewParentArray as 200 | | MutableResponseObject 201 | | MutableResponseObjectArray; 202 | } else { 203 | const parentObject: MutableResponseObject = parentObjectOrArray; 204 | parentObject[ 205 | parentResponseKey || 206 | (fieldNode.alias && fieldNode.alias.value) || 207 | fieldNode.name.value 208 | ] = responseObjectOrNewParentArray; 209 | } 210 | } 211 | 212 | interface GraphQLResponse { 213 | readonly data: RootFields; 214 | } 215 | 216 | const data = (response as GraphQLResponse).data; 217 | 218 | return { 219 | data: data, 220 | fields: usedFieldsMap 221 | }; 222 | } 223 | -------------------------------------------------------------------------------- /src/functions.ts: -------------------------------------------------------------------------------- 1 | import * as GraphQL from "graphql"; 2 | import { 3 | DocumentDefinitionTuple, 4 | FragmentMap, 5 | FieldNodeWithSelectionSet, 6 | GetObjectId, 7 | Variables, 8 | GetObjectToIdResult, 9 | ResponseObject, 10 | ResolveType 11 | } from "./types"; 12 | 13 | export function getDocumentDefinitions( 14 | definitions: ReadonlyArray 15 | ): DocumentDefinitionTuple { 16 | let operationDefinition: 17 | | GraphQL.OperationDefinitionNode 18 | | undefined = undefined; 19 | 20 | const fragmentMap: { 21 | // eslint-disable-next-line functional/prefer-readonly-type 22 | [fragmentName: string]: GraphQL.FragmentDefinitionNode; 23 | } = {}; 24 | for (const definition of definitions) { 25 | switch (definition.kind) { 26 | case "OperationDefinition": 27 | operationDefinition = definition; 28 | break; 29 | case "FragmentDefinition": 30 | fragmentMap[definition.name.value] = definition; 31 | break; 32 | default: 33 | throw new Error("This is not an executable document"); 34 | } 35 | } 36 | const rootFieldNode: FieldNodeWithSelectionSet = { 37 | kind: "Field", 38 | name: { 39 | kind: "Name", 40 | value: "data" 41 | }, 42 | selectionSet: { 43 | kind: "SelectionSet", 44 | selections: operationDefinition!.selectionSet.selections 45 | } 46 | }; 47 | return [fragmentMap, rootFieldNode]; 48 | } 49 | 50 | export function expandFragments( 51 | resolveType: ResolveType, 52 | obj: ResponseObject, 53 | selectionNodes: ReadonlyArray, 54 | fragmentMap: FragmentMap 55 | ): ReadonlyArray { 56 | const fieldNodes: Array = []; 57 | 58 | for (const selectionNode of selectionNodes) { 59 | switch (selectionNode.kind) { 60 | case "Field": 61 | fieldNodes.push(selectionNode); 62 | break; 63 | case "InlineFragment": { 64 | const fragmentTypeName = 65 | selectionNode.typeCondition && selectionNode.typeCondition.name.value; 66 | const objTypeName = resolveType(obj); 67 | // Only include this fragment if the typename matches 68 | if (fragmentTypeName === objTypeName) { 69 | fieldNodes.push( 70 | ...expandFragments( 71 | resolveType, 72 | obj, 73 | selectionNode.selectionSet.selections, 74 | fragmentMap 75 | ) 76 | ); 77 | } 78 | break; 79 | } 80 | case "FragmentSpread": { 81 | const fragment = fragmentMap[selectionNode.name.value]; 82 | const fragmentTypeName = 83 | fragment.typeCondition && fragment.typeCondition.name.value; 84 | const objTypeName = resolveType(obj); 85 | // Only include this fragment if the typename matches 86 | if (fragmentTypeName === objTypeName) { 87 | fieldNodes.push( 88 | ...expandFragments( 89 | resolveType, 90 | obj, 91 | fragment.selectionSet.selections, 92 | fragmentMap 93 | ) 94 | ); 95 | } 96 | break; 97 | } 98 | default: 99 | throw new Error( 100 | "Unknown selection node field kind: " + (selectionNode as any).kind 101 | ); 102 | } 103 | } 104 | return fieldNodes; 105 | } 106 | 107 | function resolveValueNode( 108 | valueNode: GraphQL.ValueNode, 109 | variables: Variables | undefined 110 | ): string | boolean | number | ReadonlyArray | object | null { 111 | switch (valueNode.kind) { 112 | case "Variable": 113 | return variables![valueNode.name.value]; 114 | case "NullValue": 115 | return null; 116 | case "ListValue": 117 | return valueNode.values.map(f => resolveValueNode(f, variables)); 118 | case "ObjectValue": 119 | const valueObject: { [key: string]: any } = {}; 120 | for (const field of valueNode.fields) { 121 | valueObject[field.name.value] = resolveValueNode( 122 | field.value, 123 | variables 124 | ); 125 | } 126 | return valueObject; 127 | default: 128 | return valueNode.value; 129 | } 130 | } 131 | 132 | export function fieldNameWithArguments( 133 | fieldNode: GraphQL.FieldNode, 134 | variables: Variables | undefined 135 | ): string { 136 | const argumentsObject: { [key: string]: any } = {}; 137 | for (const argumentNode of fieldNode.arguments!) { 138 | argumentsObject[argumentNode.name.value] = resolveValueNode( 139 | argumentNode.value, 140 | variables 141 | ); 142 | } 143 | const hashedArgs = JSON.stringify(argumentsObject); 144 | return fieldNode.name.value + "(" + hashedArgs + ")"; 145 | } 146 | 147 | export const defaultGetObjectId: GetObjectId = (object: { 148 | readonly id: string; 149 | readonly __typename?: string; 150 | }): GetObjectToIdResult => { 151 | return object.id === undefined 152 | ? undefined 153 | : `${object.__typename}:${object.id}`; 154 | }; 155 | 156 | export const defaultResolveType: ResolveType = (object: { 157 | readonly __typename?: string; 158 | }): string => { 159 | if (object.__typename === undefined) { 160 | throw new Error("__typename cannot be undefined."); 161 | } 162 | return object.__typename; 163 | }; 164 | 165 | /** 166 | * Evaluates @skip and @include directives on field 167 | * and returns true if the node should be included. 168 | */ 169 | export function shouldIncludeField( 170 | directives: ReadonlyArray, 171 | variables: Variables = {} 172 | ): boolean { 173 | let finalInclude = true; 174 | for (const directive of directives) { 175 | let directiveInclude = true; 176 | if (directive.name.value === "skip" || directive.name.value === "include") { 177 | if (directive.arguments) { 178 | for (const arg of directive.arguments) { 179 | if (arg.name.value === "if") { 180 | let argValue: boolean; 181 | if (arg.value.kind === "Variable") { 182 | argValue = variables && !!variables[arg.value.name.value]; 183 | } else if (arg.value.kind === "BooleanValue") { 184 | argValue = arg.value.value; 185 | } else { 186 | // The if argument must be of type Boolean! 187 | // http://facebook.github.io/graphql/June2018/#sec--skip 188 | throw new Error( 189 | `The if argument must be of type Boolean!, found '${arg.value.kind}'` 190 | ); 191 | } 192 | const argInclude = 193 | directive.name.value === "include" ? argValue : !argValue; 194 | directiveInclude = directiveInclude && argInclude; 195 | } 196 | } 197 | } 198 | finalInclude = finalInclude && directiveInclude; 199 | } 200 | } 201 | return finalInclude; 202 | } 203 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Exported functions 2 | export { normalize } from "./normalize"; 3 | export { denormalize } from "./denormalize"; 4 | export { merge } from "./norm-map"; 5 | export { defaultGetObjectId } from "./functions"; 6 | 7 | // Exported types used in signature of exported functions 8 | export { 9 | GetObjectId, // ref: normalize(), defaultGetObjectId() 10 | Variables, // ref: normalize(), denormalize() 11 | DenormalizationResult, // used in: denormalize() 12 | RootFields, // ref: normalize(), DenormalizationResult 13 | FieldsMap // DenormalizationResult 14 | } from "./types"; 15 | 16 | export { 17 | NormMap, // ref: normalize(), denormalize(), merge() 18 | NormObj, // ref: NormMap 19 | NormFieldValue, // ref: NormObj 20 | NormKey // ref: NormFieldValue 21 | } from "./norm-map"; 22 | 23 | // All below this line should be moved out of this lib 24 | 25 | export { 26 | getNormalizedObject, 27 | NormalizedObject, // ref: getNormalizedObject() 28 | NormalizedField // ref: NormalizedField 29 | } from "./normalized-object"; 30 | -------------------------------------------------------------------------------- /src/norm-map.ts: -------------------------------------------------------------------------------- 1 | export interface NormMap { 2 | readonly [key: string]: NormObj; 3 | } 4 | 5 | export type NormKey = string; 6 | 7 | export type NormFieldValue = 8 | | NormKey 9 | | string 10 | | boolean 11 | | number 12 | | null 13 | | NormFieldValueArray; 14 | 15 | export interface NormFieldValueArray extends ReadonlyArray {} 16 | 17 | export interface NormObj { 18 | readonly [field: string]: null | NormFieldValue; 19 | } 20 | 21 | /** 22 | * An optimized function to merge two maps of normalized objects (as returned from normalize) 23 | * @param normMap The first normalized map 24 | * @param newNormMap The second normalized map 25 | */ 26 | export function merge(normMap: NormMap, newNormMap: NormMap): NormMap { 27 | const updatedNormMap = Object.keys(newNormMap).reduce( 28 | (stateSoFar, current) => { 29 | const newNormObj = { 30 | ...(normMap[current] || {}), 31 | ...newNormMap[current] 32 | }; 33 | stateSoFar[current] = newNormObj; 34 | return stateSoFar; 35 | }, 36 | {} as { 37 | // eslint-disable-next-line functional/prefer-readonly-type 38 | [key: string]: any; 39 | } 40 | ); 41 | 42 | return { 43 | ...normMap, 44 | ...updatedNormMap 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/normalize.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable:no-arguments 2 | import * as GraphQL from "graphql"; 3 | import { 4 | FieldNodeWithSelectionSet, 5 | GetObjectId, 6 | RootFields, 7 | Variables, 8 | ResponseObject, 9 | ResolveType 10 | } from "./types"; 11 | import { 12 | defaultGetObjectId, 13 | expandFragments, 14 | getDocumentDefinitions, 15 | fieldNameWithArguments, 16 | shouldIncludeField, 17 | defaultResolveType 18 | } from "./functions"; 19 | import { NormMap, NormObj, NormKey, NormFieldValue } from "./norm-map"; 20 | 21 | type MutableDeep = { -readonly [P in keyof T]: MutableDeep }; // Remove readonly deep 22 | 23 | type ParentNormObj = MutableDeep; 24 | type MutableNormMap = MutableDeep; 25 | type ResponseArray = ReadonlyArray< 26 | ResponseObject | ReadonlyArray 27 | >; 28 | type ResponseObjectOrArray = ResponseObject | ResponseArray; 29 | type ParentNormObjOrArray = ParentNormObj | ParentArray; 30 | // eslint-disable-next-line functional/prefer-readonly-type 31 | type ParentArray = Array; 32 | type StackWorkItem = readonly [ 33 | FieldNodeWithSelectionSet, 34 | ParentNormObjOrArray | undefined /*parentNormObj*/, 35 | ResponseObjectOrArray, 36 | string // FallbackId 37 | ]; 38 | 39 | /** 40 | * Normalizes a graphql response. 41 | * @param query The graphql query document 42 | * @param variables The graphql query variables 43 | * @param response The graphql response 44 | * @param getObjectId Function to get normalized map key from an object 45 | * @param resolveType Function get get typeName from an object 46 | */ 47 | export function normalize( 48 | query: GraphQL.DocumentNode, 49 | variables: Variables | undefined, 50 | data: RootFields, 51 | getObjectId: GetObjectId = defaultGetObjectId, 52 | resolveType: ResolveType = defaultResolveType 53 | ): NormMap { 54 | const [fragmentMap, rootFieldNode] = getDocumentDefinitions( 55 | query.definitions 56 | ); 57 | 58 | const stack: Array = []; 59 | const normMap: MutableNormMap = Object.create(null); 60 | 61 | // Seed stack with undefined parent and "fake" getObjectId 62 | stack.push([rootFieldNode, Object.create(null), data, "ROOT_QUERY"]); 63 | let getObjectIdToUse: GetObjectId = _ => "ROOT_QUERY"; 64 | 65 | // The stack has work items, depending on the work item we have four different cases to handle: 66 | // field + responseObject + parentNormObj = normalize(responseObject) => [ID, workitems] and parentNormObj[field] = ID 67 | // field + responseObject + parentArray = normalize(responseObject) => [ID, workitems] and parentArray.push(ID) 68 | // field + responseArray + parentNormObj = stack.push(workItemsFrom(responseArray)) and parentNormObj[field] = new Array() 69 | // field + responseArray + parentArray = stack.push(workItemsFrom(responseArray)) and parentArray.push(new Array()) 70 | let firstIteration = true; 71 | while (stack.length > 0) { 72 | const [ 73 | fieldNode, 74 | parentNormObjOrArray, 75 | responseObjectOrArray, 76 | fallbackId 77 | ] = stack.pop()!; 78 | 79 | let keyOrNewParentArray: NormKey | ParentArray | null = null; 80 | if (responseObjectOrArray === null) { 81 | keyOrNewParentArray = null; 82 | } else if (!Array.isArray(responseObjectOrArray)) { 83 | const responseObject = responseObjectOrArray as ResponseObject; 84 | // console.log("responseObject", responseObject); 85 | const objectToIdResult = getObjectIdToUse(responseObject); 86 | keyOrNewParentArray = objectToIdResult ? objectToIdResult : fallbackId; 87 | // Get or create normalized object 88 | let normObj = normMap[keyOrNewParentArray]; 89 | if (!normObj) { 90 | normObj = Object.create(null); 91 | normMap[keyOrNewParentArray] = normObj; 92 | } 93 | // Expand any fragments 94 | const expandedSelections = expandFragments( 95 | resolveType, 96 | responseObjectOrArray, 97 | fieldNode.selectionSet.selections, 98 | fragmentMap 99 | ); 100 | // For each field in the selection-set that has a sub-selection-set we push a work item. 101 | // For primtivies fields we set them directly on the normalized object. 102 | for (const field of expandedSelections) { 103 | // Check if this field should be skipped according to @skip and @include directives 104 | const include = field.directives 105 | ? shouldIncludeField(field.directives, variables) 106 | : true; 107 | if (include) { 108 | const responseFieldValue = 109 | responseObject[ 110 | (field.alias && field.alias.value) || field.name.value 111 | ]; 112 | const normFieldName = 113 | field.arguments && field.arguments.length > 0 114 | ? fieldNameWithArguments(field, variables) 115 | : field.name.value; 116 | if (responseFieldValue !== null && field.selectionSet) { 117 | // Put a work-item on the stack to normalize this field and set it on the normalized object 118 | stack.push([ 119 | field as FieldNodeWithSelectionSet, 120 | normObj, 121 | responseFieldValue, 122 | //path + "." + normFieldName 123 | // Use the current key plus fieldname as fallback id 124 | keyOrNewParentArray + "." + normFieldName 125 | ]); 126 | } else { 127 | // This field is a primitive (not a array of normalized objects or a single normalized object) 128 | normObj[normFieldName] = responseFieldValue; 129 | } 130 | } 131 | } 132 | } else { 133 | const responseArray = responseObjectOrArray as ResponseArray; 134 | keyOrNewParentArray = []; 135 | for (let i = 0; i < responseArray.length; i++) { 136 | stack.push([ 137 | fieldNode, 138 | keyOrNewParentArray, 139 | responseArray[i], 140 | fallbackId + "." + i.toString() 141 | ]); 142 | } 143 | } 144 | 145 | // Add to the parent, either field or an array 146 | if (Array.isArray(parentNormObjOrArray)) { 147 | const parentArray = parentNormObjOrArray as ParentArray; 148 | parentArray.unshift(keyOrNewParentArray); 149 | } else { 150 | const key = 151 | fieldNode.arguments && fieldNode.arguments.length > 0 152 | ? fieldNameWithArguments(fieldNode, variables) 153 | : fieldNode.name.value; 154 | const parentNormObj = parentNormObjOrArray as ParentNormObj; 155 | parentNormObj[key] = keyOrNewParentArray; 156 | } 157 | 158 | // Use fake objectId function only for the first iteration, then switch to the real one 159 | if (firstIteration) { 160 | getObjectIdToUse = getObjectId; 161 | firstIteration = false; 162 | } 163 | } 164 | 165 | return normMap as NormMap; 166 | } 167 | -------------------------------------------------------------------------------- /src/normalized-object.ts: -------------------------------------------------------------------------------- 1 | import { NormKey, NormMap } from "./norm-map"; 2 | 3 | /** 4 | * Fetches an object from the normalized map, taking as type parameter the object 5 | * has when denormalized, but returning the type it will have as a normalized object. 6 | * @param key The normalized map key 7 | * @param normMap The normalized map 8 | */ 9 | export function getNormalizedObject( 10 | key: string, 11 | normMap: NormMap 12 | ): NormalizedObject { 13 | return normMap[key] as any; 14 | } 15 | 16 | export type NormalizedField = T extends string 17 | ? string 18 | : T extends number 19 | ? number 20 | : T extends boolean 21 | ? boolean 22 | : T extends {} 23 | ? string 24 | : T extends ReadonlyArray 25 | ? ReadonlyArray 26 | : T extends ReadonlyArray 27 | ? ReadonlyArray 28 | : T extends ReadonlyArray 29 | ? ReadonlyArray 30 | : T extends ReadonlyArray 31 | ? ReadonlyArray 32 | : "undefined value"; 33 | 34 | /** 35 | * This type maps a denormalized object type to a normalized object type. 36 | * It does it by converting arrays of objects into arrays of normalized map keys. 37 | */ 38 | export type NormalizedObject = { 39 | readonly [P in keyof TDenormalized]: NormalizedField; 40 | }; 41 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base-tsconfig.json", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "../lib", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "target": "es6", 10 | "module": "es6", 11 | "moduleResolution": "node", 12 | "lib": ["es6"] 13 | }, 14 | "include": ["**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import * as GraphQL from "graphql"; 2 | 3 | export interface Variables { 4 | readonly [name: string]: any; 5 | } 6 | 7 | export interface ResponseObject { 8 | readonly [key: string]: any; 9 | } 10 | 11 | export interface ResponseObject2 { 12 | readonly [key: string]: ResponseObjectFieldValue; 13 | } 14 | 15 | export type ResponseObjectFieldValue = 16 | | string 17 | | number 18 | | boolean 19 | | ResponseObject2 20 | | ResponseObjectArray; 21 | 22 | export interface ResponseObjectArray 23 | extends ReadonlyArray {} 24 | 25 | export interface RootFields { 26 | readonly [rootField: string]: any; 27 | } 28 | 29 | export interface DenormalizationResult { 30 | readonly data: RootFields | undefined; 31 | readonly fields: FieldsMap; 32 | } 33 | 34 | export interface FieldsMap { 35 | readonly [key: string]: ReadonlySet; 36 | } 37 | 38 | export interface FragmentMap { 39 | readonly [fragmentName: string]: GraphQL.FragmentDefinitionNode; 40 | } 41 | export type DocumentDefinitionTuple = readonly [ 42 | FragmentMap, 43 | FieldNodeWithSelectionSet 44 | ]; 45 | 46 | export interface FieldNodeWithSelectionSet extends GraphQL.FieldNode { 47 | readonly selectionSet: GraphQL.SelectionSetNode; 48 | } 49 | 50 | export type GetObjectToIdResult = string | undefined; 51 | 52 | export type GetObjectId = (object: { 53 | readonly id?: string; 54 | readonly __typename?: string; 55 | }) => GetObjectToIdResult; 56 | 57 | export type ResolveType = (object: { readonly __typename?: string }) => string; 58 | -------------------------------------------------------------------------------- /test/denormalize-test-data.ts: -------------------------------------------------------------------------------- 1 | import { loadTests } from "./test-data-utils"; 2 | import { DenormalizeTestDef } from "./denormalize-test-def"; 3 | 4 | export const tests = loadTests("denormalize-tests/"); 5 | -------------------------------------------------------------------------------- /test/denormalize-test-def.ts: -------------------------------------------------------------------------------- 1 | import * as GraphQL from "graphql"; 2 | import { NormMap } from "../src/norm-map"; 3 | import { RootFields, Variables } from "../src/types"; 4 | 5 | export interface DenormalizeTestDef { 6 | readonly name: string; 7 | readonly only?: boolean; 8 | readonly skip?: boolean; 9 | readonly query: GraphQL.DocumentNode; 10 | readonly variables?: Variables; 11 | readonly data: RootFields | undefined; 12 | readonly normMap: NormMap; 13 | readonly fields: { readonly [key: string]: ReadonlyArray }; 14 | } 15 | -------------------------------------------------------------------------------- /test/denormalize-tests/with-complete.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { DenormalizeTestDef } from "../denormalize-test-def"; 3 | 4 | export const test: DenormalizeTestDef = { 5 | name: "with complete", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | data: { 30 | posts: [ 31 | { 32 | id: "123", 33 | __typename: "Post", 34 | author: { 35 | id: "1", 36 | __typename: "Author", 37 | name: "Paul" 38 | }, 39 | title: "My awesome blog post", 40 | comments: null 41 | } 42 | ] 43 | }, 44 | normMap: { 45 | ROOT_QUERY: { 46 | posts: ["Post:123"] 47 | }, 48 | "Post:123": { 49 | id: "123", 50 | __typename: "Post", 51 | author: "Author:1", 52 | title: "My awesome blog post", 53 | comments: null 54 | }, 55 | "Author:1": { id: "1", __typename: "Author", name: "Paul" } 56 | }, 57 | fields: { 58 | ROOT_QUERY: ["posts"], 59 | "Post:123": ["id", "__typename", "author", "title", "comments"], 60 | "Author:1": ["id", "__typename", "name"] 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /test/denormalize-tests/with-incomplete.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { DenormalizeTestDef } from "../denormalize-test-def"; 3 | 4 | export const test: DenormalizeTestDef = { 5 | name: "with incomplete", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | data: undefined, 30 | normMap: { 31 | ROOT_QUERY: { 32 | posts: ["Post:123"] 33 | }, 34 | "Post:123": { 35 | id: "123", 36 | __typename: "Post", 37 | author: "Author:1", 38 | title: "My awesome blog post", 39 | comments: null 40 | } 41 | }, 42 | fields: { 43 | "Post:123": ["author"] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /test/denormalize-tests/with-multiple-subtree-same-query-node.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { DenormalizeTestDef } from "../denormalize-test-def"; 3 | 4 | export const test: DenormalizeTestDef = { 5 | name: "with multiple subtree on same query node", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | } 21 | comments { 22 | id 23 | __typename 24 | commenter { 25 | id 26 | __typename 27 | name 28 | } 29 | } 30 | ...TestFragment 31 | } 32 | } 33 | fragment TestFragment on Post { 34 | id 35 | title 36 | author { 37 | id 38 | __typename 39 | role 40 | } 41 | } 42 | `, 43 | data: { 44 | posts: [ 45 | { 46 | id: "123", 47 | __typename: "Post", 48 | author: { 49 | id: "1", 50 | __typename: "Author", 51 | name: "Paul", 52 | role: "admin" 53 | }, 54 | title: "My awesome blog post", 55 | comments: [ 56 | { 57 | id: 1, 58 | __typename: "Comment", 59 | commenter: { 60 | id: 1, 61 | __typename: "Commenter", 62 | name: "olle" 63 | } 64 | } 65 | ] 66 | } 67 | ] 68 | }, 69 | normMap: { 70 | ROOT_QUERY: { 71 | posts: ["Post:123"] 72 | }, 73 | "Post:123": { 74 | id: "123", 75 | __typename: "Post", 76 | author: "Author:1", 77 | title: "My awesome blog post", 78 | comments: ["Comment;1"] 79 | }, 80 | "Author:1": { 81 | id: "1", 82 | __typename: "Author", 83 | name: "Paul", 84 | role: "admin" 85 | }, 86 | "Comment;1": { 87 | id: 1, 88 | __typename: "Comment", 89 | commenter: "Commenter:1" 90 | }, 91 | "Commenter:1": { 92 | id: 1, 93 | __typename: "Commenter", 94 | name: "olle" 95 | } 96 | }, 97 | fields: { 98 | ROOT_QUERY: ["posts"], 99 | "Post:123": ["id", "__typename", "author", "title", "comments"], 100 | "Author:1": ["id", "__typename", "role", "name"], 101 | "Comment;1": ["id", "__typename", "commenter"], 102 | "Commenter:1": ["id", "__typename", "name"] 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /test/denormalize-tests/with-scalar-incomplete.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { DenormalizeTestDef } from "../denormalize-test-def"; 3 | 4 | export const test: DenormalizeTestDef = { 5 | name: "with scalar query incomplete", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | title 12 | } 13 | } 14 | `, 15 | data: undefined, 16 | normMap: { 17 | ROOT_QUERY: { 18 | posts: ["Post:123"] 19 | }, 20 | "Post:123": { 21 | id: "123", 22 | __typename: "Post" 23 | } 24 | }, 25 | fields: { "Post:123": ["title"] } 26 | }; 27 | -------------------------------------------------------------------------------- /test/denormalize.test.ts: -------------------------------------------------------------------------------- 1 | import { denormalize } from "../src/denormalize"; 2 | import * as SharedTests from "./shared-test-data"; 3 | import * as TestDataDenormalization from "./denormalize-test-data"; 4 | import { onlySkip } from "./test-data-utils"; 5 | 6 | describe("denormalize() with shared test data", () => { 7 | onlySkip(SharedTests.tests).forEach(item => { 8 | test(item.name, done => { 9 | const actual = denormalize(item.query, item.variables, item.normMap); 10 | expect(actual.data).toEqual(item.data); 11 | done(); 12 | }); 13 | }); 14 | }); 15 | 16 | describe("denormalize() with specialized test data", () => { 17 | onlySkip(TestDataDenormalization.tests).forEach(item => { 18 | test(item.name, done => { 19 | const actual = denormalize(item.query, item.variables, item.normMap); 20 | expect(actual.data).toEqual(item.data); 21 | expect(actual.fields).toEqual( 22 | Object.fromEntries( 23 | Object.entries(item.fields).map(([key, value]) => [ 24 | key, 25 | new Set(value) 26 | ]) 27 | ) 28 | ); 29 | done(); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/merge.test.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from "../src/normalize"; 2 | import gql from "graphql-tag"; 3 | import { merge } from "../src/norm-map"; 4 | import { denormalize } from "../src/denormalize"; 5 | 6 | describe("merge()", () => { 7 | test("full value objects", () => { 8 | const itemA = { 9 | name: "", 10 | query: gql` 11 | query TestQuery { 12 | user { 13 | id 14 | __typename 15 | address { 16 | __typename 17 | city 18 | region 19 | } 20 | } 21 | } 22 | `, 23 | response: { 24 | user: { 25 | id: "123", 26 | __typename: "User", 27 | address: { 28 | __typename: "Address", 29 | city: "Los Leones", 30 | region: "Miramar" 31 | } 32 | } 33 | } 34 | }; 35 | 36 | const itemB = { 37 | name: "", 38 | query: gql` 39 | query TestQuery { 40 | users { 41 | id 42 | __typename 43 | address { 44 | __typename 45 | city 46 | region 47 | } 48 | } 49 | } 50 | `, 51 | response: { 52 | users: [ 53 | { 54 | id: "123", 55 | __typename: "User", 56 | address: { 57 | __typename: "Address", 58 | city: "Los Leones", 59 | region: "Miramar" 60 | } 61 | } 62 | ] 63 | } 64 | }; 65 | 66 | const normMapA = normalize(itemA.query, {}, itemA.response); 67 | const normMapB = normalize(itemB.query, {}, itemB.response); 68 | const mergedNormMap = merge(normMapA, normMapB); 69 | const denormalizedResult = denormalize(itemA.query, {}, mergedNormMap); 70 | expect(denormalizedResult.data).toBeTruthy(); 71 | }); 72 | 73 | // When a value-object (an object with no ID, owned by it's parent) is 74 | // used, you would expect it to be merged like any other. 75 | test("merge value objects", () => { 76 | const itemA = { 77 | name: "", 78 | query: gql` 79 | query TestQuery { 80 | user { 81 | id 82 | __typename 83 | address { 84 | __typename 85 | city 86 | } 87 | } 88 | } 89 | `, 90 | response: { 91 | user: { 92 | id: "123", 93 | __typename: "User", 94 | address: { 95 | __typename: "Address", 96 | city: "Los Leones" 97 | } 98 | } 99 | } 100 | }; 101 | 102 | const itemB = { 103 | name: "", 104 | query: gql` 105 | query TestQuery { 106 | users { 107 | id 108 | __typename 109 | address { 110 | __typename 111 | region 112 | } 113 | } 114 | } 115 | `, 116 | response: { 117 | users: [ 118 | { 119 | id: "123", 120 | __typename: "User", 121 | address: { 122 | __typename: "Address", 123 | region: "Miramar" 124 | } 125 | } 126 | ] 127 | } 128 | }; 129 | 130 | const normMapA = normalize(itemA.query, {}, itemA.response); 131 | const normMapB = normalize(itemB.query, {}, itemB.response); 132 | const mergedNormMaps = merge(normMapA, normMapB); 133 | const denormalizedResult = denormalize(itemA.query, {}, mergedNormMaps); 134 | 135 | expect(denormalizedResult.data).toBeTruthy(); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/normalize.test.ts: -------------------------------------------------------------------------------- 1 | import { normalize } from "../src/normalize"; 2 | import { tests } from "./shared-test-data"; 3 | import { onlySkip } from "./test-data-utils"; 4 | 5 | describe("normalize() with shared test data", () => { 6 | onlySkip(tests).forEach(item => { 7 | test(item.name, () => { 8 | const actual = normalize(item.query, item.variables, item.data); 9 | const expected = item.normMap; 10 | expect(actual).toEqual(expected); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/shared-data/standard-norm-map.ts: -------------------------------------------------------------------------------- 1 | export const standardNormMap = { 2 | ROOT_QUERY: { 3 | posts: ["Post:123"] 4 | }, 5 | "Post:123": { 6 | id: "123", 7 | __typename: "Post", 8 | author: "Author:1", 9 | title: "My awesome blog post", 10 | comments: ["Comment:324"] 11 | }, 12 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 13 | "Comment:324": { 14 | id: "324", 15 | __typename: "Comment", 16 | commenter: "Author:2" 17 | }, 18 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 19 | }; 20 | -------------------------------------------------------------------------------- /test/shared-data/standard-response.ts: -------------------------------------------------------------------------------- 1 | export const standardResponse = { 2 | posts: [ 3 | { 4 | id: "123", 5 | __typename: "Post", 6 | author: { 7 | id: "1", 8 | __typename: "Author", 9 | name: "Paul" 10 | }, 11 | title: "My awesome blog post", 12 | comments: [ 13 | { 14 | id: "324", 15 | __typename: "Comment", 16 | commenter: { 17 | id: "2", 18 | __typename: "Author", 19 | name: "Nicole" 20 | } 21 | } 22 | ] 23 | } 24 | ] 25 | }; 26 | -------------------------------------------------------------------------------- /test/shared-test-data.ts: -------------------------------------------------------------------------------- 1 | import { loadTests } from "./test-data-utils"; 2 | import { SharedTestDef } from "./shared-test-def"; 3 | 4 | export const tests = loadTests("shared-tests/"); 5 | -------------------------------------------------------------------------------- /test/shared-test-def.ts: -------------------------------------------------------------------------------- 1 | import * as GraphQL from "graphql"; 2 | import { NormMap } from "../src/norm-map"; 3 | import { Variables, RootFields } from "../src/types"; 4 | 5 | export interface SharedTestDef { 6 | readonly name: string; 7 | readonly only?: boolean; 8 | readonly skip?: boolean; 9 | readonly query: GraphQL.DocumentNode; 10 | readonly variables?: Variables; 11 | readonly data: RootFields; 12 | readonly normMap: NormMap; 13 | } 14 | -------------------------------------------------------------------------------- /test/shared-tests/query-with-array-of-string.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "query with array of strings", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | tags 12 | } 13 | } 14 | `, 15 | data: { 16 | posts: [ 17 | { 18 | id: "123", 19 | __typename: "Post", 20 | tags: ["olle", "kalle"] 21 | } 22 | ] 23 | }, 24 | normMap: { 25 | ROOT_QUERY: { 26 | posts: ["Post:123"] 27 | }, 28 | "Post:123": { 29 | id: "123", 30 | __typename: "Post", 31 | tags: ["olle", "kalle"] 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/same-entity-twice-different-fields.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "same object twice in response but with different fields", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | age 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: { 31 | posts: [ 32 | { 33 | id: "123", 34 | __typename: "Post", 35 | author: { 36 | id: "1", 37 | __typename: "Author", 38 | name: "Paul" 39 | }, 40 | title: "My awesome blog post", 41 | comments: [ 42 | { 43 | id: "324", 44 | __typename: "Comment", 45 | commenter: { 46 | id: "1", 47 | __typename: "Author", 48 | name: "Paul", 49 | age: 33 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | }, 56 | normMap: { 57 | ROOT_QUERY: { 58 | posts: ["Post:123"] 59 | }, 60 | "Post:123": { 61 | id: "123", 62 | __typename: "Post", 63 | author: "Author:1", 64 | title: "My awesome blog post", 65 | comments: ["Comment:324"] 66 | }, 67 | "Comment:324": { 68 | id: "324", 69 | __typename: "Comment", 70 | commenter: "Author:1" 71 | }, 72 | "Author:1": { id: "1", __typename: "Author", name: "Paul", age: 33 } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /test/shared-tests/simple.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | import { standardNormMap } from "../shared-data/standard-norm-map"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "simple", 8 | query: gql` 9 | query TestQuery { 10 | posts { 11 | id 12 | __typename 13 | author { 14 | id 15 | __typename 16 | name 17 | } 18 | title 19 | comments { 20 | id 21 | __typename 22 | commenter { 23 | id 24 | __typename 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | data: standardResponse, 32 | normMap: standardNormMap 33 | }; 34 | -------------------------------------------------------------------------------- /test/shared-tests/with-alias.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardNormMap } from "../shared-data/standard-norm-map"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with alias", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | olle: author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: { 31 | posts: [ 32 | { 33 | id: "123", 34 | __typename: "Post", 35 | olle: { 36 | id: "1", 37 | __typename: "Author", 38 | name: "Paul" 39 | }, 40 | title: "My awesome blog post", 41 | comments: [ 42 | { 43 | id: "324", 44 | __typename: "Comment", 45 | commenter: { 46 | id: "2", 47 | __typename: "Author", 48 | name: "Nicole" 49 | } 50 | } 51 | ] 52 | } 53 | ] 54 | }, 55 | normMap: standardNormMap 56 | }; 57 | -------------------------------------------------------------------------------- /test/shared-tests/with-array-of-string.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with array of String", 6 | query: gql` 7 | query TestQuery { 8 | tags 9 | } 10 | `, 11 | data: { 12 | tags: ["tag1", "tag2", "tag3"] 13 | }, 14 | normMap: { 15 | ROOT_QUERY: { 16 | tags: ["tag1", "tag2", "tag3"] 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/shared-tests/with-deep-null-values.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with deep null values", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | data: { 30 | posts: [ 31 | { 32 | id: "123", 33 | __typename: "Post", 34 | author: { 35 | id: "1", 36 | __typename: "Author", 37 | name: "Paul" 38 | }, 39 | title: "My awesome blog post", 40 | comments: null 41 | } 42 | ] 43 | }, 44 | normMap: { 45 | ROOT_QUERY: { 46 | posts: ["Post:123"] 47 | }, 48 | "Post:123": { 49 | id: "123", 50 | __typename: "Post", 51 | author: "Author:1", 52 | title: "My awesome blog post", 53 | comments: null 54 | }, 55 | "Author:1": { id: "1", __typename: "Author", name: "Paul" } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /test/shared-tests/with-include-literal-false.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with skip literal true", 6 | query: gql` 7 | query TestQuery { 8 | posts @include(if: false) { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: { noPosts: true }, 30 | data: {}, 31 | normMap: { 32 | ROOT_QUERY: {} 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-include-literal-true.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | import { standardNormMap } from "../shared-data/standard-norm-map"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with skip literal false", 8 | query: gql` 9 | query TestQuery { 10 | posts @include(if: true) { 11 | id 12 | __typename 13 | author { 14 | id 15 | __typename 16 | name 17 | } 18 | title 19 | comments { 20 | id 21 | __typename 22 | commenter { 23 | id 24 | __typename 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | variables: { noPosts: false }, 32 | data: standardResponse, 33 | normMap: standardNormMap 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-include-variable-false.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with skip variable true", 6 | query: gql` 7 | query TestQuery($noPosts: Boolean!) { 8 | posts @include(if: $noPosts) { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: { noPosts: false }, 30 | data: {}, 31 | normMap: { 32 | ROOT_QUERY: {} 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-include-variable-true.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | import { standardNormMap } from "../shared-data/standard-norm-map"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with skip variable false", 8 | query: gql` 9 | query TestQuery($noPosts: Boolean!) { 10 | posts @include(if: $noPosts) { 11 | id 12 | __typename 13 | author { 14 | id 15 | __typename 16 | name 17 | } 18 | title 19 | comments { 20 | id 21 | __typename 22 | commenter { 23 | id 24 | __typename 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | variables: { noPosts: true }, 32 | data: standardResponse, 33 | normMap: standardNormMap 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-inline-fragments.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardNormMap } from "../shared-data/standard-norm-map"; 4 | import { standardResponse } from "../shared-data/standard-response"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with inline fragments", 8 | query: gql` 9 | query TestQuery { 10 | posts { 11 | id 12 | __typename 13 | ... on Post { 14 | author { 15 | ... on Author { 16 | id 17 | __typename 18 | name 19 | } 20 | } 21 | } 22 | title 23 | comments { 24 | id 25 | __typename 26 | commenter { 27 | id 28 | __typename 29 | name 30 | } 31 | } 32 | } 33 | } 34 | `, 35 | data: standardResponse, 36 | normMap: standardNormMap 37 | }; 38 | -------------------------------------------------------------------------------- /test/shared-tests/with-missing-id.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | const fallbackId1 = 'Post:123.comments({"a":{"b":"1","c":"asd"}}).0'; 5 | const fallbackId2 = 'Post:123.comments({"a":{"b":"1","c":"asd"}}).1'; 6 | const fallbackId3 = "ROOT_QUERY.testNode"; 7 | 8 | export const test: SharedTestDef = { 9 | name: "with missing id", 10 | query: gql` 11 | query TestQuery { 12 | posts { 13 | id 14 | __typename 15 | author { 16 | id 17 | __typename 18 | name 19 | } 20 | title 21 | comments(a: { b: 1, c: "asd" }) { 22 | __typename 23 | commenter { 24 | id 25 | __typename 26 | name 27 | } 28 | } 29 | } 30 | 31 | testNode { 32 | __typename 33 | nisse 34 | } 35 | } 36 | `, 37 | data: { 38 | posts: [ 39 | { 40 | id: "123", 41 | __typename: "Post", 42 | author: { 43 | id: "1", 44 | __typename: "Author", 45 | name: "Paul" 46 | }, 47 | title: "My awesome blog post", 48 | comments: [ 49 | { 50 | __typename: "Comment", 51 | commenter: { 52 | id: "2", 53 | __typename: "Author", 54 | name: "Nicole" 55 | } 56 | }, 57 | { 58 | __typename: "Comment", 59 | commenter: { 60 | id: "2", 61 | __typename: "Author", 62 | name: "Nicole" 63 | } 64 | } 65 | ] 66 | } 67 | ], 68 | testNode: { 69 | __typename: "olle", 70 | nisse: "asd" 71 | } 72 | }, 73 | normMap: { 74 | ROOT_QUERY: { 75 | posts: ["Post:123"], 76 | testNode: fallbackId3 77 | }, 78 | "Post:123": { 79 | id: "123", 80 | __typename: "Post", 81 | author: "Author:1", 82 | title: "My awesome blog post", 83 | 'comments({"a":{"b":"1","c":"asd"}})': [fallbackId1, fallbackId2] 84 | }, 85 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 86 | [fallbackId1]: { 87 | __typename: "Comment", 88 | commenter: "Author:2" 89 | }, 90 | [fallbackId2]: { 91 | __typename: "Comment", 92 | commenter: "Author:2" 93 | }, 94 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" }, 95 | [fallbackId3]: { 96 | __typename: "olle", 97 | nisse: "asd" 98 | } 99 | } 100 | }; 101 | -------------------------------------------------------------------------------- /test/shared-tests/with-named-fragments.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardNormMap } from "../shared-data/standard-norm-map"; 4 | import { standardResponse } from "../shared-data/standard-response"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with named fragments", 8 | query: gql` 9 | query TestQuery { 10 | posts { 11 | id 12 | __typename 13 | author { 14 | ...olle 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | 29 | fragment olle on Author { 30 | id 31 | __typename 32 | name 33 | } 34 | `, 35 | data: standardResponse, 36 | normMap: standardNormMap 37 | }; 38 | -------------------------------------------------------------------------------- /test/shared-tests/with-nested-array-of-entities.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with nested arrays of objects", 6 | // only: true, 7 | // skip: true, 8 | query: gql` 9 | query TestQuery { 10 | table { 11 | id 12 | __typename 13 | rows { 14 | id 15 | __typename 16 | value 17 | } 18 | } 19 | } 20 | `, 21 | data: { 22 | table: { 23 | id: "T1", 24 | __typename: "Table", 25 | rows: [ 26 | [ 27 | { id: "1.1", __typename: "Cell", value: "value 1.1" }, 28 | { id: "1.2", __typename: "Cell", value: "value 1.2" } 29 | ], 30 | [ 31 | { id: "2.1", __typename: "Cell", value: "value 2.1" }, 32 | { id: "2.2", __typename: "Cell", value: "value 2.2" } 33 | ], 34 | [ 35 | { id: "3.1", __typename: "Cell", value: "value 3.1" }, 36 | { id: "3.2", __typename: "Cell", value: "value 3.2" } 37 | ] 38 | ] 39 | } 40 | }, 41 | normMap: { 42 | ROOT_QUERY: { 43 | table: "Table:T1" 44 | }, 45 | "Table:T1": { 46 | id: "T1", 47 | __typename: "Table", 48 | rows: [ 49 | ["Cell:1.1", "Cell:1.2"], 50 | ["Cell:2.1", "Cell:2.2"], 51 | ["Cell:3.1", "Cell:3.2"] 52 | ] 53 | }, 54 | "Cell:1.1": { 55 | id: "1.1", 56 | __typename: "Cell", 57 | value: "value 1.1" 58 | }, 59 | "Cell:1.2": { 60 | id: "1.2", 61 | __typename: "Cell", 62 | value: "value 1.2" 63 | }, 64 | "Cell:2.1": { 65 | id: "2.1", 66 | __typename: "Cell", 67 | value: "value 2.1" 68 | }, 69 | "Cell:2.2": { 70 | id: "2.2", 71 | __typename: "Cell", 72 | value: "value 2.2" 73 | }, 74 | "Cell:3.1": { 75 | id: "3.1", 76 | __typename: "Cell", 77 | value: "value 3.1" 78 | }, 79 | "Cell:3.2": { 80 | id: "3.2", 81 | __typename: "Cell", 82 | value: "value 3.2" 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /test/shared-tests/with-nested-array-of-strings.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with nested array of String", 6 | query: gql` 7 | query TestQuery { 8 | tagsArray 9 | } 10 | `, 11 | data: { 12 | tagsArray: [ 13 | ["tag1.1", "tag1.2", "tag1.3"], 14 | ["tag2.1", "tag2.2", "tag2.3"], 15 | ["tag3.1", "tag3.2", "tag3.3"] 16 | ] 17 | }, 18 | normMap: { 19 | ROOT_QUERY: { 20 | tagsArray: [ 21 | ["tag1.1", "tag1.2", "tag1.3"], 22 | ["tag2.1", "tag2.2", "tag2.3"], 23 | ["tag3.1", "tag3.2", "tag3.3"] 24 | ] 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /test/shared-tests/with-null-object.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with null object", 6 | query: gql` 7 | query TestQuery($postIds: [ID!]!) { 8 | postsByIds(ids: $postIds) { 9 | id 10 | __typename 11 | title 12 | } 13 | } 14 | `, 15 | variables: { postIds: ["non-existent-id"] }, 16 | data: { 17 | postsByIds: [null] 18 | }, 19 | normMap: { 20 | ROOT_QUERY: { 21 | 'postsByIds({"ids":["non-existent-id"]})': [null] 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /test/shared-tests/with-null-values.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with null values", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | data: { 30 | posts: [ 31 | { 32 | id: "123", 33 | __typename: "Post", 34 | author: null, 35 | title: "My awesome blog post", 36 | comments: [ 37 | { 38 | id: "324", 39 | __typename: "Comment", 40 | commenter: { 41 | id: "2", 42 | __typename: "Author", 43 | name: "Nicole" 44 | } 45 | } 46 | ] 47 | } 48 | ] 49 | }, 50 | normMap: { 51 | ROOT_QUERY: { 52 | posts: ["Post:123"] 53 | }, 54 | "Post:123": { 55 | id: "123", 56 | __typename: "Post", 57 | author: null, 58 | title: "My awesome blog post", 59 | comments: ["Comment:324"] 60 | }, 61 | "Comment:324": { 62 | id: "324", 63 | __typename: "Comment", 64 | commenter: "Author:2" 65 | }, 66 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /test/shared-tests/with-object-and-null-in-array.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with object and null in array", 6 | query: gql` 7 | query TestQuery($postIds: [ID!]!) { 8 | postsByIds(ids: $postIds) { 9 | id 10 | __typename 11 | title 12 | } 13 | } 14 | `, 15 | variables: { postIds: ["123", "non-existent-id"] }, 16 | data: { 17 | postsByIds: [ 18 | { 19 | id: "123", 20 | __typename: "Post", 21 | title: "My awesome blog post" 22 | }, 23 | null 24 | ] 25 | }, 26 | normMap: { 27 | ROOT_QUERY: { 28 | 'postsByIds({"ids":["123","non-existent-id"]})': ["Post:123", null] 29 | }, 30 | "Post:123": { 31 | id: "123", 32 | __typename: "Post", 33 | title: "My awesome blog post" 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/shared-tests/with-reserved-words.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with reserved keywords", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | isProtoTypeOf { 12 | id 13 | __typename 14 | value 15 | } 16 | title 17 | hasOwnProperty { 18 | id 19 | __typename 20 | name 21 | } 22 | } 23 | } 24 | `, 25 | data: { 26 | posts: [ 27 | { 28 | id: "123", 29 | __typename: "Post", 30 | isProtoTypeOf: [ 31 | { 32 | id: "1", 33 | __typename: "Value", 34 | value: 1 35 | }, 36 | { 37 | id: "2", 38 | __typename: "Value", 39 | value: 2 40 | } 41 | ], 42 | title: "My awesome blog post", 43 | hasOwnProperty: { 44 | id: "1", 45 | __typename: "Commenter", 46 | name: "olle" 47 | } 48 | } 49 | ] 50 | }, 51 | normMap: { 52 | ROOT_QUERY: { 53 | posts: ["Post:123"] 54 | }, 55 | "Post:123": { 56 | id: "123", 57 | __typename: "Post", 58 | isProtoTypeOf: ["Value:1", "Value:2"], 59 | title: "My awesome blog post", 60 | hasOwnProperty: "Commenter:1" 61 | }, 62 | "Value:1": { id: "1", __typename: "Value", value: 1 }, 63 | "Value:2": { id: "2", __typename: "Value", value: 2 }, 64 | "Commenter:1": { id: "1", __typename: "Commenter", name: "olle" } 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /test/shared-tests/with-skip-literal-false.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | import { standardNormMap } from "../shared-data/standard-norm-map"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with skip literal false", 8 | query: gql` 9 | query TestQuery { 10 | posts @skip(if: false) { 11 | id 12 | __typename 13 | author { 14 | id 15 | __typename 16 | name 17 | } 18 | title 19 | comments { 20 | id 21 | __typename 22 | commenter { 23 | id 24 | __typename 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | variables: { noPosts: false }, 32 | data: standardResponse, 33 | normMap: standardNormMap 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-skip-literal-true.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with skip literal true", 6 | query: gql` 7 | query TestQuery { 8 | posts @skip(if: true) { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: { noPosts: true }, 30 | data: {}, 31 | normMap: { 32 | ROOT_QUERY: {} 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-skip-variable-false.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | import { standardNormMap } from "../shared-data/standard-norm-map"; 5 | 6 | export const test: SharedTestDef = { 7 | name: "with skip variable false", 8 | query: gql` 9 | query TestQuery($noPosts: Boolean!) { 10 | posts @skip(if: $noPosts) { 11 | id 12 | __typename 13 | author { 14 | id 15 | __typename 16 | name 17 | } 18 | title 19 | comments { 20 | id 21 | __typename 22 | commenter { 23 | id 24 | __typename 25 | name 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | variables: { noPosts: false }, 32 | data: standardResponse, 33 | normMap: standardNormMap 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-skip-variable-true.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with skip variable true", 6 | query: gql` 7 | query TestQuery($noPosts: Boolean!) { 8 | posts @skip(if: $noPosts) { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: { noPosts: true }, 30 | data: {}, 31 | normMap: { 32 | ROOT_QUERY: {} 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /test/shared-tests/with-union-type-fragment-spread.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | // only: true, 6 | name: "with union type fragment spread", 7 | query: gql` 8 | query TestQuery { 9 | booksAndAuthors { 10 | id 11 | __typename 12 | ...BookFragment 13 | ...AuthorFragment 14 | } 15 | } 16 | fragment BookFragment on Book { 17 | title 18 | } 19 | fragment AuthorFragment on Author { 20 | name 21 | } 22 | `, 23 | data: { 24 | booksAndAuthors: [ 25 | { 26 | id: "123", 27 | __typename: "Book", 28 | title: "My awesome blog post" 29 | }, 30 | { 31 | id: "324", 32 | __typename: "Author", 33 | name: "Nicole" 34 | } 35 | ] 36 | }, 37 | normMap: { 38 | ROOT_QUERY: { 39 | booksAndAuthors: ["Book:123", "Author:324"] 40 | }, 41 | "Book:123": { 42 | id: "123", 43 | __typename: "Book", 44 | title: "My awesome blog post" 45 | }, 46 | "Author:324": { 47 | id: "324", 48 | __typename: "Author", 49 | name: "Nicole" 50 | } 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /test/shared-tests/with-union-type-inline-fragments.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | // only: true, 6 | name: "with union type inline fragments", 7 | query: gql` 8 | query TestQuery { 9 | booksAndAuthors { 10 | id 11 | __typename 12 | ... on Book { 13 | title 14 | } 15 | ... on Author { 16 | name 17 | } 18 | } 19 | } 20 | `, 21 | data: { 22 | booksAndAuthors: [ 23 | { 24 | id: "123", 25 | __typename: "Book", 26 | title: "My awesome blog post" 27 | }, 28 | { 29 | id: "324", 30 | __typename: "Author", 31 | name: "Nicole" 32 | } 33 | ] 34 | }, 35 | normMap: { 36 | ROOT_QUERY: { 37 | booksAndAuthors: ["Book:123", "Author:324"] 38 | }, 39 | "Book:123": { 40 | id: "123", 41 | __typename: "Book", 42 | title: "My awesome blog post" 43 | }, 44 | "Author:324": { 45 | id: "324", 46 | __typename: "Author", 47 | name: "Nicole" 48 | } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object-array.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object array", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | headers { 17 | title 18 | subtitle 19 | } 20 | comments { 21 | id 22 | __typename 23 | commenter { 24 | id 25 | __typename 26 | name 27 | } 28 | } 29 | } 30 | } 31 | `, 32 | data: { 33 | posts: [ 34 | { 35 | id: "123", 36 | __typename: "Post", 37 | author: { 38 | id: "1", 39 | __typename: "Author", 40 | name: "Paul" 41 | }, 42 | headers: [ 43 | { 44 | title: "My awesome blog post", 45 | subtitle: "This is the best post ever" 46 | }, 47 | { 48 | title: "Alternate awesomeness", 49 | subtitle: "Never better" 50 | }, 51 | { 52 | title: "Also another alternative", 53 | subtitle: "Actually not that good" 54 | } 55 | ], 56 | comments: [ 57 | { 58 | id: "324", 59 | __typename: "Comment", 60 | commenter: { 61 | id: "2", 62 | __typename: "Author", 63 | name: "Nicole" 64 | } 65 | } 66 | ] 67 | } 68 | ] 69 | }, 70 | normMap: { 71 | ROOT_QUERY: { 72 | posts: ["Post:123"] 73 | }, 74 | "Post:123": { 75 | id: "123", 76 | __typename: "Post", 77 | author: "Author:1", 78 | headers: [ 79 | "Post:123.headers.0", 80 | "Post:123.headers.1", 81 | "Post:123.headers.2" 82 | ], 83 | comments: ["Comment:324"] 84 | }, 85 | "Post:123.headers.0": { 86 | title: "My awesome blog post", 87 | subtitle: "This is the best post ever" 88 | }, 89 | "Post:123.headers.1": { 90 | title: "Alternate awesomeness", 91 | subtitle: "Never better" 92 | }, 93 | "Post:123.headers.2": { 94 | title: "Also another alternative", 95 | subtitle: "Actually not that good" 96 | }, 97 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 98 | "Comment:324": { 99 | id: "324", 100 | __typename: "Comment", 101 | commenter: "Author:2" 102 | }, 103 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object-nested-parents.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object nested parents", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | address { 25 | street 26 | town 27 | } 28 | } 29 | } 30 | } 31 | } 32 | `, 33 | data: { 34 | posts: [ 35 | { 36 | id: "123", 37 | __typename: "Post", 38 | author: { 39 | id: "1", 40 | __typename: "Author", 41 | name: "Paul" 42 | }, 43 | title: "My awesome blog post", 44 | comments: [ 45 | { 46 | id: "324", 47 | __typename: "Comment", 48 | commenter: { 49 | id: "2", 50 | __typename: "Author", 51 | name: "Nicole", 52 | address: { 53 | street: "Nicolestreet", 54 | town: "Nicoletown" 55 | } 56 | } 57 | } 58 | ] 59 | } 60 | ] 61 | }, 62 | normMap: { 63 | ROOT_QUERY: { 64 | posts: ["Post:123"] 65 | }, 66 | "Post:123": { 67 | id: "123", 68 | __typename: "Post", 69 | author: "Author:1", 70 | title: "My awesome blog post", 71 | comments: ["Comment:324"] 72 | }, 73 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 74 | "Comment:324": { 75 | id: "324", 76 | __typename: "Comment", 77 | commenter: "Author:2" 78 | }, 79 | "Author:2": { 80 | id: "2", 81 | __typename: "Author", 82 | name: "Nicole", 83 | address: "Author:2.address" 84 | }, 85 | "Author:2.address": { street: "Nicolestreet", town: "Nicoletown" } 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object-nested.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object nested", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | headers { 17 | title 18 | subheader { 19 | title 20 | } 21 | } 22 | comments { 23 | id 24 | __typename 25 | commenter { 26 | id 27 | __typename 28 | name 29 | } 30 | } 31 | } 32 | } 33 | `, 34 | data: { 35 | posts: [ 36 | { 37 | id: "123", 38 | __typename: "Post", 39 | author: { 40 | id: "1", 41 | __typename: "Author", 42 | name: "Paul" 43 | }, 44 | headers: [ 45 | { 46 | title: "My awesome blog post", 47 | subheader: { title: "This is the best post ever" } 48 | }, 49 | { 50 | title: "Alternate awesomeness", 51 | subheader: { title: "Never better" } 52 | }, 53 | { 54 | title: "Also another alternative", 55 | subheader: { title: "Actually not that good" } 56 | } 57 | ], 58 | comments: [ 59 | { 60 | id: "324", 61 | __typename: "Comment", 62 | commenter: { 63 | id: "2", 64 | __typename: "Author", 65 | name: "Nicole" 66 | } 67 | } 68 | ] 69 | } 70 | ] 71 | }, 72 | normMap: { 73 | ROOT_QUERY: { 74 | posts: ["Post:123"] 75 | }, 76 | "Post:123": { 77 | id: "123", 78 | __typename: "Post", 79 | author: "Author:1", 80 | headers: [ 81 | "Post:123.headers.0", 82 | "Post:123.headers.1", 83 | "Post:123.headers.2" 84 | ], 85 | comments: ["Comment:324"] 86 | }, 87 | "Post:123.headers.0": { 88 | title: "My awesome blog post", 89 | subheader: "Post:123.headers.0.subheader" 90 | }, 91 | "Post:123.headers.0.subheader": { 92 | title: "This is the best post ever" 93 | }, 94 | "Post:123.headers.1": { 95 | title: "Alternate awesomeness", 96 | subheader: "Post:123.headers.1.subheader" 97 | }, 98 | "Post:123.headers.1.subheader": { 99 | title: "Never better" 100 | }, 101 | "Post:123.headers.2": { 102 | title: "Also another alternative", 103 | subheader: "Post:123.headers.2.subheader" 104 | }, 105 | "Post:123.headers.2.subheader": { 106 | title: "Actually not that good" 107 | }, 108 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 109 | "Comment:324": { 110 | id: "324", 111 | __typename: "Comment", 112 | commenter: "Author:2" 113 | }, 114 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object-no-parents.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object no parents", 6 | query: gql` 7 | query TestQuery { 8 | address { 9 | street 10 | } 11 | posts { 12 | title 13 | } 14 | } 15 | `, 16 | data: { 17 | address: { street: "mainstreet" }, 18 | posts: [ 19 | { 20 | title: "My awesome blog post" 21 | }, 22 | { 23 | title: "My awesome blog post2" 24 | } 25 | ] 26 | }, 27 | normMap: { 28 | ROOT_QUERY: { 29 | address: "ROOT_QUERY.address", 30 | posts: ["ROOT_QUERY.posts.0", "ROOT_QUERY.posts.1"] 31 | }, 32 | "ROOT_QUERY.address": { street: "mainstreet" }, 33 | "ROOT_QUERY.posts.0": { title: "My awesome blog post" }, 34 | "ROOT_QUERY.posts.1": { title: "My awesome blog post2" } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object-parent-with-variables.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object parent with variables", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments(a: 1) { 18 | body 19 | } 20 | } 21 | } 22 | `, 23 | data: { 24 | posts: [ 25 | { 26 | id: "123", 27 | __typename: "Post", 28 | author: { 29 | id: "1", 30 | __typename: "Author", 31 | name: "Paul" 32 | }, 33 | title: "My awesome blog post", 34 | comments: [ 35 | { 36 | body: "The comment" 37 | } 38 | ] 39 | } 40 | ] 41 | }, 42 | normMap: { 43 | ROOT_QUERY: { 44 | posts: ["Post:123"] 45 | }, 46 | "Post:123": { 47 | id: "123", 48 | __typename: "Post", 49 | author: "Author:1", 50 | title: "My awesome blog post", 51 | 'comments({"a":"1"})': ['Post:123.comments({"a":"1"}).0'] 52 | }, 53 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 54 | 'Post:123.comments({"a":"1"}).0': { 55 | body: "The comment" 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /test/shared-tests/with-value-object.ts: -------------------------------------------------------------------------------- 1 | import { SharedTestDef } from "../shared-test-def"; 2 | import gql from "graphql-tag"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with value object", 6 | query: gql` 7 | query TestQuery { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | header { 17 | title 18 | subtitle 19 | } 20 | comments { 21 | id 22 | __typename 23 | commenter { 24 | id 25 | __typename 26 | name 27 | } 28 | } 29 | } 30 | } 31 | `, 32 | data: { 33 | posts: [ 34 | { 35 | id: "123", 36 | __typename: "Post", 37 | author: { 38 | id: "1", 39 | __typename: "Author", 40 | name: "Paul" 41 | }, 42 | header: { 43 | title: "My awesome blog post", 44 | subtitle: "This is the best post ever" 45 | }, 46 | comments: [ 47 | { 48 | id: "324", 49 | __typename: "Comment", 50 | commenter: { 51 | id: "2", 52 | __typename: "Author", 53 | name: "Nicole" 54 | } 55 | } 56 | ] 57 | } 58 | ] 59 | }, 60 | normMap: { 61 | ROOT_QUERY: { 62 | posts: ["Post:123"] 63 | }, 64 | "Post:123": { 65 | id: "123", 66 | __typename: "Post", 67 | author: "Author:1", 68 | header: "Post:123.header", 69 | comments: ["Comment:324"] 70 | }, 71 | "Post:123.header": { 72 | title: "My awesome blog post", 73 | subtitle: "This is the best post ever" 74 | }, 75 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 76 | "Comment:324": { 77 | id: "324", 78 | __typename: "Comment", 79 | commenter: "Author:2" 80 | }, 81 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-boolean-external-2.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | 4 | export const test: SharedTestDef = { 5 | name: "with variables simple boolean external 2", 6 | query: gql` 7 | query TestQuery($a: Boolean) { 8 | posts { 9 | id 10 | __typename 11 | author { 12 | id 13 | __typename 14 | name 15 | } 16 | title 17 | comments(b: $a) { 18 | id 19 | __typename 20 | commenter { 21 | id 22 | __typename 23 | name 24 | } 25 | } 26 | } 27 | } 28 | `, 29 | variables: { a: true }, 30 | data: { 31 | posts: [ 32 | { 33 | id: "123", 34 | __typename: "Post", 35 | author: { 36 | id: "1", 37 | __typename: "Author", 38 | name: "Paul" 39 | }, 40 | title: "My awesome blog post", 41 | comments: [ 42 | { 43 | id: "324", 44 | __typename: "Comment", 45 | commenter: { 46 | id: "2", 47 | __typename: "Author", 48 | name: "Nicole" 49 | } 50 | } 51 | ] 52 | } 53 | ] 54 | }, 55 | normMap: { 56 | ROOT_QUERY: { 57 | posts: ["Post:123"] 58 | }, 59 | "Post:123": { 60 | id: "123", 61 | __typename: "Post", 62 | author: "Author:1", 63 | title: "My awesome blog post", 64 | 'comments({"b":true})': ["Comment:324"] 65 | }, 66 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 67 | "Comment:324": { 68 | id: "324", 69 | __typename: "Comment", 70 | commenter: "Author:2" 71 | }, 72 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-boolean-external.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple boolean external", 7 | query: gql` 8 | query TestQuery($a: Boolean) { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(b: $a) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | variables: { a: true }, 31 | data: standardResponse, 32 | normMap: { 33 | ROOT_QUERY: { 34 | posts: ["Post:123"] 35 | }, 36 | "Post:123": { 37 | id: "123", 38 | __typename: "Post", 39 | author: "Author:1", 40 | title: "My awesome blog post", 41 | 'comments({"b":true})': ["Comment:324"] 42 | }, 43 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 44 | "Comment:324": { 45 | id: "324", 46 | __typename: "Comment", 47 | commenter: "Author:2" 48 | }, 49 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-boolean.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple boolean", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(a: true) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: standardResponse, 31 | normMap: { 32 | ROOT_QUERY: { 33 | posts: ["Post:123"] 34 | }, 35 | "Post:123": { 36 | id: "123", 37 | __typename: "Post", 38 | author: "Author:1", 39 | title: "My awesome blog post", 40 | 'comments({"a":true})': ["Comment:324"] 41 | }, 42 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 43 | "Comment:324": { 44 | id: "324", 45 | __typename: "Comment", 46 | commenter: "Author:2" 47 | }, 48 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-int.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple int", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(a: 1) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: standardResponse, 31 | normMap: { 32 | ROOT_QUERY: { 33 | posts: ["Post:123"] 34 | }, 35 | "Post:123": { 36 | id: "123", 37 | __typename: "Post", 38 | author: "Author:1", 39 | title: "My awesome blog post", 40 | 'comments({"a":"1"})': ["Comment:324"] 41 | }, 42 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 43 | "Comment:324": { 44 | id: "324", 45 | __typename: "Comment", 46 | commenter: "Author:2" 47 | }, 48 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-list.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple list", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(a: [1, 2]) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: standardResponse, 31 | normMap: { 32 | ROOT_QUERY: { 33 | posts: ["Post:123"] 34 | }, 35 | "Post:123": { 36 | id: "123", 37 | __typename: "Post", 38 | author: "Author:1", 39 | title: "My awesome blog post", 40 | 'comments({"a":["1","2"]})': ["Comment:324"] 41 | }, 42 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 43 | "Comment:324": { 44 | id: "324", 45 | __typename: "Comment", 46 | commenter: "Author:2" 47 | }, 48 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-nested-object.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple nested object", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(a: { b: 1, c: { d: 1, e: "asdf" } }) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: standardResponse, 31 | normMap: { 32 | ROOT_QUERY: { 33 | posts: ["Post:123"] 34 | }, 35 | "Post:123": { 36 | id: "123", 37 | __typename: "Post", 38 | author: "Author:1", 39 | title: "My awesome blog post", 40 | 'comments({"a":{"b":"1","c":{"d":"1","e":"asdf"}}})': ["Comment:324"] 41 | }, 42 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 43 | "Comment:324": { 44 | id: "324", 45 | __typename: "Comment", 46 | commenter: "Author:2" 47 | }, 48 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/shared-tests/with-variables-simple-object.ts: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | import { SharedTestDef } from "../shared-test-def"; 3 | import { standardResponse } from "../shared-data/standard-response"; 4 | 5 | export const test: SharedTestDef = { 6 | name: "with variables simple object", 7 | query: gql` 8 | query TestQuery { 9 | posts { 10 | id 11 | __typename 12 | author { 13 | id 14 | __typename 15 | name 16 | } 17 | title 18 | comments(a: { b: 1, c: "asd" }) { 19 | id 20 | __typename 21 | commenter { 22 | id 23 | __typename 24 | name 25 | } 26 | } 27 | } 28 | } 29 | `, 30 | data: standardResponse, 31 | normMap: { 32 | ROOT_QUERY: { 33 | posts: ["Post:123"] 34 | }, 35 | "Post:123": { 36 | id: "123", 37 | __typename: "Post", 38 | author: "Author:1", 39 | title: "My awesome blog post", 40 | 'comments({"a":{"b":"1","c":"asd"}})': ["Comment:324"] 41 | }, 42 | "Author:1": { id: "1", __typename: "Author", name: "Paul" }, 43 | "Comment:324": { 44 | id: "324", 45 | __typename: "Comment", 46 | commenter: "Author:2" 47 | }, 48 | "Author:2": { id: "2", __typename: "Author", name: "Nicole" } 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /test/test-data-utils.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | export function loadTests(path: string): ReadonlyArray { 4 | const testBasePath = __dirname + "/" + path; 5 | const importedTests = fs 6 | .readdirSync(testBasePath) 7 | // .filter(f => f.match(/\.test\.ts$/i)) 8 | .map(f => require(testBasePath + f)) 9 | .map(importedModule => importedModule.test as T); 10 | 11 | return importedTests; 12 | } 13 | 14 | export interface UtilsTest { 15 | readonly only?: boolean; 16 | readonly skip?: boolean; 17 | } 18 | 19 | /** 20 | * Helper function to enable only one test to be run 21 | * in an array of test data 22 | */ 23 | export function onlySkip( 24 | tests: ReadonlyArray 25 | ): ReadonlyArray { 26 | const skips = tests.filter(t => !!!t.skip); 27 | const onlys = skips.filter(t => t.only === true); 28 | if (onlys.length > 0) { 29 | return onlys; 30 | } 31 | return skips; 32 | } 33 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../base-tsconfig.json", 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "../lib-test", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "target": "es5", 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "jsx": "react", 12 | "baseUrl": ".", 13 | "paths": {}, 14 | "lib": ["esnext"] 15 | }, 16 | "include": ["**/*"] 17 | } 18 | --------------------------------------------------------------------------------