├── .eslintrc ├── test ├── snapshots │ ├── GraphQLNormalizr.test.js.snap │ └── GraphQLNormalizr.test.js.md ├── humps.test.js ├── pluralize.test.js ├── GraphQLNormalizr.test.js └── mocks │ └── data.js ├── src ├── index.js ├── constants.js ├── helpers.js ├── humps.js ├── utils.js ├── GraphQLNormalizr.js └── pluralize.js ├── .babelrc ├── rollup.config.cjs.js ├── rollup.config.esm.js ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── LICENSE ├── rollup.config.js ├── package.json └── README.md /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ "mono" ] 3 | } 4 | -------------------------------------------------------------------------------- /test/snapshots/GraphQLNormalizr.test.js.snap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monojack/graphql-normalizr/HEAD/test/snapshots/GraphQLNormalizr.test.js.snap -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { GraphQLNormalizr, } from './GraphQLNormalizr' 2 | export { pluralize, } from './pluralize' 3 | export { camelize, decamelize, pascalize, } from './humps' 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [['@babel/preset-env', { loose: true }]], 3 | plugins: [['@babel/plugin-proposal-object-rest-spread', { loose: true }]], 4 | env: { 5 | es: { 6 | presets: [['@babel/preset-env', { loose: true, modules: false }]], 7 | }, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const CACHE_READ_ERROR = `[GraphQLNormalizr]: Could not read from cache` 2 | export const CACHE_WRITE_ERROR = `[GraphQLNormalizr]: Could not write to cache` 3 | export const PAGEINFO_WITH_USE_CONNECTIONS_FALSE = `[GraphQLNormalizr]: You are using the 'pageInfo' field but haven't set 'useConnections' to true` 4 | -------------------------------------------------------------------------------- /rollup.config.cjs.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel') 2 | const pkg = require('./package.json') 3 | 4 | const input = './src/index.js' 5 | const globals = { graphql: 'GraphQL', } 6 | const babelOpts = { 7 | exclude: '**/node_modules/**', 8 | runtimeHelpers: true, 9 | } 10 | 11 | module.exports = { 12 | input, 13 | output: { 14 | file: pkg.main, 15 | format: 'cjs', 16 | globals, 17 | }, 18 | treeshake: true, 19 | external: Object.keys(globals), 20 | plugins: [ babel(babelOpts), ], 21 | } 22 | -------------------------------------------------------------------------------- /rollup.config.esm.js: -------------------------------------------------------------------------------- 1 | const babel = require('rollup-plugin-babel') 2 | const pkg = require('./package.json') 3 | 4 | const input = './src/index.js' 5 | const globals = { graphql: 'GraphQL', } 6 | 7 | const babelOpts = { 8 | exclude: '**/node_modules/**', 9 | runtimeHelpers: true, 10 | } 11 | 12 | module.exports = { 13 | input, 14 | output: { 15 | file: pkg.module, 16 | format: 'esm', 17 | globals, 18 | }, 19 | treeshake: true, 20 | external: Object.keys(globals), 21 | plugins: [ babel(babelOpts), ], 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | - run: npm ci 17 | - run: npm test 18 | 19 | publish-npm: 20 | needs: build 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: 12 27 | registry-url: https://registry.npmjs.org/ 28 | - run: npm ci 29 | - run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | export const buildNoTypenameError = node => 2 | `[GraphQLNormalizr]: No "__typename" field found on node ${JSON.stringify( 3 | node 4 | )}` 5 | 6 | export const hasField = fieldName => set => 7 | set.some(({ alias, name, }) => (alias || name).value === fieldName) 8 | 9 | export const createField = name => ({ 10 | kind: 'Field', 11 | alias: undefined, 12 | name: { 13 | kind: 'Name', 14 | value: name, 15 | }, 16 | arguments: [], 17 | directives: [], 18 | selectionSet: undefined, 19 | }) 20 | 21 | export const toLists = (object = {}) => 22 | Object.entries(object).reduce( 23 | (acc, [ key, value, ]) => ({ 24 | ...acc, 25 | [key]: Object.values(value), 26 | }), 27 | {} 28 | ) 29 | 30 | export const getIn = (obj, keys, defaultValue) => { 31 | let result = obj 32 | let index = 0 33 | while (index < keys.length && result != null) { 34 | result = result[keys[index]] 35 | index += 1 36 | } 37 | return keys.length === index && result !== undefined ? result : defaultValue 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Dist 35 | dist 36 | es 37 | esm 38 | lib 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Typescript v1 declaration files 45 | typings/ 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Optional REPL history 54 | .node_repl_history 55 | 56 | # Output of 'npm pack' 57 | *.tgz 58 | 59 | # Yarn Integrity file 60 | .yarn-integrity 61 | 62 | # dotenv environment variables file 63 | .env 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Ionut Achim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/humps.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/domchristie/humps 3 | */ 4 | 5 | /* eslint-disable no-self-compare, no-useless-escape */ 6 | const _isNumerical = function (obj) { 7 | obj = obj - 0 8 | return obj === obj 9 | } 10 | 11 | const separateWords = function (string, options) { 12 | options = options || {} 13 | var separator = options.separator || '_' 14 | var split = options.split || /(?=[A-Z])/ 15 | 16 | return string.split(split).join(separator) 17 | } 18 | 19 | export const camelize = function (string) { 20 | if (_isNumerical(string)) { 21 | return string 22 | } 23 | string = string.replace(/[\-_\s]+(.)?/g, function (match, chr) { 24 | return chr ? chr.toUpperCase() : '' 25 | }) 26 | // Ensure 1st char is always lowercase 27 | return string.substr(0, 1).toLowerCase() + string.substr(1) 28 | } 29 | 30 | export const pascalize = function (string) { 31 | var camelized = camelize(string) 32 | // Ensure 1st char is always uppercase 33 | return camelized.substr(0, 1).toUpperCase() + camelized.substr(1) 34 | } 35 | 36 | export const decamelize = function (string, options) { 37 | return separateWords(string, options).toLowerCase() 38 | } 39 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const nodeResolve = require('rollup-plugin-node-resolve') 2 | const babel = require('rollup-plugin-babel') 3 | const replace = require('rollup-plugin-replace') 4 | const { terser, } = require('rollup-plugin-terser') 5 | 6 | const input = './src/index.js' 7 | 8 | const name = 'GraphQLNormalizr' 9 | const globals = { graphql: 'GraphQL', } 10 | const babelOpts = { 11 | exclude: '**/node_modules/**', 12 | runtimeHelpers: true, 13 | } 14 | 15 | module.exports = [ 16 | { 17 | input, 18 | output: { 19 | file: 'dist/graphql-normalizr.umd.js', 20 | format: 'umd', 21 | name, 22 | globals, 23 | }, 24 | external: Object.keys(globals), 25 | plugins: [ 26 | nodeResolve(), 27 | babel(babelOpts), 28 | replace({ 'process.env.NODE_ENV': JSON.stringify('development'), }), 29 | ], 30 | }, 31 | 32 | { 33 | input, 34 | output: { 35 | file: 'dist/graphql-normalizr.min.js', 36 | format: 'umd', 37 | name, 38 | globals, 39 | }, 40 | external: Object.keys(globals), 41 | plugins: [ 42 | nodeResolve(), 43 | babel(babelOpts), 44 | replace({ 'process.env.NODE_ENV': JSON.stringify('production'), }), 45 | terser(), 46 | ], 47 | }, 48 | require('./rollup.config.cjs'), 49 | require('./rollup.config.esm'), 50 | ] 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-normalizr", 3 | "version": "2.11.0", 4 | "description": "Normalize GraphQL response", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/monojack/graphql-normalizr.git" 8 | }, 9 | "keywords": [ 10 | "javascript", 11 | "graphql", 12 | "normalizr", 13 | "normalizer" 14 | ], 15 | "author": "Ionut Achim ", 16 | "license": "MIT", 17 | "main": "lib/graphql-normalizr.cjs.js", 18 | "module": "esm/graphql-normalizr.esm.js", 19 | "devDependencies": { 20 | "@babel/core": "^7.12.10", 21 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 22 | "@babel/preset-env": "^7.12.10", 23 | "ava": "^3.14.0", 24 | "eslint": "^7.15.0", 25 | "eslint-config-mono": "^2.0.0", 26 | "graphql": "^15.4.0", 27 | "graphql-tag": "^2.11.0", 28 | "rollup": "^2.34.2", 29 | "rollup-plugin-babel": "^4.4.0", 30 | "rollup-plugin-node-resolve": "^5.2.0", 31 | "rollup-plugin-replace": "^2.2.0", 32 | "rollup-plugin-size-snapshot": "^0.12.0", 33 | "rollup-plugin-terser": "^7.0.2" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/monojack/graphql-normalizr/issues" 37 | }, 38 | "peerDependencies": { 39 | "graphql": "^0.11.* || ^0.12.* || ^0.13.* || ^0.14.* || ^14.* || ^15.*" 40 | }, 41 | "scripts": { 42 | "rollup": "rollup -c", 43 | "dev": "npm run build:esm -- -w", 44 | "pretest": "npm run build:cjs", 45 | "test": "ava --verbose --serial", 46 | "build:esm": "rollup -c rollup.config.esm.js", 47 | "build:cjs": "rollup -c rollup.config.cjs.js", 48 | "build": "npm run rollup", 49 | "prepare": "npm run clean && npm run build", 50 | "clean": "rimraf lib esm dist" 51 | }, 52 | "ava": { 53 | "files": [ 54 | "test/*.js", 55 | "!test/mocks/**" 56 | ] 57 | }, 58 | "npmName": "graphql-normalizr", 59 | "files": [ 60 | "dist", 61 | "esm", 62 | "lib" 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /test/humps.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { camelize, decamelize, pascalize, } = require('../') 3 | 4 | const camelizeTests = { 5 | foobar: 'foobar', 6 | fooBarBaz: 'fooBarBaz', 7 | foo_bar: 'fooBar', 8 | 'foo-bar': 'fooBar', 9 | 'foo-bar_baz': 'fooBarBaz', 10 | FooBar_baz: 'fooBarBaz', 11 | 'foo-BarBaz_quux': 'fooBarBazQuux', 12 | 'foo-BarBaz_QUUX': 'fooBarBazQUUX', 13 | 'foo__bar-_baz_-quux': 'fooBarBazQuux', 14 | } 15 | 16 | const decamelizeTests = { 17 | foobar: 'foobar', 18 | foo_bar: 'foo_bar', 19 | foo_bar_baz: 'foo_bar_baz', 20 | fooBar: 'foo_bar', 21 | fooBarBaz: 'foo_bar_baz', 22 | fooBarBazQUUX: 'foo_bar_baz_q_u_u_x', 23 | } 24 | 25 | const pascalizeTests = { 26 | Foobar: 'Foobar', 27 | FooBar: 'FooBar', 28 | foo_bar_baz: 'FooBarBaz', 29 | 'foo-bar-baz-quux': 'FooBarBazQuux', 30 | 'foo-bar-baz_quux_quuz': 'FooBarBazQuuxQuuz', 31 | 'corgeGrault-garply': 'CorgeGraultGarply', 32 | 'corgeGrault-garplyWaldo': 'CorgeGraultGarplyWaldo', 33 | 'corgeGrault-garplyWaldo_Fred': 'CorgeGraultGarplyWaldoFred', 34 | 'corgeGrault-garplyWaldo_Fred-PLUGH': 'CorgeGraultGarplyWaldoFredPLUGH', 35 | } 36 | 37 | test('camelize :: returns correct form for the required case', t => { 38 | const entries = Object.entries(camelizeTests) 39 | t.plan(entries.length) 40 | 41 | for (const [ key, value, ] of entries) { 42 | const camelized = camelize(key) 43 | t.true(camelized === value) 44 | } 45 | }) 46 | 47 | test('decamelize :: returns correct form for the required case', t => { 48 | const entries = Object.entries(decamelizeTests) 49 | t.plan(entries.length) 50 | 51 | for (const [ key, value, ] of entries) { 52 | const decamelized = decamelize(key) 53 | t.true(decamelized === value) 54 | } 55 | }) 56 | 57 | test('pascalize :: returns correct form for the required case', t => { 58 | const entries = Object.entries(pascalizeTests) 59 | t.plan(entries.length) 60 | 61 | for (const [ key, value, ] of entries) { 62 | const pascalized = pascalize(key) 63 | t.true(pascalized === value) 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import { camelize, decamelize, pascalize, } from './humps' 2 | 3 | export function typeOf (value) { 4 | return Object.prototype.toString.call(value).slice(8, -1) 5 | } 6 | 7 | export function isType (type) { 8 | return function (value) { 9 | if (value === null) return type.toLowerCase() === 'null' 10 | if (typeof value === 'undefined') return type.toLowerCase() === 'undefined' 11 | 12 | return type.toLowerCase() === typeOf(value).toLowerCase() 13 | } 14 | } 15 | 16 | export function isArray (value) { 17 | return isType('array')(value) 18 | } 19 | 20 | export function isObject (value) { 21 | return isType('object')(value) 22 | } 23 | 24 | export function isEmpty (value) { 25 | if (typeof value === 'string') return !value 26 | if (isType('object')(value)) return !Object.values(value).length 27 | if (isType('array')(value)) return !value.length 28 | if (isType('Map')(value)) return !value.size 29 | if (isType('Set')(value)) return !value.size 30 | return false 31 | } 32 | 33 | export function isNil (value) { 34 | return value == null 35 | } 36 | 37 | export function isNotNil (value) { 38 | return !isNil(value) 39 | } 40 | 41 | export function prop (path) { 42 | return function (obj) { 43 | return path.split('.').reduce((acc, curr) => { 44 | try { 45 | return typeof acc[curr] !== 'undefined' ? acc[curr] : undefined 46 | } catch (e) { 47 | return undefined 48 | } 49 | }, obj) 50 | } 51 | } 52 | 53 | export function map (transform) { 54 | return function (list) { 55 | return list.map(transform) 56 | } 57 | } 58 | 59 | export function mapObject (transform) { 60 | return function (obj) { 61 | return Object.entries(obj).reduce( 62 | (acc, [ key, value, ]) => ({ 63 | ...acc, 64 | [key]: transform(value), 65 | }), 66 | {} 67 | ) 68 | } 69 | } 70 | 71 | export function toLower (str) { 72 | return str.toLowerCase() 73 | } 74 | 75 | export function toUpper (str) { 76 | return str.toUpperCase() 77 | } 78 | 79 | export const toCamel = camelize 80 | export const toSnake = decamelize 81 | export const toPascal = pascalize 82 | 83 | export function toKebab (str) { 84 | return decamelize(str).replace(/_/g, '-') 85 | } 86 | 87 | export function isScalar (value) { 88 | return !isObject(value) && !isNil(value) 89 | } 90 | -------------------------------------------------------------------------------- /test/pluralize.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { pluralize, } = require('../') 3 | 4 | const irregularPlurals = { 5 | addendum: [ 'addenda', ], 6 | alga: [ 'algae', ], 7 | alumna: [ 'alumnae', ], 8 | alumnus: [ 'alumni', ], 9 | analysis: [ 'analyses', ], 10 | antenna: [ 'antennas', 'antennae', ], 11 | apparatus: [ 'apparatuses', ], 12 | appendix: [ 'appendices', 'appendixes', ], 13 | axis: [ 'axes', ], 14 | bacillus: [ 'bacilli', ], 15 | bacterium: [ 'bacteria', ], 16 | basis: [ 'bases', ], 17 | beau: [ 'beaux', 'beaus', ], 18 | bison: [ 'bison', ], 19 | buffalo: [ 'buffalos', 'buffaloes', ], 20 | bureau: [ 'bureaus', ], 21 | bus: [ 'busses', 'buses', ], 22 | cactus: [ 'cactuses', 'cacti', ], 23 | calf: [ 'calves', ], 24 | child: [ 'children', ], 25 | corps: [ 'corps', ], 26 | corpus: [ 'corpora', 'corpuses', ], 27 | crisis: [ 'crises', ], 28 | criterion: [ 'criteria', ], 29 | curriculum: [ 'curricula', ], 30 | datum: [ 'data', ], 31 | deer: [ 'deer', ], 32 | die: [ 'dice', ], 33 | dwarf: [ 'dwarfs', 'dwarves', ], 34 | diagnosis: [ 'diagnoses', ], 35 | echo: [ 'echoes', ], 36 | elf: [ 'elves', ], 37 | ellipsis: [ 'ellipses', ], 38 | embargo: [ 'embargoes', 'embargos', ], 39 | emphasis: [ 'emphases', ], 40 | erratum: [ 'errata', ], 41 | fireman: [ 'firemen', ], 42 | fish: [ 'fish', 'fishes', ], 43 | focus: [ 'focuses', 'foci', ], 44 | foot: [ 'feet', ], 45 | formula: [ 'formulas', ], 46 | fungus: [ 'fungi', 'funguses', ], 47 | genus: [ 'genera', ], 48 | goose: [ 'geese', ], 49 | half: [ 'halves', ], 50 | hero: [ 'heroes', ], 51 | hippopotamus: [ 'hippopotami', 'hippopotamuses', ], 52 | hoof: [ 'hoofs', 'hooves', ], 53 | hypothesis: [ 'hypotheses', ], 54 | index: [ 'indices', 'indexes', ], 55 | knife: [ 'knives', ], 56 | leaf: [ 'leaves', ], 57 | life: [ 'lives', ], 58 | loaf: [ 'loaves', ], 59 | louse: [ 'lice', ], 60 | man: [ 'men', ], 61 | matrix: [ 'matrices', ], 62 | means: [ 'means', ], 63 | medium: [ 'media', 'mediums', ], 64 | memorandum: [ 'memorandums', 'memoranda', ], 65 | millennium: [ 'millenniums', 'millennia', ], 66 | moose: [ 'moose', ], 67 | mosquito: [ 'mosquitoes', 'mosquitos', ], 68 | mouse: [ 'mice', ], 69 | nebula: [ 'nebulae', 'nebulas', ], 70 | neurosis: [ 'neuroses', ], 71 | nucleus: [ 'nuclei', ], 72 | oasis: [ 'oases', ], 73 | octopus: [ 'octopi', 'octopuses', ], 74 | ovum: [ 'ova', ], 75 | ox: [ 'oxen', ], 76 | paralysis: [ 'paralyses', ], 77 | parenthesis: [ 'parentheses', ], 78 | person: [ 'people', ], 79 | phenomenon: [ 'phenomena', ], 80 | potato: [ 'potatoes', ], 81 | radius: [ 'radii', 'radiuses', ], 82 | scarf: [ 'scarfs', 'scarves', ], 83 | self: [ 'selves', ], 84 | series: [ 'series', ], 85 | sheep: [ 'sheep', ], 86 | shelf: [ 'shelves', ], 87 | scissors: [ 'scissors', ], 88 | species: [ 'species', ], 89 | stimulus: [ 'stimuli', ], 90 | stratum: [ 'strata', ], 91 | syllabus: [ 'syllabi', 'syllabuses', ], 92 | symposium: [ 'symposia', 'symposiums', ], 93 | synthesis: [ 'syntheses', ], 94 | synopsis: [ 'synopses', ], 95 | tableau: [ 'tableaux', 'tableaus', ], 96 | that: [ 'those', ], 97 | thesis: [ 'theses', ], 98 | thief: [ 'thieves', ], 99 | 'this': [ 'these', ], 100 | tomato: [ 'tomatoes', ], 101 | tooth: [ 'teeth', ], 102 | torpedo: [ 'torpedoes', ], 103 | vertebra: [ 'vertebrae', ], 104 | veto: [ 'vetoes', 'vetos', ], 105 | vita: [ 'vitae', ], 106 | watch: [ 'watches', ], 107 | wife: [ 'wives', ], 108 | wolf: [ 'wolves', ], 109 | woman: [ 'women', ], 110 | zero: [ 'zeros', 'zeroes', ], 111 | } 112 | 113 | const generalTest = { 114 | user: 'users', 115 | address: 'addresses', 116 | addresses: 'addresses', 117 | accounts: 'accounts', 118 | id: 'ids', 119 | IDS: 'IDS', 120 | APPLE: 'APPLES', 121 | } 122 | 123 | test('pluralize :: returns correct form', t => { 124 | const entries = Object.entries(generalTest) 125 | t.plan(entries.length) 126 | 127 | for (const [ key, value, ] of entries) { 128 | const plural = pluralize(key) 129 | t.true(plural === value) 130 | } 131 | }) 132 | 133 | test('pluralize :: returns correct form for irregular plural', t => { 134 | const entries = Object.entries(irregularPlurals) 135 | t.plan(entries.length) 136 | 137 | for (const [ key, values, ] of entries) { 138 | const plural = pluralize(key) 139 | t.true(values.includes(plural)) 140 | } 141 | }) 142 | -------------------------------------------------------------------------------- /src/GraphQLNormalizr.js: -------------------------------------------------------------------------------- 1 | import { visit, parse as gql, Kind, } from 'graphql' 2 | 3 | import { pluralize, } from './pluralize' 4 | import { hasField, createField, toLists, buildNoTypenameError, getIn, } from './helpers' 5 | import { 6 | map, 7 | prop, 8 | isNil, 9 | isNotNil, 10 | isArray, 11 | isObject, 12 | isEmpty, 13 | isScalar, 14 | toLower, 15 | toUpper, 16 | toCamel, 17 | toSnake, 18 | toPascal, 19 | toKebab, 20 | } from './utils' 21 | import { CACHE_READ_ERROR, CACHE_WRITE_ERROR, PAGEINFO_WITH_USE_CONNECTIONS_FALSE, } from './constants' 22 | 23 | const isProd = process?.env?.NODE_ENV === 'production' 24 | const warnings = {} 25 | const casingMethodMap = { 26 | lower: toLower, 27 | upper: toUpper, 28 | kebab: toKebab, 29 | camel: toCamel, 30 | pascal: toPascal, 31 | snake: toSnake, 32 | } 33 | 34 | export function GraphQLNormalizr ({ 35 | idKey = 'id', 36 | typeMap = {}, 37 | caching = false, 38 | lists = false, 39 | typenames = false, 40 | plural = true, 41 | casing = 'camel', 42 | useConnections = false, 43 | typePointers = false, 44 | exclude, 45 | ignore 46 | } = {}) { 47 | if(!isProd && !warnings.excludeDeprecation && exclude) { 48 | warnings.excludeDeprecation = true 49 | console.warn('[GraphQLNormalizr]: The `exclude` option is deprecated and may be removed in the future,\nplease use `ignore` instead.\n(see https://github.com/monojack/graphql-normalizr/issues/38)') 50 | } 51 | ignore = ignore || exclude || {} 52 | 53 | const hasIdField = hasField(idKey) 54 | const hasTypeNameField = hasField('__typename') 55 | const hasEdgesField = hasField('edges') 56 | 57 | const idField = createField(idKey) 58 | const typeNameField = createField('__typename') 59 | 60 | const cache = new Map() 61 | 62 | function caseTransform (type) { 63 | let str = type 64 | str = plural ? pluralize(str) : str 65 | str = casingMethodMap[casing](str) 66 | return str 67 | } 68 | 69 | function getEntityName (type, entities) { 70 | return typeMap[type] || entities[type] || caseTransform(type) 71 | } 72 | 73 | function mapper (...path) { 74 | const entities = {} 75 | return typePointers 76 | ? item => { 77 | const { __typename: typename, [idKey]: id, } = getIn(item, path, {}) 78 | entities[typename] = getEntityName(typename, entities) 79 | return { collection: getEntityName(typename, entities), [idKey]: id, } 80 | } 81 | : item => getIn(item, path, {})[idKey] 82 | } 83 | 84 | function mapNestedValue (obj) { 85 | const object = { ...obj, } 86 | !typenames && delete object.__typename 87 | 88 | const res = Object.entries(object).reduce((acc, [ key, value, ]) => { 89 | return { 90 | ...acc, 91 | [key]: isObject(value) 92 | ? useConnections && value.hasOwnProperty('edges') 93 | ? value.edges.map(mapper('node')).filter(isNotNil) 94 | : (() => { 95 | const _v = prop(idKey)(value) 96 | const { __typename, ..._value } = value 97 | return _v == null ? (!typenames ? _value : value) : _v 98 | })() 99 | : isArray(value) && !value.every(isScalar) 100 | ? map(mapper())(value) 101 | : value, 102 | } 103 | }, {}) 104 | return res 105 | } 106 | 107 | function assoc (entity, value, normalized) { 108 | if (isNil(entity)) throw new Error(buildNoTypenameError(value)) 109 | const id = value[idKey] 110 | 111 | if (isNil(id)) return 112 | 113 | const existingEntities = normalized[entity] 114 | normalized[entity] = existingEntities || {} 115 | 116 | const existing = normalized[entity][id] || {} 117 | normalized[entity][id] = { 118 | ...existing, 119 | ...value, 120 | } 121 | } 122 | 123 | function normalize ({ data, }) { 124 | const paths = {} 125 | const entities = {} 126 | let normalized = {} 127 | 128 | try { 129 | let cached 130 | caching && (cached = cache.get(JSON.stringify(data))) 131 | if (cached) { 132 | return cached 133 | } 134 | } catch (e) { 135 | !isProd && console.warn(CACHE_READ_ERROR) 136 | } 137 | 138 | ;(function walk (root, path = '', stackEntity, stackValue) { 139 | if (root && Object.prototype.hasOwnProperty.call(root, 'pageInfo') && !useConnections) { 140 | if(!isProd && !warnings.pageInfoNoConnections) { 141 | console.warn(PAGEINFO_WITH_USE_CONNECTIONS_FALSE) 142 | warnings.pageInfoNoConnections = true 143 | } 144 | } 145 | 146 | for (const [ key, value, ] of Object.entries(root)) { 147 | if (useConnections && !isNil(value) && value.hasOwnProperty('edges')) { 148 | value.edges.forEach((edge, i) => { 149 | if (edge && edge.node) { 150 | walk({ node: edge.node, }, `${path ? `${path}.` : ``}${key}.edges.${i}`) 151 | } 152 | }) 153 | } else if ((isObject(value) || isArray(value)) && isEmpty(value)) { 154 | paths[path] = { done: true, } 155 | } else if (isObject(value) || (isArray(value) && !value.every(isScalar))) { 156 | if (ignore[stackEntity] && ignore[stackEntity].includes(key)) { 157 | paths[path] = { done: true, } 158 | } else { 159 | const type = value.__typename 160 | type && (entities[type] = getEntityName(type, entities)) 161 | walk(value, `${path ? `${path}.` : ``}${key}`, entities[type], value) 162 | } 163 | } else { 164 | if (!paths[path] && isNotNil(value)) { 165 | assoc(stackEntity, mapNestedValue(stackValue), normalized) 166 | paths[path] = { done: true, } 167 | } 168 | } 169 | } 170 | })(data) 171 | 172 | try { 173 | caching && cache.set(JSON.stringify(data), normalized) 174 | } catch (e) { 175 | // eslint-disable-next-line 176 | !isProd && console.warn(CACHE_WRITE_ERROR) 177 | } 178 | 179 | normalized = lists ? toLists(normalized) : normalized 180 | 181 | return normalized 182 | } 183 | 184 | const isInlineFragment = node => node.kind === Kind.INLINE_FRAGMENT 185 | 186 | const connectionFields = [ 'edges', 'pageInfo', ] 187 | 188 | const excludeMetaFields = useConnections 189 | ? (node, key, parent, path) => 190 | node.selections.some(isInlineFragment) || 191 | hasEdgesField(node.selections) || 192 | (!isInlineFragment(parent) && connectionFields.includes(parent.name.value)) 193 | : () => false 194 | 195 | function addRequiredFields (query) { 196 | return visit(query, { 197 | SelectionSet (node, key, parent, path) { 198 | if (parent.kind === Kind.OPERATION_DEFINITION || excludeMetaFields(node, key, parent, path)) { 199 | return 200 | } 201 | 202 | !hasIdField(node.selections) && node.selections.unshift(idField) 203 | !hasTypeNameField(node.selections) && node.selections.unshift(typeNameField) 204 | 205 | return node 206 | }, 207 | }) 208 | } 209 | 210 | function parse (qs) { 211 | return addRequiredFields(gql(qs, { noLocation: true, })) 212 | } 213 | 214 | return { normalize, addRequiredFields, parse, } 215 | } 216 | -------------------------------------------------------------------------------- /src/pluralize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/blakeembrey/pluralize 3 | */ 4 | 5 | /* eslint-disable no-control-regex */ 6 | 7 | const irregularPlurals = { 8 | i: 'we', 9 | me: 'us', 10 | he: 'they', 11 | she: 'they', 12 | them: 'them', 13 | myself: 'ourselves', 14 | yourself: 'yourselves', 15 | itself: 'themselves', 16 | herself: 'themselves', 17 | himself: 'themselves', 18 | themself: 'themselves', 19 | is: 'are', 20 | was: 'were', 21 | has: 'have', 22 | 'this': 'these', 23 | that: 'those', 24 | echo: 'echoes', 25 | dingo: 'dingoes', 26 | volcano: 'volcanoes', 27 | tornado: 'tornadoes', 28 | torpedo: 'torpedoes', 29 | genus: 'genera', 30 | viscus: 'viscera', 31 | stigma: 'stigmata', 32 | stoma: 'stomata', 33 | dogma: 'dogmata', 34 | lemma: 'lemmata', 35 | schema: 'schemata', 36 | anathema: 'anathemata', 37 | ox: 'oxen', 38 | axe: 'axes', 39 | die: 'dice', 40 | yes: 'yeses', 41 | foot: 'feet', 42 | eave: 'eaves', 43 | goose: 'geese', 44 | buffalo: 'buffaloes', 45 | tooth: 'teeth', 46 | quiz: 'quizzes', 47 | human: 'humans', 48 | proof: 'proofs', 49 | carve: 'carves', 50 | valve: 'valves', 51 | looey: 'looies', 52 | thief: 'thieves', 53 | groove: 'grooves', 54 | pickaxe: 'pickaxes', 55 | whiskey: 'whiskies', 56 | vita: 'vitae', 57 | } 58 | 59 | const uncountables = { 60 | adulthood: true, 61 | advice: true, 62 | agenda: true, 63 | aid: true, 64 | alcohol: true, 65 | ammo: true, 66 | anime: true, 67 | athletics: true, 68 | audio: true, 69 | bison: true, 70 | blood: true, 71 | bream: true, 72 | buffalo: true, 73 | butter: true, 74 | carp: true, 75 | cash: true, 76 | chassis: true, 77 | chess: true, 78 | clothing: true, 79 | cod: true, 80 | commerce: true, 81 | cooperation: true, 82 | corps: true, 83 | debris: true, 84 | diabetes: true, 85 | digestion: true, 86 | elk: true, 87 | energy: true, 88 | equipment: true, 89 | excretion: true, 90 | expertise: true, 91 | flounder: true, 92 | fun: true, 93 | gallows: true, 94 | garbage: true, 95 | graffiti: true, 96 | headquarters: true, 97 | health: true, 98 | herpes: true, 99 | highjinks: true, 100 | homework: true, 101 | housework: true, 102 | information: true, 103 | jeans: true, 104 | justice: true, 105 | kudos: true, 106 | labour: true, 107 | literature: true, 108 | machinery: true, 109 | mackerel: true, 110 | mail: true, 111 | media: true, 112 | mews: true, 113 | moose: true, 114 | music: true, 115 | mud: true, 116 | manga: true, 117 | news: true, 118 | pike: true, 119 | plankton: true, 120 | pliers: true, 121 | police: true, 122 | pollution: true, 123 | premises: true, 124 | rain: true, 125 | research: true, 126 | rice: true, 127 | salmon: true, 128 | scissors: true, 129 | series: true, 130 | sewage: true, 131 | shambles: true, 132 | shrimp: true, 133 | species: true, 134 | staff: true, 135 | swine: true, 136 | tennis: true, 137 | traffic: true, 138 | transportation: true, 139 | trout: true, 140 | tuna: true, 141 | wealth: true, 142 | welfare: true, 143 | whiting: true, 144 | wildebeest: true, 145 | wildlife: true, 146 | you: true, 147 | } 148 | 149 | const pluralRules = [ 150 | [ /s?$/i, 's', ], 151 | [ /[^\u0000-\u007F]$/i, '$0', ], 152 | [ /([^aeiou]ese)$/i, '$1', ], 153 | [ /(ax|test)is$/i, '$1es', ], 154 | [ /(alias|[^aou]us|t[lm]as|gas|ris)$/i, '$1es', ], 155 | [ /(e[mn]u)s?$/i, '$1s', ], 156 | [ /([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i, '$1', ], 157 | [ 158 | /(alumn|syllab|octop|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, 159 | '$1i', 160 | ], 161 | [ /(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae', ], 162 | [ /(seraph|cherub)(?:im)?$/i, '$1im', ], 163 | [ /(her|at|gr)o$/i, '$1oes', ], 164 | [ 165 | /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i, 166 | '$1a', 167 | ], 168 | [ 169 | /(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, 170 | '$1a', 171 | ], 172 | [ /sis$/i, 'ses', ], 173 | [ /(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves', ], 174 | [ /([^aeiouy]|qu)y$/i, '$1ies', ], 175 | [ /([^ch][ieo][ln])ey$/i, '$1ies', ], 176 | [ /(x|ch|ss|sh|zz)$/i, '$1es', ], 177 | [ /(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices', ], 178 | [ /\b((?:tit)?m|l)(?:ice|ouse)$/i, '$1ice', ], 179 | [ /(pe)(?:rson|ople)$/i, '$1ople', ], 180 | [ /(child)(?:ren)?$/i, '$1ren', ], 181 | [ /eaux$/i, '$0', ], 182 | [ /m[ae]n$/i, 'men', ], 183 | [ /thou/, 'you', ], 184 | [ /[^aeiou]ese$/i, '$0', ], 185 | [ /deer/i, '$0', ], 186 | [ /fish$/i, '$0', ], 187 | [ /measles$/i, '$0', ], 188 | [ /o[iu]s$/i, '$0', ], 189 | [ /pox$/i, '$0', ], 190 | [ /sheep$/i, '$0', ], 191 | ] 192 | 193 | /** 194 | * Pass in a word token to produce a function that can replicate the case on 195 | * another word. 196 | * 197 | * @param {string} word 198 | * @param {string} token 199 | * @return {Function} 200 | */ 201 | function restoreCase (word, token) { 202 | // Tokens are an exact match. 203 | if (word === token) return token 204 | 205 | // Upper cased words. E.g. "HELLO". 206 | if (word === word.toUpperCase()) return token.toUpperCase() 207 | 208 | // Title cased words. E.g. "Title". 209 | if (word[0] === word[0].toUpperCase()) { 210 | return token.charAt(0).toUpperCase() + token.substr(1).toLowerCase() 211 | } 212 | 213 | // Lower cased words. E.g. "test". 214 | return token.toLowerCase() 215 | } 216 | 217 | /** 218 | * Interpolate a regexp string. 219 | * 220 | * @param {string} str 221 | * @param {Array} args 222 | * @return {string} 223 | */ 224 | function interpolate (str, args) { 225 | return str.replace(/\$(\d{1,2})/g, function (match, index) { 226 | return args[index] || '' 227 | }) 228 | } 229 | 230 | /** 231 | * Replace a word using a rule. 232 | * 233 | * @param {string} word 234 | * @param {Array} rule 235 | * @return {string} 236 | */ 237 | function replace (word, rule) { 238 | return word.replace(rule[0], function (match, index) { 239 | var result = interpolate(rule[1], arguments) 240 | 241 | if (match === '') { 242 | return restoreCase(word[index - 1], result) 243 | } 244 | 245 | return restoreCase(match, result) 246 | }) 247 | } 248 | 249 | /** 250 | * Sanitize a word by passing in the word and sanitization rules. 251 | * 252 | * @param {string} token 253 | * @param {string} word 254 | * @param {Array} rules 255 | * @return {string} 256 | */ 257 | function sanitizeWord (token, word, rules, uncountables) { 258 | let sanitized = word 259 | // Empty string or doesn't need fixing. 260 | if (!token.length || uncountables.hasOwnProperty(token)) { 261 | return sanitized 262 | } 263 | 264 | for (const rule of [ ...rules, ].reverse()) { 265 | if (rule[0].test(word)) { 266 | sanitized = replace(word, rule) 267 | break 268 | } 269 | } 270 | 271 | return sanitized 272 | } 273 | 274 | export const pluralize = word => { 275 | // Get the correct token and case restoration functions. 276 | var token = word.toLowerCase() 277 | // Check against the keep object map. 278 | if (irregularPlurals.hasOwnProperty(token)) { 279 | return restoreCase(word, irregularPlurals[token]) 280 | } 281 | 282 | // Run all the rules against the word. 283 | return sanitizeWord(token, word, pluralRules, uncountables) 284 | } 285 | -------------------------------------------------------------------------------- /test/GraphQLNormalizr.test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava') 2 | const { visit, print, } = require('graphql') 3 | const gql = require('graphql-tag') 4 | 5 | const { GraphQLNormalizr, } = require('../') 6 | const { 7 | allUsersConnections, 8 | customIdKey, 9 | listAndObject, 10 | listAndObjectConnections, 11 | listAndObjectConnectionsWithNullNodes, 12 | listAndObjectConnectionsWithNullNodesNormalized, 13 | mergeTestData, 14 | nested, 15 | noNested, 16 | noTypeNames, 17 | withScalarArrays, 18 | withScalarArraysConnections, 19 | withMultipleTypesConnections, 20 | typeWithSameTypeFieldsConnections, 21 | useConnectionsGraphqlQuery, 22 | typeWithNoIdentifier, 23 | typeWithNoIdentifierNormalized, 24 | withEmptyArrays, 25 | withEmptyArraysNormalized, 26 | emptyListAndObject, 27 | emptyListAndObjectNormalized, 28 | jsonContent, 29 | jsonContentNormalized, 30 | withJSONContentAndGraphQLConnections, 31 | withJSONContentAndGraphQLConnectionsNormalized, 32 | nestedAndJSONContent, 33 | nestedAndJSONContentNormalized, 34 | } = require('./mocks/data') 35 | 36 | test('GraphQLNormalizr returns an object with `normalize`, `parse` and `addRequiredFields` methdos', t => { 37 | const { normalize, parse, addRequiredFields, } = new GraphQLNormalizr() 38 | 39 | t.not(undefined, normalize) 40 | t.not(undefined, parse) 41 | t.not(undefined, addRequiredFields) 42 | }) 43 | 44 | test('`normalize` throws if a node has no `__typename` field', t => { 45 | const { normalize, } = new GraphQLNormalizr() 46 | t.throws(() => normalize({ data: noTypeNames, })) 47 | }) 48 | 49 | test('`normalize` throws if `useConnections` is false and has `pageInfo` field', t => { 50 | const { normalize, } = new GraphQLNormalizr() 51 | t.throws(() => normalize({ data: allUsersConnections, })) 52 | }) 53 | 54 | test('snapshot :: `normalize` correctly handles NULL nodes', t => { 55 | const { normalize, } = new GraphQLNormalizr({ 56 | useConnections: true, 57 | }) 58 | t.snapshot(normalize({ data: listAndObjectConnectionsWithNullNodes, })) 59 | }) 60 | 61 | test('snapshot :: `normalize` simple, not nested data', t => { 62 | const { normalize, } = new GraphQLNormalizr() 63 | t.snapshot(normalize({ data: noNested, })) 64 | }) 65 | 66 | test('snapshot :: `normalize` nested data', t => { 67 | const { normalize, } = new GraphQLNormalizr() 68 | t.snapshot(normalize({ data: nested, })) 69 | }) 70 | 71 | test('snapshot :: `normalize` data containing lists and objects', t => { 72 | const { normalize, } = new GraphQLNormalizr() 73 | t.snapshot(normalize({ data: listAndObject, })) 74 | }) 75 | 76 | test('snapshot :: `normalize` merge data on the same doc', t => { 77 | const { normalize, } = new GraphQLNormalizr() 78 | t.snapshot(normalize({ data: mergeTestData, })) 79 | }) 80 | 81 | test('snapshot :: `normalize` with custom "id" key', t => { 82 | const { normalize, } = new GraphQLNormalizr({ idKey: '_id', }) 83 | t.snapshot(normalize({ data: customIdKey, })) 84 | }) 85 | 86 | test('snapshot :: `normalize` with custom entity names', t => { 87 | const { normalize, } = new GraphQLNormalizr({ 88 | typeMap: { User: 'accounts', BlogPost: 'stories', Comment: 'messages', }, 89 | }) 90 | t.snapshot(normalize({ data: listAndObject, })) 91 | }) 92 | 93 | test('snapshot :: `normalize` with lists', t => { 94 | const { normalize, } = new GraphQLNormalizr({ 95 | lists: true, 96 | }) 97 | t.snapshot(normalize({ data: listAndObject, })) 98 | }) 99 | 100 | test('snapshot :: `normalize` with typenames', t => { 101 | const { normalize, } = new GraphQLNormalizr({ 102 | typenames: true, 103 | }) 104 | t.snapshot(normalize({ data: listAndObject, })) 105 | }) 106 | 107 | test('snapshot :: `normalize` with scalar arrays', t => { 108 | const { normalize, } = new GraphQLNormalizr({ 109 | typenames: true, 110 | }) 111 | t.snapshot(normalize({ data: withScalarArrays, })) 112 | }) 113 | 114 | test('snapshot :: `normalize` with graphql connections', t => { 115 | const { normalize, } = new GraphQLNormalizr({ 116 | useConnections: true, 117 | }) 118 | t.snapshot(normalize({ data: allUsersConnections, })) 119 | }) 120 | 121 | test('snapshot :: `normalize` with graphql connections and type with same type fields', t => { 122 | const { normalize, } = new GraphQLNormalizr({ 123 | useConnections: true, 124 | }) 125 | t.snapshot(normalize({ data: typeWithSameTypeFieldsConnections, })) 126 | }) 127 | 128 | test('snapshot :: `normalize` with graphql connections and scalar arrays', t => { 129 | const { normalize, } = new GraphQLNormalizr({ 130 | useConnections: true, 131 | }) 132 | t.snapshot(normalize({ data: withScalarArraysConnections, })) 133 | }) 134 | 135 | test('snapshot :: `normalize` with graphql connections and custom entity names', t => { 136 | const { normalize, } = new GraphQLNormalizr({ 137 | useConnections: true, 138 | typeMap: { User: 'accounts', BlogPost: 'stories', Comment: 'messages', }, 139 | }) 140 | t.snapshot(normalize({ data: listAndObjectConnections, })) 141 | }) 142 | 143 | test('snapshot :: `normalize` without graphql connections but `useConnections` flag set to true', t => { 144 | const { normalize, } = new GraphQLNormalizr({ 145 | useConnections: true, 146 | }) 147 | t.snapshot(normalize({ data: listAndObject, })) 148 | }) 149 | 150 | test('snapshot :: `normalize` without graphql connections but `useConnections` flag set to true and scalar array', t => { 151 | const { normalize, } = new GraphQLNormalizr({ 152 | useConnections: true, 153 | }) 154 | t.snapshot(normalize({ data: withScalarArrays, })) 155 | }) 156 | 157 | test('snapshot :: `normalize` without graphql connections but `useConnections` flag set to true and custom entity names', t => { 158 | const { normalize, } = new GraphQLNormalizr({ 159 | useConnections: true, 160 | typeMap: { User: 'accounts', BlogPost: 'stories', Comment: 'messages', }, 161 | }) 162 | t.snapshot(normalize({ data: listAndObject, })) 163 | }) 164 | 165 | test('snapshot :: `normalize` with { typePointers: true }', t => { 166 | const { normalize, } = new GraphQLNormalizr({ 167 | typePointers: true, 168 | }) 169 | t.snapshot(normalize({ data: listAndObject, })) 170 | }) 171 | 172 | test('snapshot :: `normalize` with { useConnections: true, typePointers: true }', t => { 173 | const { normalize, } = new GraphQLNormalizr({ 174 | useConnections: true, 175 | typePointers: true, 176 | idKey: '_id', 177 | }) 178 | t.snapshot(normalize({ data: withMultipleTypesConnections, })) 179 | }) 180 | 181 | test('snapshot :: `normalize` without graphql connections but `useConnections` and `typePointers` flags set to true and custom entity names', t => { 182 | const { normalize, } = new GraphQLNormalizr({ 183 | useConnections: true, 184 | typePointers: true, 185 | typeMap: { User: 'accounts', BlogPost: 'stories', Comment: 'messages', }, 186 | }) 187 | t.snapshot(normalize({ data: listAndObject, })) 188 | }) 189 | 190 | test('snapshot :: `normalize` without graphql connections but `useConnections` and `typePointers` flags set to true and scalar array', t => { 191 | const { normalize, } = new GraphQLNormalizr({ 192 | useConnections: true, 193 | typePointers: true, 194 | }) 195 | t.snapshot(normalize({ data: withScalarArrays, })) 196 | }) 197 | 198 | test('snapshot :: `normalize` with `{ plural: false }`', t => { 199 | const { normalize, } = new GraphQLNormalizr({ 200 | plural: false, 201 | }) 202 | t.snapshot(normalize({ data: listAndObject, })) 203 | }) 204 | 205 | test('snapshot :: `normalize` with `{ casing: "lower" }`', t => { 206 | const { normalize, } = new GraphQLNormalizr({ 207 | casing: 'lower', 208 | }) 209 | t.snapshot(normalize({ data: listAndObject, })) 210 | }) 211 | 212 | test('snapshot :: `normalize` with `{ casing: "upper" }`', t => { 213 | const { normalize, } = new GraphQLNormalizr({ 214 | casing: 'upper', 215 | }) 216 | t.snapshot(normalize({ data: listAndObject, })) 217 | }) 218 | 219 | test('snapshot :: `normalize` with `{ casing: "camel" }`', t => { 220 | const { normalize, } = new GraphQLNormalizr({ 221 | casing: 'camel', 222 | }) 223 | t.snapshot(normalize({ data: listAndObject, })) 224 | }) 225 | 226 | test('snapshot :: `normalize` with `{ casing: "pascal" }`', t => { 227 | const { normalize, } = new GraphQLNormalizr({ 228 | casing: 'pascal', 229 | }) 230 | t.snapshot(normalize({ data: listAndObject, })) 231 | }) 232 | 233 | test('snapshot :: `normalize` with `{ casing: "kebab" }`', t => { 234 | const { normalize, } = new GraphQLNormalizr({ 235 | casing: 'kebab', 236 | }) 237 | t.snapshot(normalize({ data: listAndObject, })) 238 | }) 239 | 240 | test('snapshot :: `normalize` with `{ casing: "snake" }`', t => { 241 | const { normalize, } = new GraphQLNormalizr({ 242 | casing: 'snake', 243 | }) 244 | t.snapshot(normalize({ data: listAndObject, })) 245 | }) 246 | 247 | test('snapshot :: `parse` with { useConnections: false }', t => { 248 | const { parse, } = new GraphQLNormalizr({ 249 | useConnections: false, 250 | }) 251 | t.snapshot(print(parse(useConnectionsGraphqlQuery))) 252 | }) 253 | 254 | test('snapshot :: `parse` with { useConnections: true }', t => { 255 | const { parse, } = new GraphQLNormalizr({ 256 | useConnections: true, 257 | }) 258 | t.snapshot(print(parse(useConnectionsGraphqlQuery))) 259 | }) 260 | 261 | test('`parse` adds the required ["id", "__typename"] fields', t => { 262 | const query = `{ 263 | allUsers { 264 | email 265 | } 266 | allPosts { 267 | author 268 | comments { 269 | message 270 | author 271 | } 272 | } 273 | } 274 | ` 275 | 276 | let documentAST = gql(query) 277 | 278 | let bool = false 279 | visit(documentAST, { 280 | SelectionSet (node, parent) { 281 | bool = node.selections.some(s => [ 'id', '__typename', ].includes(s.name.value)) 282 | }, 283 | }) 284 | t.false(bool) 285 | 286 | bool = false 287 | const { parse, } = new GraphQLNormalizr() 288 | documentAST = parse(query) 289 | visit(documentAST, { 290 | SelectionSet (node, parent) { 291 | bool = node.selections.some(s => [ 'id', '__typename', ].includes(s.name.value)) 292 | }, 293 | }) 294 | t.true(bool) 295 | }) 296 | 297 | test('`normalize` with cache', t => { 298 | let normalize 299 | 300 | normalize = new GraphQLNormalizr().normalize 301 | const data = normalize({ data: listAndObject, }) 302 | const newData = normalize({ data: listAndObject, }) 303 | t.not(data, newData) 304 | 305 | normalize = new GraphQLNormalizr({ caching: true, }).normalize 306 | const _data = normalize({ data: listAndObject, }) 307 | const _newData = normalize({ data: listAndObject, }) 308 | t.is(_data, _newData) 309 | }) 310 | 311 | test('`normalize` list', t => { 312 | const { normalize, } = new GraphQLNormalizr() 313 | 314 | const normalized = normalize({ data: typeWithNoIdentifier, }) 315 | t.deepEqual(normalized, typeWithNoIdentifierNormalized) 316 | }) 317 | 318 | test('`normalize` list and object connections with null nodes', t => { 319 | const { normalize, } = new GraphQLNormalizr({ useConnections: true, }) 320 | 321 | const normalized = normalize({ data: listAndObjectConnectionsWithNullNodes, }) 322 | t.deepEqual(normalized, listAndObjectConnectionsWithNullNodesNormalized) 323 | }) 324 | 325 | test('`normalize` with empty arrays', t => { 326 | const { normalize, } = new GraphQLNormalizr() 327 | 328 | const normalized = normalize({ data: withEmptyArrays, }) 329 | t.deepEqual(normalized, withEmptyArraysNormalized) 330 | }) 331 | 332 | test('`normalize` with empty list', t => { 333 | const { normalize, } = new GraphQLNormalizr() 334 | 335 | const normalized = normalize({ data: emptyListAndObject, }) 336 | t.deepEqual(normalized, emptyListAndObjectNormalized) 337 | }) 338 | 339 | test('`normalize` with ignored JSON content', t => { 340 | const { normalize, } = new GraphQLNormalizr({ 341 | ignore: { users: [ 'preferences', ], }, 342 | }) 343 | 344 | const normalized = normalize({ data: jsonContent, }) 345 | t.deepEqual(normalized, jsonContentNormalized) 346 | }) 347 | 348 | test('`normalize` with connections and ignored JSON content', t => { 349 | const { normalize, } = new GraphQLNormalizr({ 350 | useConnections: true, 351 | typePointers: true, 352 | ignore: { movies: [ 'preferences', ], shows: [ 'preferences', ], }, 353 | }) 354 | 355 | const normalized = normalize({ data: withJSONContentAndGraphQLConnections, }) 356 | t.deepEqual(normalized, withJSONContentAndGraphQLConnectionsNormalized) 357 | }) 358 | 359 | test('`normalize` with nested data and ignored JSON content', t => { 360 | const { normalize, } = new GraphQLNormalizr({ 361 | ignore: { blogPosts: [ 'preferences', ], }, 362 | }) 363 | 364 | const normalized = normalize({ data: nestedAndJSONContent, }) 365 | t.deepEqual(normalized, nestedAndJSONContentNormalized) 366 | }) 367 | 368 | test('`normalize` with ignored content and connections, regardless of field order', t => { 369 | const notificationsConfig = { 370 | close_open: { 371 | email: true, 372 | }, 373 | } 374 | const groupLeaderOf = { 375 | edges: [ 376 | { 377 | node: { 378 | id: '09fececa-8de3-4028-9802-42d069f1ff40', 379 | __typename: 'group', 380 | }, 381 | }, 382 | ], 383 | } 384 | 385 | const expected = { 386 | user: { 387 | '7fd6833d-aec8-4045-8097-2567db654710': { 388 | id: '7fd6833d-aec8-4045-8097-2567db654710', 389 | groupLeaderOf: [ '09fececa-8de3-4028-9802-42d069f1ff40', ], 390 | notificationsConfig: { close_open: { email: true, }, }, }, 391 | }, 392 | group: { 393 | '09fececa-8de3-4028-9802-42d069f1ff40': { 394 | id: '09fececa-8de3-4028-9802-42d069f1ff40', 395 | }, 396 | }, 397 | } 398 | 399 | const { normalize, } = new GraphQLNormalizr({ 400 | useConnections: true, 401 | plural: false, 402 | ignore: { user: [ 'notificationsConfig', ], }, 403 | }) 404 | 405 | t.deepEqual(normalize({ 406 | data: { 407 | user: { 408 | __typename: 'user', 409 | id: '7fd6833d-aec8-4045-8097-2567db654710', 410 | notificationsConfig, 411 | groupLeaderOf, 412 | }, 413 | }, 414 | }), expected) 415 | 416 | t.deepEqual(normalize({ 417 | data: { 418 | user: { 419 | __typename: 'user', 420 | id: '7fd6833d-aec8-4045-8097-2567db654710', 421 | groupLeaderOf, 422 | notificationsConfig, 423 | }, 424 | }, 425 | }), expected) 426 | }) 427 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **graphql-normalizr** 2 | 3 | ![publish](https://github.com/monojack/graphql-normalizr/workflows/Publish/badge.svg) 4 | [![npm version](https://img.shields.io/npm/v/graphql-normalizr.svg)](https://www.npmjs.com/package/graphql-normalizr) 5 | [![npm downloads](https://img.shields.io/npm/dm/graphql-normalizr.svg)](https://www.npmjs.com/package/graphql-normalizr) 6 | [![minified size](https://badgen.net/bundlephobia/min/graphql-normalizr)](https://bundlephobia.com/result?p=graphql-normalizr@latest) 7 | 8 | Normalize GraphQL responses for persisting in the client cache/state. 9 | 10 | > Not related, in any way, to [normalizr](https://github.com/paularmstrong/normalizr), just shamelessly piggybacking on its popularity. Also, "normaliz**E**r" is taken... 11 | 12 | **TL;DR**: Transforms: 13 | 14 | ```json 15 | { 16 | "data": { 17 | "findUser": [ 18 | { 19 | "__typename": "User", 20 | "id": "5a6efb94b0e8c36f99fba013", 21 | "email": "Lloyd.Nikolaus@yahoo.com", 22 | "posts": [ 23 | { 24 | "__typename": "BlogPost", 25 | "id": "5a6efb94b0e8c36f99fba016", 26 | "title": "Dolorem voluptatem molestiae", 27 | "comments": [ 28 | { 29 | "__typename": "Comment", 30 | "id": "5a6efb94b0e8c36f99fba019", 31 | "message": "Alias quod est voluptatibus aut quis sunt aut numquam." 32 | }, 33 | { 34 | "__typename": "Comment", 35 | "id": "5a6efb94b0e8c36f99fba01b", 36 | "message": "Harum quia asperiores nemo." 37 | }, 38 | { 39 | "__typename": "Comment", 40 | "id": "5a6efb94b0e8c36f99fba01c", 41 | "message": "Vel veniam consectetur laborum." 42 | }, 43 | { 44 | "__typename": "Comment", 45 | "id": "5a6efb94b0e8c36f99fba01e", 46 | "message": "Possimus beatae vero recusandae beatae quas ut commodi laboriosam." 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | } 55 | ``` 56 | 57 | into: 58 | 59 | ```json 60 | { 61 | "comments": { 62 | "5a6efb94b0e8c36f99fba019": { 63 | "id": "5a6efb94b0e8c36f99fba019", 64 | "message": "Alias quod est voluptatibus aut quis sunt aut numquam." 65 | }, 66 | "5a6efb94b0e8c36f99fba01b": { 67 | "id": "5a6efb94b0e8c36f99fba01b", 68 | "message": "Harum quia asperiores nemo." 69 | }, 70 | "5a6efb94b0e8c36f99fba01c": { 71 | "id": "5a6efb94b0e8c36f99fba01c", 72 | "message": "Vel veniam consectetur laborum." 73 | }, 74 | "5a6efb94b0e8c36f99fba01e": { 75 | "id": "5a6efb94b0e8c36f99fba01e", 76 | "message": "Possimus beatae vero recusandae beatae quas ut commodi laboriosam." 77 | } 78 | }, 79 | "blogPosts": { 80 | "5a6efb94b0e8c36f99fba016": { 81 | "id": "5a6efb94b0e8c36f99fba016", 82 | "title": "Dolorem voluptatem molestiae", 83 | "comments": [ 84 | "5a6efb94b0e8c36f99fba019", 85 | "5a6efb94b0e8c36f99fba01b", 86 | "5a6efb94b0e8c36f99fba01c", 87 | "5a6efb94b0e8c36f99fba01e" 88 | ] 89 | } 90 | }, 91 | "users": { 92 | "5a6efb94b0e8c36f99fba013": { 93 | "id": "5a6efb94b0e8c36f99fba013", 94 | "email": "Lloyd.Nikolaus@yahoo.com", 95 | "posts": ["5a6efb94b0e8c36f99fba016"] 96 | } 97 | } 98 | } 99 | ``` 100 | 101 | ## Motivation 102 | 103 | We all love **GraphQL** and we want to use it. There are tons of libraries and clients out there that help us do that with ease, but there is still one problem... How do you persist that data? 104 | 105 | Yes, everything is all great when the response mirrors the exact structure we asked for, but we don't want to cache it that way, do we? We probably want a normalized version of that data which we can persist to our store and read/modify it efficiently. Flux or Redux stores work best with normalized data and there are also GraphQL clients you can use to execute queries on the local cache/state ([blips](https://github.com/monojack/blips) or [apollo-link-state](https://github.com/apollographql/apollo-link-state)), in which case, we definitely need to persist normalized data. 106 | 107 | **GraphQLNormalizr** is simple, fast, light-weight and it provides all the tools needed to do just that, the only requirement is that you include the `id` and `__typename` fields for all the nodes _(but it can do that for you if you're too lazy or you want to keep your sources thin)_. 108 | 109 | ## Table of contents 110 | 111 | - [Installation](#installation) 112 | - [API by example](#api-by-example) 113 | - [`GraphQLNormalizr`](#graphqlnormalizr) 114 | - [`parse`](#parse) 115 | - [`addRequiredFields`](#addrequiredfields) 116 | - [`normalize`](#normalize) 117 | - [Migrating from 1.x to 2.x](#migrating) 118 | 119 | ## Installation 120 | 121 | ```sh 122 | npm install graphql-normalizr 123 | ``` 124 | 125 | ## API by example 126 | 127 | The **GraphQLNormalizr** constructor function returns an object containing 3 methods: 128 | 129 | 1. [parse](#parse) 130 | 2. [addRequiredFields](#addrequiredfields) 131 | 3. [normalize](#normalize) 132 | 133 | Depending on how you write your queries, you may or may not use `parse` or `addRequiredFields`, but `normalize` is the method that you will transform the GraphQL response. As you've probably seen from the **TL;DR**, all response nodes must contain the `__typename` and `id` fields. `__typename` is a [GraphQL meta field](http://graphql.org/learn/queries/#meta-fields) and the `id` key may be customized when creating the GraphQLNormalizr client. 134 | 135 | If your queries already ask for `id` and `__typename` there's no need to use **parse** or **addRequiredFields**. Otherwise, **parse** will take care of transforming your `GraphQL source` into a `Document` and add the `__typename` and `id` fields where needed. In case you already use a different parser, or only have access to the `Document` you may use **addRequiredFields** on the `Document` to add the `__typename` and `id` fields 136 | 137 | ### GraphQLNormalizr 138 | 139 | ```js 140 | import { GraphQLNormalizr } from 'graphql-normalizr' 141 | 142 | // const config = ... 143 | const normalizer = new GraphQLNormalizr(config) 144 | ``` 145 | 146 | **config**: optional - the configuration object containing information for instantiating the client. it takes the following props: 147 | 148 | - [idKey](#idkey) 149 | - [useConnections](#useconnections) 150 | - [typeMap](#typemap) 151 | - [ignore](#ignore) 152 | - [lists](#lists) 153 | - [typenames](#typenames) 154 | - [typePointers](#typepointers) 155 | - [caching](#caching) 156 | - [plural](#plural) 157 | - [casing](#casing) 158 | 159 | ##### idKey 160 | 161 | > String 162 | 163 | Default is **"id"**. Configures a custom `id` key for the client. Use this if your resource identifiers are found under a different key name _('\_id', 'key', 'uid' etc)_. 164 | 165 | Consider the following GraphQL response: 166 | 167 | ```js 168 | const response = { 169 | data: { 170 | findUser: { 171 | __typename: 'User', 172 | uid: '5a6efb94b0e8c36f99fba013', 173 | email: 'Lloyd.Nikolaus@yahoo.com', 174 | }, 175 | }, 176 | } 177 | ``` 178 | 179 | Normalize the data with our custom `id` key: 180 | 181 | ```js 182 | // using destructuring to get the `normalize` method of the client 183 | const { normalize } = new GraphQLNormalizr({ idKey: 'uid' }) 184 | normalize(response) 185 | // => 186 | // { 187 | // users: { 188 | // '5a6efb94b0e8c36f99fba013' : { 189 | // uid: '5a6efb94b0e8c36f99fba013', 190 | // email: 'Lloyd.Nikolaus@yahoo.com' 191 | // } 192 | // } 193 | // } 194 | ``` 195 | 196 | ##### useConnections 197 | 198 | > Boolean 199 | 200 | Default is `false`. If you are using GraphQL connections with `edges` and `nodes`, set this flag to **`true`** otherwise you'll get a warning and the normalization won't work. 201 | 202 | **NOTE**: _The connections implementation needs to be according to the [specification](https://facebook.github.io/relay/graphql/connections.htm)_ 203 | 204 | ```js 205 | const response = { 206 | data: { 207 | findUser: { 208 | __typename: 'User', 209 | id: '5a6efb94b0e8c36f99fba013', 210 | email: 'Lloyd.Nikolaus@yahoo.com', 211 | friends: { 212 | __typename: 'FriendsConnection', 213 | totalCount: 3, 214 | edges: [ 215 | { 216 | node: { 217 | __typename: 'User', 218 | id: '5a6cf127c2b20834f6551481', 219 | email: 'Madisen_Braun@hotmail.com', 220 | }, 221 | cursor: 'Y3Vyc29yMg==', 222 | }, 223 | { 224 | node: { 225 | __typename: 'User', 226 | id: '5a6cf127c2b20834f6551482', 227 | email: 'Robel.Ansel@yahoo.com', 228 | }, 229 | cursor: 'Y3Vyc29yMw==', 230 | }, 231 | ], 232 | pageInfo: { 233 | endCursor: 'Y3Vyc29yMw==', 234 | hasNextPage: false, 235 | }, 236 | }, 237 | }, 238 | }, 239 | } 240 | 241 | const { normalize } = new GraphQLNormalizr({ 242 | useConnections: true, 243 | }) 244 | 245 | normalize(response) 246 | // => 247 | // { 248 | // users: { 249 | // '5a6efb94b0e8c36f99fba013': { 250 | // id: '5a6efb94b0e8c36f99fba013', 251 | // email: 'Lloyd.Nikolaus@yahoo.com', 252 | // friends: ['5a6cf127c2b20834f6551481', '5a6cf127c2b20834f6551482'], 253 | // }, 254 | // '5a6cf127c2b20834f6551481': { 255 | // id: '5a6cf127c2b20834f6551481', 256 | // email: 'Madisen_Braun@hotmail.com', 257 | // }, 258 | // '5a6cf127c2b20834f6551482': { 259 | // id: '5a6cf127c2b20834f6551482', 260 | // email: 'Robel.Ansel@yahoo.com', 261 | // }, 262 | // }, 263 | // } 264 | ``` 265 | 266 | ##### typeMap 267 | 268 | > Object 269 | 270 | By default **the entity name will be the plural form of the type name, converted to camel case**, _(`PrimaryAddress` type will be stored under the `primaryAddresses` key)_. Use this option to provide specific **entity** names for some/all **Types**, or try the [plural](#plural) and [casing](#casing) options to derive the entity names. 271 | 272 | ```js 273 | const response = { 274 | data: { 275 | findUser: { 276 | __typename: 'User', 277 | id: '5a6efb94b0e8c36f99fba013', 278 | email: 'Lloyd.Nikolaus@yahoo.com', 279 | }, 280 | }, 281 | } 282 | 283 | const { normalize } = new GraphQLNormalizr({ 284 | typeMap: { User: 'accounts' }, 285 | }) 286 | normalize(response) 287 | // => 288 | // { 289 | // accounts: { 290 | // '5a6efb94b0e8c36f99fba013' : { 291 | // id: '5a6efb94b0e8c36f99fba013', 292 | // email: 'Lloyd.Nikolaus@yahoo.com' 293 | // } 294 | // } 295 | // } 296 | ``` 297 | 298 | ##### ignore 299 | 300 | > Object 301 | 302 | Prevent normalization of specified fields 303 | 304 | ```js 305 | const response = { 306 | data: { 307 | allUsers: [ 308 | { 309 | __typename: 'User', 310 | id: '5a6efb94b0e8c36f99fba013', 311 | email: 'Lloyd.Nikolaus@yahoo.com', 312 | preferences: null 313 | posts: [ 314 | { 315 | __typename: 'BlogPost', 316 | id: '5a6cf127c2b20834f6551484', 317 | likes: 10, 318 | title: 'Sunt ut aut', 319 | tags: {}, 320 | } 321 | ] 322 | }, 323 | { 324 | __typename: 'User', 325 | id: '5a6efb94b0e8c36f99fba013', 326 | email: 'Anna.Klaus@gmail.com', 327 | preferences: { foo: 'apple', bar: 1, baz: { a: 'b' }, quux: null, } 328 | posts: [ 329 | { 330 | __typename: 'BlogPost', 331 | id: '5a6cf127c2b20834f6551485', 332 | likes: 23, 333 | title: 'Nesciunt esse', 334 | tags: [], 335 | } 336 | ] 337 | }, 338 | ], 339 | }, 340 | } 341 | ``` 342 | 343 | Normalize the data excluding the `preferences` field on `users` and the `tags` field on `blogPosts`: 344 | 345 | ```js 346 | // using destructuring to get the `normalize` method of the client 347 | const { normalize } = new GraphQLNormalizr({ ignore: { users: [ 'preferences' ], blogPosts: [ 'tags' ] } }) 348 | normalize(response) 349 | // => 350 | // { 351 | // users: { 352 | // '5a6efb94b0e8c36f99fba013': {, 353 | // id: '5a6efb94b0e8c36f99fba013', 354 | // email: 'Lloyd.Nikolaus@yahoo.com', 355 | // preferences: null 356 | // }, 357 | // '5a6efb94b0e8c36f99fba013': { 358 | // id: '5a6efb94b0e8c36f99fba013', 359 | // email: 'Anna.Klaus@gmail.com', 360 | // preferences: { foo: 'apple', bar: 1, baz: { a: 'b' }, quux: null, } 361 | // }, 362 | // }, 363 | // blogPosts: { 364 | // '5a6cf127c2b20834f6551484': { 365 | // id: '5a6cf127c2b20834f6551484', 366 | // likes: 10, 367 | // title: 'Sunt ut aut', 368 | // tags: {}, 369 | // }, 370 | // '5a6cf127c2b20834f6551485': { 371 | // id: '5a6cf127c2b20834f6551485', 372 | // likes: 23, 373 | // title: 'Nesciunt esse', 374 | // tags: [], 375 | // }, 376 | // } 377 | // } 378 | ``` 379 | 380 | ##### plural 381 | 382 | > Boolean 383 | 384 | Default is `true`. Set this to `false` if you don't want to [pluralize](https://github.com/blakeembrey/pluralize) entity names. Considering the previous response example: 385 | 386 | ```js 387 | const { normalize } = new GraphQLNormalizr({ 388 | plural: false, 389 | }) 390 | normalize(response) 391 | // => 392 | // { 393 | // user: { 394 | // '5a6efb94b0e8c36f99fba013' : { 395 | // id: '5a6efb94b0e8c36f99fba013', 396 | // email: 'Lloyd.Nikolaus@yahoo.com' 397 | // } 398 | // } 399 | // } 400 | ``` 401 | 402 | ##### casing 403 | 404 | > 'lower'|'upper'|'camel'|'pascal'|'snake'|'kebab' 405 | 406 | You can also specify the preferred casing for entity names. Again, consider the above response example. 407 | 408 | ```js 409 | // casing: 'lower' 410 | // User => user 411 | 412 | // casing: 'upper' 413 | // User => USER 414 | 415 | // casing: 'camel' 416 | // PrimaryAddress => primaryAddress 417 | 418 | // casing: 'pascal' 419 | // PrimaryAddress => PrimaryAddress 420 | 421 | // casing: 'snake' 422 | // PrimaryAddress => primary_address 423 | 424 | // casing: 'kebab' 425 | // PrimaryAddress => primary-address 426 | ``` 427 | 428 | Combine `plural` and `casing` options to get the desired entity names 429 | 430 | ##### lists 431 | 432 | > Boolean 433 | 434 | Default is `false`. All the data is stored in key/value pairs, for easy access. If you want to use arrays, for whatever reason, set this to `true` 435 | 436 | For the same response object in our previous example: 437 | 438 | ```js 439 | const { normalize } = new GraphQLNormalizr({ 440 | lists: true, 441 | }) 442 | normalize(response) 443 | // => 444 | // { 445 | // users: [ 446 | // { 447 | // id: '5a6efb94b0e8c36f99fba013', 448 | // email: 'Lloyd.Nikolaus@yahoo.com' 449 | // } 450 | // ] 451 | // } 452 | ``` 453 | 454 | ##### typenames 455 | 456 | > Boolean 457 | 458 | Default is `false`. The normalized data will not contain the `__typename` field. Set this to `true` if you need to persist them. 459 | 460 | ```js 461 | const { normalize } = new GraphQLNormalizr({ 462 | typenames: true, 463 | }) 464 | 465 | normalize(response) 466 | // => 467 | // { 468 | // users: { 469 | // '5a6efb94b0e8c36f99fba013' : { 470 | // __typename: 'User', 471 | // id: '5a6efb94b0e8c36f99fba013', 472 | // email: 'Lloyd.Nikolaus@yahoo.com' 473 | // } 474 | // } 475 | // } 476 | ``` 477 | 478 | ##### typePointers 479 | 480 | > Boolean 481 | 482 | Default is `false`. Enables explicit type pointers - instead of an array of only identifiers and having to figure out which collection they point to, it will return objects containing the identifier as well as the collection name. Works especially well with Union types and Interfaces. 483 | 484 | ```js 485 | 486 | const { normalize } = new GraphQLNormalizr({ 487 | typePointers: true, 488 | }) 489 | 490 | // ['5a6cf127c2b20834f655148a', '5a6cf127c2b20834f655148b', '5a6cf127c2b20834f655148c'] 491 | users: [ 492 | { 493 | _id: '5a6cf127c2b20834f655148a', // '_id' or the specified key 494 | collection: 'members', // type Member 495 | }, 496 | { 497 | _id: '5a6cf127c2b20834f655148b', // '_id' or the specified key 498 | collection: 'authors', // type Author 499 | }, 500 | { 501 | _id: '5a6cf127c2b20834f655148c', // '_id' or the specified key 502 | collection: 'members', // type Member 503 | }, 504 | ], 505 | ``` 506 | 507 | ##### caching 508 | 509 | > Boolean 510 | 511 | Default is `false`. The **normalize** method is pretty fast by itself, it does a single iteration and associates the values only for each response node and not for all the fields. Enable this if you think you'd be normalizing the same response multiple times, like when you're polling for data and it may not have changed. 512 | 513 | ```js 514 | const { normalize } = new GraphQLNormalizr({ 515 | caching: true, 516 | }) 517 | 518 | const normalized = normalize(response) 519 | const cached = normalize(response) 520 | 521 | cached === normalized // => true 522 | ``` 523 | 524 | ### `parse` 525 | 526 | Turns a **GraphQL source** into a **Document** and adds the required fields where necessary. 527 | 528 | ```js 529 | // ... 530 | import { GraphQLNormalizr } from 'graphql-normalizr' 531 | 532 | const source = `{ 533 | allUsers { 534 | email 535 | posts { 536 | title 537 | comments { 538 | message 539 | } 540 | } 541 | } 542 | }` 543 | 544 | const { parse } = new GraphQLNormalizr() 545 | 546 | const query = parse(source) // will add `id` and `__typename` fields to all the nodes 547 | 548 | // We can use the print method from `graphql` to see/use the updated source 549 | const { print } = require('graphql') 550 | print(query) 551 | // => 552 | // `{ 553 | // allUsers { 554 | // __typename 555 | // id 556 | // email 557 | // posts { 558 | // __typename 559 | // id 560 | // comments { 561 | // __typename 562 | // id 563 | // message 564 | // } 565 | // } 566 | // } 567 | // }` 568 | 569 | // ... 570 | ``` 571 | 572 | ### `addRequiredFields` 573 | 574 | If you only have access to the **Document**, you can use the **print** method from `graphql` to get the **source** and parse it. But that may be expensive and you shouldn't have to print a document just to parse it again. `addRequiredFields` will add the `id` and `__typename` fields to that document, without the need of extracting its source. 575 | 576 | ```js 577 | // ... 578 | import { GraphQLNormalizr } from 'graphql-normalizr' 579 | import { allUsersQuery } from './queries' 580 | 581 | const { addRequiredFields } = new GraphQLNormalizr() 582 | 583 | const query = addRequiredFields(allUsersQuery) 584 | 585 | // ... 586 | ``` 587 | 588 | ### `normalize` 589 | 590 | The following is a full example where we use [apollo-fetch](https://github.com/apollographql/apollo-fetch/tree/master/packages/apollo-fetch) to execute a query and then normalize it with **GraphQLNormalizr** 591 | 592 | ```js 593 | const { GraphQLNormalizr } = require('graphql-normalizr') 594 | const { createApolloFetch } = require('apollo-fetch') 595 | 596 | const uri = 'http://localhost:8080/graphql' 597 | const fetch = createApolloFetch({ uri }) 598 | 599 | const source = ` 600 | query { 601 | allUsers { 602 | ...userFields 603 | } 604 | } 605 | fragment userFields on User { 606 | email 607 | posts { 608 | title 609 | comments { 610 | message 611 | } 612 | } 613 | } 614 | ` 615 | 616 | const { normalize, parse } = new GraphQLNormalizr() 617 | const query = parse(source) 618 | 619 | fetch({ query }).then(response => { 620 | const normalized = normalize(response) 621 | // persist the normalized data to our app state. 622 | }).catch(...) 623 | ``` 624 | 625 | ## Migrating 626 | 627 | **from _v1.x_ to _v2.x_** 628 | 629 | There aren't many breaking changes between v1.x and v2.x. In fact, there's only one and it's about how **Type** names get converted into **entity** names. 630 | 631 | With the default configuration, v1.x will transfrom a `PrimaryAddress` type name into `primaryaddresses` entity name. With 2.x, the default configuiration will transform `PrimaryAddress` to `primaryAddresses`. The only difference is that now, it changes to _camelcase_ instead of _lowercase_ 632 | 633 | If you don't want to change your code everywhere you are accessing entities, you can configure the way **GraphQLNormalizr** makes the transformation with the [plural](#plural) and [casing](#casing) options: 634 | 635 | ```js 636 | const { normalize } = new GraphQLNormalizr({ 637 | plural: true, // true is the default value, so you can omit this 638 | casing: 'lower', 639 | }) 640 | 641 | // this above configuration will change `PrimaryAddress` to `primaryaddresses` 642 | ``` 643 | -------------------------------------------------------------------------------- /test/mocks/data.js: -------------------------------------------------------------------------------- 1 | const customIdKey = { 2 | allUsers: [ 3 | { 4 | __typename: 'User', 5 | _id: '5a6cf127c2b20834f6551481', 6 | email: 'Madisen_Braun@hotmail.com', 7 | posts: [ 8 | { 9 | __typename: 'BlogPost', 10 | _id: '5a6cf127c2b20834f6551483', 11 | title: 'Aut aut reiciendis', 12 | }, 13 | { 14 | __typename: 'BlogPost', 15 | _id: '5a6cf127c2b20834f6551485', 16 | title: 'Nesciunt esse', 17 | }, 18 | ], 19 | }, 20 | { 21 | __typename: 'User', 22 | _id: '5a6cf127c2b20834f6551482', 23 | email: 'Robel.Ansel@yahoo.com', 24 | posts: [ 25 | { 26 | __typename: 'BlogPost', 27 | _id: '5a6cf127c2b20834f6551484', 28 | title: 'Sunt ut aut', 29 | }, 30 | { 31 | __typename: 'BlogPost', 32 | _id: '5a6cf127c2b20834f6551486', 33 | title: 'Nihil assumenda', 34 | }, 35 | ], 36 | }, 37 | ], 38 | findComment: { 39 | __typename: 'Comment', 40 | _id: '5a6cf127c2b20834f655148a', 41 | message: 'Voluptates ex sint amet repellendus impedit nam.', 42 | }, 43 | } 44 | 45 | const typeWithSameTypeFieldsConnections = { 46 | findUser: { 47 | __typename: 'User', 48 | id: '5a6efb94b0e8c36f99fba013', 49 | email: 'Lloyd.Nikolaus@yahoo.com', 50 | referredBy: { 51 | __typename: 'User', 52 | id: '5a6cf127c2b20834f6551481', 53 | email: 'Madisen_Braun@hotmail.com', 54 | }, 55 | friends: { 56 | __typename: 'FriendsConnection', 57 | totalCount: 3, 58 | edges: [ 59 | { 60 | node: { 61 | __typename: 'User', 62 | id: '5a6cf127c2b20834f6551481', 63 | email: 'Madisen_Braun@hotmail.com', 64 | }, 65 | cursor: 'Y3Vyc29yMg==', 66 | }, 67 | { 68 | node: { 69 | __typename: 'User', 70 | id: '5a6cf127c2b20834f6551482', 71 | email: 'Robel.Ansel@yahoo.com', 72 | }, 73 | cursor: 'Y3Vyc29yMw==', 74 | }, 75 | ], 76 | pageInfo: { 77 | endCursor: 'Y3Vyc29yMw==', 78 | hasNextPage: false, 79 | }, 80 | }, 81 | }, 82 | } 83 | 84 | const allUsersConnections = { 85 | allUsers: [ 86 | { 87 | __typename: 'User', 88 | id: '5a6cf127c2b20834f6551481', 89 | email: 'Madisen_Braun@hotmail.com', 90 | posts: { 91 | __typename: 'PostsConnection', 92 | pageInfo: { 93 | endCursor: 'Y3Vyc29yMw==', 94 | hasNextPage: false, 95 | }, 96 | edges: [ 97 | { 98 | node: { 99 | __typename: 'BlogPost', 100 | id: '5a6cf127c2b20834f6551483', 101 | title: 'Aut aut reiciendis', 102 | }, 103 | }, 104 | { 105 | node: { 106 | __typename: 'BlogPost', 107 | id: '5a6cf127c2b20834f6551485', 108 | title: 'Nesciunt esse', 109 | }, 110 | }, 111 | ], 112 | }, 113 | }, 114 | { 115 | __typename: 'User', 116 | id: '5a6cf127c2b20834f6551482', 117 | email: 'Robel.Ansel@yahoo.com', 118 | posts: { 119 | __typename: 'PostsConnection', 120 | pageInfo: { 121 | endCursor: 'Y3Vyc29yMw==', 122 | hasNextPage: true, 123 | }, 124 | edges: [ 125 | { 126 | node: { 127 | __typename: 'BlogPost', 128 | id: '5a6cf127c2b20834f6551484', 129 | title: 'Sunt ut aut', 130 | }, 131 | }, 132 | { 133 | node: { 134 | __typename: 'BlogPost', 135 | id: '5a6cf127c2b20834f6551486', 136 | title: 'Nihil assumenda', 137 | }, 138 | }, 139 | ], 140 | }, 141 | }, 142 | ], 143 | } 144 | 145 | const mergeTestData = { 146 | allComments: [ 147 | { 148 | __typename: 'Comment', 149 | id: '5a6cf127c2b20834f655148e', 150 | author: { 151 | __typename: 'User', 152 | id: '5a6cf127c2b20834f6551481', 153 | email: 'Madisen_Braun@hotmail.com', 154 | }, 155 | }, 156 | ], 157 | findComment: { 158 | __typename: 'Comment', 159 | id: '5a6cf127c2b20834f655148e', 160 | message: 'Voluptates aut eum.', 161 | }, 162 | } 163 | 164 | const noTypeNames = { 165 | allUsers: [ 166 | { 167 | id: '5a6cf127c2b20834f6551481', 168 | email: 'Madisen_Braun@hotmail.com', 169 | }, 170 | { 171 | id: '5a6cf127c2b20834f6551482', 172 | email: 'Robel.Ansel@yahoo.com', 173 | }, 174 | ], 175 | } 176 | 177 | const noNested = { 178 | allUsers: [ 179 | { 180 | __typename: 'User', 181 | id: '5a6cf127c2b20834f6551481', 182 | email: 'Madisen_Braun@hotmail.com', 183 | }, 184 | { 185 | __typename: 'User', 186 | id: '5a6cf127c2b20834f6551482', 187 | email: 'Robel.Ansel@yahoo.com', 188 | }, 189 | ], 190 | allBlogPosts: [ 191 | { 192 | __typename: 'BlogPost', 193 | id: '5a6cf127c2b20834f6551483', 194 | likes: 0, 195 | }, 196 | { 197 | __typename: 'BlogPost', 198 | id: '5a6cf127c2b20834f6551484', 199 | likes: 10, 200 | }, 201 | { 202 | __typename: 'BlogPost', 203 | id: '5a6cf127c2b20834f6551485', 204 | likes: 23, 205 | }, 206 | { 207 | __typename: 'BlogPost', 208 | id: '5a6cf127c2b20834f6551486', 209 | likes: 3, 210 | }, 211 | ], 212 | allComments: [ 213 | { 214 | __typename: 'Comment', 215 | id: '5a6cf127c2b20834f655148e', 216 | message: 'Voluptates aut eum.', 217 | }, 218 | { 219 | __typename: 'Comment', 220 | id: '5a6cf127c2b20834f6551487', 221 | message: 'Et soluta ipsam quas facilis possimus et.', 222 | }, 223 | { 224 | __typename: 'Comment', 225 | id: '5a6cf127c2b20834f6551488', 226 | message: 'Tempore sed enim qui aliquam est saepe qui.', 227 | }, 228 | { 229 | __typename: 'Comment', 230 | id: '5a6cf127c2b20834f6551489', 231 | message: 'Ea et est autem dicta necessitatibus vel.', 232 | }, 233 | { 234 | __typename: 'Comment', 235 | id: '5a6cf127c2b20834f655148b', 236 | message: 'Consectetur cum est odit et qui.', 237 | }, 238 | { 239 | __typename: 'Comment', 240 | id: '5a6cf127c2b20834f655148d', 241 | message: 'Aut vel possimus nisi qui.', 242 | }, 243 | { 244 | __typename: 'Comment', 245 | id: '5a6cf127c2b20834f655148a', 246 | message: 'Voluptates ex sint amet repellendus impedit nam.', 247 | }, 248 | { 249 | __typename: 'Comment', 250 | id: '5a6cf127c2b20834f655148c', 251 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 252 | }, 253 | ], 254 | } 255 | 256 | const nested = { 257 | allUsers: [ 258 | { 259 | __typename: 'User', 260 | id: '5a6cf127c2b20834f6551481', 261 | email: 'Madisen_Braun@hotmail.com', 262 | posts: [ 263 | { 264 | __typename: 'BlogPost', 265 | id: '5a6cf127c2b20834f6551483', 266 | title: 'Aut aut reiciendis', 267 | }, 268 | { 269 | __typename: 'BlogPost', 270 | id: '5a6cf127c2b20834f6551485', 271 | title: 'Nesciunt esse', 272 | }, 273 | ], 274 | }, 275 | { 276 | __typename: 'User', 277 | id: '5a6cf127c2b20834f6551482', 278 | email: 'Robel.Ansel@yahoo.com', 279 | posts: [ 280 | { 281 | __typename: 'BlogPost', 282 | id: '5a6cf127c2b20834f6551484', 283 | title: 'Sunt ut aut', 284 | }, 285 | { 286 | __typename: 'BlogPost', 287 | id: '5a6cf127c2b20834f6551486', 288 | title: 'Nihil assumenda', 289 | }, 290 | ], 291 | }, 292 | ], 293 | allBlogPosts: [ 294 | { 295 | __typename: 'BlogPost', 296 | id: '5a6cf127c2b20834f6551483', 297 | likes: 0, 298 | comments: [ 299 | { 300 | __typename: 'Comment', 301 | id: '5a6cf127c2b20834f655148e', 302 | message: 'Voluptates aut eum.', 303 | }, 304 | ], 305 | }, 306 | { 307 | __typename: 'BlogPost', 308 | id: '5a6cf127c2b20834f6551484', 309 | likes: 10, 310 | comments: [ 311 | { 312 | __typename: 'Comment', 313 | id: '5a6cf127c2b20834f6551487', 314 | message: 'Et soluta ipsam quas facilis possimus et.', 315 | }, 316 | { 317 | __typename: 'Comment', 318 | id: '5a6cf127c2b20834f6551488', 319 | message: 'Tempore sed enim qui aliquam est saepe qui.', 320 | }, 321 | { 322 | __typename: 'Comment', 323 | id: '5a6cf127c2b20834f6551489', 324 | message: 'Ea et est autem dicta necessitatibus vel.', 325 | }, 326 | ], 327 | }, 328 | { 329 | __typename: 'BlogPost', 330 | id: '5a6cf127c2b20834f6551485', 331 | likes: 23, 332 | comments: [ 333 | { 334 | __typename: 'Comment', 335 | id: '5a6cf127c2b20834f655148b', 336 | message: 'Consectetur cum est odit et qui.', 337 | }, 338 | { 339 | __typename: 'Comment', 340 | id: '5a6cf127c2b20834f655148d', 341 | message: 'Aut vel possimus nisi qui.', 342 | }, 343 | ], 344 | }, 345 | { 346 | __typename: 'BlogPost', 347 | id: '5a6cf127c2b20834f6551486', 348 | likes: 3, 349 | comments: [ 350 | { 351 | __typename: 'Comment', 352 | id: '5a6cf127c2b20834f655148a', 353 | message: 'Voluptates ex sint amet repellendus impedit nam.', 354 | }, 355 | { 356 | __typename: 'Comment', 357 | id: '5a6cf127c2b20834f655148c', 358 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 359 | }, 360 | ], 361 | }, 362 | ], 363 | } 364 | 365 | const listAndObject = { 366 | allUsers: [ 367 | { 368 | __typename: 'User', 369 | id: '5a6cf127c2b20834f6551481', 370 | email: 'Madisen_Braun@hotmail.com', 371 | posts: [ 372 | { 373 | __typename: 'BlogPost', 374 | id: '5a6cf127c2b20834f6551483', 375 | title: 'Aut aut reiciendis', 376 | }, 377 | { 378 | __typename: 'BlogPost', 379 | id: '5a6cf127c2b20834f6551485', 380 | title: 'Nesciunt esse', 381 | }, 382 | ], 383 | }, 384 | { 385 | __typename: 'User', 386 | id: '5a6cf127c2b20834f6551482', 387 | email: 'Robel.Ansel@yahoo.com', 388 | posts: [ 389 | { 390 | __typename: 'BlogPost', 391 | id: '5a6cf127c2b20834f6551484', 392 | title: 'Sunt ut aut', 393 | }, 394 | { 395 | __typename: 'BlogPost', 396 | id: '5a6cf127c2b20834f6551486', 397 | title: 'Nihil assumenda', 398 | }, 399 | ], 400 | }, 401 | ], 402 | findComment: { 403 | __typename: 'Comment', 404 | id: '5a6cf127c2b20834f655148a', 405 | message: 'Voluptates ex sint amet repellendus impedit nam.', 406 | }, 407 | } 408 | 409 | const listAndObjectConnections = { 410 | allUsers: [ 411 | { 412 | __typename: 'User', 413 | id: '5a6cf127c2b20834f6551481', 414 | email: 'Madisen_Braun@hotmail.com', 415 | posts: { 416 | edges: [ 417 | { 418 | node: { 419 | __typename: 'BlogPost', 420 | id: '5a6cf127c2b20834f6551483', 421 | title: 'Aut aut reiciendis', 422 | }, 423 | }, 424 | { 425 | node: { 426 | __typename: 'BlogPost', 427 | id: '5a6cf127c2b20834f6551485', 428 | title: 'Nesciunt esse', 429 | }, 430 | }, 431 | ], 432 | }, 433 | }, 434 | { 435 | __typename: 'User', 436 | id: '5a6cf127c2b20834f6551482', 437 | email: 'Robel.Ansel@yahoo.com', 438 | posts: { 439 | edges: [ 440 | { 441 | node: { 442 | __typename: 'BlogPost', 443 | id: '5a6cf127c2b20834f6551484', 444 | title: 'Sunt ut aut', 445 | }, 446 | }, 447 | { 448 | node: { 449 | __typename: 'BlogPost', 450 | id: '5a6cf127c2b20834f6551486', 451 | title: 'Nihil assumenda', 452 | }, 453 | }, 454 | ], 455 | }, 456 | }, 457 | ], 458 | findComment: { 459 | __typename: 'Comment', 460 | id: '5a6cf127c2b20834f655148a', 461 | message: 'Voluptates ex sint amet repellendus impedit nam.', 462 | }, 463 | } 464 | 465 | const listAndObjectConnectionsWithNullNodes = { 466 | allUsers: [ 467 | null, 468 | { 469 | __typename: 'User', 470 | id: '5a6cf127c2b20834f6551481', 471 | email: 'Madisen_Braun@hotmail.com', 472 | posts: { 473 | edges: [ 474 | { 475 | node: { 476 | __typename: 'BlogPost', 477 | id: '5a6cf127c2b20834f6551483', 478 | title: 'Aut aut reiciendis', 479 | }, 480 | }, 481 | { 482 | node: { 483 | __typename: 'BlogPost', 484 | id: '5a6cf127c2b20834f6551485', 485 | title: 'Nesciunt esse', 486 | }, 487 | }, 488 | ], 489 | }, 490 | }, 491 | { 492 | __typename: 'User', 493 | id: '5a6cf127c2b20834f6551482', 494 | email: 'Robel.Ansel@yahoo.com', 495 | posts: { 496 | edges: [ 497 | { 498 | node: { 499 | __typename: 'BlogPost', 500 | id: '5a6cf127c2b20834f6551484', 501 | title: 'Sunt ut aut', 502 | }, 503 | }, 504 | null, 505 | { 506 | node: { 507 | __typename: 'BlogPost', 508 | id: '5a6cf127c2b20834f6551486', 509 | title: 'Nihil assumenda', 510 | }, 511 | }, 512 | ], 513 | }, 514 | }, 515 | ], 516 | findComment: { 517 | __typename: 'Comment', 518 | id: '5a6cf127c2b20834f655148a', 519 | message: 'Voluptates ex sint amet repellendus impedit nam.', 520 | }, 521 | } 522 | 523 | const listAndObjectConnectionsWithNullNodesNormalized = { 524 | blogPosts: { 525 | '5a6cf127c2b20834f6551483': { 526 | id: '5a6cf127c2b20834f6551483', 527 | title: 'Aut aut reiciendis', 528 | }, 529 | '5a6cf127c2b20834f6551484': { 530 | id: '5a6cf127c2b20834f6551484', 531 | title: 'Sunt ut aut', 532 | }, 533 | '5a6cf127c2b20834f6551485': { 534 | id: '5a6cf127c2b20834f6551485', 535 | title: 'Nesciunt esse', 536 | }, 537 | '5a6cf127c2b20834f6551486': { 538 | id: '5a6cf127c2b20834f6551486', 539 | title: 'Nihil assumenda', 540 | }, 541 | }, 542 | comments: { 543 | '5a6cf127c2b20834f655148a': { 544 | id: '5a6cf127c2b20834f655148a', 545 | message: 'Voluptates ex sint amet repellendus impedit nam.', 546 | }, 547 | }, 548 | users: { 549 | '5a6cf127c2b20834f6551481': { 550 | email: 'Madisen_Braun@hotmail.com', 551 | id: '5a6cf127c2b20834f6551481', 552 | posts: [ '5a6cf127c2b20834f6551483', '5a6cf127c2b20834f6551485', ], 553 | }, 554 | '5a6cf127c2b20834f6551482': { 555 | email: 'Robel.Ansel@yahoo.com', 556 | id: '5a6cf127c2b20834f6551482', 557 | posts: [ '5a6cf127c2b20834f6551484', '5a6cf127c2b20834f6551486', ], 558 | }, 559 | }, 560 | } 561 | 562 | const withScalarArrays = { 563 | allBlogPosts: [ 564 | { 565 | __typename: 'BlogPost', 566 | id: '5a6cf127c2b20834f6551483', 567 | likes: 0, 568 | comments: [ 569 | { 570 | __typename: 'Comment', 571 | id: '5a6cf127c2b20834f655148e', 572 | message: 'Voluptates aut eum.', 573 | }, 574 | ], 575 | tags: [ 'tags', 'are', 'boring', ], 576 | }, 577 | { 578 | __typename: 'BlogPost', 579 | id: '5a6cf127c2b20834f6551485', 580 | likes: 23, 581 | comments: [ 582 | { 583 | __typename: 'Comment', 584 | id: '5a6cf127c2b20834f655148b', 585 | message: 'Consectetur cum est odit et qui.', 586 | }, 587 | { 588 | __typename: 'Comment', 589 | id: '5a6cf127c2b20834f655148d', 590 | message: 'Aut vel possimus nisi qui.', 591 | }, 592 | ], 593 | }, 594 | ], 595 | } 596 | 597 | const withScalarArraysConnections = { 598 | allBlogPosts: [ 599 | { 600 | __typename: 'BlogPost', 601 | id: '5a6cf127c2b20834f6551483', 602 | likes: 0, 603 | comments: { 604 | edges: [ 605 | { 606 | node: { 607 | __typename: 'Comment', 608 | id: '5a6cf127c2b20834f655148e', 609 | message: 'Voluptates aut eum.', 610 | }, 611 | }, 612 | ], 613 | }, 614 | tags: [ 'tags', 'are', 'boring', ], 615 | }, 616 | { 617 | __typename: 'BlogPost', 618 | id: '5a6cf127c2b20834f6551485', 619 | likes: 23, 620 | comments: { 621 | edges: [ 622 | { 623 | node: { 624 | __typename: 'Comment', 625 | id: '5a6cf127c2b20834f655148b', 626 | message: 'Consectetur cum est odit et qui.', 627 | }, 628 | }, 629 | { 630 | node: { 631 | __typename: 'Comment', 632 | id: '5a6cf127c2b20834f655148d', 633 | message: 'Aut vel possimus nisi qui.', 634 | }, 635 | }, 636 | ], 637 | }, 638 | }, 639 | ], 640 | } 641 | 642 | const withMultipleTypesConnections = { 643 | collections: [ 644 | { 645 | __typename: 'Collection', 646 | _id: '5a6cf127c2b20834f655148d', 647 | name: 'Continue Watching', 648 | videos: { 649 | edges: [ 650 | { 651 | node: { 652 | __typename: 'Movie', 653 | _id: '5a6cf127c2b20834f655148a', 654 | name: 'Batman', 655 | }, 656 | }, 657 | { 658 | node: { 659 | __typename: 'Show', 660 | _id: '5a6cf127c2b20834f655148b', 661 | name: 'Prison Break', 662 | }, 663 | }, 664 | { 665 | node: { 666 | __typename: 'Movie', 667 | _id: '5a6cf127c2b20834f655148c', 668 | name: 'Superman', 669 | }, 670 | }, 671 | ], 672 | }, 673 | }, 674 | ], 675 | } 676 | 677 | const useConnectionsGraphqlQuery = ` 678 | query getCollections { 679 | users { 680 | friends { 681 | edges { 682 | node { 683 | name 684 | } 685 | } 686 | pageInfo { 687 | hasNextPage 688 | } 689 | } 690 | } 691 | } 692 | ` 693 | 694 | const typeWithNoIdentifier = { 695 | allProducts: [ 696 | { 697 | __typename: 'Product', 698 | id: 1, 699 | name: 'Product A', 700 | price: { 701 | __typename: 'CurrencyModel', 702 | value: 1000, 703 | currency: 'USD', 704 | }, 705 | }, 706 | { 707 | __typename: 'Product', 708 | id: 2, 709 | name: 'Product B', 710 | price: { 711 | __typename: 'CurrencyModel', 712 | value: 800, 713 | currency: 'EUR', 714 | }, 715 | }, 716 | { 717 | __typename: 'Product', 718 | id: 3, 719 | name: 'Product C', 720 | price: { 721 | __typename: 'CurrencyModel', 722 | value: 1000, 723 | currency: 'USD', 724 | }, 725 | }, 726 | ], 727 | } 728 | 729 | const typeWithNoIdentifierNormalized = { 730 | products: { 731 | 1: { 732 | id: 1, 733 | name: 'Product A', 734 | price: { 735 | value: 1000, 736 | currency: 'USD', 737 | }, 738 | }, 739 | 2: { 740 | id: 2, 741 | name: 'Product B', 742 | price: { 743 | value: 800, 744 | currency: 'EUR', 745 | }, 746 | }, 747 | 3: { 748 | id: 3, 749 | name: 'Product C', 750 | price: { 751 | value: 1000, 752 | currency: 'USD', 753 | }, 754 | }, 755 | }, 756 | } 757 | 758 | const withEmptyArrays = { 759 | foos: [], 760 | } 761 | 762 | const withEmptyArraysNormalized = {} 763 | 764 | const emptyListAndObject = { 765 | allUsers: [ 766 | { 767 | __typename: 'User', 768 | id: '5a6cf127c2b20834f6551481', 769 | email: 'Madisen_Braun@hotmail.com', 770 | posts: [], 771 | }, 772 | { 773 | __typename: 'User', 774 | id: '5a6cf127c2b20834f6551482', 775 | email: 'Robel.Ansel@yahoo.com', 776 | posts: [ 777 | { 778 | __typename: 'BlogPost', 779 | id: '5a6cf127c2b20834f6551484', 780 | title: 'Sunt ut aut', 781 | }, 782 | { 783 | __typename: 'BlogPost', 784 | id: '5a6cf127c2b20834f6551486', 785 | title: 'Nihil assumenda', 786 | }, 787 | ], 788 | }, 789 | ], 790 | findComment: { 791 | __typename: 'Comment', 792 | id: '5a6cf127c2b20834f655148a', 793 | message: 'Voluptates ex sint amet repellendus impedit nam.', 794 | }, 795 | } 796 | 797 | const emptyListAndObjectNormalized = { 798 | blogPosts: { 799 | '5a6cf127c2b20834f6551484': { 800 | id: '5a6cf127c2b20834f6551484', 801 | title: 'Sunt ut aut', 802 | }, 803 | '5a6cf127c2b20834f6551486': { 804 | id: '5a6cf127c2b20834f6551486', 805 | title: 'Nihil assumenda', 806 | }, 807 | }, 808 | comments: { 809 | '5a6cf127c2b20834f655148a': { 810 | id: '5a6cf127c2b20834f655148a', 811 | message: 'Voluptates ex sint amet repellendus impedit nam.', 812 | }, 813 | }, 814 | users: { 815 | '5a6cf127c2b20834f6551481': { 816 | email: 'Madisen_Braun@hotmail.com', 817 | id: '5a6cf127c2b20834f6551481', 818 | posts: [], 819 | }, 820 | '5a6cf127c2b20834f6551482': { 821 | email: 'Robel.Ansel@yahoo.com', 822 | id: '5a6cf127c2b20834f6551482', 823 | posts: [ '5a6cf127c2b20834f6551484', '5a6cf127c2b20834f6551486', ], 824 | }, 825 | }, 826 | } 827 | 828 | const preferenceObject = { 829 | pref1: null, 830 | pref2: 'String', 831 | pref3: [], 832 | pref4: {}, 833 | pref5: { a: 1, b: 2, }, 834 | } 835 | 836 | const jsonContent = { 837 | allUsers: [ 838 | { 839 | __typename: 'User', 840 | id: '5a6cf127c2b20834f6551480', 841 | preferences: null, 842 | }, 843 | { 844 | __typename: 'User', 845 | id: '5a6cf127c2b20834f6551481', 846 | preferences: {}, 847 | }, 848 | { 849 | __typename: 'User', 850 | id: '5a6cf127c2b20834f6551482', 851 | preferences: [], 852 | }, 853 | { 854 | __typename: 'User', 855 | id: '5a6cf127c2b20834f6551483', 856 | preferences: preferenceObject, 857 | }, 858 | ], 859 | } 860 | 861 | const jsonContentNormalized = { 862 | users: { 863 | '5a6cf127c2b20834f6551480': { 864 | id: '5a6cf127c2b20834f6551480', 865 | preferences: null, 866 | }, 867 | '5a6cf127c2b20834f6551481': { 868 | id: '5a6cf127c2b20834f6551481', 869 | preferences: {}, 870 | }, 871 | '5a6cf127c2b20834f6551482': { 872 | id: '5a6cf127c2b20834f6551482', 873 | preferences: [], 874 | }, 875 | '5a6cf127c2b20834f6551483': { 876 | id: '5a6cf127c2b20834f6551483', 877 | preferences: preferenceObject, 878 | }, 879 | }, 880 | } 881 | 882 | const withJSONContentAndGraphQLConnections = { 883 | collections: [ 884 | { 885 | __typename: 'Collection', 886 | id: '5a6cf127c2b20834f655148d', 887 | name: 'Continue Watching', 888 | videos: { 889 | edges: [ 890 | { 891 | node: { 892 | __typename: 'Movie', 893 | id: '5a6cf127c2b20834f655148a', 894 | name: 'Batman', 895 | preferences: null, 896 | }, 897 | }, 898 | { 899 | node: { 900 | __typename: 'Show', 901 | id: '5a6cf127c2b20834f655148b', 902 | name: 'Prison Break', 903 | preferences: {}, 904 | }, 905 | }, 906 | { 907 | node: { 908 | __typename: 'Movie', 909 | id: '5a6cf127c2b20834f655148c', 910 | name: 'Superman', 911 | preferences: [], 912 | }, 913 | }, 914 | { 915 | node: { 916 | __typename: 'Show', 917 | id: '5a6cf127c2b20834f655148d', 918 | name: 'Avengers', 919 | preferences: preferenceObject, 920 | }, 921 | }, 922 | ], 923 | }, 924 | }, 925 | ], 926 | } 927 | const withJSONContentAndGraphQLConnectionsNormalized = { 928 | collections: { 929 | '5a6cf127c2b20834f655148d': { 930 | id: '5a6cf127c2b20834f655148d', 931 | name: 'Continue Watching', 932 | videos: [ 933 | { 934 | id: '5a6cf127c2b20834f655148a', 935 | collection: 'movies', 936 | }, 937 | { 938 | id: '5a6cf127c2b20834f655148b', 939 | collection: 'shows', 940 | }, 941 | { 942 | id: '5a6cf127c2b20834f655148c', 943 | collection: 'movies', 944 | }, 945 | { 946 | id: '5a6cf127c2b20834f655148d', 947 | collection: 'shows', 948 | }, 949 | ], 950 | }, 951 | }, 952 | movies: { 953 | '5a6cf127c2b20834f655148a': { 954 | id: '5a6cf127c2b20834f655148a', 955 | name: 'Batman', 956 | preferences: null, 957 | }, 958 | '5a6cf127c2b20834f655148c': { 959 | id: '5a6cf127c2b20834f655148c', 960 | name: 'Superman', 961 | preferences: [], 962 | }, 963 | }, 964 | shows: { 965 | '5a6cf127c2b20834f655148b': { 966 | id: '5a6cf127c2b20834f655148b', 967 | name: 'Prison Break', 968 | preferences: {}, 969 | }, 970 | '5a6cf127c2b20834f655148d': { 971 | id: '5a6cf127c2b20834f655148d', 972 | name: 'Avengers', 973 | preferences: preferenceObject, 974 | }, 975 | }, 976 | } 977 | 978 | const nestedAndJSONContent = { 979 | allUsers: [ 980 | { 981 | __typename: 'User', 982 | id: '5a6cf127c2b20834f6551481', 983 | email: 'Madisen_Braun@hotmail.com', 984 | posts: [ 985 | { 986 | __typename: 'BlogPost', 987 | id: '5a6cf127c2b20834f6551483', 988 | title: 'Aut aut reiciendis', 989 | preferences: null, 990 | }, 991 | { 992 | __typename: 'BlogPost', 993 | id: '5a6cf127c2b20834f6551485', 994 | title: 'Nesciunt esse', 995 | preferences: [], 996 | }, 997 | ], 998 | }, 999 | { 1000 | __typename: 'User', 1001 | id: '5a6cf127c2b20834f6551482', 1002 | email: 'Robel.Ansel@yahoo.com', 1003 | posts: [ 1004 | { 1005 | __typename: 'BlogPost', 1006 | id: '5a6cf127c2b20834f6551484', 1007 | title: 'Sunt ut aut', 1008 | preferences: {}, 1009 | }, 1010 | { 1011 | __typename: 'BlogPost', 1012 | id: '5a6cf127c2b20834f6551486', 1013 | title: 'Nihil assumenda', 1014 | preferences: preferenceObject, 1015 | }, 1016 | ], 1017 | }, 1018 | ], 1019 | allBlogPosts: [ 1020 | { 1021 | __typename: 'BlogPost', 1022 | id: '5a6cf127c2b20834f6551483', 1023 | likes: 0, 1024 | comments: [ 1025 | { 1026 | __typename: 'Comment', 1027 | id: '5a6cf127c2b20834f655148e', 1028 | message: 'Voluptates aut eum.', 1029 | }, 1030 | ], 1031 | }, 1032 | { 1033 | __typename: 'BlogPost', 1034 | id: '5a6cf127c2b20834f6551484', 1035 | likes: 10, 1036 | comments: [ 1037 | { 1038 | __typename: 'Comment', 1039 | id: '5a6cf127c2b20834f6551487', 1040 | message: 'Et soluta ipsam quas facilis possimus et.', 1041 | }, 1042 | { 1043 | __typename: 'Comment', 1044 | id: '5a6cf127c2b20834f6551488', 1045 | message: 'Tempore sed enim qui aliquam est saepe qui.', 1046 | }, 1047 | { 1048 | __typename: 'Comment', 1049 | id: '5a6cf127c2b20834f6551489', 1050 | message: 'Ea et est autem dicta necessitatibus vel.', 1051 | }, 1052 | ], 1053 | }, 1054 | { 1055 | __typename: 'BlogPost', 1056 | id: '5a6cf127c2b20834f6551485', 1057 | likes: 23, 1058 | comments: [ 1059 | { 1060 | __typename: 'Comment', 1061 | id: '5a6cf127c2b20834f655148b', 1062 | message: 'Consectetur cum est odit et qui.', 1063 | }, 1064 | { 1065 | __typename: 'Comment', 1066 | id: '5a6cf127c2b20834f655148d', 1067 | message: 'Aut vel possimus nisi qui.', 1068 | }, 1069 | ], 1070 | }, 1071 | { 1072 | __typename: 'BlogPost', 1073 | id: '5a6cf127c2b20834f6551486', 1074 | likes: 3, 1075 | comments: [ 1076 | { 1077 | __typename: 'Comment', 1078 | id: '5a6cf127c2b20834f655148a', 1079 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1080 | }, 1081 | { 1082 | __typename: 'Comment', 1083 | id: '5a6cf127c2b20834f655148c', 1084 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 1085 | }, 1086 | ], 1087 | }, 1088 | ], 1089 | } 1090 | 1091 | const nestedAndJSONContentNormalized = { 1092 | blogPosts: { 1093 | '5a6cf127c2b20834f6551483': { 1094 | comments: [ 1095 | '5a6cf127c2b20834f655148e', 1096 | ], 1097 | id: '5a6cf127c2b20834f6551483', 1098 | likes: 0, 1099 | title: 'Aut aut reiciendis', 1100 | preferences: null, 1101 | }, 1102 | '5a6cf127c2b20834f6551484': { 1103 | comments: [ 1104 | '5a6cf127c2b20834f6551487', 1105 | '5a6cf127c2b20834f6551488', 1106 | '5a6cf127c2b20834f6551489', 1107 | ], 1108 | id: '5a6cf127c2b20834f6551484', 1109 | likes: 10, 1110 | title: 'Sunt ut aut', 1111 | preferences: {}, 1112 | }, 1113 | '5a6cf127c2b20834f6551485': { 1114 | comments: [ 1115 | '5a6cf127c2b20834f655148b', 1116 | '5a6cf127c2b20834f655148d', 1117 | ], 1118 | id: '5a6cf127c2b20834f6551485', 1119 | likes: 23, 1120 | title: 'Nesciunt esse', 1121 | preferences: [], 1122 | }, 1123 | '5a6cf127c2b20834f6551486': { 1124 | comments: [ 1125 | '5a6cf127c2b20834f655148a', 1126 | '5a6cf127c2b20834f655148c', 1127 | ], 1128 | id: '5a6cf127c2b20834f6551486', 1129 | likes: 3, 1130 | title: 'Nihil assumenda', 1131 | preferences: preferenceObject, 1132 | }, 1133 | }, 1134 | comments: { 1135 | '5a6cf127c2b20834f6551487': { 1136 | id: '5a6cf127c2b20834f6551487', 1137 | message: 'Et soluta ipsam quas facilis possimus et.', 1138 | }, 1139 | '5a6cf127c2b20834f6551488': { 1140 | id: '5a6cf127c2b20834f6551488', 1141 | message: 'Tempore sed enim qui aliquam est saepe qui.', 1142 | }, 1143 | '5a6cf127c2b20834f6551489': { 1144 | id: '5a6cf127c2b20834f6551489', 1145 | message: 'Ea et est autem dicta necessitatibus vel.', 1146 | }, 1147 | '5a6cf127c2b20834f655148a': { 1148 | id: '5a6cf127c2b20834f655148a', 1149 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1150 | }, 1151 | '5a6cf127c2b20834f655148b': { 1152 | id: '5a6cf127c2b20834f655148b', 1153 | message: 'Consectetur cum est odit et qui.', 1154 | }, 1155 | '5a6cf127c2b20834f655148c': { 1156 | id: '5a6cf127c2b20834f655148c', 1157 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 1158 | }, 1159 | '5a6cf127c2b20834f655148d': { 1160 | id: '5a6cf127c2b20834f655148d', 1161 | message: 'Aut vel possimus nisi qui.', 1162 | }, 1163 | '5a6cf127c2b20834f655148e': { 1164 | id: '5a6cf127c2b20834f655148e', 1165 | message: 'Voluptates aut eum.', 1166 | }, 1167 | }, 1168 | users: { 1169 | '5a6cf127c2b20834f6551481': { 1170 | email: 'Madisen_Braun@hotmail.com', 1171 | id: '5a6cf127c2b20834f6551481', 1172 | posts: [ 1173 | '5a6cf127c2b20834f6551483', 1174 | '5a6cf127c2b20834f6551485', 1175 | ], 1176 | }, 1177 | '5a6cf127c2b20834f6551482': { 1178 | email: 'Robel.Ansel@yahoo.com', 1179 | id: '5a6cf127c2b20834f6551482', 1180 | posts: [ 1181 | '5a6cf127c2b20834f6551484', 1182 | '5a6cf127c2b20834f6551486', 1183 | ], 1184 | }, 1185 | }, 1186 | } 1187 | 1188 | module.exports = { 1189 | typeWithSameTypeFieldsConnections, 1190 | allUsersConnections, 1191 | customIdKey, 1192 | listAndObject, 1193 | listAndObjectConnections, 1194 | listAndObjectConnectionsWithNullNodes, 1195 | listAndObjectConnectionsWithNullNodesNormalized, 1196 | mergeTestData, 1197 | nested, 1198 | noNested, 1199 | noTypeNames, 1200 | withScalarArrays, 1201 | withScalarArraysConnections, 1202 | withMultipleTypesConnections, 1203 | useConnectionsGraphqlQuery, 1204 | typeWithNoIdentifier, 1205 | typeWithNoIdentifierNormalized, 1206 | withEmptyArrays, 1207 | withEmptyArraysNormalized, 1208 | emptyListAndObject, 1209 | emptyListAndObjectNormalized, 1210 | jsonContent, 1211 | jsonContentNormalized, 1212 | withJSONContentAndGraphQLConnections, 1213 | withJSONContentAndGraphQLConnectionsNormalized, 1214 | nestedAndJSONContent, 1215 | nestedAndJSONContentNormalized, 1216 | } 1217 | -------------------------------------------------------------------------------- /test/snapshots/GraphQLNormalizr.test.js.md: -------------------------------------------------------------------------------- 1 | # Snapshot report for `test/GraphQLNormalizr.test.js` 2 | 3 | The actual snapshot is saved in `GraphQLNormalizr.test.js.snap`. 4 | 5 | Generated by [AVA](https://ava.li). 6 | 7 | ## snapshot :: `normalize` correctly handles NULL nodes 8 | 9 | > Snapshot 1 10 | 11 | { 12 | blogPosts: { 13 | '5a6cf127c2b20834f6551483': { 14 | id: '5a6cf127c2b20834f6551483', 15 | title: 'Aut aut reiciendis', 16 | }, 17 | '5a6cf127c2b20834f6551484': { 18 | id: '5a6cf127c2b20834f6551484', 19 | title: 'Sunt ut aut', 20 | }, 21 | '5a6cf127c2b20834f6551485': { 22 | id: '5a6cf127c2b20834f6551485', 23 | title: 'Nesciunt esse', 24 | }, 25 | '5a6cf127c2b20834f6551486': { 26 | id: '5a6cf127c2b20834f6551486', 27 | title: 'Nihil assumenda', 28 | }, 29 | }, 30 | comments: { 31 | '5a6cf127c2b20834f655148a': { 32 | id: '5a6cf127c2b20834f655148a', 33 | message: 'Voluptates ex sint amet repellendus impedit nam.', 34 | }, 35 | }, 36 | users: { 37 | '5a6cf127c2b20834f6551481': { 38 | email: 'Madisen_Braun@hotmail.com', 39 | id: '5a6cf127c2b20834f6551481', 40 | posts: [ 41 | '5a6cf127c2b20834f6551483', 42 | '5a6cf127c2b20834f6551485', 43 | ], 44 | }, 45 | '5a6cf127c2b20834f6551482': { 46 | email: 'Robel.Ansel@yahoo.com', 47 | id: '5a6cf127c2b20834f6551482', 48 | posts: [ 49 | '5a6cf127c2b20834f6551484', 50 | '5a6cf127c2b20834f6551486', 51 | ], 52 | }, 53 | }, 54 | } 55 | 56 | ## snapshot :: `normalize` data containing lists and objects 57 | 58 | > Snapshot 1 59 | 60 | { 61 | blogPosts: { 62 | '5a6cf127c2b20834f6551483': { 63 | id: '5a6cf127c2b20834f6551483', 64 | title: 'Aut aut reiciendis', 65 | }, 66 | '5a6cf127c2b20834f6551484': { 67 | id: '5a6cf127c2b20834f6551484', 68 | title: 'Sunt ut aut', 69 | }, 70 | '5a6cf127c2b20834f6551485': { 71 | id: '5a6cf127c2b20834f6551485', 72 | title: 'Nesciunt esse', 73 | }, 74 | '5a6cf127c2b20834f6551486': { 75 | id: '5a6cf127c2b20834f6551486', 76 | title: 'Nihil assumenda', 77 | }, 78 | }, 79 | comments: { 80 | '5a6cf127c2b20834f655148a': { 81 | id: '5a6cf127c2b20834f655148a', 82 | message: 'Voluptates ex sint amet repellendus impedit nam.', 83 | }, 84 | }, 85 | users: { 86 | '5a6cf127c2b20834f6551481': { 87 | email: 'Madisen_Braun@hotmail.com', 88 | id: '5a6cf127c2b20834f6551481', 89 | posts: [ 90 | '5a6cf127c2b20834f6551483', 91 | '5a6cf127c2b20834f6551485', 92 | ], 93 | }, 94 | '5a6cf127c2b20834f6551482': { 95 | email: 'Robel.Ansel@yahoo.com', 96 | id: '5a6cf127c2b20834f6551482', 97 | posts: [ 98 | '5a6cf127c2b20834f6551484', 99 | '5a6cf127c2b20834f6551486', 100 | ], 101 | }, 102 | }, 103 | } 104 | 105 | ## snapshot :: `normalize` merge data on the same doc 106 | 107 | > Snapshot 1 108 | 109 | { 110 | comments: { 111 | '5a6cf127c2b20834f655148e': { 112 | author: '5a6cf127c2b20834f6551481', 113 | id: '5a6cf127c2b20834f655148e', 114 | message: 'Voluptates aut eum.', 115 | }, 116 | }, 117 | users: { 118 | '5a6cf127c2b20834f6551481': { 119 | email: 'Madisen_Braun@hotmail.com', 120 | id: '5a6cf127c2b20834f6551481', 121 | }, 122 | }, 123 | } 124 | 125 | ## snapshot :: `normalize` nested data 126 | 127 | > Snapshot 1 128 | 129 | { 130 | blogPosts: { 131 | '5a6cf127c2b20834f6551483': { 132 | comments: [ 133 | '5a6cf127c2b20834f655148e', 134 | ], 135 | id: '5a6cf127c2b20834f6551483', 136 | likes: 0, 137 | title: 'Aut aut reiciendis', 138 | }, 139 | '5a6cf127c2b20834f6551484': { 140 | comments: [ 141 | '5a6cf127c2b20834f6551487', 142 | '5a6cf127c2b20834f6551488', 143 | '5a6cf127c2b20834f6551489', 144 | ], 145 | id: '5a6cf127c2b20834f6551484', 146 | likes: 10, 147 | title: 'Sunt ut aut', 148 | }, 149 | '5a6cf127c2b20834f6551485': { 150 | comments: [ 151 | '5a6cf127c2b20834f655148b', 152 | '5a6cf127c2b20834f655148d', 153 | ], 154 | id: '5a6cf127c2b20834f6551485', 155 | likes: 23, 156 | title: 'Nesciunt esse', 157 | }, 158 | '5a6cf127c2b20834f6551486': { 159 | comments: [ 160 | '5a6cf127c2b20834f655148a', 161 | '5a6cf127c2b20834f655148c', 162 | ], 163 | id: '5a6cf127c2b20834f6551486', 164 | likes: 3, 165 | title: 'Nihil assumenda', 166 | }, 167 | }, 168 | comments: { 169 | '5a6cf127c2b20834f6551487': { 170 | id: '5a6cf127c2b20834f6551487', 171 | message: 'Et soluta ipsam quas facilis possimus et.', 172 | }, 173 | '5a6cf127c2b20834f6551488': { 174 | id: '5a6cf127c2b20834f6551488', 175 | message: 'Tempore sed enim qui aliquam est saepe qui.', 176 | }, 177 | '5a6cf127c2b20834f6551489': { 178 | id: '5a6cf127c2b20834f6551489', 179 | message: 'Ea et est autem dicta necessitatibus vel.', 180 | }, 181 | '5a6cf127c2b20834f655148a': { 182 | id: '5a6cf127c2b20834f655148a', 183 | message: 'Voluptates ex sint amet repellendus impedit nam.', 184 | }, 185 | '5a6cf127c2b20834f655148b': { 186 | id: '5a6cf127c2b20834f655148b', 187 | message: 'Consectetur cum est odit et qui.', 188 | }, 189 | '5a6cf127c2b20834f655148c': { 190 | id: '5a6cf127c2b20834f655148c', 191 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 192 | }, 193 | '5a6cf127c2b20834f655148d': { 194 | id: '5a6cf127c2b20834f655148d', 195 | message: 'Aut vel possimus nisi qui.', 196 | }, 197 | '5a6cf127c2b20834f655148e': { 198 | id: '5a6cf127c2b20834f655148e', 199 | message: 'Voluptates aut eum.', 200 | }, 201 | }, 202 | users: { 203 | '5a6cf127c2b20834f6551481': { 204 | email: 'Madisen_Braun@hotmail.com', 205 | id: '5a6cf127c2b20834f6551481', 206 | posts: [ 207 | '5a6cf127c2b20834f6551483', 208 | '5a6cf127c2b20834f6551485', 209 | ], 210 | }, 211 | '5a6cf127c2b20834f6551482': { 212 | email: 'Robel.Ansel@yahoo.com', 213 | id: '5a6cf127c2b20834f6551482', 214 | posts: [ 215 | '5a6cf127c2b20834f6551484', 216 | '5a6cf127c2b20834f6551486', 217 | ], 218 | }, 219 | }, 220 | } 221 | 222 | ## snapshot :: `normalize` simple, not nested data 223 | 224 | > Snapshot 1 225 | 226 | { 227 | blogPosts: { 228 | '5a6cf127c2b20834f6551483': { 229 | id: '5a6cf127c2b20834f6551483', 230 | likes: 0, 231 | }, 232 | '5a6cf127c2b20834f6551484': { 233 | id: '5a6cf127c2b20834f6551484', 234 | likes: 10, 235 | }, 236 | '5a6cf127c2b20834f6551485': { 237 | id: '5a6cf127c2b20834f6551485', 238 | likes: 23, 239 | }, 240 | '5a6cf127c2b20834f6551486': { 241 | id: '5a6cf127c2b20834f6551486', 242 | likes: 3, 243 | }, 244 | }, 245 | comments: { 246 | '5a6cf127c2b20834f6551487': { 247 | id: '5a6cf127c2b20834f6551487', 248 | message: 'Et soluta ipsam quas facilis possimus et.', 249 | }, 250 | '5a6cf127c2b20834f6551488': { 251 | id: '5a6cf127c2b20834f6551488', 252 | message: 'Tempore sed enim qui aliquam est saepe qui.', 253 | }, 254 | '5a6cf127c2b20834f6551489': { 255 | id: '5a6cf127c2b20834f6551489', 256 | message: 'Ea et est autem dicta necessitatibus vel.', 257 | }, 258 | '5a6cf127c2b20834f655148a': { 259 | id: '5a6cf127c2b20834f655148a', 260 | message: 'Voluptates ex sint amet repellendus impedit nam.', 261 | }, 262 | '5a6cf127c2b20834f655148b': { 263 | id: '5a6cf127c2b20834f655148b', 264 | message: 'Consectetur cum est odit et qui.', 265 | }, 266 | '5a6cf127c2b20834f655148c': { 267 | id: '5a6cf127c2b20834f655148c', 268 | message: 'Voluptas quidem et saepe voluptatibus enim est.', 269 | }, 270 | '5a6cf127c2b20834f655148d': { 271 | id: '5a6cf127c2b20834f655148d', 272 | message: 'Aut vel possimus nisi qui.', 273 | }, 274 | '5a6cf127c2b20834f655148e': { 275 | id: '5a6cf127c2b20834f655148e', 276 | message: 'Voluptates aut eum.', 277 | }, 278 | }, 279 | users: { 280 | '5a6cf127c2b20834f6551481': { 281 | email: 'Madisen_Braun@hotmail.com', 282 | id: '5a6cf127c2b20834f6551481', 283 | }, 284 | '5a6cf127c2b20834f6551482': { 285 | email: 'Robel.Ansel@yahoo.com', 286 | id: '5a6cf127c2b20834f6551482', 287 | }, 288 | }, 289 | } 290 | 291 | ## snapshot :: `normalize` with `{ casing: "camel" }` 292 | 293 | > Snapshot 1 294 | 295 | { 296 | blogPosts: { 297 | '5a6cf127c2b20834f6551483': { 298 | id: '5a6cf127c2b20834f6551483', 299 | title: 'Aut aut reiciendis', 300 | }, 301 | '5a6cf127c2b20834f6551484': { 302 | id: '5a6cf127c2b20834f6551484', 303 | title: 'Sunt ut aut', 304 | }, 305 | '5a6cf127c2b20834f6551485': { 306 | id: '5a6cf127c2b20834f6551485', 307 | title: 'Nesciunt esse', 308 | }, 309 | '5a6cf127c2b20834f6551486': { 310 | id: '5a6cf127c2b20834f6551486', 311 | title: 'Nihil assumenda', 312 | }, 313 | }, 314 | comments: { 315 | '5a6cf127c2b20834f655148a': { 316 | id: '5a6cf127c2b20834f655148a', 317 | message: 'Voluptates ex sint amet repellendus impedit nam.', 318 | }, 319 | }, 320 | users: { 321 | '5a6cf127c2b20834f6551481': { 322 | email: 'Madisen_Braun@hotmail.com', 323 | id: '5a6cf127c2b20834f6551481', 324 | posts: [ 325 | '5a6cf127c2b20834f6551483', 326 | '5a6cf127c2b20834f6551485', 327 | ], 328 | }, 329 | '5a6cf127c2b20834f6551482': { 330 | email: 'Robel.Ansel@yahoo.com', 331 | id: '5a6cf127c2b20834f6551482', 332 | posts: [ 333 | '5a6cf127c2b20834f6551484', 334 | '5a6cf127c2b20834f6551486', 335 | ], 336 | }, 337 | }, 338 | } 339 | 340 | ## snapshot :: `normalize` with `{ casing: "kebab" }` 341 | 342 | > Snapshot 1 343 | 344 | { 345 | 'blog-posts': { 346 | '5a6cf127c2b20834f6551483': { 347 | id: '5a6cf127c2b20834f6551483', 348 | title: 'Aut aut reiciendis', 349 | }, 350 | '5a6cf127c2b20834f6551484': { 351 | id: '5a6cf127c2b20834f6551484', 352 | title: 'Sunt ut aut', 353 | }, 354 | '5a6cf127c2b20834f6551485': { 355 | id: '5a6cf127c2b20834f6551485', 356 | title: 'Nesciunt esse', 357 | }, 358 | '5a6cf127c2b20834f6551486': { 359 | id: '5a6cf127c2b20834f6551486', 360 | title: 'Nihil assumenda', 361 | }, 362 | }, 363 | comments: { 364 | '5a6cf127c2b20834f655148a': { 365 | id: '5a6cf127c2b20834f655148a', 366 | message: 'Voluptates ex sint amet repellendus impedit nam.', 367 | }, 368 | }, 369 | users: { 370 | '5a6cf127c2b20834f6551481': { 371 | email: 'Madisen_Braun@hotmail.com', 372 | id: '5a6cf127c2b20834f6551481', 373 | posts: [ 374 | '5a6cf127c2b20834f6551483', 375 | '5a6cf127c2b20834f6551485', 376 | ], 377 | }, 378 | '5a6cf127c2b20834f6551482': { 379 | email: 'Robel.Ansel@yahoo.com', 380 | id: '5a6cf127c2b20834f6551482', 381 | posts: [ 382 | '5a6cf127c2b20834f6551484', 383 | '5a6cf127c2b20834f6551486', 384 | ], 385 | }, 386 | }, 387 | } 388 | 389 | ## snapshot :: `normalize` with `{ casing: "lower" }` 390 | 391 | > Snapshot 1 392 | 393 | { 394 | blogposts: { 395 | '5a6cf127c2b20834f6551483': { 396 | id: '5a6cf127c2b20834f6551483', 397 | title: 'Aut aut reiciendis', 398 | }, 399 | '5a6cf127c2b20834f6551484': { 400 | id: '5a6cf127c2b20834f6551484', 401 | title: 'Sunt ut aut', 402 | }, 403 | '5a6cf127c2b20834f6551485': { 404 | id: '5a6cf127c2b20834f6551485', 405 | title: 'Nesciunt esse', 406 | }, 407 | '5a6cf127c2b20834f6551486': { 408 | id: '5a6cf127c2b20834f6551486', 409 | title: 'Nihil assumenda', 410 | }, 411 | }, 412 | comments: { 413 | '5a6cf127c2b20834f655148a': { 414 | id: '5a6cf127c2b20834f655148a', 415 | message: 'Voluptates ex sint amet repellendus impedit nam.', 416 | }, 417 | }, 418 | users: { 419 | '5a6cf127c2b20834f6551481': { 420 | email: 'Madisen_Braun@hotmail.com', 421 | id: '5a6cf127c2b20834f6551481', 422 | posts: [ 423 | '5a6cf127c2b20834f6551483', 424 | '5a6cf127c2b20834f6551485', 425 | ], 426 | }, 427 | '5a6cf127c2b20834f6551482': { 428 | email: 'Robel.Ansel@yahoo.com', 429 | id: '5a6cf127c2b20834f6551482', 430 | posts: [ 431 | '5a6cf127c2b20834f6551484', 432 | '5a6cf127c2b20834f6551486', 433 | ], 434 | }, 435 | }, 436 | } 437 | 438 | ## snapshot :: `normalize` with `{ casing: "pascal" }` 439 | 440 | > Snapshot 1 441 | 442 | { 443 | BlogPosts: { 444 | '5a6cf127c2b20834f6551483': { 445 | id: '5a6cf127c2b20834f6551483', 446 | title: 'Aut aut reiciendis', 447 | }, 448 | '5a6cf127c2b20834f6551484': { 449 | id: '5a6cf127c2b20834f6551484', 450 | title: 'Sunt ut aut', 451 | }, 452 | '5a6cf127c2b20834f6551485': { 453 | id: '5a6cf127c2b20834f6551485', 454 | title: 'Nesciunt esse', 455 | }, 456 | '5a6cf127c2b20834f6551486': { 457 | id: '5a6cf127c2b20834f6551486', 458 | title: 'Nihil assumenda', 459 | }, 460 | }, 461 | Comments: { 462 | '5a6cf127c2b20834f655148a': { 463 | id: '5a6cf127c2b20834f655148a', 464 | message: 'Voluptates ex sint amet repellendus impedit nam.', 465 | }, 466 | }, 467 | Users: { 468 | '5a6cf127c2b20834f6551481': { 469 | email: 'Madisen_Braun@hotmail.com', 470 | id: '5a6cf127c2b20834f6551481', 471 | posts: [ 472 | '5a6cf127c2b20834f6551483', 473 | '5a6cf127c2b20834f6551485', 474 | ], 475 | }, 476 | '5a6cf127c2b20834f6551482': { 477 | email: 'Robel.Ansel@yahoo.com', 478 | id: '5a6cf127c2b20834f6551482', 479 | posts: [ 480 | '5a6cf127c2b20834f6551484', 481 | '5a6cf127c2b20834f6551486', 482 | ], 483 | }, 484 | }, 485 | } 486 | 487 | ## snapshot :: `normalize` with `{ casing: "snake" }` 488 | 489 | > Snapshot 1 490 | 491 | { 492 | blog_posts: { 493 | '5a6cf127c2b20834f6551483': { 494 | id: '5a6cf127c2b20834f6551483', 495 | title: 'Aut aut reiciendis', 496 | }, 497 | '5a6cf127c2b20834f6551484': { 498 | id: '5a6cf127c2b20834f6551484', 499 | title: 'Sunt ut aut', 500 | }, 501 | '5a6cf127c2b20834f6551485': { 502 | id: '5a6cf127c2b20834f6551485', 503 | title: 'Nesciunt esse', 504 | }, 505 | '5a6cf127c2b20834f6551486': { 506 | id: '5a6cf127c2b20834f6551486', 507 | title: 'Nihil assumenda', 508 | }, 509 | }, 510 | comments: { 511 | '5a6cf127c2b20834f655148a': { 512 | id: '5a6cf127c2b20834f655148a', 513 | message: 'Voluptates ex sint amet repellendus impedit nam.', 514 | }, 515 | }, 516 | users: { 517 | '5a6cf127c2b20834f6551481': { 518 | email: 'Madisen_Braun@hotmail.com', 519 | id: '5a6cf127c2b20834f6551481', 520 | posts: [ 521 | '5a6cf127c2b20834f6551483', 522 | '5a6cf127c2b20834f6551485', 523 | ], 524 | }, 525 | '5a6cf127c2b20834f6551482': { 526 | email: 'Robel.Ansel@yahoo.com', 527 | id: '5a6cf127c2b20834f6551482', 528 | posts: [ 529 | '5a6cf127c2b20834f6551484', 530 | '5a6cf127c2b20834f6551486', 531 | ], 532 | }, 533 | }, 534 | } 535 | 536 | ## snapshot :: `normalize` with `{ casing: "upper" }` 537 | 538 | > Snapshot 1 539 | 540 | { 541 | BLOGPOSTS: { 542 | '5a6cf127c2b20834f6551483': { 543 | id: '5a6cf127c2b20834f6551483', 544 | title: 'Aut aut reiciendis', 545 | }, 546 | '5a6cf127c2b20834f6551484': { 547 | id: '5a6cf127c2b20834f6551484', 548 | title: 'Sunt ut aut', 549 | }, 550 | '5a6cf127c2b20834f6551485': { 551 | id: '5a6cf127c2b20834f6551485', 552 | title: 'Nesciunt esse', 553 | }, 554 | '5a6cf127c2b20834f6551486': { 555 | id: '5a6cf127c2b20834f6551486', 556 | title: 'Nihil assumenda', 557 | }, 558 | }, 559 | COMMENTS: { 560 | '5a6cf127c2b20834f655148a': { 561 | id: '5a6cf127c2b20834f655148a', 562 | message: 'Voluptates ex sint amet repellendus impedit nam.', 563 | }, 564 | }, 565 | USERS: { 566 | '5a6cf127c2b20834f6551481': { 567 | email: 'Madisen_Braun@hotmail.com', 568 | id: '5a6cf127c2b20834f6551481', 569 | posts: [ 570 | '5a6cf127c2b20834f6551483', 571 | '5a6cf127c2b20834f6551485', 572 | ], 573 | }, 574 | '5a6cf127c2b20834f6551482': { 575 | email: 'Robel.Ansel@yahoo.com', 576 | id: '5a6cf127c2b20834f6551482', 577 | posts: [ 578 | '5a6cf127c2b20834f6551484', 579 | '5a6cf127c2b20834f6551486', 580 | ], 581 | }, 582 | }, 583 | } 584 | 585 | ## snapshot :: `normalize` with `{ plural: false }` 586 | 587 | > Snapshot 1 588 | 589 | { 590 | blogPost: { 591 | '5a6cf127c2b20834f6551483': { 592 | id: '5a6cf127c2b20834f6551483', 593 | title: 'Aut aut reiciendis', 594 | }, 595 | '5a6cf127c2b20834f6551484': { 596 | id: '5a6cf127c2b20834f6551484', 597 | title: 'Sunt ut aut', 598 | }, 599 | '5a6cf127c2b20834f6551485': { 600 | id: '5a6cf127c2b20834f6551485', 601 | title: 'Nesciunt esse', 602 | }, 603 | '5a6cf127c2b20834f6551486': { 604 | id: '5a6cf127c2b20834f6551486', 605 | title: 'Nihil assumenda', 606 | }, 607 | }, 608 | comment: { 609 | '5a6cf127c2b20834f655148a': { 610 | id: '5a6cf127c2b20834f655148a', 611 | message: 'Voluptates ex sint amet repellendus impedit nam.', 612 | }, 613 | }, 614 | user: { 615 | '5a6cf127c2b20834f6551481': { 616 | email: 'Madisen_Braun@hotmail.com', 617 | id: '5a6cf127c2b20834f6551481', 618 | posts: [ 619 | '5a6cf127c2b20834f6551483', 620 | '5a6cf127c2b20834f6551485', 621 | ], 622 | }, 623 | '5a6cf127c2b20834f6551482': { 624 | email: 'Robel.Ansel@yahoo.com', 625 | id: '5a6cf127c2b20834f6551482', 626 | posts: [ 627 | '5a6cf127c2b20834f6551484', 628 | '5a6cf127c2b20834f6551486', 629 | ], 630 | }, 631 | }, 632 | } 633 | 634 | ## snapshot :: `normalize` with custom "id" key 635 | 636 | > Snapshot 1 637 | 638 | { 639 | blogPosts: { 640 | '5a6cf127c2b20834f6551483': { 641 | _id: '5a6cf127c2b20834f6551483', 642 | title: 'Aut aut reiciendis', 643 | }, 644 | '5a6cf127c2b20834f6551484': { 645 | _id: '5a6cf127c2b20834f6551484', 646 | title: 'Sunt ut aut', 647 | }, 648 | '5a6cf127c2b20834f6551485': { 649 | _id: '5a6cf127c2b20834f6551485', 650 | title: 'Nesciunt esse', 651 | }, 652 | '5a6cf127c2b20834f6551486': { 653 | _id: '5a6cf127c2b20834f6551486', 654 | title: 'Nihil assumenda', 655 | }, 656 | }, 657 | comments: { 658 | '5a6cf127c2b20834f655148a': { 659 | _id: '5a6cf127c2b20834f655148a', 660 | message: 'Voluptates ex sint amet repellendus impedit nam.', 661 | }, 662 | }, 663 | users: { 664 | '5a6cf127c2b20834f6551481': { 665 | _id: '5a6cf127c2b20834f6551481', 666 | email: 'Madisen_Braun@hotmail.com', 667 | posts: [ 668 | '5a6cf127c2b20834f6551483', 669 | '5a6cf127c2b20834f6551485', 670 | ], 671 | }, 672 | '5a6cf127c2b20834f6551482': { 673 | _id: '5a6cf127c2b20834f6551482', 674 | email: 'Robel.Ansel@yahoo.com', 675 | posts: [ 676 | '5a6cf127c2b20834f6551484', 677 | '5a6cf127c2b20834f6551486', 678 | ], 679 | }, 680 | }, 681 | } 682 | 683 | ## snapshot :: `normalize` with custom entity names 684 | 685 | > Snapshot 1 686 | 687 | { 688 | accounts: { 689 | '5a6cf127c2b20834f6551481': { 690 | email: 'Madisen_Braun@hotmail.com', 691 | id: '5a6cf127c2b20834f6551481', 692 | posts: [ 693 | '5a6cf127c2b20834f6551483', 694 | '5a6cf127c2b20834f6551485', 695 | ], 696 | }, 697 | '5a6cf127c2b20834f6551482': { 698 | email: 'Robel.Ansel@yahoo.com', 699 | id: '5a6cf127c2b20834f6551482', 700 | posts: [ 701 | '5a6cf127c2b20834f6551484', 702 | '5a6cf127c2b20834f6551486', 703 | ], 704 | }, 705 | }, 706 | messages: { 707 | '5a6cf127c2b20834f655148a': { 708 | id: '5a6cf127c2b20834f655148a', 709 | message: 'Voluptates ex sint amet repellendus impedit nam.', 710 | }, 711 | }, 712 | stories: { 713 | '5a6cf127c2b20834f6551483': { 714 | id: '5a6cf127c2b20834f6551483', 715 | title: 'Aut aut reiciendis', 716 | }, 717 | '5a6cf127c2b20834f6551484': { 718 | id: '5a6cf127c2b20834f6551484', 719 | title: 'Sunt ut aut', 720 | }, 721 | '5a6cf127c2b20834f6551485': { 722 | id: '5a6cf127c2b20834f6551485', 723 | title: 'Nesciunt esse', 724 | }, 725 | '5a6cf127c2b20834f6551486': { 726 | id: '5a6cf127c2b20834f6551486', 727 | title: 'Nihil assumenda', 728 | }, 729 | }, 730 | } 731 | 732 | ## snapshot :: `normalize` with graphql connections 733 | 734 | > Snapshot 1 735 | 736 | { 737 | blogPosts: { 738 | '5a6cf127c2b20834f6551483': { 739 | id: '5a6cf127c2b20834f6551483', 740 | title: 'Aut aut reiciendis', 741 | }, 742 | '5a6cf127c2b20834f6551484': { 743 | id: '5a6cf127c2b20834f6551484', 744 | title: 'Sunt ut aut', 745 | }, 746 | '5a6cf127c2b20834f6551485': { 747 | id: '5a6cf127c2b20834f6551485', 748 | title: 'Nesciunt esse', 749 | }, 750 | '5a6cf127c2b20834f6551486': { 751 | id: '5a6cf127c2b20834f6551486', 752 | title: 'Nihil assumenda', 753 | }, 754 | }, 755 | users: { 756 | '5a6cf127c2b20834f6551481': { 757 | email: 'Madisen_Braun@hotmail.com', 758 | id: '5a6cf127c2b20834f6551481', 759 | posts: [ 760 | '5a6cf127c2b20834f6551483', 761 | '5a6cf127c2b20834f6551485', 762 | ], 763 | }, 764 | '5a6cf127c2b20834f6551482': { 765 | email: 'Robel.Ansel@yahoo.com', 766 | id: '5a6cf127c2b20834f6551482', 767 | posts: [ 768 | '5a6cf127c2b20834f6551484', 769 | '5a6cf127c2b20834f6551486', 770 | ], 771 | }, 772 | }, 773 | } 774 | 775 | ## snapshot :: `normalize` with graphql connections and custom entity names 776 | 777 | > Snapshot 1 778 | 779 | { 780 | accounts: { 781 | '5a6cf127c2b20834f6551481': { 782 | email: 'Madisen_Braun@hotmail.com', 783 | id: '5a6cf127c2b20834f6551481', 784 | posts: [ 785 | '5a6cf127c2b20834f6551483', 786 | '5a6cf127c2b20834f6551485', 787 | ], 788 | }, 789 | '5a6cf127c2b20834f6551482': { 790 | email: 'Robel.Ansel@yahoo.com', 791 | id: '5a6cf127c2b20834f6551482', 792 | posts: [ 793 | '5a6cf127c2b20834f6551484', 794 | '5a6cf127c2b20834f6551486', 795 | ], 796 | }, 797 | }, 798 | messages: { 799 | '5a6cf127c2b20834f655148a': { 800 | id: '5a6cf127c2b20834f655148a', 801 | message: 'Voluptates ex sint amet repellendus impedit nam.', 802 | }, 803 | }, 804 | stories: { 805 | '5a6cf127c2b20834f6551483': { 806 | id: '5a6cf127c2b20834f6551483', 807 | title: 'Aut aut reiciendis', 808 | }, 809 | '5a6cf127c2b20834f6551484': { 810 | id: '5a6cf127c2b20834f6551484', 811 | title: 'Sunt ut aut', 812 | }, 813 | '5a6cf127c2b20834f6551485': { 814 | id: '5a6cf127c2b20834f6551485', 815 | title: 'Nesciunt esse', 816 | }, 817 | '5a6cf127c2b20834f6551486': { 818 | id: '5a6cf127c2b20834f6551486', 819 | title: 'Nihil assumenda', 820 | }, 821 | }, 822 | } 823 | 824 | ## snapshot :: `normalize` with graphql connections and scalar arrays 825 | 826 | > Snapshot 1 827 | 828 | { 829 | blogPosts: { 830 | '5a6cf127c2b20834f6551483': { 831 | comments: [ 832 | '5a6cf127c2b20834f655148e', 833 | ], 834 | id: '5a6cf127c2b20834f6551483', 835 | likes: 0, 836 | tags: [ 837 | 'tags', 838 | 'are', 839 | 'boring', 840 | ], 841 | }, 842 | '5a6cf127c2b20834f6551485': { 843 | comments: [ 844 | '5a6cf127c2b20834f655148b', 845 | '5a6cf127c2b20834f655148d', 846 | ], 847 | id: '5a6cf127c2b20834f6551485', 848 | likes: 23, 849 | }, 850 | }, 851 | comments: { 852 | '5a6cf127c2b20834f655148b': { 853 | id: '5a6cf127c2b20834f655148b', 854 | message: 'Consectetur cum est odit et qui.', 855 | }, 856 | '5a6cf127c2b20834f655148d': { 857 | id: '5a6cf127c2b20834f655148d', 858 | message: 'Aut vel possimus nisi qui.', 859 | }, 860 | '5a6cf127c2b20834f655148e': { 861 | id: '5a6cf127c2b20834f655148e', 862 | message: 'Voluptates aut eum.', 863 | }, 864 | }, 865 | } 866 | 867 | ## snapshot :: `normalize` with graphql connections and type with same type fields 868 | 869 | > Snapshot 1 870 | 871 | { 872 | users: { 873 | '5a6cf127c2b20834f6551481': { 874 | email: 'Madisen_Braun@hotmail.com', 875 | id: '5a6cf127c2b20834f6551481', 876 | }, 877 | '5a6cf127c2b20834f6551482': { 878 | email: 'Robel.Ansel@yahoo.com', 879 | id: '5a6cf127c2b20834f6551482', 880 | }, 881 | '5a6efb94b0e8c36f99fba013': { 882 | email: 'Lloyd.Nikolaus@yahoo.com', 883 | friends: [ 884 | '5a6cf127c2b20834f6551481', 885 | '5a6cf127c2b20834f6551482', 886 | ], 887 | id: '5a6efb94b0e8c36f99fba013', 888 | referredBy: '5a6cf127c2b20834f6551481', 889 | }, 890 | }, 891 | } 892 | 893 | ## snapshot :: `normalize` with lists 894 | 895 | > Snapshot 1 896 | 897 | { 898 | blogPosts: [ 899 | { 900 | id: '5a6cf127c2b20834f6551483', 901 | title: 'Aut aut reiciendis', 902 | }, 903 | { 904 | id: '5a6cf127c2b20834f6551485', 905 | title: 'Nesciunt esse', 906 | }, 907 | { 908 | id: '5a6cf127c2b20834f6551484', 909 | title: 'Sunt ut aut', 910 | }, 911 | { 912 | id: '5a6cf127c2b20834f6551486', 913 | title: 'Nihil assumenda', 914 | }, 915 | ], 916 | comments: [ 917 | { 918 | id: '5a6cf127c2b20834f655148a', 919 | message: 'Voluptates ex sint amet repellendus impedit nam.', 920 | }, 921 | ], 922 | users: [ 923 | { 924 | email: 'Madisen_Braun@hotmail.com', 925 | id: '5a6cf127c2b20834f6551481', 926 | posts: [ 927 | '5a6cf127c2b20834f6551483', 928 | '5a6cf127c2b20834f6551485', 929 | ], 930 | }, 931 | { 932 | email: 'Robel.Ansel@yahoo.com', 933 | id: '5a6cf127c2b20834f6551482', 934 | posts: [ 935 | '5a6cf127c2b20834f6551484', 936 | '5a6cf127c2b20834f6551486', 937 | ], 938 | }, 939 | ], 940 | } 941 | 942 | ## snapshot :: `normalize` with scalar arrays 943 | 944 | > Snapshot 1 945 | 946 | { 947 | blogPosts: { 948 | '5a6cf127c2b20834f6551483': { 949 | __typename: 'BlogPost', 950 | comments: [ 951 | '5a6cf127c2b20834f655148e', 952 | ], 953 | id: '5a6cf127c2b20834f6551483', 954 | likes: 0, 955 | tags: [ 956 | 'tags', 957 | 'are', 958 | 'boring', 959 | ], 960 | }, 961 | '5a6cf127c2b20834f6551485': { 962 | __typename: 'BlogPost', 963 | comments: [ 964 | '5a6cf127c2b20834f655148b', 965 | '5a6cf127c2b20834f655148d', 966 | ], 967 | id: '5a6cf127c2b20834f6551485', 968 | likes: 23, 969 | }, 970 | }, 971 | comments: { 972 | '5a6cf127c2b20834f655148b': { 973 | __typename: 'Comment', 974 | id: '5a6cf127c2b20834f655148b', 975 | message: 'Consectetur cum est odit et qui.', 976 | }, 977 | '5a6cf127c2b20834f655148d': { 978 | __typename: 'Comment', 979 | id: '5a6cf127c2b20834f655148d', 980 | message: 'Aut vel possimus nisi qui.', 981 | }, 982 | '5a6cf127c2b20834f655148e': { 983 | __typename: 'Comment', 984 | id: '5a6cf127c2b20834f655148e', 985 | message: 'Voluptates aut eum.', 986 | }, 987 | }, 988 | } 989 | 990 | ## snapshot :: `normalize` with typenames 991 | 992 | > Snapshot 1 993 | 994 | { 995 | blogPosts: { 996 | '5a6cf127c2b20834f6551483': { 997 | __typename: 'BlogPost', 998 | id: '5a6cf127c2b20834f6551483', 999 | title: 'Aut aut reiciendis', 1000 | }, 1001 | '5a6cf127c2b20834f6551484': { 1002 | __typename: 'BlogPost', 1003 | id: '5a6cf127c2b20834f6551484', 1004 | title: 'Sunt ut aut', 1005 | }, 1006 | '5a6cf127c2b20834f6551485': { 1007 | __typename: 'BlogPost', 1008 | id: '5a6cf127c2b20834f6551485', 1009 | title: 'Nesciunt esse', 1010 | }, 1011 | '5a6cf127c2b20834f6551486': { 1012 | __typename: 'BlogPost', 1013 | id: '5a6cf127c2b20834f6551486', 1014 | title: 'Nihil assumenda', 1015 | }, 1016 | }, 1017 | comments: { 1018 | '5a6cf127c2b20834f655148a': { 1019 | __typename: 'Comment', 1020 | id: '5a6cf127c2b20834f655148a', 1021 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1022 | }, 1023 | }, 1024 | users: { 1025 | '5a6cf127c2b20834f6551481': { 1026 | __typename: 'User', 1027 | email: 'Madisen_Braun@hotmail.com', 1028 | id: '5a6cf127c2b20834f6551481', 1029 | posts: [ 1030 | '5a6cf127c2b20834f6551483', 1031 | '5a6cf127c2b20834f6551485', 1032 | ], 1033 | }, 1034 | '5a6cf127c2b20834f6551482': { 1035 | __typename: 'User', 1036 | email: 'Robel.Ansel@yahoo.com', 1037 | id: '5a6cf127c2b20834f6551482', 1038 | posts: [ 1039 | '5a6cf127c2b20834f6551484', 1040 | '5a6cf127c2b20834f6551486', 1041 | ], 1042 | }, 1043 | }, 1044 | } 1045 | 1046 | ## snapshot :: `normalize` with { typePointers: true } 1047 | 1048 | > Snapshot 1 1049 | 1050 | { 1051 | blogPosts: { 1052 | '5a6cf127c2b20834f6551483': { 1053 | id: '5a6cf127c2b20834f6551483', 1054 | title: 'Aut aut reiciendis', 1055 | }, 1056 | '5a6cf127c2b20834f6551484': { 1057 | id: '5a6cf127c2b20834f6551484', 1058 | title: 'Sunt ut aut', 1059 | }, 1060 | '5a6cf127c2b20834f6551485': { 1061 | id: '5a6cf127c2b20834f6551485', 1062 | title: 'Nesciunt esse', 1063 | }, 1064 | '5a6cf127c2b20834f6551486': { 1065 | id: '5a6cf127c2b20834f6551486', 1066 | title: 'Nihil assumenda', 1067 | }, 1068 | }, 1069 | comments: { 1070 | '5a6cf127c2b20834f655148a': { 1071 | id: '5a6cf127c2b20834f655148a', 1072 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1073 | }, 1074 | }, 1075 | users: { 1076 | '5a6cf127c2b20834f6551481': { 1077 | email: 'Madisen_Braun@hotmail.com', 1078 | id: '5a6cf127c2b20834f6551481', 1079 | posts: [ 1080 | { 1081 | collection: 'blogPosts', 1082 | id: '5a6cf127c2b20834f6551483', 1083 | }, 1084 | { 1085 | collection: 'blogPosts', 1086 | id: '5a6cf127c2b20834f6551485', 1087 | }, 1088 | ], 1089 | }, 1090 | '5a6cf127c2b20834f6551482': { 1091 | email: 'Robel.Ansel@yahoo.com', 1092 | id: '5a6cf127c2b20834f6551482', 1093 | posts: [ 1094 | { 1095 | collection: 'blogPosts', 1096 | id: '5a6cf127c2b20834f6551484', 1097 | }, 1098 | { 1099 | collection: 'blogPosts', 1100 | id: '5a6cf127c2b20834f6551486', 1101 | }, 1102 | ], 1103 | }, 1104 | }, 1105 | } 1106 | 1107 | ## snapshot :: `normalize` with { useConnections: true, typePointers: true } 1108 | 1109 | > Snapshot 1 1110 | 1111 | { 1112 | collections: { 1113 | '5a6cf127c2b20834f655148d': { 1114 | _id: '5a6cf127c2b20834f655148d', 1115 | name: 'Continue Watching', 1116 | videos: [ 1117 | { 1118 | _id: '5a6cf127c2b20834f655148a', 1119 | collection: 'movies', 1120 | }, 1121 | { 1122 | _id: '5a6cf127c2b20834f655148b', 1123 | collection: 'shows', 1124 | }, 1125 | { 1126 | _id: '5a6cf127c2b20834f655148c', 1127 | collection: 'movies', 1128 | }, 1129 | ], 1130 | }, 1131 | }, 1132 | movies: { 1133 | '5a6cf127c2b20834f655148a': { 1134 | _id: '5a6cf127c2b20834f655148a', 1135 | name: 'Batman', 1136 | }, 1137 | '5a6cf127c2b20834f655148c': { 1138 | _id: '5a6cf127c2b20834f655148c', 1139 | name: 'Superman', 1140 | }, 1141 | }, 1142 | shows: { 1143 | '5a6cf127c2b20834f655148b': { 1144 | _id: '5a6cf127c2b20834f655148b', 1145 | name: 'Prison Break', 1146 | }, 1147 | }, 1148 | } 1149 | 1150 | ## snapshot :: `normalize` without graphql connections but `useConnections` and `typePointers` flags set to true and custom entity names 1151 | 1152 | > Snapshot 1 1153 | 1154 | { 1155 | accounts: { 1156 | '5a6cf127c2b20834f6551481': { 1157 | email: 'Madisen_Braun@hotmail.com', 1158 | id: '5a6cf127c2b20834f6551481', 1159 | posts: [ 1160 | { 1161 | collection: 'stories', 1162 | id: '5a6cf127c2b20834f6551483', 1163 | }, 1164 | { 1165 | collection: 'stories', 1166 | id: '5a6cf127c2b20834f6551485', 1167 | }, 1168 | ], 1169 | }, 1170 | '5a6cf127c2b20834f6551482': { 1171 | email: 'Robel.Ansel@yahoo.com', 1172 | id: '5a6cf127c2b20834f6551482', 1173 | posts: [ 1174 | { 1175 | collection: 'stories', 1176 | id: '5a6cf127c2b20834f6551484', 1177 | }, 1178 | { 1179 | collection: 'stories', 1180 | id: '5a6cf127c2b20834f6551486', 1181 | }, 1182 | ], 1183 | }, 1184 | }, 1185 | messages: { 1186 | '5a6cf127c2b20834f655148a': { 1187 | id: '5a6cf127c2b20834f655148a', 1188 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1189 | }, 1190 | }, 1191 | stories: { 1192 | '5a6cf127c2b20834f6551483': { 1193 | id: '5a6cf127c2b20834f6551483', 1194 | title: 'Aut aut reiciendis', 1195 | }, 1196 | '5a6cf127c2b20834f6551484': { 1197 | id: '5a6cf127c2b20834f6551484', 1198 | title: 'Sunt ut aut', 1199 | }, 1200 | '5a6cf127c2b20834f6551485': { 1201 | id: '5a6cf127c2b20834f6551485', 1202 | title: 'Nesciunt esse', 1203 | }, 1204 | '5a6cf127c2b20834f6551486': { 1205 | id: '5a6cf127c2b20834f6551486', 1206 | title: 'Nihil assumenda', 1207 | }, 1208 | }, 1209 | } 1210 | 1211 | ## snapshot :: `normalize` without graphql connections but `useConnections` and `typePointers` flags set to true and scalar array 1212 | 1213 | > Snapshot 1 1214 | 1215 | { 1216 | blogPosts: { 1217 | '5a6cf127c2b20834f6551483': { 1218 | comments: [ 1219 | { 1220 | collection: 'comments', 1221 | id: '5a6cf127c2b20834f655148e', 1222 | }, 1223 | ], 1224 | id: '5a6cf127c2b20834f6551483', 1225 | likes: 0, 1226 | tags: [ 1227 | 'tags', 1228 | 'are', 1229 | 'boring', 1230 | ], 1231 | }, 1232 | '5a6cf127c2b20834f6551485': { 1233 | comments: [ 1234 | { 1235 | collection: 'comments', 1236 | id: '5a6cf127c2b20834f655148b', 1237 | }, 1238 | { 1239 | collection: 'comments', 1240 | id: '5a6cf127c2b20834f655148d', 1241 | }, 1242 | ], 1243 | id: '5a6cf127c2b20834f6551485', 1244 | likes: 23, 1245 | }, 1246 | }, 1247 | comments: { 1248 | '5a6cf127c2b20834f655148b': { 1249 | id: '5a6cf127c2b20834f655148b', 1250 | message: 'Consectetur cum est odit et qui.', 1251 | }, 1252 | '5a6cf127c2b20834f655148d': { 1253 | id: '5a6cf127c2b20834f655148d', 1254 | message: 'Aut vel possimus nisi qui.', 1255 | }, 1256 | '5a6cf127c2b20834f655148e': { 1257 | id: '5a6cf127c2b20834f655148e', 1258 | message: 'Voluptates aut eum.', 1259 | }, 1260 | }, 1261 | } 1262 | 1263 | ## snapshot :: `normalize` without graphql connections but `useConnections` flag set to true 1264 | 1265 | > Snapshot 1 1266 | 1267 | { 1268 | blogPosts: { 1269 | '5a6cf127c2b20834f6551483': { 1270 | id: '5a6cf127c2b20834f6551483', 1271 | title: 'Aut aut reiciendis', 1272 | }, 1273 | '5a6cf127c2b20834f6551484': { 1274 | id: '5a6cf127c2b20834f6551484', 1275 | title: 'Sunt ut aut', 1276 | }, 1277 | '5a6cf127c2b20834f6551485': { 1278 | id: '5a6cf127c2b20834f6551485', 1279 | title: 'Nesciunt esse', 1280 | }, 1281 | '5a6cf127c2b20834f6551486': { 1282 | id: '5a6cf127c2b20834f6551486', 1283 | title: 'Nihil assumenda', 1284 | }, 1285 | }, 1286 | comments: { 1287 | '5a6cf127c2b20834f655148a': { 1288 | id: '5a6cf127c2b20834f655148a', 1289 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1290 | }, 1291 | }, 1292 | users: { 1293 | '5a6cf127c2b20834f6551481': { 1294 | email: 'Madisen_Braun@hotmail.com', 1295 | id: '5a6cf127c2b20834f6551481', 1296 | posts: [ 1297 | '5a6cf127c2b20834f6551483', 1298 | '5a6cf127c2b20834f6551485', 1299 | ], 1300 | }, 1301 | '5a6cf127c2b20834f6551482': { 1302 | email: 'Robel.Ansel@yahoo.com', 1303 | id: '5a6cf127c2b20834f6551482', 1304 | posts: [ 1305 | '5a6cf127c2b20834f6551484', 1306 | '5a6cf127c2b20834f6551486', 1307 | ], 1308 | }, 1309 | }, 1310 | } 1311 | 1312 | ## snapshot :: `normalize` without graphql connections but `useConnections` flag set to true and custom entity names 1313 | 1314 | > Snapshot 1 1315 | 1316 | { 1317 | accounts: { 1318 | '5a6cf127c2b20834f6551481': { 1319 | email: 'Madisen_Braun@hotmail.com', 1320 | id: '5a6cf127c2b20834f6551481', 1321 | posts: [ 1322 | '5a6cf127c2b20834f6551483', 1323 | '5a6cf127c2b20834f6551485', 1324 | ], 1325 | }, 1326 | '5a6cf127c2b20834f6551482': { 1327 | email: 'Robel.Ansel@yahoo.com', 1328 | id: '5a6cf127c2b20834f6551482', 1329 | posts: [ 1330 | '5a6cf127c2b20834f6551484', 1331 | '5a6cf127c2b20834f6551486', 1332 | ], 1333 | }, 1334 | }, 1335 | messages: { 1336 | '5a6cf127c2b20834f655148a': { 1337 | id: '5a6cf127c2b20834f655148a', 1338 | message: 'Voluptates ex sint amet repellendus impedit nam.', 1339 | }, 1340 | }, 1341 | stories: { 1342 | '5a6cf127c2b20834f6551483': { 1343 | id: '5a6cf127c2b20834f6551483', 1344 | title: 'Aut aut reiciendis', 1345 | }, 1346 | '5a6cf127c2b20834f6551484': { 1347 | id: '5a6cf127c2b20834f6551484', 1348 | title: 'Sunt ut aut', 1349 | }, 1350 | '5a6cf127c2b20834f6551485': { 1351 | id: '5a6cf127c2b20834f6551485', 1352 | title: 'Nesciunt esse', 1353 | }, 1354 | '5a6cf127c2b20834f6551486': { 1355 | id: '5a6cf127c2b20834f6551486', 1356 | title: 'Nihil assumenda', 1357 | }, 1358 | }, 1359 | } 1360 | 1361 | ## snapshot :: `normalize` without graphql connections but `useConnections` flag set to true and scalar array 1362 | 1363 | > Snapshot 1 1364 | 1365 | { 1366 | blogPosts: { 1367 | '5a6cf127c2b20834f6551483': { 1368 | comments: [ 1369 | '5a6cf127c2b20834f655148e', 1370 | ], 1371 | id: '5a6cf127c2b20834f6551483', 1372 | likes: 0, 1373 | tags: [ 1374 | 'tags', 1375 | 'are', 1376 | 'boring', 1377 | ], 1378 | }, 1379 | '5a6cf127c2b20834f6551485': { 1380 | comments: [ 1381 | '5a6cf127c2b20834f655148b', 1382 | '5a6cf127c2b20834f655148d', 1383 | ], 1384 | id: '5a6cf127c2b20834f6551485', 1385 | likes: 23, 1386 | }, 1387 | }, 1388 | comments: { 1389 | '5a6cf127c2b20834f655148b': { 1390 | id: '5a6cf127c2b20834f655148b', 1391 | message: 'Consectetur cum est odit et qui.', 1392 | }, 1393 | '5a6cf127c2b20834f655148d': { 1394 | id: '5a6cf127c2b20834f655148d', 1395 | message: 'Aut vel possimus nisi qui.', 1396 | }, 1397 | '5a6cf127c2b20834f655148e': { 1398 | id: '5a6cf127c2b20834f655148e', 1399 | message: 'Voluptates aut eum.', 1400 | }, 1401 | }, 1402 | } 1403 | 1404 | ## snapshot :: `parse` with { useConnections: false } 1405 | 1406 | > Snapshot 1 1407 | 1408 | `query getCollections {␊ 1409 | users {␊ 1410 | __typename␊ 1411 | id␊ 1412 | friends {␊ 1413 | __typename␊ 1414 | id␊ 1415 | edges {␊ 1416 | __typename␊ 1417 | id␊ 1418 | node {␊ 1419 | __typename␊ 1420 | id␊ 1421 | name␊ 1422 | }␊ 1423 | }␊ 1424 | pageInfo {␊ 1425 | __typename␊ 1426 | id␊ 1427 | hasNextPage␊ 1428 | }␊ 1429 | }␊ 1430 | }␊ 1431 | }␊ 1432 | ` 1433 | 1434 | ## snapshot :: `parse` with { useConnections: true } 1435 | 1436 | > Snapshot 1 1437 | 1438 | `query getCollections {␊ 1439 | users {␊ 1440 | __typename␊ 1441 | id␊ 1442 | friends {␊ 1443 | edges {␊ 1444 | node {␊ 1445 | __typename␊ 1446 | id␊ 1447 | name␊ 1448 | }␊ 1449 | }␊ 1450 | pageInfo {␊ 1451 | hasNextPage␊ 1452 | }␊ 1453 | }␊ 1454 | }␊ 1455 | }␊ 1456 | ` 1457 | --------------------------------------------------------------------------------