├── .babelrc.js ├── .eslintignore ├── .eslintrc.js ├── .flowconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature_request.md │ └── support_question.md ├── PULL_REQUEST_TEMPLATE.md ├── lock.yml └── workflows │ └── close-pull-request.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── README.md ├── api.md ├── faqs.md ├── introduction.md ├── jsonapi.md └── quickstart.md ├── examples ├── .eslintrc ├── github │ ├── README.md │ ├── index.js │ ├── output.json │ └── schema.js ├── redux │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── index.js │ ├── package.json │ ├── src │ │ ├── api │ │ │ ├── index.js │ │ │ └── schema.js │ │ └── redux │ │ │ ├── actions.js │ │ │ ├── index.js │ │ │ ├── modules │ │ │ ├── commits.js │ │ │ ├── issues.js │ │ │ ├── labels.js │ │ │ ├── milestones.js │ │ │ ├── pull-requests.js │ │ │ ├── repos.js │ │ │ └── users.js │ │ │ ├── reducer.js │ │ │ └── selectors.js │ └── usage.gif └── relationships │ ├── README.md │ ├── index.js │ ├── input.json │ ├── output.json │ └── schema.js ├── husky.config.js ├── index.d.ts ├── jest.config.js ├── lint-staged.config.js ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── __tests__ │ ├── __snapshots__ │ │ └── index.test.js.snap │ └── index.test.js ├── index.js └── schemas │ ├── Array.js │ ├── Entity.js │ ├── ImmutableUtils.js │ ├── Object.js │ ├── Polymorphic.js │ ├── Union.js │ ├── Values.js │ └── __tests__ │ ├── Array.test.js │ ├── Entity.test.js │ ├── Object.test.js │ ├── Union.test.js │ ├── Values.test.js │ └── __snapshots__ │ ├── Array.test.js.snap │ ├── Entity.test.js.snap │ ├── Object.test.js.snap │ ├── Union.test.js.snap │ └── Values.test.js.snap ├── typescript-tests ├── array.ts ├── array_schema.ts ├── entity.ts ├── github.ts ├── object.ts ├── relationships.ts ├── union.ts └── values.ts └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, BABEL_ENV } = process.env; 2 | 3 | const cjs = BABEL_ENV === 'cjs' || NODE_ENV === 'test'; 4 | 5 | module.exports = { 6 | presets: [['@babel/preset-env', { loose: true }]], 7 | plugins: [ 8 | // cjs && 'transform-es2015-modules-commonjs', 9 | ['@babel/plugin-proposal-object-rest-spread', { loose: true }], 10 | ['@babel/plugin-proposal-class-properties', { loose: true }] 11 | ].filter(Boolean) 12 | }; 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | node_modules/* 3 | examples/*/node_modules/* 4 | coverage/* 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // babel parser to support ES6/7 features 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 7, 6 | ecmaFeatures: { 7 | experimentalObjectRestSpread: true, 8 | jsx: true, 9 | }, 10 | sourceType: 'module', 11 | }, 12 | extends: ['plugin:jest/recommended', 'prettier'], 13 | plugins: ['json', 'prettier'], 14 | env: { 15 | es6: true, 16 | node: true, 17 | }, 18 | globals: { 19 | document: false, 20 | navigator: false, 21 | window: false, 22 | }, 23 | rules: { 24 | 'accessor-pairs': 'error', 25 | camelcase: 'off', 26 | 'constructor-super': 'error', 27 | curly: ['error', 'all'], 28 | 'default-case': ['error', { commentPattern: '^no default$' }], 29 | eqeqeq: ['error', 'allow-null'], 30 | 'handle-callback-err': ['error', '^(err|error)$'], 31 | 'new-cap': ['error', { newIsCap: true, capIsNew: false }], 32 | 'no-alert': 'warn', 33 | 'no-array-constructor': 'error', 34 | 'no-caller': 'error', 35 | 'no-case-declarations': 'error', 36 | 'no-class-assign': 'error', 37 | 'no-compare-neg-zero': 'error', 38 | 'no-cond-assign': 'error', 39 | 'no-console': ['error', { allow: ['warn', 'error'] }], 40 | 'no-const-assign': 'error', 41 | 'no-control-regex': 'error', 42 | 'no-debugger': 'error', 43 | 'no-delete-var': 'error', 44 | 'no-dupe-args': 'error', 45 | 'no-dupe-class-members': 'error', 46 | 'no-dupe-keys': 'error', 47 | 'no-duplicate-case': 'error', 48 | 'no-empty-character-class': 'error', 49 | 'no-empty-pattern': 'error', 50 | 'no-eval': 'error', 51 | 'no-ex-assign': 'error', 52 | 'no-extend-native': 'error', 53 | 'no-extra-bind': 'error', 54 | 'no-extra-boolean-cast': 'error', 55 | 'no-fallthrough': 'error', 56 | 'no-func-assign': 'error', 57 | 'no-implied-eval': 'error', 58 | 'no-inner-declarations': ['error', 'functions'], 59 | 'no-invalid-regexp': 'error', 60 | 'no-iterator': 'error', 61 | 'no-label-var': 'error', 62 | 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], 63 | 'no-lone-blocks': 'error', 64 | 'no-loop-func': 'error', 65 | 'no-multi-str': 'error', 66 | 'no-native-reassign': 'error', 67 | 'no-negated-in-lhs': 'error', 68 | 'no-new': 'error', 69 | 'no-new-func': 'error', 70 | 'no-new-object': 'error', 71 | 'no-new-require': 'error', 72 | 'no-new-symbol': 'error', 73 | 'no-new-wrappers': 'error', 74 | 'no-obj-calls': 'error', 75 | 'no-octal': 'error', 76 | 'no-octal-escape': 'error', 77 | 'no-path-concat': 'error', 78 | 'no-proto': 'error', 79 | 'no-redeclare': 'error', 80 | 'no-regex-spaces': 'error', 81 | 'no-return-assign': ['error', 'except-parens'], 82 | 'no-script-url': 'error', 83 | 'no-self-assign': 'error', 84 | 'no-self-compare': 'error', 85 | 'no-sequences': 'error', 86 | 'no-shadow-restricted-names': 'error', 87 | 'no-sparse-arrays': 'error', 88 | 'no-this-before-super': 'error', 89 | 'no-throw-literal': 'error', 90 | 'no-undef': 'error', 91 | 'no-undef-init': 'error', 92 | 'no-unexpected-multiline': 'error', 93 | 'no-unmodified-loop-condition': 'error', 94 | 'no-unneeded-ternary': ['error', { defaultAssignment: false }], 95 | 'no-unreachable': 'error', 96 | 'no-unsafe-finally': 'error', 97 | 'no-unused-vars': ['error', { vars: 'all', args: 'none' }], 98 | 'no-useless-call': 'error', 99 | 'no-useless-computed-key': 'error', 100 | 'no-useless-concat': 'error', 101 | 'no-useless-constructor': 'error', 102 | 'no-useless-escape': 'error', 103 | 'no-var': 'error', 104 | 'no-with': 'error', 105 | 'prefer-const': 'error', 106 | 'prefer-rest-params': 'error', 107 | 'prefer-template': 'error', 108 | radix: 'error', 109 | 'require-yield': 'error', 110 | 'sort-imports': ['error', { memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'], ignoreCase: true }], 111 | 'spaced-comment': [ 112 | 'error', 113 | 'always', 114 | { markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] }, 115 | ], 116 | 'use-isnan': 'error', 117 | 'valid-typeof': 'error', 118 | yoda: ['error', 'never'], 119 | 120 | 'prettier/prettier': 'error', 121 | 122 | 'jest/consistent-test-it': ['error', { fn: 'test' }], 123 | 'jest/no-disabled-tests': 'error', 124 | 'jest/no-test-prefixes': 'error', 125 | 'jest/prefer-to-be-null': 'error', 126 | 'jest/prefer-to-be-undefined': 'error', 127 | 'jest/prefer-to-have-length': 'error', 128 | 'jest/valid-describe': 'error', 129 | 'jest/valid-expect': 'error', 130 | 'jest/valid-expect-in-promise': 'error', 131 | }, 132 | }; 133 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paularmstrong/normalizr/c2ab080641c7ebc6f5dc085a4ac074947f48ca58/.flowconfig -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: 4 | - paularmstrong 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: If something isn't working as expected 🤔. 4 | --- 5 | 6 | # Problem 7 | 8 | A short explanation of your problem or use-case is helpful! 9 | 10 | 15 | 16 | **Input** 17 | 18 | Here's how I'm using normalizr: 19 | 20 | ```js 21 | // Add as much relevant code and input as possible. 22 | const myData = { 23 | // This section is really helpful! A minimum test case goes a long way! 24 | }; 25 | const mySchema = new schema.Entity('myschema'); 26 | normalize(myData, mySchema); 27 | ``` 28 | 29 | **Output** 30 | 31 | Here's what I expect to see when I run the above: 32 | 33 | ```js 34 | { 35 | result: [1, 2], 36 | entities: { ... } 37 | } 38 | ``` 39 | 40 | Here's what I _actually_ see when I run the above: 41 | 42 | ```js 43 | { 44 | result: [1, 2], 45 | entities: { ... } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | about: I have a suggestion (and I might like to implement it myself 😀)! 4 | --- 5 | 6 | 13 | 14 | # Problem 15 | 16 | Explain the problem you'd like to have Normalizr handle. 17 | 18 | # Solution 19 | 20 | Explain a possible way to solve the problem. Keep in mind that there may be alternate ways to do the same thing. Try to think of those and weight the tradeoffs. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/support_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🤗 Support or Question 3 | about: If you have a question 💬, please check for help on StackOverflow! 4 | --- 5 | 6 | Issues on GitHub are intended to be related to problems with Normalizr itself. You are not likely to receive support with how to use it here 😁. 7 | 8 | --- 9 | 10 | If you have a support request or question please submit them to one of this resources: 11 | 12 | * StackOverflow: https://stackoverflow.com/questions/tagged/normalizr using the tag `normalizr` 13 | * Also have a look at the docs for more information on how to get do many things: https://github.com/paularmstrong/normalizr/tree/master/docs 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | # Problem 5 | 6 | Explain the problem that this pull request aims to resolve. 7 | 8 | # Solution 9 | 10 | Explain your approach. Sometimes it helps to justify your approach against some others that you didn't choose to explain why yours is better. 11 | 12 | # TODO 13 | 14 | - [ ] Add & update tests 15 | - [ ] Ensure CI is passing (lint, tests, flow) 16 | - [ ] Update relevant documentation 17 | -------------------------------------------------------------------------------- /.github/lock.yml: -------------------------------------------------------------------------------- 1 | # Configuration for lock-threads - https://github.com/dessant/lock-threads 2 | 3 | # Number of days of inactivity before a closed issue or pull request is locked 4 | daysUntilLock: 182 5 | 6 | # Issues and pull requests with these labels will not be locked. Set to `[]` to disable 7 | exemptLabels: [] 8 | 9 | # Label to add before locking, such as `outdated`. Set to `false` to disable 10 | lockLabel: Outdated 11 | 12 | # Comment to post before locking. Set to `false` to disable 13 | lockComment: > 14 | This thread has been automatically locked since there has not been 15 | any recent activity after it was closed. Please open a new issue for 16 | related bugs. 17 | -------------------------------------------------------------------------------- /.github/workflows/close-pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Close Pull Request 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: superbrothers/close-pull-request@v3 12 | with: 13 | comment: 'Normalizr is no longer maintained and does not accept pull requests. Please maintain your own fork of this repository.' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/* 4 | dist/* 5 | *.log 6 | coverage/* 7 | .coveralls.yml 8 | .eslintcache 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '12' 5 | - '14' 6 | script: 7 | - npm run lint:ci 8 | - npm run test:ci 9 | - npm run build 10 | - npm run typecheck 11 | after_success: 12 | - npm run test:coverage 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v3.6.1 2 | 3 | - **Fixed** Add types for fallback strategy 4 | - **Chore** Upgraded development dependencies 5 | 6 | # v3.6.0 7 | 8 | - **Added** `fallbackStrategy` for denormalization (#422) 9 | - **Fixed** entities can be `undefined` in TS defs if none found (#435) 10 | 11 | # v3.5.0 12 | 13 | - **Added** ability to dynamically set nested schema type (#415) 14 | - **Changed** Enable loose transformation for object spread operator to improve performance (#431) 15 | - **Fixed** don't use schema to attribute mapping on singular array schemas (#387) 16 | - **Fixed** When normalize() receives null input, don't say it is an object (#411) 17 | - **Fixed** Improve performance of circular reference detection (#420) 18 | 19 | # v3.4.0 20 | 21 | - **Changed** Now built with Babel 7 22 | - **Added** Support for circular references (gh-335) 23 | - **Added** Symbols are valid keys for Entity keys (gh-369) 24 | - **Added/Changed** Typescript definitions include generics for `normalize` (gh-363) 25 | - **Fixed** denormalization skipping of falsy valued ids used in `Object` schemas (gh-345) 26 | - **Chore** Update dev dependencies 27 | - **Chore** Added Greenkeeper 28 | 29 | # v3.3.0 30 | 31 | - **Added** ES Module builds 32 | - **Fixed** type error with typescript on array+object shorthand (gh-322) 33 | 34 | # v3.2.0 35 | 36 | - **Added** Support denormalizing from Immutable entities (gh-228) 37 | - **Added** Brought back `get idAttribute()` to `schema.Entity` (gh-226) 38 | - **Fixed** Gracefully handle missing data in `denormalize` (gh-232) 39 | - **Fixed** Prevent infinite recursion in `denormalize` (gh-220) 40 | 41 | # v3.1.0 42 | 43 | - **Added** `denormalize`. (gh-214) 44 | - **Changed** No longer requires all input in a polymorphic schema (`Array`, `Union`, `Values`) have a matching schema definition. (gh-208) 45 | - **Changed** Builds do both rollup and plain babel file conversions. `"main"` property in package.json points to babel-converted files. 46 | 47 | # v3.0.0 48 | 49 | The entire normalizr package has been rewritten from v2.x for this version. Please refer to the [documentation](/docs) for all changes. 50 | 51 | ## Added 52 | 53 | - `schema.Entity` 54 | - `processStrategy` for modifying `Entity` objects before they're moved to the `entities` stack. 55 | - `mergeStrategy` for merging with multiple entities with the same ID. 56 | - Added `schema.Object`, with a shorthand of `{}` 57 | - Added `schema.Array`, with a shorthand of `[ schema ]` 58 | 59 | ## Changed 60 | 61 | - `Schema` has been moved to a `schema` namespace, available at `schema.Entity` 62 | - `arrayOf` has been replaced by `schema.Array` or `[]` 63 | - `unionOf` has been replaced by `schema.Union` 64 | - `valuesOf` has been replaced by `schema.Values` 65 | 66 | ## Removed 67 | 68 | - `normalize` no longer accepts an optional `options` argument. All options are assigned at the schema level. 69 | - Entity schema no longer accepts `defaults` as an option. Use a custom `processStrategy` option to apply defaults as needed. 70 | - `assignEntity` has been replaced by `processStrategy` 71 | - `meta` option. See `processStrategy` 72 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issues 4 | 5 | 1. Follow the [Issue Template](/.github/ISSUE_TEMPLATE.md) provided when opening a new Issue. 6 | 2. Provide a minimal, reproducible test-case. 7 | 3. Do not ask for help or usage questions in Issues. Use [StackOverflow](http://stackoverflow.com/questions/tagged/normalizr) for those. 8 | 9 | ## Pull Requests 10 | 11 | First, thank you so much for contributing to open source and the Normalizr project! 12 | 13 | Follow the instructions on the Pull Request Template (shown when you open a new PR) and make sure you've done the following: 14 | 15 | - [ ] Add & update tests 16 | - [ ] Ensure CI is passing (lint, tests) 17 | - [ ] Update relevant documentation 18 | 19 | ## Setup 20 | 21 | Normalizr uses [yarn](https://yarnpkg.com) for development dependency management. Ensure you have it installed before continuing. 22 | 23 | ```sh 24 | yarn 25 | ``` 26 | 27 | ## Running Tests 28 | 29 | ```sh 30 | npm run test 31 | ``` 32 | 33 | ## Lint 34 | 35 | Standard code style is nice. ESLint is used to ensure we continue to write similar code. The following command will also fix simple issues, like spacing and alphabetized imports: 36 | 37 | ```sh 38 | npm run lint 39 | ``` 40 | 41 | ## Building 42 | 43 | Normalizr aims to keep its byte-size as low as possible. Ensure your changes don't incur more space than seems necessary for your feature or change: 44 | 45 | ```sh 46 | npm run build 47 | ``` 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dan Abramov, Paul Armstrong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # normalizr [![build status](https://img.shields.io/travis/paularmstrong/normalizr/master.svg?style=flat-square)](https://travis-ci.org/paularmstrong/normalizr) [![Coverage Status](https://img.shields.io/coveralls/paularmstrong/normalizr/master.svg?style=flat-square)](https://coveralls.io/github/paularmstrong/normalizr?branch=master) [![npm version](https://img.shields.io/npm/v/normalizr.svg?style=flat-square)](https://www.npmjs.com/package/normalizr) [![npm downloads](https://img.shields.io/npm/dm/normalizr.svg?style=flat-square)](https://www.npmjs.com/package/normalizr) 2 | 3 | # 📣 Normalizr is no longer maintained 4 | 5 | Due to lack of ability to find an invested maintainer and inability to find time to do routine maintenance and community building, this package is no longer maintained. Please see the discussion [🤝 Maintainer help wanted](https://github.com/paularmstrong/normalizr/discussions/493) for more information. 6 | 7 | ## FAQs 8 | 9 | ### Should I still use Normalizr? 10 | 11 | If you need it, yes. Normalizr is and has been at a stable release for a very long time, used by thousands of others without issue. 12 | 13 | ### What should I do if I want other features or found a bug? 14 | 15 | Fork [Normalizr on Github](https://github.com/paularmstrong/normalizr) and maintain a version yourself. 16 | 17 | ### Can I contribute back to Normalizr? 18 | 19 | There are no current plans to resurrect this origin of Normalizr. If a forked version becomes sufficiently maintained and popular, please reach out about merging the fork and changing maintainers. 20 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # normalizr [![build status](https://img.shields.io/travis/paularmstrong/normalizr/master.svg?style=flat-square)](https://travis-ci.org/paularmstrong/normalizr) [![Coverage Status](https://img.shields.io/coveralls/paularmstrong/normalizr/master.svg?style=flat-square)](https://coveralls.io/github/paularmstrong/normalizr?branch=master) [![npm version](https://img.shields.io/npm/v/normalizr.svg?style=flat-square)](https://www.npmjs.com/package/normalizr) [![npm downloads](https://img.shields.io/npm/dm/normalizr.svg?style=flat-square)](https://www.npmjs.com/package/normalizr) 2 | 3 | ## Install 4 | 5 | Install from the NPM repository using yarn or npm: 6 | 7 | ```shell 8 | yarn add normalizr 9 | ``` 10 | 11 | ```shell 12 | npm install normalizr 13 | ``` 14 | 15 | ## Motivation 16 | 17 | Many APIs, public or not, return JSON data that has deeply nested objects. Using data in this kind of structure is often very difficult for JavaScript applications, especially those using [Flux](http://facebook.github.io/flux/) or [Redux](http://redux.js.org/). 18 | 19 | ## Solution 20 | 21 | Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries. 22 | 23 | ## Documentation 24 | 25 | - [Introduction](/docs/introduction.md) 26 | - [Build Files](/docs/introduction.md#build-files) 27 | - [Quick Start](/docs/quickstart.md) 28 | - [API](/docs/api.md) 29 | - [normalize](/docs/api.md#normalizedata-schema) 30 | - [denormalize](/docs/api.md#denormalizeinput-schema-entities) 31 | - [schema](/docs/api.md#schema) 32 | - [Using with JSONAPI](/docs/jsonapi.md) 33 | 34 | ## Examples 35 | 36 | - [Normalizing GitHub Issues](/examples/github) 37 | - [Relational Data](/examples/relationships) 38 | - [Interactive Redux](/examples/redux) 39 | 40 | ## Quick Start 41 | 42 | Consider a typical blog post. The API response for a single post might look something like this: 43 | 44 | ```json 45 | { 46 | "id": "123", 47 | "author": { 48 | "id": "1", 49 | "name": "Paul" 50 | }, 51 | "title": "My awesome blog post", 52 | "comments": [ 53 | { 54 | "id": "324", 55 | "commenter": { 56 | "id": "2", 57 | "name": "Nicole" 58 | } 59 | } 60 | ] 61 | } 62 | ``` 63 | 64 | We have two nested entity types within our `article`: `users` and `comments`. Using various `schema`, we can normalize all three entity types down: 65 | 66 | ```js 67 | import { normalize, schema } from 'normalizr'; 68 | 69 | // Define a users schema 70 | const user = new schema.Entity('users'); 71 | 72 | // Define your comments schema 73 | const comment = new schema.Entity('comments', { 74 | commenter: user, 75 | }); 76 | 77 | // Define your article 78 | const article = new schema.Entity('articles', { 79 | author: user, 80 | comments: [comment], 81 | }); 82 | 83 | const normalizedData = normalize(originalData, article); 84 | ``` 85 | 86 | Now, `normalizedData` will be: 87 | 88 | ```js 89 | { 90 | result: "123", 91 | entities: { 92 | "articles": { 93 | "123": { 94 | id: "123", 95 | author: "1", 96 | title: "My awesome blog post", 97 | comments: [ "324" ] 98 | } 99 | }, 100 | "users": { 101 | "1": { "id": "1", "name": "Paul" }, 102 | "2": { "id": "2", "name": "Nicole" } 103 | }, 104 | "comments": { 105 | "324": { id: "324", "commenter": "2" } 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | ## Dependencies 112 | 113 | None. 114 | 115 | ## Credits 116 | 117 | Normalizr was originally created by [Dan Abramov](http://github.com/gaearon) and inspired by a conversation with [Jing Chen](https://twitter.com/jingc). Since v3, it was completely rewritten and maintained by [Paul Armstrong](https://twitter.com/paularmstrong). It has also received much help, enthusiasm, and contributions from [community members](https://github.com/paularmstrong/normalizr/graphs/contributors). 118 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | - [normalize](#normalizedata-schema) 4 | - [denormalize](#denormalizeinput-schema-entities) 5 | - [schema](#schema) 6 | - [Array](#arraydefinition-schemaattribute) 7 | - [Entity](#entitykey-definition---options--) 8 | - [Object](#objectdefinition) 9 | - [Union](#uniondefinition-schemaattribute) 10 | - [Values](#valuesdefinition-schemaattribute) 11 | 12 | ## `normalize(data, schema)` 13 | 14 | Normalizes input data per the schema definition provided. 15 | 16 | - `data`: **required** Input JSON (or plain JS object) data that needs normalization. 17 | - `schema`: **required** A schema definition 18 | 19 | ### Usage 20 | 21 | ```js 22 | import { normalize, schema } from 'normalizr'; 23 | 24 | const myData = { users: [{ id: 1 }, { id: 2 }] }; 25 | const user = new schema.Entity('users'); 26 | const mySchema = { users: [user] }; 27 | const normalizedData = normalize(myData, mySchema); 28 | ``` 29 | 30 | ### Output 31 | 32 | ```js 33 | { 34 | result: { users: [ 1, 2 ] }, 35 | entities: { 36 | users: { 37 | '1': { id: 1 }, 38 | '2': { id: 2 } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | ## `denormalize(input, schema, entities)` 45 | 46 | Denormalizes an input based on schema and provided entities from a plain object or Immutable data. The reverse of `normalize`. 47 | 48 | _Special Note:_ Be careful with denormalization. Prematurely reverting your data to large, nested objects could cause performance impacts in React (and other) applications. 49 | 50 | If your schema and data have recursive references, only the first instance of an entity will be given. Subsequent references will be returned as the `id` provided. 51 | 52 | - `input`: **required** The normalized result that should be _de-normalized_. Usually the same value that was given in the `result` key of the output of `normalize`. 53 | - `schema`: **required** A schema definition that was used to get the value for `input`. 54 | - `entities`: **required** An object, keyed by entity schema names that may appear in the denormalized output. Also accepts an object with Immutable data. 55 | 56 | ### Usage 57 | 58 | ```js 59 | import { denormalize, schema } from 'normalizr'; 60 | 61 | const user = new schema.Entity('users'); 62 | const mySchema = { users: [user] }; 63 | const entities = { users: { '1': { id: 1 }, '2': { id: 2 } } }; 64 | const denormalizedData = denormalize({ users: [1, 2] }, mySchema, entities); 65 | ``` 66 | 67 | ### Output 68 | 69 | ```js 70 | { 71 | users: [{ id: 1 }, { id: 2 }]; 72 | } 73 | ``` 74 | 75 | ## `schema` 76 | 77 | ### `Array(definition, schemaAttribute)` 78 | 79 | Creates a schema to normalize an array of schemas. If the input value is an `Object` instead of an `Array`, the normalized result will be an `Array` of the `Object`'s values. 80 | 81 | _Note: The same behavior can be defined with shorthand syntax: `[ mySchema ]`_ 82 | 83 | - `definition`: **required** A singular schema that this array contains _or_ a mapping of schema to attribute values. 84 | - `schemaAttribute`: _optional_ (required if `definition` is not a singular schema) The attribute on each entity found that defines what schema, per the definition mapping, to use when normalizing. 85 | Can be a string or a function. If given a function, accepts the following arguments: 86 | _ `value`: The input value of the entity. 87 | _ `parent`: The parent object of the input array. \* `key`: The key at which the input array appears on the parent object. 88 | 89 | #### Instance Methods 90 | 91 | - `define(definition)`: When used, the `definition` passed in will be merged with the original definition passed to the `Array` constructor. This method tends to be useful for creating circular references in schema. 92 | 93 | #### Usage 94 | 95 | To describe a simple array of a singular entity type: 96 | 97 | ```js 98 | const data = [{ id: '123', name: 'Jim' }, { id: '456', name: 'Jane' }]; 99 | const userSchema = new schema.Entity('users'); 100 | 101 | const userListSchema = new schema.Array(userSchema); 102 | // or use shorthand syntax: 103 | const userListSchema = [userSchema]; 104 | 105 | const normalizedData = normalize(data, userListSchema); 106 | ``` 107 | 108 | #### Output 109 | 110 | ```js 111 | { 112 | entities: { 113 | users: { 114 | '123': { id: '123', name: 'Jim' }, 115 | '456': { id: '456', name: 'Jane' } 116 | } 117 | }, 118 | result: [ '123', '456' ] 119 | } 120 | ``` 121 | 122 | If your input data is an array of more than one type of entity, it is necessary to define a schema mapping. 123 | 124 | _Note: If your data returns an object that you did not provide a mapping for, the original object will be returned in the result and an entity will not be created._ 125 | 126 | For example: 127 | 128 | ```js 129 | const data = [{ id: 1, type: 'admin' }, { id: 2, type: 'user' }]; 130 | 131 | const userSchema = new schema.Entity('users'); 132 | const adminSchema = new schema.Entity('admins'); 133 | const myArray = new schema.Array( 134 | { 135 | admins: adminSchema, 136 | users: userSchema 137 | }, 138 | (input, parent, key) => `${input.type}s` 139 | ); 140 | 141 | const normalizedData = normalize(data, myArray); 142 | ``` 143 | 144 | #### Output 145 | 146 | ```js 147 | { 148 | entities: { 149 | admins: { '1': { id: 1, type: 'admin' } }, 150 | users: { '2': { id: 2, type: 'user' } } 151 | }, 152 | result: [ 153 | { id: 1, schema: 'admins' }, 154 | { id: 2, schema: 'users' } 155 | ] 156 | } 157 | ``` 158 | 159 | ### `Entity(key, definition = {}, options = {})` 160 | 161 | - `key`: **required** The key name under which all entities of this type will be listed in the normalized response. Must be a string name. 162 | - `definition`: A definition of the nested entities found within this entity. Defaults to empty object. 163 | You _do not_ need to define any keys in your entity other than those that hold nested entities. All other values will be copied to the normalized entity's output. 164 | - `options`: 165 | - `idAttribute`: The attribute where unique IDs for each of this entity type can be found. 166 | Accepts either a string `key` or a function that returns the IDs `value`. Defaults to `'id'`. This function can and will be run multiple times – which means your generated ID _must_ be the same every time the function is run. Using a random number/string generator like `uuid` will cause unexpected errors. 167 | As a function, accepts the following arguments, in order: 168 | - `value`: The input value of the entity. 169 | - `parent`: The parent object of the input array. 170 | - `key`: The key at which the input array appears on the parent object. 171 | - `mergeStrategy(entityA, entityB)`: Strategy to use when merging two entities with the same `id` value. Defaults to merge the more recently found entity onto the previous. 172 | - `processStrategy(value, parent, key)`: Strategy to use when pre-processing the entity. Use this method to add extra data, defaults, and/or completely change the entity before normalization is complete. Defaults to returning a shallow copy of the input entity. 173 | _Note: It is recommended to always return a copy of your input and not modify the original._ 174 | The function accepts the following arguments, in order: 175 | - `value`: The input value of the entity. 176 | - `parent`: The parent object of the input array. 177 | - `key`: The key at which the input array appears on the parent object. 178 | - `fallbackStrategy(key, schema)`: Strategy to use when denormalizing data structures with id references to missing entities. 179 | - `key`: The key at which the input array appears on the parent object. 180 | - `schema`: The schema of the missing entity 181 | 182 | #### Instance Methods 183 | 184 | - `define(definition)`: When used, the `definition` passed in will be merged with the original definition passed to the `Entity` constructor. This method tends to be useful for creating circular references in schema. 185 | 186 | #### Instance Attributes 187 | 188 | - `key`: Returns the key provided to the constructor. 189 | - `idAttribute`: Returns the idAttribute provided to the constructor in options. 190 | 191 | #### Usage 192 | 193 | ```js 194 | const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } }; 195 | 196 | const user = new schema.Entity('users', {}, { idAttribute: 'id_str' }); 197 | const tweet = new schema.Entity( 198 | 'tweets', 199 | { user: user }, 200 | { 201 | idAttribute: 'id_str', 202 | // Apply everything from entityB over entityA, except for "favorites" 203 | mergeStrategy: (entityA, entityB) => ({ 204 | ...entityA, 205 | ...entityB, 206 | favorites: entityA.favorites 207 | }), 208 | // Remove the URL field from the entity 209 | processStrategy: (entity) => omit(entity, 'url') 210 | } 211 | ); 212 | 213 | const normalizedData = normalize(data, tweet); 214 | ``` 215 | 216 | #### Output 217 | 218 | ```js 219 | { 220 | entities: { 221 | tweets: { '123': { id_str: '123', user: '456' } }, 222 | users: { '456': { id_str: '456', name: 'Jimmy' } } 223 | }, 224 | result: '123' 225 | } 226 | ``` 227 | 228 | #### `idAttribute` Usage 229 | 230 | When passing the `idAttribute` a function, it should return the IDs value. 231 | 232 | For Example: 233 | 234 | ```js 235 | const data = [{ id: '1', guest_id: null, name: 'Esther' }, { id: '1', guest_id: '22', name: 'Tom' }]; 236 | 237 | const patronsSchema = new schema.Entity('patrons', undefined, { 238 | // idAttribute *functions* must return the ids **value** (not key) 239 | idAttribute: (value) => (value.guest_id ? `${value.id}-${value.guest_id}` : value.id) 240 | }); 241 | 242 | normalize(data, [patronsSchema]); 243 | ``` 244 | 245 | #### Output 246 | 247 | ```js 248 | { 249 | entities: { 250 | patrons: { 251 | '1': { id: '1', guest_id: null, name: 'Esther' }, 252 | '1-22': { id: '1', guest_id: '22', name: 'Tom' }, 253 | } 254 | }, 255 | result: ['1', '1-22'] 256 | } 257 | ``` 258 | 259 | #### `fallbackStrategy` Usage 260 | ```js 261 | const users = { 262 | '1': { id: '1', name: "Emily", requestState: 'SUCCEEDED' }, 263 | '2': { id: '2', name: "Douglas", requestState: 'SUCCEEDED' } 264 | }; 265 | const books = { 266 | '1': {id: '1', name: "Book 1", author: 1 }, 267 | '2': {id: '2', name: "Book 2", author: 2 }, 268 | '3': {id: '3', name: "Book 3", author: 3 } 269 | }; 270 | 271 | const authorSchema = new schema.Entity('authors', {}, { 272 | fallbackStrategy: (key, schema) => { 273 | return { 274 | [schema.idAttribute]: key, 275 | name: 'Unknown', 276 | requestState: 'NONE' 277 | }; 278 | } 279 | }); 280 | const bookSchema = new schema.Entity('books', { 281 | author: authorSchema 282 | }); 283 | 284 | denormalize([1, 2, 3], [bookSchema], { 285 | books, 286 | authors: users 287 | }) 288 | 289 | ``` 290 | 291 | 292 | #### Output 293 | ```js 294 | [ 295 | { 296 | id: '1', 297 | name: "Book 1", 298 | author: { id: '1', name: "Emily", requestState: 'SUCCEEDED' } 299 | }, 300 | { 301 | id: '2', 302 | name: "Book 2", 303 | author: { id: '2', name: "Douglas", requestState: 'SUCCEEDED' }, 304 | }, 305 | { 306 | id: '3', 307 | name: "Book 3", 308 | author: { id: '3', name: "Unknown", requestState: 'NONE' }, 309 | } 310 | ] 311 | 312 | ``` 313 | 314 | ### `Object(definition)` 315 | 316 | Define a plain object mapping that has values needing to be normalized into Entities. _Note: The same behavior can be defined with shorthand syntax: `{ ... }`_ 317 | 318 | - `definition`: **required** A definition of the nested entities found within this object. Defaults to empty object. 319 | You _do not_ need to define any keys in your object other than those that hold other entities. All other values will be copied to the normalized output. 320 | 321 | #### Instance Methods 322 | 323 | - `define(definition)`: When used, the `definition` passed in will be merged with the original definition passed to the `Object` constructor. This method tends to be useful for creating circular references in schema. 324 | 325 | #### Usage 326 | 327 | ```js 328 | // Example data response 329 | const data = { users: [{ id: '123', name: 'Beth' }] }; 330 | 331 | const user = new schema.Entity('users'); 332 | const responseSchema = new schema.Object({ users: new schema.Array(user) }); 333 | // or shorthand 334 | const responseSchema = { users: new schema.Array(user) }; 335 | 336 | const normalizedData = normalize(data, responseSchema); 337 | ``` 338 | 339 | #### Output 340 | 341 | ```js 342 | { 343 | entities: { 344 | users: { '123': { id: '123', name: 'Beth' } } 345 | }, 346 | result: { users: [ '123' ] } 347 | } 348 | ``` 349 | 350 | ### `Union(definition, schemaAttribute)` 351 | 352 | Describe a schema which is a union of multiple schemas. This is useful if you need the polymorphic behavior provided by `schema.Array` or `schema.Values` but for non-collection fields. 353 | 354 | - `definition`: **required** An object mapping the definition of the nested entities found within the input array 355 | - `schemaAttribute`: **required** The attribute on each entity found that defines what schema, per the definition mapping, to use when normalizing. 356 | Can be a string or a function. If given a function, accepts the following arguments: 357 | - `value`: The input value of the entity. 358 | - `parent`: The parent object of the input array. 359 | - `key`: The key at which the input array appears on the parent object. 360 | 361 | #### Instance Methods 362 | 363 | - `define(definition)`: When used, the `definition` passed in will be merged with the original definition passed to the `Union` constructor. This method tends to be useful for creating circular references in schema. 364 | 365 | #### Usage 366 | 367 | _Note: If your data returns an object that you did not provide a mapping for, the original object will be returned in the result and an entity will not be created._ 368 | 369 | ```js 370 | const data = { owner: { id: 1, type: 'user', name: 'Anne' } }; 371 | 372 | const user = new schema.Entity('users'); 373 | const group = new schema.Entity('groups'); 374 | const unionSchema = new schema.Union( 375 | { 376 | user: user, 377 | group: group 378 | }, 379 | 'type' 380 | ); 381 | 382 | const normalizedData = normalize(data, { owner: unionSchema }); 383 | ``` 384 | 385 | #### Output 386 | 387 | ```js 388 | { 389 | entities: { 390 | users: { '1': { id: 1, type: 'user', name: 'Anne' } } 391 | }, 392 | result: { owner: { id: 1, schema: 'user' } } 393 | } 394 | ``` 395 | 396 | ### `Values(definition, schemaAttribute)` 397 | 398 | Describes a map whose values follow the given schema. 399 | 400 | - `definition`: **required** A singular schema that this array contains _or_ a mapping of schema to attribute values. 401 | - `schemaAttribute`: _optional_ (required if `definition` is not a singular schema) The attribute on each entity found that defines what schema, per the definition mapping, to use when normalizing. 402 | Can be a string or a function. If given a function, accepts the following arguments: 403 | - `value`: The input value of the entity. 404 | - `parent`: The parent object of the input array. 405 | - `key`: The key at which the input array appears on the parent object. 406 | 407 | #### Instance Methods 408 | 409 | - `define(definition)`: When used, the `definition` passed in will be merged with the original definition passed to the `Values` constructor. This method tends to be useful for creating circular references in schema. 410 | 411 | #### Usage 412 | 413 | ```js 414 | const data = { firstThing: { id: 1 }, secondThing: { id: 2 } }; 415 | 416 | const item = new schema.Entity('items'); 417 | const valuesSchema = new schema.Values(item); 418 | 419 | const normalizedData = normalize(data, valuesSchema); 420 | ``` 421 | 422 | #### Output 423 | 424 | ```js 425 | { 426 | entities: { 427 | items: { '1': { id: 1 }, '2': { id: 2 } } 428 | }, 429 | result: { firstThing: 1, secondThing: 2 } 430 | } 431 | ``` 432 | 433 | If your input data is an object that has values of more than one type of entity, but their schema is not easily defined by the key, you can use a mapping of schema, much like `schema.Union` and `schema.Array`. 434 | 435 | _Note: If your data returns an object that you did not provide a mapping for, the original object will be returned in the result and an entity will not be created._ 436 | 437 | For example: 438 | 439 | ```js 440 | const data = { 441 | '1': { id: 1, type: 'admin' }, 442 | '2': { id: 2, type: 'user' } 443 | }; 444 | 445 | const userSchema = new schema.Entity('users'); 446 | const adminSchema = new schema.Entity('admins'); 447 | const valuesSchema = new schema.Values( 448 | { 449 | admins: adminSchema, 450 | users: userSchema 451 | }, 452 | (input, parent, key) => `${input.type}s` 453 | ); 454 | 455 | const normalizedData = normalize(data, valuesSchema); 456 | ``` 457 | 458 | #### Output 459 | 460 | ```js 461 | { 462 | entities: { 463 | admins: { '1': { id: 1, type: 'admin' } }, 464 | users: { '2': { id: 2, type: 'user' } } 465 | }, 466 | result: { 467 | '1': { id: 1, schema: 'admins' }, 468 | '2': { id: 2, schema: 'users' } 469 | } 470 | } 471 | ``` 472 | -------------------------------------------------------------------------------- /docs/faqs.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | If you are having trouble with Normalizr, try [StackOverflow](http://stackoverflow.com/questions/tagged/normalizr). There is a larger community there that will help you solve issues a lot quicker than opening an Issue on the Normalizr GitHub page. 4 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ## Motivation 4 | 5 | Many APIs, public or not, return JSON data that has deeply nested objects. Using data in this kind of structure is often very difficult for JavaScript applications, especially those using [Flux](http://facebook.github.io/flux/) or [Redux](http://redux.js.org/). 6 | 7 | ## Solution 8 | 9 | Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries. 10 | 11 | ### Example 12 | 13 | The following nested object: 14 | 15 | ```js 16 | [ 17 | { 18 | id: 1, 19 | title: 'Some Article', 20 | author: { 21 | id: 1, 22 | name: 'Dan' 23 | } 24 | }, 25 | { 26 | id: 2, 27 | title: 'Other Article', 28 | author: { 29 | id: 1, 30 | name: 'Dan' 31 | } 32 | } 33 | ]; 34 | ``` 35 | 36 | Can be normalized to: 37 | 38 | ```js 39 | { 40 | result: [1, 2], 41 | entities: { 42 | articles: { 43 | 1: { 44 | id: 1, 45 | title: 'Some Article', 46 | author: 1 47 | }, 48 | 2: { 49 | id: 2, 50 | title: 'Other Article', 51 | author: 1 52 | } 53 | }, 54 | users: { 55 | 1: { 56 | id: 1, 57 | name: 'Dan' 58 | } 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | ## Build Files 65 | 66 | Normalizr is built for various environments 67 | 68 | - `src/*` 69 | - CommonJS, unpacked files. These are the recommended files for use with your own package bundler and are the default in-point as defined by this modules `package.json`. 70 | - `normalizr.js`, `normalizr.min.js` 71 | - [CommonJS](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) 72 | - `normalizr.amd.js`, `normalizr.amd.min.js` 73 | - [Asynchronous Module Definition](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) 74 | - `normalizr.umd.js`, `normalizr.umn.min.js` 75 | - [Universal Module Definition](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) 76 | - `normalizr.browser.js`, `normalizr.browser.min.js` 77 | - [IIFE](http://benalman.com/news/2010/11/immediately-invoked-function-expression/) / Immediately-Invoked Function Expression, suitable for use as a standalone script import in the browser. 78 | - Note: It is not recommended to use packages like Normalizr with direct browser `` tags. Consider a package bundler like [webpack](https://webpack.github.io/), [rollup](https://rollupjs.org/), or [browserify](http://browserify.org/) instead. 79 | -------------------------------------------------------------------------------- /docs/jsonapi.md: -------------------------------------------------------------------------------- 1 | # Normalizr and JSONAPI 2 | 3 | If you're using JSONAPI, you're ahead of the curve, but also in a bit of a tough spot. JSONAPI is a great spec, but doesn't play nicely with the way that you want to manage data in Redux/Flux style state management applications. 4 | 5 | Just as well, Normalizr was not written for JSONAPI and really doesn't work well. Instead, stop what you're doing now and check out some of the other great libraries and packages available that are written specifically for normalizing JSONAPI data\*: 6 | 7 | - [stevenpetryk/jsonapi-normalizer](https://github.com/stevenpetryk/jsonapi-normalizer) 8 | - [yury-dymov/json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer) 9 | - [JSONAPI client libraries](http://jsonapi.org/implementations/#client-libraries-javascript) 10 | 11 | **Note:** These are in no particular order. Review all libraries on your own before deciding which is best for your particular use-case. 12 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Consider a typical blog post. The API response for a single post might look something like this: 4 | 5 | ```json 6 | { 7 | "id": "123", 8 | "author": { 9 | "id": "1", 10 | "name": "Paul" 11 | }, 12 | "title": "My awesome blog post", 13 | "comments": [ 14 | { 15 | "id": "324", 16 | "commenter": { 17 | "id": "2", 18 | "name": "Nicole" 19 | } 20 | } 21 | ] 22 | } 23 | ``` 24 | 25 | We have two nested entity types within our `article`: `users` and `comments`. Using various `schema`, we can normalize all three entity types down: 26 | 27 | ```js 28 | import { normalize, schema } from 'normalizr'; 29 | 30 | // Define a users schema 31 | const user = new schema.Entity('users'); 32 | 33 | // Define your comments schema 34 | const comment = new schema.Entity('comments', { 35 | commenter: user 36 | }); 37 | 38 | // Define your article 39 | const article = new schema.Entity('articles', { 40 | author: user, 41 | comments: [comment] 42 | }); 43 | 44 | const normalizedData = normalize(originalData, article); 45 | ``` 46 | 47 | Now, `normalizedData` will be: 48 | 49 | ```js 50 | { 51 | result: "123", 52 | entities: { 53 | "articles": { 54 | "123": { 55 | id: "123", 56 | author: "1", 57 | title: "My awesome blog post", 58 | comments: [ "324" ] 59 | } 60 | }, 61 | "users": { 62 | "1": { "id": "1", "name": "Paul" }, 63 | "2": { "id": "2", "name": "Nicole" } 64 | }, 65 | "comments": { 66 | "324": { id: "324", "commenter": "2" } 67 | } 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": "off" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/github/README.md: -------------------------------------------------------------------------------- 1 | # Normalizing GitHub Issues 2 | 3 | This is a barebones example for node to illustrate how normalizing the GitHub Issues API endpoint could work. 4 | 5 | ## Running 6 | 7 | ```sh 8 | # from the root directory: 9 | yarn 10 | # from this directory: 11 | ../../node_modules/.bin/babel-node ./index.js 12 | ``` 13 | 14 | ## Files 15 | 16 | * [index.js](/examples/github/index.js): Pulls live data from the GitHub API for this project's issues and normalizes the JSON. 17 | * [output.json](/examples/github/output.json): A sample of the normalized output. 18 | * [schema.js](/examples/github/schema.js): The schema used to normalize the GitHub issues. 19 | -------------------------------------------------------------------------------- /examples/github/index.js: -------------------------------------------------------------------------------- 1 | import * as schema from './schema'; 2 | import fs from 'fs'; 3 | import https from 'https'; 4 | import { normalize } from '../../src'; 5 | import path from 'path'; 6 | 7 | let data = ''; 8 | const request = https.request( 9 | { 10 | host: 'api.github.com', 11 | path: '/repos/paularmstrong/normalizr/issues', 12 | method: 'get', 13 | headers: { 14 | 'user-agent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)', 15 | }, 16 | }, 17 | (res) => { 18 | res.on('data', (d) => { 19 | data += d; 20 | }); 21 | 22 | res.on('end', () => { 23 | const normalizedData = normalize(JSON.parse(data), schema.issueOrPullRequest); 24 | const out = JSON.stringify(normalizedData, null, 2); 25 | fs.writeFileSync(path.resolve(__dirname, './output.json'), out); 26 | }); 27 | 28 | res.on('error', (e) => { 29 | console.log(e); 30 | }); 31 | } 32 | ); 33 | 34 | request.end(); 35 | -------------------------------------------------------------------------------- /examples/github/schema.js: -------------------------------------------------------------------------------- 1 | import { schema } from '../../src'; 2 | 3 | export const user = new schema.Entity('users'); 4 | 5 | export const label = new schema.Entity('labels'); 6 | 7 | export const milestone = new schema.Entity('milestones', { 8 | creator: user, 9 | }); 10 | 11 | export const issue = new schema.Entity('issues', { 12 | assignee: user, 13 | assignees: [user], 14 | labels: label, 15 | milestone, 16 | user, 17 | }); 18 | 19 | export const pullRequest = new schema.Entity('pullRequests', { 20 | assignee: user, 21 | assignees: [user], 22 | labels: label, 23 | milestone, 24 | user, 25 | }); 26 | 27 | export const issueOrPullRequest = new schema.Array( 28 | { 29 | issues: issue, 30 | pullRequests: pullRequest, 31 | }, 32 | (entity) => (entity.pull_request ? 'pullRequests' : 'issues') 33 | ); 34 | -------------------------------------------------------------------------------- /examples/redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/redux/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | *.log 3 | -------------------------------------------------------------------------------- /examples/redux/README.md: -------------------------------------------------------------------------------- 1 | # Redux 2 | 3 | This is a simple example of using Normalizr with Redux and Redux-Thunk. The command-line utility allows you to pull some data from public GitHub repos and browse/display the normalized data as saved to the Redux state tree. 4 | 5 | ![redux example in use](/examples/redux/usage.gif) 6 | 7 | ## Running 8 | 9 | From this directory, run the following and follow the on-screen options: 10 | 11 | ```sh 12 | # from the root directory: 13 | yarn # or npm install 14 | # from this directory: 15 | yarn # or npm install 16 | npm run start 17 | ``` 18 | -------------------------------------------------------------------------------- /examples/redux/index.js: -------------------------------------------------------------------------------- 1 | import * as Action from './src/redux/actions'; 2 | import * as Selector from './src/redux/selectors'; 3 | import inquirer from 'inquirer'; 4 | import store from './src/redux'; 5 | 6 | const REPO = 'paularmstrong/normalizr'; 7 | 8 | const start = () => { 9 | inquirer 10 | .prompt([ 11 | { 12 | type: 'input', 13 | name: 'repo', 14 | message: 'What is the slug of the repo you wish to browseMain?', 15 | default: REPO, 16 | validate: (input) => { 17 | if (!/^[a-zA-Z0-9]+\/[a-zA-Z0-9]+/.test(input)) { 18 | return 'Repo slug must be in the form "user/project"'; 19 | } 20 | return true; 21 | }, 22 | }, 23 | ]) 24 | .then(({ repo }) => { 25 | store.dispatch(Action.setRepo(repo)); 26 | main(); 27 | }); 28 | }; 29 | 30 | const main = () => { 31 | return inquirer 32 | .prompt([ 33 | { 34 | type: 'list', 35 | name: 'action', 36 | message: 'What would you like to do?', 37 | choices: ['Browse current state', 'Get new data', new inquirer.Separator(), 'Quit'], 38 | }, 39 | ]) 40 | .then(({ action }) => { 41 | switch (action) { 42 | case 'Browse current state': 43 | return browseMain(); 44 | case 'Get new data': 45 | return pull(); 46 | default: 47 | return process.exit(); 48 | } 49 | }); 50 | }; 51 | 52 | const browseMain = () => { 53 | return inquirer 54 | .prompt([ 55 | { 56 | type: 'list', 57 | name: 'browseMainAction', 58 | message: 'What would you like to do?', 59 | choices: () => { 60 | return [ 61 | { value: 'print', name: 'Print the entire state tree' }, 62 | new inquirer.Separator(), 63 | ...Object.keys(store.getState()).map((value) => ({ value, name: `Browse ${value}` })), 64 | new inquirer.Separator(), 65 | { value: 'main', name: 'Go Back to Main Menu' }, 66 | ]; 67 | }, 68 | }, 69 | ]) 70 | .then((answers) => { 71 | switch (answers.browseMainAction) { 72 | case 'main': 73 | return main(); 74 | case 'print': 75 | console.log(JSON.stringify(store.getState(), null, 2)); 76 | return browseMain(); 77 | default: 78 | return browse(answers.browseMainAction); 79 | } 80 | }); 81 | }; 82 | 83 | const browse = (stateKey) => { 84 | return inquirer 85 | .prompt([ 86 | { 87 | type: 'list', 88 | name: 'action', 89 | message: `Browse ${stateKey}`, 90 | choices: [ 91 | { value: 'count', name: 'Show # of Objects' }, 92 | { value: 'keys', name: 'List All Keys' }, 93 | { value: 'view', name: 'View by Key' }, 94 | { value: 'all', name: 'View All' }, 95 | { value: 'denormalize', name: 'Denormalize' }, 96 | new inquirer.Separator(), 97 | { value: 'browseMain', name: 'Go Back to Browse Menu' }, 98 | { value: 'main', name: 'Go Back to Main Menu' }, 99 | ], 100 | }, 101 | { 102 | type: 'list', 103 | name: 'list', 104 | message: `Select the ${stateKey} to view:`, 105 | choices: Object.keys(store.getState()[stateKey]), 106 | when: ({ action }) => action === 'view', 107 | }, 108 | ]) 109 | .then(({ action, list }) => { 110 | const state = store.getState()[stateKey]; 111 | if (list) { 112 | console.log(JSON.stringify(state[list], null, 2)); 113 | } 114 | switch (action) { 115 | case 'count': 116 | console.log(`-> ${Object.keys(state).length} items.`); 117 | return browse(stateKey); 118 | case 'keys': 119 | Object.keys(state).map((key) => console.log(key)); 120 | return browse(stateKey); 121 | case 'all': 122 | console.log(JSON.stringify(state, null, 2)); 123 | return browse(stateKey); 124 | case 'denormalize': 125 | return browseDenormalized(stateKey); 126 | case 'browseMain': 127 | return browseMain(); 128 | case 'main': 129 | return main(); 130 | default: 131 | return browse(stateKey); 132 | } 133 | }); 134 | }; 135 | 136 | const browseDenormalized = (stateKey) => { 137 | return inquirer 138 | .prompt([ 139 | { 140 | type: 'list', 141 | name: 'selector', 142 | message: `Denormalize a/and ${stateKey} entity`, 143 | choices: [ 144 | ...Object.keys(store.getState()[stateKey]), 145 | new inquirer.Separator(), 146 | { value: 'browse', name: 'Go Back to Browse Menu' }, 147 | { value: 'main', name: 'Go Back to Main Menu' }, 148 | ], 149 | }, 150 | ]) 151 | .then(({ selector }) => { 152 | switch (selector) { 153 | case 'browse': 154 | return browse(stateKey); 155 | case 'main': 156 | return main(); 157 | default: { 158 | const data = Selector[`select${stateKey.replace(/s$/, '')}`](store.getState(), selector); 159 | console.log(JSON.stringify(data, null, 2)); 160 | return browseDenormalized(stateKey); 161 | } 162 | } 163 | }); 164 | }; 165 | 166 | const pull = () => { 167 | return inquirer 168 | .prompt([ 169 | { 170 | type: 'list', 171 | name: 'pullAction', 172 | message: 'What data would you like to fetch?', 173 | choices: () => { 174 | return [ 175 | ...Object.keys(store.getState()).map((value) => ({ value, name: value })), 176 | new inquirer.Separator(), 177 | { value: 'main', name: 'Go Back to Main Menu' }, 178 | ]; 179 | }, 180 | }, 181 | ]) 182 | .then((answers) => { 183 | switch (answers.pullAction) { 184 | case 'commits': 185 | return store.dispatch(Action.getCommits()).then(pull); 186 | case 'issues': 187 | return store.dispatch(Action.getIssues()).then(pull); 188 | case 'labels': 189 | return store.dispatch(Action.getLabels()).then(pull); 190 | case 'milestones': 191 | return store.dispatch(Action.getMilestones()).then(pull); 192 | case 'pullRequests': 193 | return store.dispatch(Action.getPullRequests()).then(pull); 194 | case 'main': 195 | default: 196 | return main(); 197 | } 198 | }); 199 | }; 200 | 201 | start(); 202 | -------------------------------------------------------------------------------- /examples/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normalizr-redux-example", 3 | "version": "0.0.0", 4 | "description": "And example of using Normalizr with Redux", 5 | "main": "index.js", 6 | "author": "Paul Armstrong", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "start": "babel-node ./" 11 | }, 12 | "dependencies": { 13 | "babel-cli": "^6.18.0", 14 | "babel-preset-es2015": "^6.18.0", 15 | "babel-preset-stage-1": "^6.16.0", 16 | "github": "^14.0.0", 17 | "inquirer": "^6.3.1", 18 | "redux": "^4.0.1", 19 | "redux-thunk": "^2.1.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/redux/src/api/index.js: -------------------------------------------------------------------------------- 1 | import GitHubApi from 'github'; 2 | 3 | export default new GitHubApi({ 4 | headers: { 5 | 'user-agent': 'Normalizr Redux Example', 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/redux/src/api/schema.js: -------------------------------------------------------------------------------- 1 | import { schema } from '../../../../src'; 2 | 3 | export const user = new schema.Entity('users'); 4 | 5 | export const commit = new schema.Entity( 6 | 'commits', 7 | { 8 | author: user, 9 | committer: user, 10 | }, 11 | { idAttribute: 'sha' } 12 | ); 13 | 14 | export const label = new schema.Entity('labels'); 15 | 16 | export const milestone = new schema.Entity('milestones', { 17 | creator: user, 18 | }); 19 | 20 | export const issue = new schema.Entity('issues', { 21 | assignee: user, 22 | assignees: [user], 23 | labels: [label], 24 | milestone, 25 | user, 26 | }); 27 | 28 | export const pullRequest = new schema.Entity('pullRequests', { 29 | assignee: user, 30 | assignees: [user], 31 | labels: [label], 32 | milestone, 33 | user, 34 | }); 35 | 36 | export const issueOrPullRequest = new schema.Array( 37 | { 38 | issues: issue, 39 | pullRequests: pullRequest, 40 | }, 41 | (entity) => (entity.pull_request ? 'pullRequests' : 'issues') 42 | ); 43 | -------------------------------------------------------------------------------- /examples/redux/src/redux/actions.js: -------------------------------------------------------------------------------- 1 | export { getCommits } from './modules/commits'; 2 | export { getIssues } from './modules/issues'; 3 | export { getLabels } from './modules/labels'; 4 | export { getMilestones } from './modules/milestones'; 5 | export { getPullRequests } from './modules/pull-requests'; 6 | export { setRepo } from './modules/repos'; 7 | 8 | export const ADD_ENTITIES = 'ADD_ENTITIES'; 9 | export const addEntities = (entities) => ({ 10 | type: ADD_ENTITIES, 11 | payload: entities, 12 | }); 13 | -------------------------------------------------------------------------------- /examples/redux/src/redux/index.js: -------------------------------------------------------------------------------- 1 | import * as schema from '../api/schema'; 2 | import api from '../api'; 3 | import reducer from './reducer'; 4 | import thunk from 'redux-thunk'; 5 | import { applyMiddleware, createStore } from 'redux'; 6 | 7 | export default createStore(reducer, applyMiddleware(thunk.withExtraArgument({ api, schema }))); 8 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/commits.js: -------------------------------------------------------------------------------- 1 | import * as Repo from './repos'; 2 | import { commit } from '../../api/schema'; 3 | import { ADD_ENTITIES, addEntities } from '../actions'; 4 | import { denormalize, normalize } from '../../../../../src'; 5 | 6 | export const STATE_KEY = 'commits'; 7 | 8 | export default function reducer(state = {}, action) { 9 | switch (action.type) { 10 | case ADD_ENTITIES: 11 | return { 12 | ...state, 13 | ...action.payload.commits, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export const getCommits = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { 22 | const state = getState(); 23 | const owner = Repo.selectOwner(state); 24 | const repo = Repo.selectRepo(state); 25 | return api.repos 26 | .getCommits({ 27 | owner, 28 | repo, 29 | }) 30 | .then((response) => { 31 | const data = normalize(response, [schema.commit]); 32 | dispatch(addEntities(data.entities)); 33 | return response; 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | }; 39 | 40 | export const selectHydrated = (state, id) => denormalize(id, commit, state); 41 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/issues.js: -------------------------------------------------------------------------------- 1 | import * as Repo from './repos'; 2 | import { issue } from '../../api/schema'; 3 | import { ADD_ENTITIES, addEntities } from '../actions'; 4 | import { denormalize, normalize } from '../../../../../src'; 5 | 6 | export const STATE_KEY = 'issues'; 7 | 8 | export default function reducer(state = {}, action) { 9 | switch (action.type) { 10 | case ADD_ENTITIES: 11 | return { 12 | ...state, 13 | ...action.payload.issues, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export const getIssues = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { 22 | const state = getState(); 23 | const owner = Repo.selectOwner(state); 24 | const repo = Repo.selectRepo(state); 25 | return api.issues 26 | .getForRepo({ 27 | owner, 28 | repo, 29 | }) 30 | .then((response) => { 31 | const data = normalize(response, [schema.issue]); 32 | dispatch(addEntities(data.entities)); 33 | return response; 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | }; 39 | 40 | export const selectHydrated = (state, id) => denormalize(id, issue, state); 41 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/labels.js: -------------------------------------------------------------------------------- 1 | import * as Repo from './repos'; 2 | import { label } from '../../api/schema'; 3 | import { ADD_ENTITIES, addEntities } from '../actions'; 4 | import { denormalize, normalize } from '../../../../../src'; 5 | 6 | export const STATE_KEY = 'labels'; 7 | 8 | export default function reducer(state = {}, action) { 9 | switch (action.type) { 10 | case ADD_ENTITIES: 11 | return { 12 | ...state, 13 | ...action.payload.labels, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export const getLabels = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { 22 | const state = getState(); 23 | const owner = Repo.selectOwner(state); 24 | const repo = Repo.selectRepo(state); 25 | return api.issues 26 | .getLabels({ 27 | owner, 28 | repo, 29 | }) 30 | .then((response) => { 31 | const data = normalize(response, [schema.label]); 32 | dispatch(addEntities(data.entities)); 33 | return response; 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | }; 39 | 40 | export const selectHydrated = (state, id) => denormalize(id, label, state); 41 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/milestones.js: -------------------------------------------------------------------------------- 1 | import * as Repo from './repos'; 2 | import { milestone } from '../../api/schema'; 3 | import { ADD_ENTITIES, addEntities } from '../actions'; 4 | import { denormalize, normalize } from '../../../../../src'; 5 | 6 | export const STATE_KEY = 'milestones'; 7 | 8 | export default function reducer(state = {}, action) { 9 | switch (action.type) { 10 | case ADD_ENTITIES: 11 | return { 12 | ...state, 13 | ...action.payload.milestones, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export const getMilestones = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { 22 | const state = getState(); 23 | const owner = Repo.selectOwner(state); 24 | const repo = Repo.selectRepo(state); 25 | return api.issues 26 | .getMilestones({ 27 | owner, 28 | repo, 29 | }) 30 | .then((response) => { 31 | const data = normalize(response, [schema.milestone]); 32 | dispatch(addEntities(data.entities)); 33 | return response; 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | }; 39 | 40 | export const selectHydrated = (state, id) => denormalize(id, milestone, state); 41 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/pull-requests.js: -------------------------------------------------------------------------------- 1 | import * as Repo from './repos'; 2 | import { pullRequest } from '../../api/schema'; 3 | import { ADD_ENTITIES, addEntities } from '../actions'; 4 | import { denormalize, normalize } from '../../../../../src'; 5 | 6 | export const STATE_KEY = 'pullRequests'; 7 | 8 | export default function reducer(state = {}, action) { 9 | switch (action.type) { 10 | case ADD_ENTITIES: 11 | return { 12 | ...state, 13 | ...action.payload.pullRequests, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export const getPullRequests = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { 22 | const state = getState(); 23 | const owner = Repo.selectOwner(state); 24 | const repo = Repo.selectRepo(state); 25 | return api.pullRequests 26 | .getAll({ 27 | owner, 28 | repo, 29 | }) 30 | .then((response) => { 31 | const data = normalize(response, [schema.pullRequest]); 32 | dispatch(addEntities(data.entities)); 33 | return response; 34 | }) 35 | .catch((error) => { 36 | console.error(error); 37 | }); 38 | }; 39 | 40 | export const selectHydrated = (state, id) => denormalize(id, pullRequest, state); 41 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/repos.js: -------------------------------------------------------------------------------- 1 | export const STATE_KEY = 'repo'; 2 | 3 | export default function reducer(state = {}, action) { 4 | switch (action.type) { 5 | case Action.SET_REPO: 6 | return { 7 | ...state, 8 | ...action.payload, 9 | }; 10 | 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | const Action = { 17 | SET_REPO: 'SET_REPO', 18 | }; 19 | 20 | export const setRepo = (slug) => { 21 | const [owner, repo] = slug.split('/'); 22 | return { 23 | type: Action.SET_REPO, 24 | payload: { owner, repo }, 25 | }; 26 | }; 27 | 28 | export const selectOwner = (state) => state[STATE_KEY].owner; 29 | export const selectRepo = (state) => state[STATE_KEY].repo; 30 | -------------------------------------------------------------------------------- /examples/redux/src/redux/modules/users.js: -------------------------------------------------------------------------------- 1 | import { ADD_ENTITIES } from '../actions'; 2 | import { denormalize } from '../../../../../src'; 3 | import { user } from '../../api/schema'; 4 | 5 | export const STATE_KEY = 'users'; 6 | 7 | export default function reducer(state = {}, action) { 8 | switch (action.type) { 9 | case ADD_ENTITIES: 10 | return Object.entries(action.payload.users).reduce((mergedUsers, [id, user]) => { 11 | return { 12 | ...mergedUsers, 13 | [id]: { 14 | ...(mergedUsers[id] || {}), 15 | ...user, 16 | }, 17 | }; 18 | }, state); 19 | 20 | default: 21 | return state; 22 | } 23 | } 24 | 25 | export const selectHydrated = (state, id) => denormalize(id, user, state); 26 | -------------------------------------------------------------------------------- /examples/redux/src/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import commits, { STATE_KEY as COMMITS_STATE_KEY } from './modules/commits'; 3 | import issues, { STATE_KEY as ISSUES_STATE_KEY } from './modules/issues'; 4 | import labels, { STATE_KEY as LABELS_STATE_KEY } from './modules/labels'; 5 | import milestones, { STATE_KEY as MILESTONES_STATE_KEY } from './modules/milestones'; 6 | import pullRequests, { STATE_KEY as PULLREQUESTS_STATE_KEY } from './modules/pull-requests'; 7 | import repos, { STATE_KEY as REPO_STATE_KEY } from './modules/repos'; 8 | import users, { STATE_KEY as USERS_STATE_KEY } from './modules/users'; 9 | 10 | const reducer = combineReducers({ 11 | [COMMITS_STATE_KEY]: commits, 12 | [ISSUES_STATE_KEY]: issues, 13 | [LABELS_STATE_KEY]: labels, 14 | [MILESTONES_STATE_KEY]: milestones, 15 | [PULLREQUESTS_STATE_KEY]: pullRequests, 16 | [REPO_STATE_KEY]: repos, 17 | [USERS_STATE_KEY]: users, 18 | }); 19 | 20 | export default reducer; 21 | -------------------------------------------------------------------------------- /examples/redux/src/redux/selectors.js: -------------------------------------------------------------------------------- 1 | export { selectHydrated as selectcommit } from './modules/commits'; 2 | export { selectHydrated as selectissue } from './modules/issues'; 3 | export { selectHydrated as selectlabel } from './modules/labels'; 4 | export { selectHydrated as selectmilestone } from './modules/milestones'; 5 | export { selectHydrated as selectpullRequest } from './modules/pull-requests'; 6 | -------------------------------------------------------------------------------- /examples/redux/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paularmstrong/normalizr/c2ab080641c7ebc6f5dc085a4ac074947f48ca58/examples/redux/usage.gif -------------------------------------------------------------------------------- /examples/relationships/README.md: -------------------------------------------------------------------------------- 1 | # Dealing with Relationships 2 | 3 | Occasionally, it is useful to have all one-to-one, one-to-many, and many-to-many relationship data on entities. Normalizr does not handle this automatically, but this example shows a simple way of adding relationship handling on a special-case basis. 4 | 5 | ## Running 6 | 7 | ```sh 8 | # from the root directory: 9 | yarn 10 | # from this directory: 11 | ../../node_modules/.bin/babel-node ./index.js 12 | ``` 13 | 14 | ## Files 15 | 16 | * [index.js](/examples/relationships/index.js): Pulls live data from the GitHub API for this project's issues and normalizes the JSON. 17 | * [input.json](/examples/relationships/input.json): The raw JSON data before normalization. 18 | * [output.json](/examples/relationships/output.json): The normalized output. 19 | * [schema.js](/examples/relationships/schema.js): The schema used to normalize the GitHub issues. 20 | -------------------------------------------------------------------------------- /examples/relationships/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import input from './input.json'; 3 | import { normalize } from '../../src'; 4 | import path from 'path'; 5 | import postsSchema from './schema'; 6 | 7 | const normalizedData = normalize(input, postsSchema); 8 | const output = JSON.stringify(normalizedData, null, 2); 9 | fs.writeFileSync(path.resolve(__dirname, './output.json'), output); 10 | -------------------------------------------------------------------------------- /examples/relationships/input.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "1", 4 | "title": "My first post!", 5 | "author": { 6 | "id": "123", 7 | "name": "Paul" 8 | }, 9 | "comments": [ 10 | { 11 | "id": "249", 12 | "content": "Nice post!", 13 | "commenter": { 14 | "id": "245", 15 | "name": "Jane" 16 | } 17 | }, 18 | { 19 | "id": "250", 20 | "content": "Thanks!", 21 | "commenter": { 22 | "id": "123", 23 | "name": "Paul" 24 | } 25 | } 26 | ] 27 | }, 28 | { 29 | "id": "2", 30 | "title": "This other post", 31 | "author": { 32 | "id": "123", 33 | "name": "Paul" 34 | }, 35 | "comments": [ 36 | { 37 | "id": "251", 38 | "content": "Your other post was nicer", 39 | "commenter": { 40 | "id": "245", 41 | "name": "Jane" 42 | } 43 | }, 44 | { 45 | "id": "252", 46 | "content": "I am a spammer!", 47 | "commenter": { 48 | "id": "246", 49 | "name": "Spambot5000" 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /examples/relationships/output.json: -------------------------------------------------------------------------------- 1 | { 2 | "entities": { 3 | "users": { 4 | "123": { 5 | "id": "123", 6 | "name": "Paul", 7 | "posts": [ 8 | "1", 9 | "2" 10 | ], 11 | "comments": [ 12 | "250" 13 | ] 14 | }, 15 | "245": { 16 | "id": "245", 17 | "name": "Jane", 18 | "comments": [ 19 | "249", 20 | "251" 21 | ], 22 | "posts": [] 23 | }, 24 | "246": { 25 | "id": "246", 26 | "name": "Spambot5000", 27 | "comments": [ 28 | "252" 29 | ] 30 | } 31 | }, 32 | "comments": { 33 | "249": { 34 | "id": "249", 35 | "content": "Nice post!", 36 | "commenter": "245", 37 | "post": "1" 38 | }, 39 | "250": { 40 | "id": "250", 41 | "content": "Thanks!", 42 | "commenter": "123", 43 | "post": "1" 44 | }, 45 | "251": { 46 | "id": "251", 47 | "content": "Your other post was nicer", 48 | "commenter": "245", 49 | "post": "2" 50 | }, 51 | "252": { 52 | "id": "252", 53 | "content": "I am a spammer!", 54 | "commenter": "246", 55 | "post": "2" 56 | } 57 | }, 58 | "posts": { 59 | "1": { 60 | "id": "1", 61 | "title": "My first post!", 62 | "author": "123", 63 | "comments": [ 64 | "249", 65 | "250" 66 | ] 67 | }, 68 | "2": { 69 | "id": "2", 70 | "title": "This other post", 71 | "author": "123", 72 | "comments": [ 73 | "251", 74 | "252" 75 | ] 76 | } 77 | } 78 | }, 79 | "result": [ 80 | "1", 81 | "2" 82 | ] 83 | } -------------------------------------------------------------------------------- /examples/relationships/schema.js: -------------------------------------------------------------------------------- 1 | import { schema } from '../../src'; 2 | 3 | const userProcessStrategy = (value, parent, key) => { 4 | switch (key) { 5 | case 'author': 6 | return { ...value, posts: [parent.id] }; 7 | case 'commenter': 8 | return { ...value, comments: [parent.id] }; 9 | default: 10 | return { ...value }; 11 | } 12 | }; 13 | 14 | const userMergeStrategy = (entityA, entityB) => { 15 | return { 16 | ...entityA, 17 | ...entityB, 18 | posts: [...(entityA.posts || []), ...(entityB.posts || [])], 19 | comments: [...(entityA.comments || []), ...(entityB.comments || [])], 20 | }; 21 | }; 22 | 23 | const user = new schema.Entity( 24 | 'users', 25 | {}, 26 | { 27 | mergeStrategy: userMergeStrategy, 28 | processStrategy: userProcessStrategy, 29 | } 30 | ); 31 | 32 | const comment = new schema.Entity( 33 | 'comments', 34 | { 35 | commenter: user, 36 | }, 37 | { 38 | processStrategy: (value, parent, key) => { 39 | return { ...value, post: parent.id }; 40 | }, 41 | } 42 | ); 43 | 44 | const post = new schema.Entity('posts', { 45 | author: user, 46 | comments: [comment], 47 | }); 48 | 49 | export default [post]; 50 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | const runYarnLock = 'yarn install --frozen-lockfile'; 2 | 3 | module.exports = { 4 | hooks: { 5 | 'post-checkout': `if [[ $HUSKY_GIT_PARAMS =~ 1$ ]]; then ${runYarnLock}; fi`, 6 | 'post-merge': runYarnLock, 7 | 'post-rebase': 'yarn install', 8 | 'pre-commit': 'yarn typecheck && yarn lint-staged', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace schema { 2 | export type StrategyFunction = (value: any, parent: any, key: string) => T; 3 | export type SchemaFunction = (value: any, parent: any, key: string) => string; 4 | export type MergeFunction = (entityA: any, entityB: any) => any; 5 | export type FallbackFunction = (key: string, schema: schema.Entity) => T; 6 | 7 | export class Array { 8 | constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) 9 | define(definition: Schema): void 10 | } 11 | 12 | export interface EntityOptions { 13 | idAttribute?: string | SchemaFunction 14 | mergeStrategy?: MergeFunction 15 | processStrategy?: StrategyFunction 16 | fallbackStrategy?: FallbackFunction 17 | } 18 | 19 | export class Entity { 20 | constructor(key: string | symbol, definition?: Schema, options?: EntityOptions) 21 | define(definition: Schema): void 22 | key: string 23 | getId: SchemaFunction 24 | _processStrategy: StrategyFunction 25 | } 26 | 27 | export class Object { 28 | constructor(definition: SchemaObject) 29 | define(definition: Schema): void 30 | } 31 | 32 | export class Union { 33 | constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) 34 | define(definition: Schema): void 35 | } 36 | 37 | export class Values { 38 | constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) 39 | define(definition: Schema): void 40 | } 41 | } 42 | 43 | export type Schema = 44 | | schema.Entity 45 | | schema.Object 46 | | schema.Union 47 | | schema.Values 48 | | SchemaObject 49 | | SchemaArray; 50 | 51 | export type SchemaValueFunction = (t: T) => Schema; 52 | export type SchemaValue = Schema | SchemaValueFunction; 53 | 54 | export interface SchemaObject { 55 | [key: string]: SchemaValue 56 | } 57 | 58 | export interface SchemaArray extends Array> {} 59 | 60 | export type NormalizedSchema = { entities: E, result: R }; 61 | 62 | export function normalize( 63 | data: any, 64 | schema: Schema 65 | ): NormalizedSchema; 66 | 67 | export function denormalize( 68 | input: any, 69 | schema: Schema, 70 | entities: any 71 | ): any; 72 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ['**/__tests__/**/*.test.js'], 3 | }; 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{md}': ['prettier --write', 'git add'], 3 | '*.{js,jsx,json}': ['yarn lint', 'prettier --write', 'git add'], 4 | '*.{js,jsx,ts,tsx}': ['jest --bail --findRelatedTests'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "normalizr", 3 | "version": "3.6.2", 4 | "description": "Normalizes and denormalizes JSON according to schema for Redux and Flux applications", 5 | "bugs": { 6 | "url": "https://github.com/paularmstrong/normalizr/issues" 7 | }, 8 | "homepage": "https://github.com/paularmstrong/normalizr", 9 | "repository": { 10 | "url": "https://github.com/paularmstrong/normalizr.git", 11 | "type": "git" 12 | }, 13 | "keywords": [ 14 | "flux", 15 | "redux", 16 | "normalize", 17 | "denormalize", 18 | "api", 19 | "json" 20 | ], 21 | "files": [ 22 | "dist/", 23 | "index.d.ts", 24 | "LICENSE", 25 | "README.md" 26 | ], 27 | "main": "dist/normalizr.js", 28 | "module": "dist/normalizr.es.js", 29 | "typings": "index.d.ts", 30 | "sideEffects": false, 31 | "scripts": { 32 | "build": "npm run clean && run-p build:*", 33 | "build:development": "NODE_ENV=development rollup -c", 34 | "build:production": "NODE_ENV=production rollup -c", 35 | "clean": "rimraf dist", 36 | "flow": "flow", 37 | "flow:ci": "flow check", 38 | "lint": "yarn lint:cmd --fix", 39 | "lint:ci": "yarn lint:cmd", 40 | "lint:cmd": "eslint . --ext '.js,.json,.snap' --cache", 41 | "prebuild": "npm run clean", 42 | "precommit": "flow check && lint-staged", 43 | "prepublishOnly": "npm run build", 44 | "test": "jest", 45 | "test:ci": "jest --ci", 46 | "test:coverage": "npm run test -- --coverage && cat ./coverage/lcov.info | coveralls", 47 | "tsc:ci": "tsc --noEmit typescript-tests/*", 48 | "typecheck": "run-p flow:ci tsc:ci" 49 | }, 50 | "author": "Paul Armstrong", 51 | "contributors": [ 52 | "Dan Abramov" 53 | ], 54 | "license": "MIT", 55 | "devDependencies": { 56 | "@babel/core": "^7.0.0", 57 | "@babel/plugin-proposal-class-properties": "^7.0.0", 58 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 59 | "@babel/preset-env": "^7.0.0", 60 | "@babel/preset-flow": "^7.0.0", 61 | "babel-eslint": "^10.0.1", 62 | "babel-jest": "^26.5.2", 63 | "coveralls": "^3.1.0", 64 | "eslint": "^7.11.0", 65 | "eslint-config-prettier": "^6.13.0", 66 | "eslint-plugin-jest": "^24.1.0", 67 | "eslint-plugin-json": "^2.1.2", 68 | "eslint-plugin-prettier": "^3.1.4", 69 | "flow-bin": "^0.136.0", 70 | "husky": "^2.3.0", 71 | "immutable": "^3.8.1", 72 | "jest": "^26.5.3", 73 | "lint-staged": "^8.1.7", 74 | "npm-run-all": "^4.1.5", 75 | "prettier": "^2.1.2", 76 | "rimraf": "^3.0.2", 77 | "rollup": "^2.32.0", 78 | "rollup-plugin-babel": "^4.4.0", 79 | "rollup-plugin-filesize": "^9.0.2", 80 | "rollup-plugin-terser": "^7.0.2", 81 | "typescript": "^3.4.5" 82 | }, 83 | "dependencies": {} 84 | } 85 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'arrowParens': 'always', 3 | 'printWidth': 120, 4 | 'singleQuote': true, 5 | 'quoteProps': 'preserve', 6 | }; 7 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import filesize from 'rollup-plugin-filesize'; 3 | import { name } from './package.json'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | 6 | const isProduction = process.env.NODE_ENV === 'production'; 7 | 8 | const destBase = 'dist/normalizr'; 9 | const destExtension = `${isProduction ? '.min' : ''}.js`; 10 | 11 | export default { 12 | input: 'src/index.js', 13 | output: [ 14 | { file: `${destBase}${destExtension}`, format: 'cjs' }, 15 | { file: `${destBase}.es${destExtension}`, format: 'es' }, 16 | { file: `${destBase}.umd${destExtension}`, format: 'umd', name }, 17 | { file: `${destBase}.amd${destExtension}`, format: 'amd', name }, 18 | { file: `${destBase}.browser${destExtension}`, format: 'iife', name }, 19 | ], 20 | plugins: [babel({}), isProduction && terser(), filesize()].filter(Boolean), 21 | }; 22 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`denormalize denormalizes entities 1`] = ` 4 | Array [ 5 | Object { 6 | "id": 1, 7 | "type": "foo", 8 | }, 9 | Object { 10 | "id": 2, 11 | "type": "bar", 12 | }, 13 | ] 14 | `; 15 | 16 | exports[`denormalize denormalizes nested entities 1`] = ` 17 | Object { 18 | "author": Object { 19 | "id": "8472", 20 | "name": "Paul", 21 | }, 22 | "body": "This article is great.", 23 | "comments": Array [ 24 | Object { 25 | "comment": "I like it!", 26 | "id": "comment-123-4738", 27 | "user": Object { 28 | "id": "10293", 29 | "name": "Jane", 30 | }, 31 | }, 32 | ], 33 | "id": "123", 34 | "title": "A Great Article", 35 | } 36 | `; 37 | 38 | exports[`denormalize denormalizes with function as idAttribute 1`] = ` 39 | Array [ 40 | Object { 41 | "guest": null, 42 | "id": "1", 43 | "name": "Esther", 44 | }, 45 | Object { 46 | "guest": Object { 47 | "guest_id": 1, 48 | }, 49 | "id": "2", 50 | "name": "Tom", 51 | }, 52 | ] 53 | `; 54 | 55 | exports[`denormalize set to undefined if schema key is not in entities 1`] = ` 56 | Object { 57 | "author": undefined, 58 | "comments": Array [ 59 | Object { 60 | "user": undefined, 61 | }, 62 | ], 63 | "id": "123", 64 | } 65 | `; 66 | 67 | exports[`normalize can normalize entity nested inside entity using property from parent 1`] = ` 68 | Object { 69 | "entities": Object { 70 | "linkables": Object { 71 | "1": Object { 72 | "data": 2, 73 | "id": 1, 74 | "module_type": "article", 75 | "schema_type": "media", 76 | }, 77 | }, 78 | "media": Object { 79 | "2": Object { 80 | "id": 2, 81 | "url": "catimage.jpg", 82 | }, 83 | }, 84 | }, 85 | "result": 1, 86 | } 87 | `; 88 | 89 | exports[`normalize can normalize entity nested inside object using property from parent 1`] = ` 90 | Object { 91 | "entities": Object { 92 | "media": Object { 93 | "2": Object { 94 | "id": 2, 95 | "url": "catimage.jpg", 96 | }, 97 | }, 98 | }, 99 | "result": Object { 100 | "data": 2, 101 | "id": 1, 102 | "module_type": "article", 103 | "schema_type": "media", 104 | }, 105 | } 106 | `; 107 | 108 | exports[`normalize can use fully custom entity classes 1`] = ` 109 | Object { 110 | "entities": Object { 111 | "children": Object { 112 | "4": Object { 113 | "id": 4, 114 | "name": "lettuce", 115 | }, 116 | }, 117 | "food": Object { 118 | "1234": Object { 119 | "children": Array [ 120 | 4, 121 | ], 122 | "name": "tacos", 123 | "uuid": "1234", 124 | }, 125 | }, 126 | }, 127 | "result": Object { 128 | "schema": "food", 129 | "uuid": "1234", 130 | }, 131 | } 132 | `; 133 | 134 | exports[`normalize ignores null values 1`] = ` 135 | Object { 136 | "entities": Object {}, 137 | "result": Array [ 138 | null, 139 | ], 140 | } 141 | `; 142 | 143 | exports[`normalize ignores null values 2`] = ` 144 | Object { 145 | "entities": Object {}, 146 | "result": Array [ 147 | undefined, 148 | ], 149 | } 150 | `; 151 | 152 | exports[`normalize ignores null values 3`] = ` 153 | Object { 154 | "entities": Object {}, 155 | "result": Array [ 156 | false, 157 | ], 158 | } 159 | `; 160 | 161 | exports[`normalize normalizes entities 1`] = ` 162 | Object { 163 | "entities": Object { 164 | "tacos": Object { 165 | "1": Object { 166 | "id": 1, 167 | "type": "foo", 168 | }, 169 | "2": Object { 170 | "id": 2, 171 | "type": "bar", 172 | }, 173 | }, 174 | }, 175 | "result": Array [ 176 | 1, 177 | 2, 178 | ], 179 | } 180 | `; 181 | 182 | exports[`normalize normalizes entities with circular references 1`] = ` 183 | Object { 184 | "entities": Object { 185 | "users": Object { 186 | "123": Object { 187 | "friends": Array [ 188 | 123, 189 | ], 190 | "id": 123, 191 | }, 192 | }, 193 | }, 194 | "result": 123, 195 | } 196 | `; 197 | 198 | exports[`normalize normalizes nested entities 1`] = ` 199 | Object { 200 | "entities": Object { 201 | "articles": Object { 202 | "123": Object { 203 | "author": "8472", 204 | "body": "This article is great.", 205 | "comments": Array [ 206 | "comment-123-4738", 207 | ], 208 | "id": "123", 209 | "title": "A Great Article", 210 | }, 211 | }, 212 | "comments": Object { 213 | "comment-123-4738": Object { 214 | "comment": "I like it!", 215 | "id": "comment-123-4738", 216 | "user": "10293", 217 | }, 218 | }, 219 | "users": Object { 220 | "10293": Object { 221 | "id": "10293", 222 | "name": "Jane", 223 | }, 224 | "8472": Object { 225 | "id": "8472", 226 | "name": "Paul", 227 | }, 228 | }, 229 | }, 230 | "result": "123", 231 | } 232 | `; 233 | 234 | exports[`normalize passes over pre-normalized values 1`] = ` 235 | Object { 236 | "entities": Object { 237 | "articles": Object { 238 | "123": Object { 239 | "author": 1, 240 | "id": "123", 241 | "title": "normalizr is great!", 242 | }, 243 | }, 244 | }, 245 | "result": "123", 246 | } 247 | `; 248 | 249 | exports[`normalize uses the non-normalized input when getting the ID for an entity 1`] = ` 250 | Object { 251 | "entities": Object { 252 | "recommendations": Object { 253 | "456": Object { 254 | "user": "456", 255 | }, 256 | }, 257 | "users": Object { 258 | "456": Object { 259 | "id": "456", 260 | }, 261 | }, 262 | }, 263 | "result": "456", 264 | } 265 | `; 266 | 267 | exports[`normalize uses the non-normalized input when getting the ID for an entity 2`] = ` 268 | Array [ 269 | Array [ 270 | Object { 271 | "user": Object { 272 | "id": "456", 273 | }, 274 | }, 275 | Object { 276 | "user": Object { 277 | "id": "456", 278 | }, 279 | }, 280 | null, 281 | ], 282 | Array [ 283 | Object { 284 | "user": Object { 285 | "id": "456", 286 | }, 287 | }, 288 | Object { 289 | "user": Object { 290 | "id": "456", 291 | }, 292 | }, 293 | null, 294 | ], 295 | ] 296 | `; 297 | -------------------------------------------------------------------------------- /src/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { denormalize, normalize, schema } from '../'; 3 | 4 | describe('normalize', () => { 5 | [42, null, undefined, '42', () => {}].forEach((input) => { 6 | test(`cannot normalize input that == ${input}`, () => { 7 | expect(() => normalize(input, new schema.Entity('test'))).toThrow(); 8 | }); 9 | }); 10 | 11 | test('cannot normalize without a schema', () => { 12 | expect(() => normalize({})).toThrow(); 13 | }); 14 | 15 | test('cannot normalize with null input', () => { 16 | const mySchema = new schema.Entity('tacos'); 17 | expect(() => normalize(null, mySchema)).toThrow(/null/); 18 | }); 19 | 20 | test('normalizes entities', () => { 21 | const mySchema = new schema.Entity('tacos'); 22 | 23 | expect( 24 | normalize( 25 | [ 26 | { id: 1, type: 'foo' }, 27 | { id: 2, type: 'bar' }, 28 | ], 29 | [mySchema] 30 | ) 31 | ).toMatchSnapshot(); 32 | }); 33 | 34 | test('normalizes entities with circular references', () => { 35 | const user = new schema.Entity('users'); 36 | user.define({ 37 | friends: [user], 38 | }); 39 | 40 | const input = { id: 123, friends: [] }; 41 | input.friends.push(input); 42 | 43 | expect(normalize(input, user)).toMatchSnapshot(); 44 | }); 45 | 46 | test('normalizes nested entities', () => { 47 | const user = new schema.Entity('users'); 48 | const comment = new schema.Entity('comments', { 49 | user: user, 50 | }); 51 | const article = new schema.Entity('articles', { 52 | author: user, 53 | comments: [comment], 54 | }); 55 | 56 | const input = { 57 | id: '123', 58 | title: 'A Great Article', 59 | author: { 60 | id: '8472', 61 | name: 'Paul', 62 | }, 63 | body: 'This article is great.', 64 | comments: [ 65 | { 66 | id: 'comment-123-4738', 67 | comment: 'I like it!', 68 | user: { 69 | id: '10293', 70 | name: 'Jane', 71 | }, 72 | }, 73 | ], 74 | }; 75 | expect(normalize(input, article)).toMatchSnapshot(); 76 | }); 77 | 78 | test('does not modify the original input', () => { 79 | const user = new schema.Entity('users'); 80 | const article = new schema.Entity('articles', { author: user }); 81 | const input = Object.freeze({ 82 | id: '123', 83 | title: 'A Great Article', 84 | author: Object.freeze({ 85 | id: '8472', 86 | name: 'Paul', 87 | }), 88 | }); 89 | expect(() => normalize(input, article)).not.toThrow(); 90 | }); 91 | 92 | test('ignores null values', () => { 93 | const myEntity = new schema.Entity('myentities'); 94 | expect(normalize([null], [myEntity])).toMatchSnapshot(); 95 | expect(normalize([undefined], [myEntity])).toMatchSnapshot(); 96 | expect(normalize([false], [myEntity])).toMatchSnapshot(); 97 | }); 98 | 99 | test('can use fully custom entity classes', () => { 100 | class MyEntity extends schema.Entity { 101 | schema = { 102 | children: [new schema.Entity('children')], 103 | }; 104 | 105 | getId(entity, parent, key) { 106 | return entity.uuid; 107 | } 108 | 109 | normalize(input, parent, key, visit, addEntity, visitedEntities) { 110 | const entity = { ...input }; 111 | Object.keys(this.schema).forEach((key) => { 112 | const schema = this.schema[key]; 113 | entity[key] = visit(input[key], input, key, schema, addEntity, visitedEntities); 114 | }); 115 | addEntity(this, entity, parent, key); 116 | return { 117 | uuid: this.getId(entity), 118 | schema: this.key, 119 | }; 120 | } 121 | } 122 | 123 | const mySchema = new MyEntity('food'); 124 | expect( 125 | normalize( 126 | { 127 | uuid: '1234', 128 | name: 'tacos', 129 | children: [{ id: 4, name: 'lettuce' }], 130 | }, 131 | mySchema 132 | ) 133 | ).toMatchSnapshot(); 134 | }); 135 | 136 | test('uses the non-normalized input when getting the ID for an entity', () => { 137 | const userEntity = new schema.Entity('users'); 138 | const idAttributeFn = jest.fn((nonNormalized, parent, key) => nonNormalized.user.id); 139 | const recommendation = new schema.Entity( 140 | 'recommendations', 141 | { user: userEntity }, 142 | { 143 | idAttribute: idAttributeFn, 144 | } 145 | ); 146 | expect(normalize({ user: { id: '456' } }, recommendation)).toMatchSnapshot(); 147 | expect(idAttributeFn.mock.calls).toMatchSnapshot(); 148 | expect(recommendation.idAttribute).toBe(idAttributeFn); 149 | }); 150 | 151 | test('passes over pre-normalized values', () => { 152 | const userEntity = new schema.Entity('users'); 153 | const articleEntity = new schema.Entity('articles', { author: userEntity }); 154 | 155 | expect(normalize({ id: '123', title: 'normalizr is great!', author: 1 }, articleEntity)).toMatchSnapshot(); 156 | }); 157 | 158 | test('can normalize object without proper object prototype inheritance', () => { 159 | const test = { id: 1, elements: [] }; 160 | test.elements.push( 161 | Object.assign(Object.create(null), { 162 | id: 18, 163 | name: 'test', 164 | }) 165 | ); 166 | 167 | const testEntity = new schema.Entity('test', { 168 | elements: [new schema.Entity('elements')], 169 | }); 170 | 171 | expect(() => normalize(test, testEntity)).not.toThrow(); 172 | }); 173 | 174 | test('can normalize entity nested inside entity using property from parent', () => { 175 | const linkablesSchema = new schema.Entity('linkables'); 176 | const mediaSchema = new schema.Entity('media'); 177 | const listsSchema = new schema.Entity('lists'); 178 | 179 | const schemaMap = { 180 | media: mediaSchema, 181 | lists: listsSchema, 182 | }; 183 | 184 | linkablesSchema.define({ 185 | data: (parent) => schemaMap[parent.schema_type], 186 | }); 187 | 188 | const input = { 189 | id: 1, 190 | module_type: 'article', 191 | schema_type: 'media', 192 | data: { 193 | id: 2, 194 | url: 'catimage.jpg', 195 | }, 196 | }; 197 | 198 | expect(normalize(input, linkablesSchema)).toMatchSnapshot(); 199 | }); 200 | 201 | test('can normalize entity nested inside object using property from parent', () => { 202 | const mediaSchema = new schema.Entity('media'); 203 | const listsSchema = new schema.Entity('lists'); 204 | 205 | const schemaMap = { 206 | media: mediaSchema, 207 | lists: listsSchema, 208 | }; 209 | 210 | const linkablesSchema = { 211 | data: (parent) => schemaMap[parent.schema_type], 212 | }; 213 | 214 | const input = { 215 | id: 1, 216 | module_type: 'article', 217 | schema_type: 'media', 218 | data: { 219 | id: 2, 220 | url: 'catimage.jpg', 221 | }, 222 | }; 223 | 224 | expect(normalize(input, linkablesSchema)).toMatchSnapshot(); 225 | }); 226 | }); 227 | 228 | describe('denormalize', () => { 229 | test('cannot denormalize without a schema', () => { 230 | expect(() => denormalize({})).toThrow(); 231 | }); 232 | 233 | test('returns the input if undefined', () => { 234 | expect(denormalize(undefined, {}, {})).toBeUndefined(); 235 | }); 236 | 237 | test('denormalizes entities', () => { 238 | const mySchema = new schema.Entity('tacos'); 239 | const entities = { 240 | tacos: { 241 | 1: { id: 1, type: 'foo' }, 242 | 2: { id: 2, type: 'bar' }, 243 | }, 244 | }; 245 | expect(denormalize([1, 2], [mySchema], entities)).toMatchSnapshot(); 246 | }); 247 | 248 | test('denormalizes nested entities', () => { 249 | const user = new schema.Entity('users'); 250 | const comment = new schema.Entity('comments', { 251 | user: user, 252 | }); 253 | const article = new schema.Entity('articles', { 254 | author: user, 255 | comments: [comment], 256 | }); 257 | 258 | const entities = { 259 | articles: { 260 | 123: { 261 | author: '8472', 262 | body: 'This article is great.', 263 | comments: ['comment-123-4738'], 264 | id: '123', 265 | title: 'A Great Article', 266 | }, 267 | }, 268 | comments: { 269 | 'comment-123-4738': { 270 | comment: 'I like it!', 271 | id: 'comment-123-4738', 272 | user: '10293', 273 | }, 274 | }, 275 | users: { 276 | 10293: { 277 | id: '10293', 278 | name: 'Jane', 279 | }, 280 | 8472: { 281 | id: '8472', 282 | name: 'Paul', 283 | }, 284 | }, 285 | }; 286 | expect(denormalize('123', article, entities)).toMatchSnapshot(); 287 | }); 288 | 289 | test('set to undefined if schema key is not in entities', () => { 290 | const user = new schema.Entity('users'); 291 | const comment = new schema.Entity('comments', { 292 | user: user, 293 | }); 294 | const article = new schema.Entity('articles', { 295 | author: user, 296 | comments: [comment], 297 | }); 298 | 299 | const entities = { 300 | articles: { 301 | 123: { 302 | id: '123', 303 | author: '8472', 304 | comments: ['1'], 305 | }, 306 | }, 307 | comments: { 308 | 1: { 309 | user: '123', 310 | }, 311 | }, 312 | }; 313 | expect(denormalize('123', article, entities)).toMatchSnapshot(); 314 | }); 315 | 316 | test('does not modify the original entities', () => { 317 | const user = new schema.Entity('users'); 318 | const article = new schema.Entity('articles', { author: user }); 319 | const entities = Object.freeze({ 320 | articles: Object.freeze({ 321 | 123: Object.freeze({ 322 | id: '123', 323 | title: 'A Great Article', 324 | author: '8472', 325 | }), 326 | }), 327 | users: Object.freeze({ 328 | 8472: Object.freeze({ 329 | id: '8472', 330 | name: 'Paul', 331 | }), 332 | }), 333 | }); 334 | expect(() => denormalize('123', article, entities)).not.toThrow(); 335 | }); 336 | 337 | test('denormalizes with function as idAttribute', () => { 338 | const normalizedData = { 339 | entities: { 340 | patrons: { 341 | 1: { id: '1', guest: null, name: 'Esther' }, 342 | 2: { id: '2', guest: 'guest-2-1', name: 'Tom' }, 343 | }, 344 | guests: { 'guest-2-1': { guest_id: 1 } }, 345 | }, 346 | result: ['1', '2'], 347 | }; 348 | 349 | const guestSchema = new schema.Entity( 350 | 'guests', 351 | {}, 352 | { 353 | idAttribute: (value, parent, key) => `${key}-${parent.id}-${value.guest_id}`, 354 | } 355 | ); 356 | 357 | const patronsSchema = new schema.Entity('patrons', { 358 | guest: guestSchema, 359 | }); 360 | 361 | expect(denormalize(normalizedData.result, [patronsSchema], normalizedData.entities)).toMatchSnapshot(); 362 | }); 363 | }); 364 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as ImmutableUtils from './schemas/ImmutableUtils'; 2 | import EntitySchema from './schemas/Entity'; 3 | import UnionSchema from './schemas/Union'; 4 | import ValuesSchema from './schemas/Values'; 5 | import ArraySchema, * as ArrayUtils from './schemas/Array'; 6 | import ObjectSchema, * as ObjectUtils from './schemas/Object'; 7 | 8 | const visit = (value, parent, key, schema, addEntity, visitedEntities) => { 9 | if (typeof value !== 'object' || !value) { 10 | return value; 11 | } 12 | 13 | if (typeof schema === 'object' && (!schema.normalize || typeof schema.normalize !== 'function')) { 14 | const method = Array.isArray(schema) ? ArrayUtils.normalize : ObjectUtils.normalize; 15 | return method(schema, value, parent, key, visit, addEntity, visitedEntities); 16 | } 17 | 18 | return schema.normalize(value, parent, key, visit, addEntity, visitedEntities); 19 | }; 20 | 21 | const addEntities = (entities) => (schema, processedEntity, value, parent, key) => { 22 | const schemaKey = schema.key; 23 | const id = schema.getId(value, parent, key); 24 | if (!(schemaKey in entities)) { 25 | entities[schemaKey] = {}; 26 | } 27 | 28 | const existingEntity = entities[schemaKey][id]; 29 | if (existingEntity) { 30 | entities[schemaKey][id] = schema.merge(existingEntity, processedEntity); 31 | } else { 32 | entities[schemaKey][id] = processedEntity; 33 | } 34 | }; 35 | 36 | export const schema = { 37 | Array: ArraySchema, 38 | Entity: EntitySchema, 39 | Object: ObjectSchema, 40 | Union: UnionSchema, 41 | Values: ValuesSchema, 42 | }; 43 | 44 | export const normalize = (input, schema) => { 45 | if (!input || typeof input !== 'object') { 46 | throw new Error( 47 | `Unexpected input given to normalize. Expected type to be "object", found "${ 48 | input === null ? 'null' : typeof input 49 | }".` 50 | ); 51 | } 52 | 53 | const entities = {}; 54 | const addEntity = addEntities(entities); 55 | const visitedEntities = {}; 56 | 57 | const result = visit(input, input, null, schema, addEntity, visitedEntities); 58 | return { entities, result }; 59 | }; 60 | 61 | const unvisitEntity = (id, schema, unvisit, getEntity, cache) => { 62 | let entity = getEntity(id, schema); 63 | 64 | if (entity === undefined && schema instanceof EntitySchema) { 65 | entity = schema.fallback(id, schema); 66 | } 67 | 68 | if (typeof entity !== 'object' || entity === null) { 69 | return entity; 70 | } 71 | 72 | if (!cache[schema.key]) { 73 | cache[schema.key] = {}; 74 | } 75 | 76 | if (!cache[schema.key][id]) { 77 | // Ensure we don't mutate it non-immutable objects 78 | const entityCopy = ImmutableUtils.isImmutable(entity) ? entity : { ...entity }; 79 | 80 | // Need to set this first so that if it is referenced further within the 81 | // denormalization the reference will already exist. 82 | cache[schema.key][id] = entityCopy; 83 | cache[schema.key][id] = schema.denormalize(entityCopy, unvisit); 84 | } 85 | 86 | return cache[schema.key][id]; 87 | }; 88 | 89 | const getUnvisit = (entities) => { 90 | const cache = {}; 91 | const getEntity = getEntities(entities); 92 | 93 | return function unvisit(input, schema) { 94 | if (typeof schema === 'object' && (!schema.denormalize || typeof schema.denormalize !== 'function')) { 95 | const method = Array.isArray(schema) ? ArrayUtils.denormalize : ObjectUtils.denormalize; 96 | return method(schema, input, unvisit); 97 | } 98 | 99 | if (input === undefined || input === null) { 100 | return input; 101 | } 102 | 103 | if (schema instanceof EntitySchema) { 104 | return unvisitEntity(input, schema, unvisit, getEntity, cache); 105 | } 106 | 107 | return schema.denormalize(input, unvisit); 108 | }; 109 | }; 110 | 111 | const getEntities = (entities) => { 112 | const isImmutable = ImmutableUtils.isImmutable(entities); 113 | 114 | return (entityOrId, schema) => { 115 | const schemaKey = schema.key; 116 | 117 | if (typeof entityOrId === 'object') { 118 | return entityOrId; 119 | } 120 | 121 | if (isImmutable) { 122 | return entities.getIn([schemaKey, entityOrId.toString()]); 123 | } 124 | 125 | return entities[schemaKey] && entities[schemaKey][entityOrId]; 126 | }; 127 | }; 128 | 129 | export const denormalize = (input, schema, entities) => { 130 | if (typeof input !== 'undefined') { 131 | return getUnvisit(entities)(input, schema); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/schemas/Array.js: -------------------------------------------------------------------------------- 1 | import PolymorphicSchema from './Polymorphic'; 2 | 3 | const validateSchema = (definition) => { 4 | const isArray = Array.isArray(definition); 5 | if (isArray && definition.length > 1) { 6 | throw new Error(`Expected schema definition to be a single schema, but found ${definition.length}.`); 7 | } 8 | 9 | return definition[0]; 10 | }; 11 | 12 | const getValues = (input) => (Array.isArray(input) ? input : Object.keys(input).map((key) => input[key])); 13 | 14 | export const normalize = (schema, input, parent, key, visit, addEntity, visitedEntities) => { 15 | schema = validateSchema(schema); 16 | 17 | const values = getValues(input); 18 | 19 | // Special case: Arrays pass *their* parent on to their children, since there 20 | // is not any special information that can be gathered from themselves directly 21 | return values.map((value, index) => visit(value, parent, key, schema, addEntity, visitedEntities)); 22 | }; 23 | 24 | export const denormalize = (schema, input, unvisit) => { 25 | schema = validateSchema(schema); 26 | return input && input.map ? input.map((entityOrId) => unvisit(entityOrId, schema)) : input; 27 | }; 28 | 29 | export default class ArraySchema extends PolymorphicSchema { 30 | normalize(input, parent, key, visit, addEntity, visitedEntities) { 31 | const values = getValues(input); 32 | 33 | return values 34 | .map((value, index) => this.normalizeValue(value, parent, key, visit, addEntity, visitedEntities)) 35 | .filter((value) => value !== undefined && value !== null); 36 | } 37 | 38 | denormalize(input, unvisit) { 39 | return input && input.map ? input.map((value) => this.denormalizeValue(value, unvisit)) : input; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/schemas/Entity.js: -------------------------------------------------------------------------------- 1 | import * as ImmutableUtils from './ImmutableUtils'; 2 | 3 | const getDefaultGetId = (idAttribute) => (input) => 4 | ImmutableUtils.isImmutable(input) ? input.get(idAttribute) : input[idAttribute]; 5 | 6 | export default class EntitySchema { 7 | constructor(key, definition = {}, options = {}) { 8 | if (!key || typeof key !== 'string') { 9 | throw new Error(`Expected a string key for Entity, but found ${key}.`); 10 | } 11 | 12 | const { 13 | idAttribute = 'id', 14 | mergeStrategy = (entityA, entityB) => { 15 | return { ...entityA, ...entityB }; 16 | }, 17 | processStrategy = (input) => ({ ...input }), 18 | fallbackStrategy = (key, schema) => undefined, 19 | } = options; 20 | 21 | this._key = key; 22 | this._getId = typeof idAttribute === 'function' ? idAttribute : getDefaultGetId(idAttribute); 23 | this._idAttribute = idAttribute; 24 | this._mergeStrategy = mergeStrategy; 25 | this._processStrategy = processStrategy; 26 | this._fallbackStrategy = fallbackStrategy; 27 | this.define(definition); 28 | } 29 | 30 | get key() { 31 | return this._key; 32 | } 33 | 34 | get idAttribute() { 35 | return this._idAttribute; 36 | } 37 | 38 | define(definition) { 39 | this.schema = Object.keys(definition).reduce((entitySchema, key) => { 40 | const schema = definition[key]; 41 | return { ...entitySchema, [key]: schema }; 42 | }, this.schema || {}); 43 | } 44 | 45 | getId(input, parent, key) { 46 | return this._getId(input, parent, key); 47 | } 48 | 49 | merge(entityA, entityB) { 50 | return this._mergeStrategy(entityA, entityB); 51 | } 52 | 53 | fallback(id, schema) { 54 | return this._fallbackStrategy(id, schema); 55 | } 56 | 57 | normalize(input, parent, key, visit, addEntity, visitedEntities) { 58 | const id = this.getId(input, parent, key); 59 | const entityType = this.key; 60 | 61 | if (!(entityType in visitedEntities)) { 62 | visitedEntities[entityType] = {}; 63 | } 64 | if (!(id in visitedEntities[entityType])) { 65 | visitedEntities[entityType][id] = []; 66 | } 67 | if (visitedEntities[entityType][id].some((entity) => entity === input)) { 68 | return id; 69 | } 70 | visitedEntities[entityType][id].push(input); 71 | 72 | const processedEntity = this._processStrategy(input, parent, key); 73 | Object.keys(this.schema).forEach((key) => { 74 | if (processedEntity.hasOwnProperty(key) && typeof processedEntity[key] === 'object') { 75 | const schema = this.schema[key]; 76 | const resolvedSchema = typeof schema === 'function' ? schema(input) : schema; 77 | processedEntity[key] = visit( 78 | processedEntity[key], 79 | processedEntity, 80 | key, 81 | resolvedSchema, 82 | addEntity, 83 | visitedEntities 84 | ); 85 | } 86 | }); 87 | 88 | addEntity(this, processedEntity, input, parent, key); 89 | return id; 90 | } 91 | 92 | denormalize(entity, unvisit) { 93 | if (ImmutableUtils.isImmutable(entity)) { 94 | return ImmutableUtils.denormalizeImmutable(this.schema, entity, unvisit); 95 | } 96 | 97 | Object.keys(this.schema).forEach((key) => { 98 | if (entity.hasOwnProperty(key)) { 99 | const schema = this.schema[key]; 100 | entity[key] = unvisit(entity[key], schema); 101 | } 102 | }); 103 | return entity; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/schemas/ImmutableUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers to enable Immutable compatibility *without* bringing in 3 | * the 'immutable' package as a dependency. 4 | */ 5 | 6 | /** 7 | * Check if an object is immutable by checking if it has a key specific 8 | * to the immutable library. 9 | * 10 | * @param {any} object 11 | * @return {bool} 12 | */ 13 | export function isImmutable(object) { 14 | return !!( 15 | object && 16 | typeof object.hasOwnProperty === 'function' && 17 | (object.hasOwnProperty('__ownerID') || // Immutable.Map 18 | (object._map && object._map.hasOwnProperty('__ownerID'))) 19 | ); // Immutable.Record 20 | } 21 | 22 | /** 23 | * Denormalize an immutable entity. 24 | * 25 | * @param {Schema} schema 26 | * @param {Immutable.Map|Immutable.Record} input 27 | * @param {function} unvisit 28 | * @param {function} getDenormalizedEntity 29 | * @return {Immutable.Map|Immutable.Record} 30 | */ 31 | export function denormalizeImmutable(schema, input, unvisit) { 32 | return Object.keys(schema).reduce((object, key) => { 33 | // Immutable maps cast keys to strings on write so we need to ensure 34 | // we're accessing them using string keys. 35 | const stringKey = `${key}`; 36 | 37 | if (object.has(stringKey)) { 38 | return object.set(stringKey, unvisit(object.get(stringKey), schema[stringKey])); 39 | } else { 40 | return object; 41 | } 42 | }, input); 43 | } 44 | -------------------------------------------------------------------------------- /src/schemas/Object.js: -------------------------------------------------------------------------------- 1 | import * as ImmutableUtils from './ImmutableUtils'; 2 | 3 | export const normalize = (schema, input, parent, key, visit, addEntity, visitedEntities) => { 4 | const object = { ...input }; 5 | Object.keys(schema).forEach((key) => { 6 | const localSchema = schema[key]; 7 | const resolvedLocalSchema = typeof localSchema === 'function' ? localSchema(input) : localSchema; 8 | const value = visit(input[key], input, key, resolvedLocalSchema, addEntity, visitedEntities); 9 | if (value === undefined || value === null) { 10 | delete object[key]; 11 | } else { 12 | object[key] = value; 13 | } 14 | }); 15 | return object; 16 | }; 17 | 18 | export const denormalize = (schema, input, unvisit) => { 19 | if (ImmutableUtils.isImmutable(input)) { 20 | return ImmutableUtils.denormalizeImmutable(schema, input, unvisit); 21 | } 22 | 23 | const object = { ...input }; 24 | Object.keys(schema).forEach((key) => { 25 | if (object[key] != null) { 26 | object[key] = unvisit(object[key], schema[key]); 27 | } 28 | }); 29 | return object; 30 | }; 31 | 32 | export default class ObjectSchema { 33 | constructor(definition) { 34 | this.define(definition); 35 | } 36 | 37 | define(definition) { 38 | this.schema = Object.keys(definition).reduce((entitySchema, key) => { 39 | const schema = definition[key]; 40 | return { ...entitySchema, [key]: schema }; 41 | }, this.schema || {}); 42 | } 43 | 44 | normalize(...args) { 45 | return normalize(this.schema, ...args); 46 | } 47 | 48 | denormalize(...args) { 49 | return denormalize(this.schema, ...args); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/schemas/Polymorphic.js: -------------------------------------------------------------------------------- 1 | import { isImmutable } from './ImmutableUtils'; 2 | 3 | export default class PolymorphicSchema { 4 | constructor(definition, schemaAttribute) { 5 | if (schemaAttribute) { 6 | this._schemaAttribute = typeof schemaAttribute === 'string' ? (input) => input[schemaAttribute] : schemaAttribute; 7 | } 8 | this.define(definition); 9 | } 10 | 11 | get isSingleSchema() { 12 | return !this._schemaAttribute; 13 | } 14 | 15 | define(definition) { 16 | this.schema = definition; 17 | } 18 | 19 | getSchemaAttribute(input, parent, key) { 20 | return !this.isSingleSchema && this._schemaAttribute(input, parent, key); 21 | } 22 | 23 | inferSchema(input, parent, key) { 24 | if (this.isSingleSchema) { 25 | return this.schema; 26 | } 27 | 28 | const attr = this.getSchemaAttribute(input, parent, key); 29 | return this.schema[attr]; 30 | } 31 | 32 | normalizeValue(value, parent, key, visit, addEntity, visitedEntities) { 33 | const schema = this.inferSchema(value, parent, key); 34 | if (!schema) { 35 | return value; 36 | } 37 | const normalizedValue = visit(value, parent, key, schema, addEntity, visitedEntities); 38 | return this.isSingleSchema || normalizedValue === undefined || normalizedValue === null 39 | ? normalizedValue 40 | : { id: normalizedValue, schema: this.getSchemaAttribute(value, parent, key) }; 41 | } 42 | 43 | denormalizeValue(value, unvisit) { 44 | const schemaKey = isImmutable(value) ? value.get('schema') : value.schema; 45 | if (!this.isSingleSchema && !schemaKey) { 46 | return value; 47 | } 48 | const id = this.isSingleSchema ? undefined : isImmutable(value) ? value.get('id') : value.id; 49 | const schema = this.isSingleSchema ? this.schema : this.schema[schemaKey]; 50 | return unvisit(id || value, schema); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/schemas/Union.js: -------------------------------------------------------------------------------- 1 | import PolymorphicSchema from './Polymorphic'; 2 | 3 | export default class UnionSchema extends PolymorphicSchema { 4 | constructor(definition, schemaAttribute) { 5 | if (!schemaAttribute) { 6 | throw new Error('Expected option "schemaAttribute" not found on UnionSchema.'); 7 | } 8 | super(definition, schemaAttribute); 9 | } 10 | 11 | normalize(input, parent, key, visit, addEntity, visitedEntities) { 12 | return this.normalizeValue(input, parent, key, visit, addEntity, visitedEntities); 13 | } 14 | 15 | denormalize(input, unvisit) { 16 | return this.denormalizeValue(input, unvisit); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/schemas/Values.js: -------------------------------------------------------------------------------- 1 | import PolymorphicSchema from './Polymorphic'; 2 | 3 | export default class ValuesSchema extends PolymorphicSchema { 4 | normalize(input, parent, key, visit, addEntity, visitedEntities) { 5 | return Object.keys(input).reduce((output, key, index) => { 6 | const value = input[key]; 7 | return value !== undefined && value !== null 8 | ? { 9 | ...output, 10 | [key]: this.normalizeValue(value, input, key, visit, addEntity, visitedEntities), 11 | } 12 | : output; 13 | }, {}); 14 | } 15 | 16 | denormalize(input, unvisit) { 17 | return Object.keys(input).reduce((output, key) => { 18 | const entityOrId = input[key]; 19 | return { 20 | ...output, 21 | [key]: this.denormalizeValue(entityOrId, unvisit), 22 | }; 23 | }, {}); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/schemas/__tests__/Array.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { fromJS } from 'immutable'; 3 | import { denormalize, normalize, schema } from '../../'; 4 | 5 | describe(`${schema.Array.name} normalization`, () => { 6 | describe('Object', () => { 7 | test(`normalizes plain arrays as shorthand for ${schema.Array.name}`, () => { 8 | const userSchema = new schema.Entity('user'); 9 | expect(normalize([{ id: 1 }, { id: 2 }], [userSchema])).toMatchSnapshot(); 10 | }); 11 | 12 | test('throws an error if created with more than one schema', () => { 13 | const userSchema = new schema.Entity('users'); 14 | const catSchema = new schema.Entity('cats'); 15 | expect(() => normalize([{ id: 1 }], [catSchema, userSchema])).toThrow(); 16 | }); 17 | 18 | test('passes its parent to its children when normalizing', () => { 19 | const processStrategy = (entity, parent, key) => { 20 | return { ...entity, parentId: parent.id, parentKey: key }; 21 | }; 22 | const childEntity = new schema.Entity('children', {}, { processStrategy }); 23 | const parentEntity = new schema.Entity('parents', { 24 | children: [childEntity], 25 | }); 26 | 27 | expect( 28 | normalize( 29 | { 30 | id: 1, 31 | content: 'parent', 32 | children: [{ id: 4, content: 'child' }], 33 | }, 34 | parentEntity 35 | ) 36 | ).toMatchSnapshot(); 37 | }); 38 | 39 | test('normalizes Objects using their values', () => { 40 | const userSchema = new schema.Entity('user'); 41 | expect(normalize({ foo: { id: 1 }, bar: { id: 2 } }, [userSchema])).toMatchSnapshot(); 42 | }); 43 | }); 44 | 45 | describe('Class', () => { 46 | test('normalizes a single entity', () => { 47 | const cats = new schema.Entity('cats'); 48 | const listSchema = new schema.Array(cats); 49 | expect(normalize([{ id: 1 }, { id: 2 }], listSchema)).toMatchSnapshot(); 50 | }); 51 | 52 | test('normalizes multiple entities', () => { 53 | const inferSchemaFn = jest.fn((input, parent, key) => input.type || 'dogs'); 54 | const catSchema = new schema.Entity('cats'); 55 | const peopleSchema = new schema.Entity('person'); 56 | const listSchema = new schema.Array( 57 | { 58 | cats: catSchema, 59 | people: peopleSchema, 60 | }, 61 | inferSchemaFn 62 | ); 63 | 64 | expect( 65 | normalize( 66 | [ 67 | { type: 'cats', id: '123' }, 68 | { type: 'people', id: '123' }, 69 | { id: '789', name: 'fido' }, 70 | { type: 'cats', id: '456' }, 71 | ], 72 | listSchema 73 | ) 74 | ).toMatchSnapshot(); 75 | expect(inferSchemaFn.mock.calls).toMatchSnapshot(); 76 | }); 77 | 78 | test('normalizes Objects using their values', () => { 79 | const userSchema = new schema.Entity('user'); 80 | const users = new schema.Array(userSchema); 81 | expect(normalize({ foo: { id: 1 }, bar: { id: 2 } }, users)).toMatchSnapshot(); 82 | }); 83 | 84 | test('filters out undefined and null normalized values', () => { 85 | const userSchema = new schema.Entity('user'); 86 | const users = new schema.Array(userSchema); 87 | expect(normalize([undefined, { id: 123 }, null], users)).toMatchSnapshot(); 88 | }); 89 | }); 90 | }); 91 | 92 | describe(`${schema.Array.name} denormalization`, () => { 93 | describe('Object', () => { 94 | test('denormalizes a single entity', () => { 95 | const cats = new schema.Entity('cats'); 96 | const entities = { 97 | cats: { 98 | 1: { id: 1, name: 'Milo' }, 99 | 2: { id: 2, name: 'Jake' }, 100 | }, 101 | }; 102 | expect(denormalize([1, 2], [cats], entities)).toMatchSnapshot(); 103 | expect(denormalize([1, 2], [cats], fromJS(entities))).toMatchSnapshot(); 104 | }); 105 | 106 | test('returns the input value if is not an array', () => { 107 | const filling = new schema.Entity('fillings'); 108 | const taco = new schema.Entity('tacos', { fillings: [filling] }); 109 | const entities = { 110 | tacos: { 111 | 123: { 112 | id: '123', 113 | fillings: null, 114 | }, 115 | }, 116 | }; 117 | 118 | expect(denormalize('123', taco, entities)).toMatchSnapshot(); 119 | expect(denormalize('123', taco, fromJS(entities))).toMatchSnapshot(); 120 | }); 121 | }); 122 | 123 | describe('Class', () => { 124 | test('denormalizes a single entity', () => { 125 | const cats = new schema.Entity('cats'); 126 | const entities = { 127 | cats: { 128 | 1: { id: 1, name: 'Milo' }, 129 | 2: { id: 2, name: 'Jake' }, 130 | }, 131 | }; 132 | const catList = new schema.Array(cats); 133 | expect(denormalize([1, 2], catList, entities)).toMatchSnapshot(); 134 | expect(denormalize([1, 2], catList, fromJS(entities))).toMatchSnapshot(); 135 | }); 136 | 137 | test('denormalizes multiple entities', () => { 138 | const catSchema = new schema.Entity('cats'); 139 | const peopleSchema = new schema.Entity('person'); 140 | const listSchema = new schema.Array( 141 | { 142 | cats: catSchema, 143 | dogs: {}, 144 | people: peopleSchema, 145 | }, 146 | (input, parent, key) => input.type || 'dogs' 147 | ); 148 | 149 | const entities = { 150 | cats: { 151 | 123: { 152 | id: '123', 153 | type: 'cats', 154 | }, 155 | 456: { 156 | id: '456', 157 | type: 'cats', 158 | }, 159 | }, 160 | person: { 161 | 123: { 162 | id: '123', 163 | type: 'people', 164 | }, 165 | }, 166 | }; 167 | 168 | const input = [ 169 | { id: '123', schema: 'cats' }, 170 | { id: '123', schema: 'people' }, 171 | { id: { id: '789' }, schema: 'dogs' }, 172 | { id: '456', schema: 'cats' }, 173 | ]; 174 | 175 | expect(denormalize(input, listSchema, entities)).toMatchSnapshot(); 176 | expect(denormalize(input, listSchema, fromJS(entities))).toMatchSnapshot(); 177 | }); 178 | 179 | test('returns the input value if is not an array', () => { 180 | const filling = new schema.Entity('fillings'); 181 | const fillings = new schema.Array(filling); 182 | const taco = new schema.Entity('tacos', { fillings }); 183 | const entities = { 184 | tacos: { 185 | 123: { 186 | id: '123', 187 | fillings: {}, 188 | }, 189 | }, 190 | }; 191 | 192 | expect(denormalize('123', taco, entities)).toMatchSnapshot(); 193 | expect(denormalize('123', taco, fromJS(entities))).toMatchSnapshot(); 194 | }); 195 | 196 | test('does not assume mapping of schema to attribute values when schemaAttribute is not set', () => { 197 | const cats = new schema.Entity('cats'); 198 | const catRecord = new schema.Object({ 199 | cat: cats, 200 | }); 201 | const catList = new schema.Array(catRecord); 202 | const input = [ 203 | { cat: { id: 1 }, id: 5 }, 204 | { cat: { id: 2 }, id: 6 }, 205 | ]; 206 | const output = normalize(input, catList); 207 | expect(output).toMatchSnapshot(); 208 | expect(denormalize(output.result, catList, output.entities)).toEqual(input); 209 | }); 210 | }); 211 | }); 212 | -------------------------------------------------------------------------------- /src/schemas/__tests__/Entity.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { denormalize, normalize, schema } from '../../'; 3 | import { fromJS, Record } from 'immutable'; 4 | 5 | const values = (obj) => Object.keys(obj).map((key) => obj[key]); 6 | 7 | describe(`${schema.Entity.name} normalization`, () => { 8 | test('normalizes an entity', () => { 9 | const entity = new schema.Entity('item'); 10 | expect(normalize({ id: 1 }, entity)).toMatchSnapshot(); 11 | }); 12 | 13 | describe('key', () => { 14 | test('must be created with a key name', () => { 15 | expect(() => new schema.Entity()).toThrow(); 16 | }); 17 | 18 | test('key name must be a string', () => { 19 | expect(() => new schema.Entity(42)).toThrow(); 20 | }); 21 | 22 | test('key getter should return key passed to constructor', () => { 23 | const user = new schema.Entity('users'); 24 | expect(user.key).toEqual('users'); 25 | }); 26 | }); 27 | 28 | describe('idAttribute', () => { 29 | test('can use a custom idAttribute string', () => { 30 | const user = new schema.Entity('users', {}, { idAttribute: 'id_str' }); 31 | expect(normalize({ id_str: '134351', name: 'Kathy' }, user)).toMatchSnapshot(); 32 | }); 33 | 34 | test('can normalize entity IDs based on their object key', () => { 35 | const user = new schema.Entity('users', {}, { idAttribute: (entity, parent, key) => key }); 36 | const inputSchema = new schema.Values({ users: user }, () => 'users'); 37 | 38 | expect(normalize({ 4: { name: 'taco' }, 56: { name: 'burrito' } }, inputSchema)).toMatchSnapshot(); 39 | }); 40 | 41 | test("can build the entity's ID from the parent object", () => { 42 | const user = new schema.Entity( 43 | 'users', 44 | {}, 45 | { 46 | idAttribute: (entity, parent, key) => `${parent.name}-${key}-${entity.id}`, 47 | } 48 | ); 49 | const inputSchema = new schema.Object({ user }); 50 | 51 | expect(normalize({ name: 'tacos', user: { id: '4', name: 'Jimmy' } }, inputSchema)).toMatchSnapshot(); 52 | }); 53 | }); 54 | 55 | describe('mergeStrategy', () => { 56 | test('defaults to plain merging', () => { 57 | const mySchema = new schema.Entity('tacos'); 58 | expect( 59 | normalize( 60 | [ 61 | { id: 1, name: 'foo' }, 62 | { id: 1, name: 'bar', alias: 'bar' }, 63 | ], 64 | [mySchema] 65 | ) 66 | ).toMatchSnapshot(); 67 | }); 68 | 69 | test('can use a custom merging strategy', () => { 70 | const mergeStrategy = (entityA, entityB) => { 71 | return { ...entityA, ...entityB, name: entityA.name }; 72 | }; 73 | const mySchema = new schema.Entity('tacos', {}, { mergeStrategy }); 74 | 75 | expect( 76 | normalize( 77 | [ 78 | { id: 1, name: 'foo' }, 79 | { id: 1, name: 'bar', alias: 'bar' }, 80 | ], 81 | [mySchema] 82 | ) 83 | ).toMatchSnapshot(); 84 | }); 85 | }); 86 | 87 | describe('processStrategy', () => { 88 | test('can use a custom processing strategy', () => { 89 | const processStrategy = (entity) => { 90 | return { ...entity, slug: `thing-${entity.id}` }; 91 | }; 92 | const mySchema = new schema.Entity('tacos', {}, { processStrategy }); 93 | 94 | expect(normalize({ id: 1, name: 'foo' }, mySchema)).toMatchSnapshot(); 95 | }); 96 | 97 | test('can use information from the parent in the process strategy', () => { 98 | const processStrategy = (entity, parent, key) => { 99 | return { ...entity, parentId: parent.id, parentKey: key }; 100 | }; 101 | const childEntity = new schema.Entity('children', {}, { processStrategy }); 102 | const parentEntity = new schema.Entity('parents', { 103 | child: childEntity, 104 | }); 105 | 106 | expect( 107 | normalize( 108 | { 109 | id: 1, 110 | content: 'parent', 111 | child: { id: 4, content: 'child' }, 112 | }, 113 | parentEntity 114 | ) 115 | ).toMatchSnapshot(); 116 | }); 117 | 118 | test('is run before and passed to the schema normalization', () => { 119 | const processStrategy = (input) => ({ ...values(input)[0], type: Object.keys(input)[0] }); 120 | const attachmentEntity = new schema.Entity('attachments'); 121 | // If not run before, this schema would require a parent object with key "message" 122 | const myEntity = new schema.Entity( 123 | 'entries', 124 | { 125 | data: { attachment: attachmentEntity }, 126 | }, 127 | { idAttribute: (input) => values(input)[0].id, processStrategy } 128 | ); 129 | 130 | expect(normalize({ message: { id: '123', data: { attachment: { id: '456' } } } }, myEntity)).toMatchSnapshot(); 131 | }); 132 | }); 133 | }); 134 | 135 | describe(`${schema.Entity.name} denormalization`, () => { 136 | test('denormalizes an entity', () => { 137 | const mySchema = new schema.Entity('tacos'); 138 | const entities = { 139 | tacos: { 140 | 1: { id: 1, type: 'foo' }, 141 | }, 142 | }; 143 | expect(denormalize(1, mySchema, entities)).toMatchSnapshot(); 144 | expect(denormalize(1, mySchema, fromJS(entities))).toMatchSnapshot(); 145 | }); 146 | 147 | test('denormalizes deep entities', () => { 148 | const foodSchema = new schema.Entity('foods'); 149 | const menuSchema = new schema.Entity('menus', { 150 | food: foodSchema, 151 | }); 152 | 153 | const entities = { 154 | menus: { 155 | 1: { id: 1, food: 1 }, 156 | 2: { id: 2 }, 157 | }, 158 | foods: { 159 | 1: { id: 1 }, 160 | }, 161 | }; 162 | 163 | expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); 164 | expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); 165 | 166 | expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); 167 | expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); 168 | }); 169 | 170 | test('denormalizes to undefined for missing data', () => { 171 | const foodSchema = new schema.Entity('foods'); 172 | const menuSchema = new schema.Entity('menus', { 173 | food: foodSchema, 174 | }); 175 | 176 | const entities = { 177 | menus: { 178 | 1: { id: 1, food: 2 }, 179 | }, 180 | foods: { 181 | 1: { id: 1 }, 182 | }, 183 | }; 184 | 185 | expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); 186 | expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); 187 | 188 | expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); 189 | expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); 190 | }); 191 | 192 | test('denormalizes deep entities with records', () => { 193 | const foodSchema = new schema.Entity('foods'); 194 | const menuSchema = new schema.Entity('menus', { 195 | food: foodSchema, 196 | }); 197 | 198 | const Food = new Record({ id: null }); 199 | const Menu = new Record({ id: null, food: null }); 200 | 201 | const entities = { 202 | menus: { 203 | 1: new Menu({ id: 1, food: 1 }), 204 | 2: new Menu({ id: 2 }), 205 | }, 206 | foods: { 207 | 1: new Food({ id: 1 }), 208 | }, 209 | }; 210 | 211 | expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); 212 | expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); 213 | 214 | expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); 215 | expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); 216 | }); 217 | 218 | test('can denormalize already partially denormalized data', () => { 219 | const foodSchema = new schema.Entity('foods'); 220 | const menuSchema = new schema.Entity('menus', { 221 | food: foodSchema, 222 | }); 223 | 224 | const entities = { 225 | menus: { 226 | 1: { id: 1, food: { id: 1 } }, 227 | }, 228 | foods: { 229 | 1: { id: 1 }, 230 | }, 231 | }; 232 | 233 | expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); 234 | expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); 235 | }); 236 | 237 | test('denormalizes recursive dependencies', () => { 238 | const user = new schema.Entity('users'); 239 | const report = new schema.Entity('reports'); 240 | 241 | user.define({ 242 | reports: [report], 243 | }); 244 | report.define({ 245 | draftedBy: user, 246 | publishedBy: user, 247 | }); 248 | 249 | const entities = { 250 | reports: { 251 | 123: { 252 | id: '123', 253 | title: 'Weekly report', 254 | draftedBy: '456', 255 | publishedBy: '456', 256 | }, 257 | }, 258 | users: { 259 | 456: { 260 | id: '456', 261 | role: 'manager', 262 | reports: ['123'], 263 | }, 264 | }, 265 | }; 266 | expect(denormalize('123', report, entities)).toMatchSnapshot(); 267 | expect(denormalize('123', report, fromJS(entities))).toMatchSnapshot(); 268 | 269 | expect(denormalize('456', user, entities)).toMatchSnapshot(); 270 | expect(denormalize('456', user, fromJS(entities))).toMatchSnapshot(); 271 | }); 272 | 273 | test('denormalizes entities with referential equality', () => { 274 | const user = new schema.Entity('users'); 275 | const report = new schema.Entity('reports'); 276 | 277 | user.define({ 278 | reports: [report], 279 | }); 280 | report.define({ 281 | draftedBy: user, 282 | publishedBy: user, 283 | }); 284 | 285 | const entities = { 286 | reports: { 287 | 123: { 288 | id: '123', 289 | title: 'Weekly report', 290 | draftedBy: '456', 291 | publishedBy: '456', 292 | }, 293 | }, 294 | users: { 295 | 456: { 296 | id: '456', 297 | role: 'manager', 298 | reports: ['123'], 299 | }, 300 | }, 301 | }; 302 | 303 | const denormalizedReport = denormalize('123', report, entities); 304 | 305 | expect(denormalizedReport).toBe(denormalizedReport.draftedBy.reports[0]); 306 | expect(denormalizedReport.publishedBy).toBe(denormalizedReport.draftedBy); 307 | 308 | // NOTE: Given how immutable data works, referential equality can't be 309 | // maintained with nested denormalization. 310 | }); 311 | 312 | test('denormalizes with fallback strategy', () => { 313 | const user = new schema.Entity( 314 | 'users', 315 | {}, 316 | { 317 | idAttribute: 'userId', 318 | fallbackStrategy: (id, schema) => ({ 319 | [schema.idAttribute]: id, 320 | name: 'John Doe', 321 | }), 322 | } 323 | ); 324 | const report = new schema.Entity('reports', { 325 | draftedBy: user, 326 | publishedBy: user, 327 | }); 328 | 329 | const entities = { 330 | reports: { 331 | 123: { 332 | id: '123', 333 | title: 'Weekly report', 334 | draftedBy: '456', 335 | publishedBy: '456', 336 | }, 337 | }, 338 | users: {}, 339 | }; 340 | 341 | const denormalizedReport = denormalize('123', report, entities); 342 | 343 | expect(denormalizedReport.publishedBy).toBe(denormalizedReport.draftedBy); 344 | expect(denormalizedReport.publishedBy.name).toBe('John Doe'); 345 | expect(denormalizedReport.publishedBy.userId).toBe('456'); 346 | // 347 | }); 348 | }); 349 | -------------------------------------------------------------------------------- /src/schemas/__tests__/Object.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { fromJS } from 'immutable'; 3 | import { denormalize, normalize, schema } from '../../'; 4 | 5 | describe(`${schema.Object.name} normalization`, () => { 6 | test('normalizes an object', () => { 7 | const userSchema = new schema.Entity('user'); 8 | const object = new schema.Object({ 9 | user: userSchema, 10 | }); 11 | expect(normalize({ user: { id: 1 } }, object)).toMatchSnapshot(); 12 | }); 13 | 14 | test(`normalizes plain objects as shorthand for ${schema.Object.name}`, () => { 15 | const userSchema = new schema.Entity('user'); 16 | expect(normalize({ user: { id: 1 } }, { user: userSchema })).toMatchSnapshot(); 17 | }); 18 | 19 | test('filters out undefined and null values', () => { 20 | const userSchema = new schema.Entity('user'); 21 | const users = { foo: userSchema, bar: userSchema, baz: userSchema }; 22 | expect(normalize({ foo: {}, bar: { id: '1' } }, users)).toMatchSnapshot(); 23 | }); 24 | }); 25 | 26 | describe(`${schema.Object.name} denormalization`, () => { 27 | test('denormalizes an object', () => { 28 | const userSchema = new schema.Entity('user'); 29 | const object = new schema.Object({ 30 | user: userSchema, 31 | }); 32 | const entities = { 33 | user: { 34 | 1: { id: 1, name: 'Nacho' }, 35 | }, 36 | }; 37 | expect(denormalize({ user: 1 }, object, entities)).toMatchSnapshot(); 38 | expect(denormalize({ user: 1 }, object, fromJS(entities))).toMatchSnapshot(); 39 | expect(denormalize(fromJS({ user: 1 }), object, fromJS(entities))).toMatchSnapshot(); 40 | }); 41 | 42 | test('denormalizes plain object shorthand', () => { 43 | const userSchema = new schema.Entity('user'); 44 | const entities = { 45 | user: { 46 | 1: { id: 1, name: 'Jane' }, 47 | }, 48 | }; 49 | expect(denormalize({ user: 1 }, { user: userSchema, tacos: {} }, entities)).toMatchSnapshot(); 50 | expect(denormalize({ user: 1 }, { user: userSchema, tacos: {} }, fromJS(entities))).toMatchSnapshot(); 51 | expect(denormalize(fromJS({ user: 1 }), { user: userSchema, tacos: {} }, fromJS(entities))).toMatchSnapshot(); 52 | }); 53 | 54 | test('denormalizes an object that contains a property representing a an object with an id of zero', () => { 55 | const userSchema = new schema.Entity('user'); 56 | const object = new schema.Object({ 57 | user: userSchema, 58 | }); 59 | const entities = { 60 | user: { 61 | 0: { id: 0, name: 'Chancho' }, 62 | }, 63 | }; 64 | expect(denormalize({ user: 0 }, object, entities)).toMatchSnapshot(); 65 | expect(denormalize({ user: 0 }, object, fromJS(entities))).toMatchSnapshot(); 66 | expect(denormalize(fromJS({ user: 0 }), object, fromJS(entities))).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/schemas/__tests__/Union.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { fromJS } from 'immutable'; 3 | import { denormalize, normalize, schema } from '../../'; 4 | 5 | describe(`${schema.Union.name} normalization`, () => { 6 | test('throws if not given a schemaAttribute', () => { 7 | expect(() => new schema.Union({})).toThrow(); 8 | }); 9 | 10 | test('normalizes an object using string schemaAttribute', () => { 11 | const user = new schema.Entity('users'); 12 | const group = new schema.Entity('groups'); 13 | const union = new schema.Union( 14 | { 15 | users: user, 16 | groups: group, 17 | }, 18 | 'type' 19 | ); 20 | 21 | expect(normalize({ id: 1, type: 'users' }, union)).toMatchSnapshot(); 22 | expect(normalize({ id: 2, type: 'groups' }, union)).toMatchSnapshot(); 23 | }); 24 | 25 | test('normalizes an array of multiple entities using a function to infer the schemaAttribute', () => { 26 | const user = new schema.Entity('users'); 27 | const group = new schema.Entity('groups'); 28 | const union = new schema.Union( 29 | { 30 | users: user, 31 | groups: group, 32 | }, 33 | (input) => { 34 | return input.username ? 'users' : input.groupname ? 'groups' : null; 35 | } 36 | ); 37 | 38 | expect(normalize({ id: 1, username: 'Janey' }, union)).toMatchSnapshot(); 39 | expect(normalize({ id: 2, groupname: 'People' }, union)).toMatchSnapshot(); 40 | expect(normalize({ id: 3, notdefined: 'yep' }, union)).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | describe(`${schema.Union.name} denormalization`, () => { 45 | const user = new schema.Entity('users'); 46 | const group = new schema.Entity('groups'); 47 | const entities = { 48 | users: { 49 | 1: { id: 1, username: 'Janey', type: 'users' }, 50 | }, 51 | groups: { 52 | 2: { id: 2, groupname: 'People', type: 'groups' }, 53 | }, 54 | }; 55 | 56 | test('denormalizes an object using string schemaAttribute', () => { 57 | const union = new schema.Union( 58 | { 59 | users: user, 60 | groups: group, 61 | }, 62 | 'type' 63 | ); 64 | 65 | expect(denormalize({ id: 1, schema: 'users' }, union, entities)).toMatchSnapshot(); 66 | expect(denormalize(fromJS({ id: 1, schema: 'users' }), union, fromJS(entities))).toMatchSnapshot(); 67 | 68 | expect(denormalize({ id: 2, schema: 'groups' }, union, entities)).toMatchSnapshot(); 69 | expect(denormalize(fromJS({ id: 2, schema: 'groups' }), union, fromJS(entities))).toMatchSnapshot(); 70 | }); 71 | 72 | test('denormalizes an array of multiple entities using a function to infer the schemaAttribute', () => { 73 | const union = new schema.Union( 74 | { 75 | users: user, 76 | groups: group, 77 | }, 78 | (input) => { 79 | return input.username ? 'users' : 'groups'; 80 | } 81 | ); 82 | 83 | expect(denormalize({ id: 1, schema: 'users' }, union, entities)).toMatchSnapshot(); 84 | expect(denormalize(fromJS({ id: 1, schema: 'users' }), union, fromJS(entities))).toMatchSnapshot(); 85 | 86 | expect(denormalize({ id: 2, schema: 'groups' }, union, entities)).toMatchSnapshot(); 87 | expect(denormalize(fromJS({ id: 2, schema: 'groups' }), union, fromJS(entities))).toMatchSnapshot(); 88 | }); 89 | 90 | test('returns the original value no schema is given', () => { 91 | const union = new schema.Union( 92 | { 93 | users: user, 94 | groups: group, 95 | }, 96 | (input) => { 97 | return input.username ? 'users' : 'groups'; 98 | } 99 | ); 100 | 101 | expect(denormalize({ id: 1 }, union, entities)).toMatchSnapshot(); 102 | expect(denormalize(fromJS({ id: 1 }), union, fromJS(entities))).toMatchSnapshot(); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/schemas/__tests__/Values.test.js: -------------------------------------------------------------------------------- 1 | // eslint-env jest 2 | import { fromJS } from 'immutable'; 3 | import { denormalize, normalize, schema } from '../../'; 4 | 5 | describe(`${schema.Values.name} normalization`, () => { 6 | test('normalizes the values of an object with the given schema', () => { 7 | const cat = new schema.Entity('cats'); 8 | const dog = new schema.Entity('dogs'); 9 | const valuesSchema = new schema.Values( 10 | { 11 | dogs: dog, 12 | cats: cat, 13 | }, 14 | (entity, key) => entity.type 15 | ); 16 | 17 | expect( 18 | normalize( 19 | { 20 | fido: { id: 1, type: 'dogs' }, 21 | fluffy: { id: 1, type: 'cats' }, 22 | }, 23 | valuesSchema 24 | ) 25 | ).toMatchSnapshot(); 26 | }); 27 | 28 | test('can use a function to determine the schema when normalizing', () => { 29 | const cat = new schema.Entity('cats'); 30 | const dog = new schema.Entity('dogs'); 31 | const valuesSchema = new schema.Values( 32 | { 33 | dogs: dog, 34 | cats: cat, 35 | }, 36 | (entity, key) => `${entity.type}s` 37 | ); 38 | 39 | expect( 40 | normalize( 41 | { 42 | fido: { id: 1, type: 'dog' }, 43 | fluffy: { id: 1, type: 'cat' }, 44 | jim: { id: 2, type: 'lizard' }, 45 | }, 46 | valuesSchema 47 | ) 48 | ).toMatchSnapshot(); 49 | }); 50 | 51 | test('filters out null and undefined values', () => { 52 | const cat = new schema.Entity('cats'); 53 | const dog = new schema.Entity('dogs'); 54 | const valuesSchema = new schema.Values( 55 | { 56 | dogs: dog, 57 | cats: cat, 58 | }, 59 | (entity, key) => entity.type 60 | ); 61 | 62 | expect( 63 | normalize( 64 | { 65 | fido: undefined, 66 | milo: null, 67 | fluffy: { id: 1, type: 'cats' }, 68 | }, 69 | valuesSchema 70 | ) 71 | ).toMatchSnapshot(); 72 | }); 73 | }); 74 | 75 | describe(`${schema.Values.name} denormalization`, () => { 76 | test('denormalizes the values of an object with the given schema', () => { 77 | const cat = new schema.Entity('cats'); 78 | const dog = new schema.Entity('dogs'); 79 | const valuesSchema = new schema.Values( 80 | { 81 | dogs: dog, 82 | cats: cat, 83 | }, 84 | (entity, key) => entity.type 85 | ); 86 | 87 | const entities = { 88 | cats: { 1: { id: 1, type: 'cats' } }, 89 | dogs: { 1: { id: 1, type: 'dogs' } }, 90 | }; 91 | 92 | expect( 93 | denormalize( 94 | { 95 | fido: { id: 1, schema: 'dogs' }, 96 | fluffy: { id: 1, schema: 'cats' }, 97 | }, 98 | valuesSchema, 99 | entities 100 | ) 101 | ).toMatchSnapshot(); 102 | 103 | expect( 104 | denormalize( 105 | { 106 | fido: { id: 1, schema: 'dogs' }, 107 | fluffy: { id: 1, schema: 'cats' }, 108 | }, 109 | valuesSchema, 110 | fromJS(entities) 111 | ) 112 | ).toMatchSnapshot(); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /src/schemas/__tests__/__snapshots__/Array.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ArraySchema denormalization Class denormalizes a single entity 1`] = ` 4 | Array [ 5 | Object { 6 | "id": 1, 7 | "name": "Milo", 8 | }, 9 | Object { 10 | "id": 2, 11 | "name": "Jake", 12 | }, 13 | ] 14 | `; 15 | 16 | exports[`ArraySchema denormalization Class denormalizes a single entity 2`] = ` 17 | Array [ 18 | Immutable.Map { 19 | "id": 1, 20 | "name": "Milo", 21 | }, 22 | Immutable.Map { 23 | "id": 2, 24 | "name": "Jake", 25 | }, 26 | ] 27 | `; 28 | 29 | exports[`ArraySchema denormalization Class denormalizes multiple entities 1`] = ` 30 | Array [ 31 | Object { 32 | "id": "123", 33 | "type": "cats", 34 | }, 35 | Object { 36 | "id": "123", 37 | "type": "people", 38 | }, 39 | Object { 40 | "id": "789", 41 | }, 42 | Object { 43 | "id": "456", 44 | "type": "cats", 45 | }, 46 | ] 47 | `; 48 | 49 | exports[`ArraySchema denormalization Class denormalizes multiple entities 2`] = ` 50 | Array [ 51 | Immutable.Map { 52 | "id": "123", 53 | "type": "cats", 54 | }, 55 | Immutable.Map { 56 | "id": "123", 57 | "type": "people", 58 | }, 59 | Object { 60 | "id": "789", 61 | }, 62 | Immutable.Map { 63 | "id": "456", 64 | "type": "cats", 65 | }, 66 | ] 67 | `; 68 | 69 | exports[`ArraySchema denormalization Class does not assume mapping of schema to attribute values when schemaAttribute is not set 1`] = ` 70 | Object { 71 | "entities": Object { 72 | "cats": Object { 73 | "1": Object { 74 | "id": 1, 75 | }, 76 | "2": Object { 77 | "id": 2, 78 | }, 79 | }, 80 | }, 81 | "result": Array [ 82 | Object { 83 | "cat": 1, 84 | "id": 5, 85 | }, 86 | Object { 87 | "cat": 2, 88 | "id": 6, 89 | }, 90 | ], 91 | } 92 | `; 93 | 94 | exports[`ArraySchema denormalization Class returns the input value if is not an array 1`] = ` 95 | Object { 96 | "fillings": Object {}, 97 | "id": "123", 98 | } 99 | `; 100 | 101 | exports[`ArraySchema denormalization Class returns the input value if is not an array 2`] = ` 102 | Immutable.Map { 103 | "id": "123", 104 | "fillings": Immutable.Map {}, 105 | } 106 | `; 107 | 108 | exports[`ArraySchema denormalization Object denormalizes a single entity 1`] = ` 109 | Array [ 110 | Object { 111 | "id": 1, 112 | "name": "Milo", 113 | }, 114 | Object { 115 | "id": 2, 116 | "name": "Jake", 117 | }, 118 | ] 119 | `; 120 | 121 | exports[`ArraySchema denormalization Object denormalizes a single entity 2`] = ` 122 | Array [ 123 | Immutable.Map { 124 | "id": 1, 125 | "name": "Milo", 126 | }, 127 | Immutable.Map { 128 | "id": 2, 129 | "name": "Jake", 130 | }, 131 | ] 132 | `; 133 | 134 | exports[`ArraySchema denormalization Object returns the input value if is not an array 1`] = ` 135 | Object { 136 | "fillings": null, 137 | "id": "123", 138 | } 139 | `; 140 | 141 | exports[`ArraySchema denormalization Object returns the input value if is not an array 2`] = ` 142 | Immutable.Map { 143 | "id": "123", 144 | "fillings": null, 145 | } 146 | `; 147 | 148 | exports[`ArraySchema normalization Class filters out undefined and null normalized values 1`] = ` 149 | Object { 150 | "entities": Object { 151 | "user": Object { 152 | "123": Object { 153 | "id": 123, 154 | }, 155 | }, 156 | }, 157 | "result": Array [ 158 | 123, 159 | ], 160 | } 161 | `; 162 | 163 | exports[`ArraySchema normalization Class normalizes Objects using their values 1`] = ` 164 | Object { 165 | "entities": Object { 166 | "user": Object { 167 | "1": Object { 168 | "id": 1, 169 | }, 170 | "2": Object { 171 | "id": 2, 172 | }, 173 | }, 174 | }, 175 | "result": Array [ 176 | 1, 177 | 2, 178 | ], 179 | } 180 | `; 181 | 182 | exports[`ArraySchema normalization Class normalizes a single entity 1`] = ` 183 | Object { 184 | "entities": Object { 185 | "cats": Object { 186 | "1": Object { 187 | "id": 1, 188 | }, 189 | "2": Object { 190 | "id": 2, 191 | }, 192 | }, 193 | }, 194 | "result": Array [ 195 | 1, 196 | 2, 197 | ], 198 | } 199 | `; 200 | 201 | exports[`ArraySchema normalization Class normalizes multiple entities 1`] = ` 202 | Object { 203 | "entities": Object { 204 | "cats": Object { 205 | "123": Object { 206 | "id": "123", 207 | "type": "cats", 208 | }, 209 | "456": Object { 210 | "id": "456", 211 | "type": "cats", 212 | }, 213 | }, 214 | "person": Object { 215 | "123": Object { 216 | "id": "123", 217 | "type": "people", 218 | }, 219 | }, 220 | }, 221 | "result": Array [ 222 | Object { 223 | "id": "123", 224 | "schema": "cats", 225 | }, 226 | Object { 227 | "id": "123", 228 | "schema": "people", 229 | }, 230 | Object { 231 | "id": "789", 232 | "name": "fido", 233 | }, 234 | Object { 235 | "id": "456", 236 | "schema": "cats", 237 | }, 238 | ], 239 | } 240 | `; 241 | 242 | exports[`ArraySchema normalization Class normalizes multiple entities 2`] = ` 243 | Array [ 244 | Array [ 245 | Object { 246 | "id": "123", 247 | "type": "cats", 248 | }, 249 | Array [ 250 | Object { 251 | "id": "123", 252 | "type": "cats", 253 | }, 254 | Object { 255 | "id": "123", 256 | "type": "people", 257 | }, 258 | Object { 259 | "id": "789", 260 | "name": "fido", 261 | }, 262 | Object { 263 | "id": "456", 264 | "type": "cats", 265 | }, 266 | ], 267 | null, 268 | ], 269 | Array [ 270 | Object { 271 | "id": "123", 272 | "type": "cats", 273 | }, 274 | Array [ 275 | Object { 276 | "id": "123", 277 | "type": "cats", 278 | }, 279 | Object { 280 | "id": "123", 281 | "type": "people", 282 | }, 283 | Object { 284 | "id": "789", 285 | "name": "fido", 286 | }, 287 | Object { 288 | "id": "456", 289 | "type": "cats", 290 | }, 291 | ], 292 | null, 293 | ], 294 | Array [ 295 | Object { 296 | "id": "123", 297 | "type": "people", 298 | }, 299 | Array [ 300 | Object { 301 | "id": "123", 302 | "type": "cats", 303 | }, 304 | Object { 305 | "id": "123", 306 | "type": "people", 307 | }, 308 | Object { 309 | "id": "789", 310 | "name": "fido", 311 | }, 312 | Object { 313 | "id": "456", 314 | "type": "cats", 315 | }, 316 | ], 317 | null, 318 | ], 319 | Array [ 320 | Object { 321 | "id": "123", 322 | "type": "people", 323 | }, 324 | Array [ 325 | Object { 326 | "id": "123", 327 | "type": "cats", 328 | }, 329 | Object { 330 | "id": "123", 331 | "type": "people", 332 | }, 333 | Object { 334 | "id": "789", 335 | "name": "fido", 336 | }, 337 | Object { 338 | "id": "456", 339 | "type": "cats", 340 | }, 341 | ], 342 | null, 343 | ], 344 | Array [ 345 | Object { 346 | "id": "789", 347 | "name": "fido", 348 | }, 349 | Array [ 350 | Object { 351 | "id": "123", 352 | "type": "cats", 353 | }, 354 | Object { 355 | "id": "123", 356 | "type": "people", 357 | }, 358 | Object { 359 | "id": "789", 360 | "name": "fido", 361 | }, 362 | Object { 363 | "id": "456", 364 | "type": "cats", 365 | }, 366 | ], 367 | null, 368 | ], 369 | Array [ 370 | Object { 371 | "id": "456", 372 | "type": "cats", 373 | }, 374 | Array [ 375 | Object { 376 | "id": "123", 377 | "type": "cats", 378 | }, 379 | Object { 380 | "id": "123", 381 | "type": "people", 382 | }, 383 | Object { 384 | "id": "789", 385 | "name": "fido", 386 | }, 387 | Object { 388 | "id": "456", 389 | "type": "cats", 390 | }, 391 | ], 392 | null, 393 | ], 394 | Array [ 395 | Object { 396 | "id": "456", 397 | "type": "cats", 398 | }, 399 | Array [ 400 | Object { 401 | "id": "123", 402 | "type": "cats", 403 | }, 404 | Object { 405 | "id": "123", 406 | "type": "people", 407 | }, 408 | Object { 409 | "id": "789", 410 | "name": "fido", 411 | }, 412 | Object { 413 | "id": "456", 414 | "type": "cats", 415 | }, 416 | ], 417 | null, 418 | ], 419 | ] 420 | `; 421 | 422 | exports[`ArraySchema normalization Object normalizes Objects using their values 1`] = ` 423 | Object { 424 | "entities": Object { 425 | "user": Object { 426 | "1": Object { 427 | "id": 1, 428 | }, 429 | "2": Object { 430 | "id": 2, 431 | }, 432 | }, 433 | }, 434 | "result": Array [ 435 | 1, 436 | 2, 437 | ], 438 | } 439 | `; 440 | 441 | exports[`ArraySchema normalization Object normalizes plain arrays as shorthand for ArraySchema 1`] = ` 442 | Object { 443 | "entities": Object { 444 | "user": Object { 445 | "1": Object { 446 | "id": 1, 447 | }, 448 | "2": Object { 449 | "id": 2, 450 | }, 451 | }, 452 | }, 453 | "result": Array [ 454 | 1, 455 | 2, 456 | ], 457 | } 458 | `; 459 | 460 | exports[`ArraySchema normalization Object passes its parent to its children when normalizing 1`] = ` 461 | Object { 462 | "entities": Object { 463 | "children": Object { 464 | "4": Object { 465 | "content": "child", 466 | "id": 4, 467 | "parentId": 1, 468 | "parentKey": "children", 469 | }, 470 | }, 471 | "parents": Object { 472 | "1": Object { 473 | "children": Array [ 474 | 4, 475 | ], 476 | "content": "parent", 477 | "id": 1, 478 | }, 479 | }, 480 | }, 481 | "result": 1, 482 | } 483 | `; 484 | -------------------------------------------------------------------------------- /src/schemas/__tests__/__snapshots__/Entity.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EntitySchema denormalization can denormalize already partially denormalized data 1`] = ` 4 | Object { 5 | "food": Object { 6 | "id": 1, 7 | }, 8 | "id": 1, 9 | } 10 | `; 11 | 12 | exports[`EntitySchema denormalization can denormalize already partially denormalized data 2`] = ` 13 | Immutable.Map { 14 | "id": 1, 15 | "food": Immutable.Map { 16 | "id": 1, 17 | }, 18 | } 19 | `; 20 | 21 | exports[`EntitySchema denormalization denormalizes an entity 1`] = ` 22 | Object { 23 | "id": 1, 24 | "type": "foo", 25 | } 26 | `; 27 | 28 | exports[`EntitySchema denormalization denormalizes an entity 2`] = ` 29 | Immutable.Map { 30 | "id": 1, 31 | "type": "foo", 32 | } 33 | `; 34 | 35 | exports[`EntitySchema denormalization denormalizes deep entities 1`] = ` 36 | Object { 37 | "food": Object { 38 | "id": 1, 39 | }, 40 | "id": 1, 41 | } 42 | `; 43 | 44 | exports[`EntitySchema denormalization denormalizes deep entities 2`] = ` 45 | Immutable.Map { 46 | "id": 1, 47 | "food": Immutable.Map { 48 | "id": 1, 49 | }, 50 | } 51 | `; 52 | 53 | exports[`EntitySchema denormalization denormalizes deep entities 3`] = ` 54 | Object { 55 | "id": 2, 56 | } 57 | `; 58 | 59 | exports[`EntitySchema denormalization denormalizes deep entities 4`] = ` 60 | Immutable.Map { 61 | "id": 2, 62 | } 63 | `; 64 | 65 | exports[`EntitySchema denormalization denormalizes deep entities with records 1`] = ` 66 | Immutable.Record { 67 | "id": 1, 68 | "food": Immutable.Record { 69 | "id": 1, 70 | }, 71 | } 72 | `; 73 | 74 | exports[`EntitySchema denormalization denormalizes deep entities with records 2`] = ` 75 | Immutable.Record { 76 | "id": 1, 77 | "food": Immutable.Record { 78 | "id": 1, 79 | }, 80 | } 81 | `; 82 | 83 | exports[`EntitySchema denormalization denormalizes deep entities with records 3`] = ` 84 | Immutable.Record { 85 | "id": 2, 86 | "food": null, 87 | } 88 | `; 89 | 90 | exports[`EntitySchema denormalization denormalizes deep entities with records 4`] = ` 91 | Immutable.Record { 92 | "id": 2, 93 | "food": null, 94 | } 95 | `; 96 | 97 | exports[`EntitySchema denormalization denormalizes recursive dependencies 1`] = ` 98 | Object { 99 | "draftedBy": Object { 100 | "id": "456", 101 | "reports": Array [ 102 | [Circular], 103 | ], 104 | "role": "manager", 105 | }, 106 | "id": "123", 107 | "publishedBy": Object { 108 | "id": "456", 109 | "reports": Array [ 110 | [Circular], 111 | ], 112 | "role": "manager", 113 | }, 114 | "title": "Weekly report", 115 | } 116 | `; 117 | 118 | exports[`EntitySchema denormalization denormalizes recursive dependencies 2`] = ` 119 | Immutable.Map { 120 | "id": "123", 121 | "title": "Weekly report", 122 | "draftedBy": Immutable.Map { 123 | "id": "456", 124 | "role": "manager", 125 | "reports": Immutable.List [ 126 | Immutable.Map { 127 | "id": "123", 128 | "title": "Weekly report", 129 | "draftedBy": "456", 130 | "publishedBy": "456", 131 | }, 132 | ], 133 | }, 134 | "publishedBy": Immutable.Map { 135 | "id": "456", 136 | "role": "manager", 137 | "reports": Immutable.List [ 138 | Immutable.Map { 139 | "id": "123", 140 | "title": "Weekly report", 141 | "draftedBy": "456", 142 | "publishedBy": "456", 143 | }, 144 | ], 145 | }, 146 | } 147 | `; 148 | 149 | exports[`EntitySchema denormalization denormalizes recursive dependencies 3`] = ` 150 | Object { 151 | "id": "456", 152 | "reports": Array [ 153 | Object { 154 | "draftedBy": [Circular], 155 | "id": "123", 156 | "publishedBy": [Circular], 157 | "title": "Weekly report", 158 | }, 159 | ], 160 | "role": "manager", 161 | } 162 | `; 163 | 164 | exports[`EntitySchema denormalization denormalizes recursive dependencies 4`] = ` 165 | Immutable.Map { 166 | "id": "456", 167 | "role": "manager", 168 | "reports": Immutable.List [ 169 | Immutable.Map { 170 | "id": "123", 171 | "title": "Weekly report", 172 | "draftedBy": Immutable.Map { 173 | "id": "456", 174 | "role": "manager", 175 | "reports": Immutable.List [ 176 | "123", 177 | ], 178 | }, 179 | "publishedBy": Immutable.Map { 180 | "id": "456", 181 | "role": "manager", 182 | "reports": Immutable.List [ 183 | "123", 184 | ], 185 | }, 186 | }, 187 | ], 188 | } 189 | `; 190 | 191 | exports[`EntitySchema denormalization denormalizes to undefined for missing data 1`] = ` 192 | Object { 193 | "food": undefined, 194 | "id": 1, 195 | } 196 | `; 197 | 198 | exports[`EntitySchema denormalization denormalizes to undefined for missing data 2`] = ` 199 | Immutable.Map { 200 | "id": 1, 201 | "food": undefined, 202 | } 203 | `; 204 | 205 | exports[`EntitySchema denormalization denormalizes to undefined for missing data 3`] = `undefined`; 206 | 207 | exports[`EntitySchema denormalization denormalizes to undefined for missing data 4`] = `undefined`; 208 | 209 | exports[`EntitySchema normalization idAttribute can build the entity's ID from the parent object 1`] = ` 210 | Object { 211 | "entities": Object { 212 | "users": Object { 213 | "tacos-user-4": Object { 214 | "id": "4", 215 | "name": "Jimmy", 216 | }, 217 | }, 218 | }, 219 | "result": Object { 220 | "name": "tacos", 221 | "user": "tacos-user-4", 222 | }, 223 | } 224 | `; 225 | 226 | exports[`EntitySchema normalization idAttribute can normalize entity IDs based on their object key 1`] = ` 227 | Object { 228 | "entities": Object { 229 | "users": Object { 230 | "4": Object { 231 | "name": "taco", 232 | }, 233 | "56": Object { 234 | "name": "burrito", 235 | }, 236 | }, 237 | }, 238 | "result": Object { 239 | "4": Object { 240 | "id": "4", 241 | "schema": "users", 242 | }, 243 | "56": Object { 244 | "id": "56", 245 | "schema": "users", 246 | }, 247 | }, 248 | } 249 | `; 250 | 251 | exports[`EntitySchema normalization idAttribute can use a custom idAttribute string 1`] = ` 252 | Object { 253 | "entities": Object { 254 | "users": Object { 255 | "134351": Object { 256 | "id_str": "134351", 257 | "name": "Kathy", 258 | }, 259 | }, 260 | }, 261 | "result": "134351", 262 | } 263 | `; 264 | 265 | exports[`EntitySchema normalization mergeStrategy can use a custom merging strategy 1`] = ` 266 | Object { 267 | "entities": Object { 268 | "tacos": Object { 269 | "1": Object { 270 | "alias": "bar", 271 | "id": 1, 272 | "name": "foo", 273 | }, 274 | }, 275 | }, 276 | "result": Array [ 277 | 1, 278 | 1, 279 | ], 280 | } 281 | `; 282 | 283 | exports[`EntitySchema normalization mergeStrategy defaults to plain merging 1`] = ` 284 | Object { 285 | "entities": Object { 286 | "tacos": Object { 287 | "1": Object { 288 | "alias": "bar", 289 | "id": 1, 290 | "name": "bar", 291 | }, 292 | }, 293 | }, 294 | "result": Array [ 295 | 1, 296 | 1, 297 | ], 298 | } 299 | `; 300 | 301 | exports[`EntitySchema normalization normalizes an entity 1`] = ` 302 | Object { 303 | "entities": Object { 304 | "item": Object { 305 | "1": Object { 306 | "id": 1, 307 | }, 308 | }, 309 | }, 310 | "result": 1, 311 | } 312 | `; 313 | 314 | exports[`EntitySchema normalization processStrategy can use a custom processing strategy 1`] = ` 315 | Object { 316 | "entities": Object { 317 | "tacos": Object { 318 | "1": Object { 319 | "id": 1, 320 | "name": "foo", 321 | "slug": "thing-1", 322 | }, 323 | }, 324 | }, 325 | "result": 1, 326 | } 327 | `; 328 | 329 | exports[`EntitySchema normalization processStrategy can use information from the parent in the process strategy 1`] = ` 330 | Object { 331 | "entities": Object { 332 | "children": Object { 333 | "4": Object { 334 | "content": "child", 335 | "id": 4, 336 | "parentId": 1, 337 | "parentKey": "child", 338 | }, 339 | }, 340 | "parents": Object { 341 | "1": Object { 342 | "child": 4, 343 | "content": "parent", 344 | "id": 1, 345 | }, 346 | }, 347 | }, 348 | "result": 1, 349 | } 350 | `; 351 | 352 | exports[`EntitySchema normalization processStrategy is run before and passed to the schema normalization 1`] = ` 353 | Object { 354 | "entities": Object { 355 | "attachments": Object { 356 | "456": Object { 357 | "id": "456", 358 | }, 359 | }, 360 | "entries": Object { 361 | "123": Object { 362 | "data": Object { 363 | "attachment": "456", 364 | }, 365 | "id": "123", 366 | "type": "message", 367 | }, 368 | }, 369 | }, 370 | "result": "123", 371 | } 372 | `; 373 | -------------------------------------------------------------------------------- /src/schemas/__tests__/__snapshots__/Object.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ObjectSchema denormalization denormalizes an object 1`] = ` 4 | Object { 5 | "user": Object { 6 | "id": 1, 7 | "name": "Nacho", 8 | }, 9 | } 10 | `; 11 | 12 | exports[`ObjectSchema denormalization denormalizes an object 2`] = ` 13 | Object { 14 | "user": Immutable.Map { 15 | "id": 1, 16 | "name": "Nacho", 17 | }, 18 | } 19 | `; 20 | 21 | exports[`ObjectSchema denormalization denormalizes an object 3`] = ` 22 | Immutable.Map { 23 | "user": Immutable.Map { 24 | "id": 1, 25 | "name": "Nacho", 26 | }, 27 | } 28 | `; 29 | 30 | exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 1`] = ` 31 | Object { 32 | "user": Object { 33 | "id": 0, 34 | "name": "Chancho", 35 | }, 36 | } 37 | `; 38 | 39 | exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 2`] = ` 40 | Object { 41 | "user": Immutable.Map { 42 | "id": 0, 43 | "name": "Chancho", 44 | }, 45 | } 46 | `; 47 | 48 | exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 3`] = ` 49 | Immutable.Map { 50 | "user": Immutable.Map { 51 | "id": 0, 52 | "name": "Chancho", 53 | }, 54 | } 55 | `; 56 | 57 | exports[`ObjectSchema denormalization denormalizes plain object shorthand 1`] = ` 58 | Object { 59 | "user": Object { 60 | "id": 1, 61 | "name": "Jane", 62 | }, 63 | } 64 | `; 65 | 66 | exports[`ObjectSchema denormalization denormalizes plain object shorthand 2`] = ` 67 | Object { 68 | "user": Immutable.Map { 69 | "id": 1, 70 | "name": "Jane", 71 | }, 72 | } 73 | `; 74 | 75 | exports[`ObjectSchema denormalization denormalizes plain object shorthand 3`] = ` 76 | Immutable.Map { 77 | "user": Immutable.Map { 78 | "id": 1, 79 | "name": "Jane", 80 | }, 81 | } 82 | `; 83 | 84 | exports[`ObjectSchema normalization filters out undefined and null values 1`] = ` 85 | Object { 86 | "entities": Object { 87 | "user": Object { 88 | "1": Object { 89 | "id": "1", 90 | }, 91 | "undefined": Object {}, 92 | }, 93 | }, 94 | "result": Object { 95 | "bar": "1", 96 | }, 97 | } 98 | `; 99 | 100 | exports[`ObjectSchema normalization normalizes an object 1`] = ` 101 | Object { 102 | "entities": Object { 103 | "user": Object { 104 | "1": Object { 105 | "id": 1, 106 | }, 107 | }, 108 | }, 109 | "result": Object { 110 | "user": 1, 111 | }, 112 | } 113 | `; 114 | 115 | exports[`ObjectSchema normalization normalizes plain objects as shorthand for ObjectSchema 1`] = ` 116 | Object { 117 | "entities": Object { 118 | "user": Object { 119 | "1": Object { 120 | "id": 1, 121 | }, 122 | }, 123 | }, 124 | "result": Object { 125 | "user": 1, 126 | }, 127 | } 128 | `; 129 | -------------------------------------------------------------------------------- /src/schemas/__tests__/__snapshots__/Union.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 1`] = ` 4 | Object { 5 | "id": 1, 6 | "type": "users", 7 | "username": "Janey", 8 | } 9 | `; 10 | 11 | exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 2`] = ` 12 | Immutable.Map { 13 | "id": 1, 14 | "username": "Janey", 15 | "type": "users", 16 | } 17 | `; 18 | 19 | exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 3`] = ` 20 | Object { 21 | "groupname": "People", 22 | "id": 2, 23 | "type": "groups", 24 | } 25 | `; 26 | 27 | exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 4`] = ` 28 | Immutable.Map { 29 | "id": 2, 30 | "groupname": "People", 31 | "type": "groups", 32 | } 33 | `; 34 | 35 | exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 1`] = ` 36 | Object { 37 | "id": 1, 38 | "type": "users", 39 | "username": "Janey", 40 | } 41 | `; 42 | 43 | exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 2`] = ` 44 | Immutable.Map { 45 | "id": 1, 46 | "username": "Janey", 47 | "type": "users", 48 | } 49 | `; 50 | 51 | exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 3`] = ` 52 | Object { 53 | "groupname": "People", 54 | "id": 2, 55 | "type": "groups", 56 | } 57 | `; 58 | 59 | exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 4`] = ` 60 | Immutable.Map { 61 | "id": 2, 62 | "groupname": "People", 63 | "type": "groups", 64 | } 65 | `; 66 | 67 | exports[`UnionSchema denormalization returns the original value no schema is given 1`] = ` 68 | Object { 69 | "id": 1, 70 | } 71 | `; 72 | 73 | exports[`UnionSchema denormalization returns the original value no schema is given 2`] = ` 74 | Immutable.Map { 75 | "id": 1, 76 | } 77 | `; 78 | 79 | exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 1`] = ` 80 | Object { 81 | "entities": Object { 82 | "users": Object { 83 | "1": Object { 84 | "id": 1, 85 | "username": "Janey", 86 | }, 87 | }, 88 | }, 89 | "result": Object { 90 | "id": 1, 91 | "schema": "users", 92 | }, 93 | } 94 | `; 95 | 96 | exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 2`] = ` 97 | Object { 98 | "entities": Object { 99 | "groups": Object { 100 | "2": Object { 101 | "groupname": "People", 102 | "id": 2, 103 | }, 104 | }, 105 | }, 106 | "result": Object { 107 | "id": 2, 108 | "schema": "groups", 109 | }, 110 | } 111 | `; 112 | 113 | exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 3`] = ` 114 | Object { 115 | "entities": Object {}, 116 | "result": Object { 117 | "id": 3, 118 | "notdefined": "yep", 119 | }, 120 | } 121 | `; 122 | 123 | exports[`UnionSchema normalization normalizes an object using string schemaAttribute 1`] = ` 124 | Object { 125 | "entities": Object { 126 | "users": Object { 127 | "1": Object { 128 | "id": 1, 129 | "type": "users", 130 | }, 131 | }, 132 | }, 133 | "result": Object { 134 | "id": 1, 135 | "schema": "users", 136 | }, 137 | } 138 | `; 139 | 140 | exports[`UnionSchema normalization normalizes an object using string schemaAttribute 2`] = ` 141 | Object { 142 | "entities": Object { 143 | "groups": Object { 144 | "2": Object { 145 | "id": 2, 146 | "type": "groups", 147 | }, 148 | }, 149 | }, 150 | "result": Object { 151 | "id": 2, 152 | "schema": "groups", 153 | }, 154 | } 155 | `; 156 | -------------------------------------------------------------------------------- /src/schemas/__tests__/__snapshots__/Values.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ValuesSchema denormalization denormalizes the values of an object with the given schema 1`] = ` 4 | Object { 5 | "fido": Object { 6 | "id": 1, 7 | "type": "dogs", 8 | }, 9 | "fluffy": Object { 10 | "id": 1, 11 | "type": "cats", 12 | }, 13 | } 14 | `; 15 | 16 | exports[`ValuesSchema denormalization denormalizes the values of an object with the given schema 2`] = ` 17 | Object { 18 | "fido": Immutable.Map { 19 | "id": 1, 20 | "type": "dogs", 21 | }, 22 | "fluffy": Immutable.Map { 23 | "id": 1, 24 | "type": "cats", 25 | }, 26 | } 27 | `; 28 | 29 | exports[`ValuesSchema normalization can use a function to determine the schema when normalizing 1`] = ` 30 | Object { 31 | "entities": Object { 32 | "cats": Object { 33 | "1": Object { 34 | "id": 1, 35 | "type": "cat", 36 | }, 37 | }, 38 | "dogs": Object { 39 | "1": Object { 40 | "id": 1, 41 | "type": "dog", 42 | }, 43 | }, 44 | }, 45 | "result": Object { 46 | "fido": Object { 47 | "id": 1, 48 | "schema": "dogs", 49 | }, 50 | "fluffy": Object { 51 | "id": 1, 52 | "schema": "cats", 53 | }, 54 | "jim": Object { 55 | "id": 2, 56 | "type": "lizard", 57 | }, 58 | }, 59 | } 60 | `; 61 | 62 | exports[`ValuesSchema normalization filters out null and undefined values 1`] = ` 63 | Object { 64 | "entities": Object { 65 | "cats": Object { 66 | "1": Object { 67 | "id": 1, 68 | "type": "cats", 69 | }, 70 | }, 71 | }, 72 | "result": Object { 73 | "fluffy": Object { 74 | "id": 1, 75 | "schema": "cats", 76 | }, 77 | }, 78 | } 79 | `; 80 | 81 | exports[`ValuesSchema normalization normalizes the values of an object with the given schema 1`] = ` 82 | Object { 83 | "entities": Object { 84 | "cats": Object { 85 | "1": Object { 86 | "id": 1, 87 | "type": "cats", 88 | }, 89 | }, 90 | "dogs": Object { 91 | "1": Object { 92 | "id": 1, 93 | "type": "dogs", 94 | }, 95 | }, 96 | }, 97 | "result": Object { 98 | "fido": Object { 99 | "id": 1, 100 | "schema": "dogs", 101 | }, 102 | "fluffy": Object { 103 | "id": 1, 104 | "schema": "cats", 105 | }, 106 | }, 107 | } 108 | `; 109 | -------------------------------------------------------------------------------- /typescript-tests/array.ts: -------------------------------------------------------------------------------- 1 | import { denormalize, normalize, schema } from '../index' 2 | 3 | const data = [{ id: '123', name: 'Jim' }, { id: '456', name: 'Jane' }]; 4 | const userSchema = new schema.Entity('users'); 5 | 6 | const userListSchema = new schema.Array(userSchema); 7 | const normalizedData = normalize(data, userListSchema); 8 | 9 | const userListSchemaAlt = [userSchema]; 10 | const normalizedDataAlt = normalize(data, userListSchemaAlt); 11 | 12 | const denormalizedData = denormalize(normalizedData.result, userListSchema, normalizedData.entities); 13 | -------------------------------------------------------------------------------- /typescript-tests/array_schema.ts: -------------------------------------------------------------------------------- 1 | import { denormalize, normalize, schema } from '../index' 2 | 3 | const data = [{ id: 1, type: 'admin' }, { id: 2, type: 'user' }]; 4 | const userSchema = new schema.Entity('users'); 5 | const adminSchema = new schema.Entity('admins'); 6 | 7 | const myArray = new schema.Array( 8 | { 9 | admins: adminSchema, 10 | users: userSchema 11 | }, 12 | (input, parent, key) => `${input.type}s` 13 | ); 14 | 15 | const normalizedData = normalize(data, myArray); 16 | 17 | const denormalizedData = denormalize(normalizedData.result, myArray, normalizedData.entities); 18 | -------------------------------------------------------------------------------- /typescript-tests/entity.ts: -------------------------------------------------------------------------------- 1 | import { denormalize, normalize, schema } from '../index' 2 | 3 | type User = { 4 | id_str: string; 5 | name: string; 6 | }; 7 | 8 | type Tweet = { 9 | id_str: string; 10 | url: string; 11 | user: User; 12 | }; 13 | 14 | const data = { 15 | /* ...*/ 16 | }; 17 | const user = new schema.Entity( 18 | 'users', 19 | {}, 20 | { idAttribute: 'id_str', fallbackStrategy: (key) => ({ id_str: key, name: 'Unknown' }) } 21 | ); 22 | const tweet = new schema.Entity( 23 | 'tweets', 24 | { user: user }, 25 | { 26 | idAttribute: 'id_str', 27 | // Apply everything from entityB over entityA, except for "favorites" 28 | mergeStrategy: (entityA, entityB) => ({ 29 | ...entityA, 30 | ...entityB, 31 | favorites: entityA.favorites 32 | }), 33 | // Remove the URL field from the entity 34 | processStrategy: (entity: Tweet, parent, key) => { 35 | const { url, ...entityWithoutUrl } = entity; 36 | return entityWithoutUrl; 37 | } 38 | } 39 | ); 40 | 41 | const normalizedData = normalize(data, tweet); 42 | const denormalizedData = denormalize(normalizedData.result, tweet, normalizedData.entities); 43 | 44 | const isTweet = tweet.key === 'tweets'; 45 | -------------------------------------------------------------------------------- /typescript-tests/github.ts: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from '../index' 2 | 3 | const user = new schema.Entity('users'); 4 | 5 | const label = new schema.Entity('labels'); 6 | 7 | const milestone = new schema.Entity('milestones', { 8 | creator: user 9 | }); 10 | 11 | const issue = new schema.Entity('issues', { 12 | assignee: user, 13 | assignees: [user], 14 | labels: label, 15 | milestone, 16 | user 17 | }); 18 | 19 | const pullRequest = new schema.Entity('pullRequests', { 20 | assignee: user, 21 | assignees: [user], 22 | labels: label, 23 | milestone, 24 | user 25 | }); 26 | 27 | const issueOrPullRequest = new schema.Array( 28 | { 29 | issues: issue, 30 | pullRequests: pullRequest 31 | }, 32 | (entity: any) => (entity.pull_request ? 'pullRequests' : 'issues') 33 | ); 34 | 35 | const data = { 36 | /* ...*/ 37 | }; 38 | const normalizedData = normalize(data, issueOrPullRequest); 39 | console.log(normalizedData); 40 | -------------------------------------------------------------------------------- /typescript-tests/object.ts: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from '../index' 2 | 3 | type Response = { 4 | users: Array<{ id: string }> 5 | } 6 | const data: Response = { users: [ { id: 'foo' } ] }; 7 | const user = new schema.Entity('users'); 8 | 9 | { 10 | const responseSchema = new schema.Object({ users: new schema.Array(user) }); 11 | const normalizedData = normalize(data, responseSchema); 12 | } 13 | 14 | { 15 | const responseSchema = new schema.Object({ users: (response: Response) => new schema.Array(user) }); 16 | const normalizedData = normalize(data, responseSchema); 17 | } 18 | 19 | { 20 | const responseSchema = { users: new schema.Array(user) }; 21 | const normalizedData = normalize(data, responseSchema); 22 | } 23 | -------------------------------------------------------------------------------- /typescript-tests/relationships.ts: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from '../index' 2 | 3 | const userProcessStrategy = (value: any, parent: any, key: string) => { 4 | switch (key) { 5 | case 'author': 6 | return { ...value, posts: [parent.id] }; 7 | case 'commenter': 8 | return { ...value, comments: [parent.id] }; 9 | default: 10 | return { ...value }; 11 | } 12 | }; 13 | 14 | const userMergeStrategy = (entityA: any, entityB: any) => { 15 | return { 16 | ...entityA, 17 | ...entityB, 18 | posts: [...(entityA.posts || []), ...(entityB.posts || [])], 19 | comments: [...(entityA.comments || []), ...(entityB.comments || [])] 20 | }; 21 | }; 22 | 23 | const user = new schema.Entity( 24 | 'users', 25 | {}, 26 | { 27 | mergeStrategy: userMergeStrategy, 28 | processStrategy: userProcessStrategy 29 | } 30 | ); 31 | 32 | const comment = new schema.Entity( 33 | 'comments', 34 | { 35 | commenter: user 36 | }, 37 | { 38 | processStrategy: (value: any, parent: any, key: string) => { 39 | return { ...value, post: parent.id }; 40 | } 41 | } 42 | ); 43 | 44 | const post = new schema.Entity('posts', { 45 | author: user, 46 | comments: [comment] 47 | }); 48 | 49 | const data = { 50 | /* ...*/ 51 | }; 52 | const normalizedData = normalize(data, post); 53 | console.log(normalizedData); 54 | -------------------------------------------------------------------------------- /typescript-tests/union.ts: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from '../index' 2 | 3 | const data = { owner: { id: 1, type: 'user' } }; 4 | 5 | const user = new schema.Entity('users'); 6 | const group = new schema.Entity('groups'); 7 | const unionSchema = new schema.Union( 8 | { 9 | user: user, 10 | group: group 11 | }, 12 | 'type' 13 | ); 14 | 15 | const normalizedData = normalize(data, { owner: unionSchema }); 16 | -------------------------------------------------------------------------------- /typescript-tests/values.ts: -------------------------------------------------------------------------------- 1 | import { normalize, schema } from '../index' 2 | 3 | const data = { firstThing: { id: 1 }, secondThing: { id: 2 } }; 4 | 5 | const item = new schema.Entity('items'); 6 | const valuesSchema = new schema.Values(item); 7 | 8 | const normalizedData = normalize(data, valuesSchema); 9 | --------------------------------------------------------------------------------