Repository: paularmstrong/normalizr Branch: master Commit: c2ab080641c7 Files: 86 Total size: 196.2 KB Directory structure: gitextract_b0ejolxe/ ├── .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 │ └── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ const { NODE_ENV, BABEL_ENV } = process.env; const cjs = BABEL_ENV === 'cjs' || NODE_ENV === 'test'; module.exports = { presets: [['@babel/preset-env', { loose: true }]], plugins: [ // cjs && 'transform-es2015-modules-commonjs', ['@babel/plugin-proposal-object-rest-spread', { loose: true }], ['@babel/plugin-proposal-class-properties', { loose: true }] ].filter(Boolean) }; ================================================ FILE: .eslintignore ================================================ dist/* node_modules/* examples/*/node_modules/* coverage/* ================================================ FILE: .eslintrc.js ================================================ module.exports = { // babel parser to support ES6/7 features parser: 'babel-eslint', parserOptions: { ecmaVersion: 7, ecmaFeatures: { experimentalObjectRestSpread: true, jsx: true, }, sourceType: 'module', }, extends: ['plugin:jest/recommended', 'prettier'], plugins: ['json', 'prettier'], env: { es6: true, node: true, }, globals: { document: false, navigator: false, window: false, }, rules: { 'accessor-pairs': 'error', camelcase: 'off', 'constructor-super': 'error', curly: ['error', 'all'], 'default-case': ['error', { commentPattern: '^no default$' }], eqeqeq: ['error', 'allow-null'], 'handle-callback-err': ['error', '^(err|error)$'], 'new-cap': ['error', { newIsCap: true, capIsNew: false }], 'no-alert': 'warn', 'no-array-constructor': 'error', 'no-caller': 'error', 'no-case-declarations': 'error', 'no-class-assign': 'error', 'no-compare-neg-zero': 'error', 'no-cond-assign': 'error', 'no-console': ['error', { allow: ['warn', 'error'] }], 'no-const-assign': 'error', 'no-control-regex': 'error', 'no-debugger': 'error', 'no-delete-var': 'error', 'no-dupe-args': 'error', 'no-dupe-class-members': 'error', 'no-dupe-keys': 'error', 'no-duplicate-case': 'error', 'no-empty-character-class': 'error', 'no-empty-pattern': 'error', 'no-eval': 'error', 'no-ex-assign': 'error', 'no-extend-native': 'error', 'no-extra-bind': 'error', 'no-extra-boolean-cast': 'error', 'no-fallthrough': 'error', 'no-func-assign': 'error', 'no-implied-eval': 'error', 'no-inner-declarations': ['error', 'functions'], 'no-invalid-regexp': 'error', 'no-iterator': 'error', 'no-label-var': 'error', 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], 'no-lone-blocks': 'error', 'no-loop-func': 'error', 'no-multi-str': 'error', 'no-native-reassign': 'error', 'no-negated-in-lhs': 'error', 'no-new': 'error', 'no-new-func': 'error', 'no-new-object': 'error', 'no-new-require': 'error', 'no-new-symbol': 'error', 'no-new-wrappers': 'error', 'no-obj-calls': 'error', 'no-octal': 'error', 'no-octal-escape': 'error', 'no-path-concat': 'error', 'no-proto': 'error', 'no-redeclare': 'error', 'no-regex-spaces': 'error', 'no-return-assign': ['error', 'except-parens'], 'no-script-url': 'error', 'no-self-assign': 'error', 'no-self-compare': 'error', 'no-sequences': 'error', 'no-shadow-restricted-names': 'error', 'no-sparse-arrays': 'error', 'no-this-before-super': 'error', 'no-throw-literal': 'error', 'no-undef': 'error', 'no-undef-init': 'error', 'no-unexpected-multiline': 'error', 'no-unmodified-loop-condition': 'error', 'no-unneeded-ternary': ['error', { defaultAssignment: false }], 'no-unreachable': 'error', 'no-unsafe-finally': 'error', 'no-unused-vars': ['error', { vars: 'all', args: 'none' }], 'no-useless-call': 'error', 'no-useless-computed-key': 'error', 'no-useless-concat': 'error', 'no-useless-constructor': 'error', 'no-useless-escape': 'error', 'no-var': 'error', 'no-with': 'error', 'prefer-const': 'error', 'prefer-rest-params': 'error', 'prefer-template': 'error', radix: 'error', 'require-yield': 'error', 'sort-imports': ['error', { memberSyntaxSortOrder: ['none', 'all', 'single', 'multiple'], ignoreCase: true }], 'spaced-comment': [ 'error', 'always', { markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] }, ], 'use-isnan': 'error', 'valid-typeof': 'error', yoda: ['error', 'never'], 'prettier/prettier': 'error', 'jest/consistent-test-it': ['error', { fn: 'test' }], 'jest/no-disabled-tests': 'error', 'jest/no-test-prefixes': 'error', 'jest/prefer-to-be-null': 'error', 'jest/prefer-to-be-undefined': 'error', 'jest/prefer-to-have-length': 'error', 'jest/valid-describe': 'error', 'jest/valid-expect': 'error', 'jest/valid-expect-in-promise': 'error', }, }; ================================================ FILE: .flowconfig ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: - paularmstrong ================================================ FILE: .github/ISSUE_TEMPLATE/bug.md ================================================ --- name: 🐛 Bug Report about: If something isn't working as expected 🤔. --- # Problem A short explanation of your problem or use-case is helpful! **Input** Here's how I'm using normalizr: ```js // Add as much relevant code and input as possible. const myData = { // This section is really helpful! A minimum test case goes a long way! }; const mySchema = new schema.Entity('myschema'); normalize(myData, mySchema); ``` **Output** Here's what I expect to see when I run the above: ```js { result: [1, 2], entities: { ... } } ``` Here's what I _actually_ see when I run the above: ```js { result: [1, 2], entities: { ... } } ``` ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: 🚀 Feature Request about: I have a suggestion (and I might like to implement it myself 😀)! --- # Problem Explain the problem you'd like to have Normalizr handle. # Solution 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. ================================================ FILE: .github/ISSUE_TEMPLATE/support_question.md ================================================ --- name: 🤗 Support or Question about: If you have a question 💬, please check for help on StackOverflow! --- 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 😁. --- If you have a support request or question please submit them to one of this resources: * StackOverflow: https://stackoverflow.com/questions/tagged/normalizr using the tag `normalizr` * 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 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Problem Explain the problem that this pull request aims to resolve. # Solution Explain your approach. Sometimes it helps to justify your approach against some others that you didn't choose to explain why yours is better. # TODO - [ ] Add & update tests - [ ] Ensure CI is passing (lint, tests, flow) - [ ] Update relevant documentation ================================================ FILE: .github/lock.yml ================================================ # Configuration for lock-threads - https://github.com/dessant/lock-threads # Number of days of inactivity before a closed issue or pull request is locked daysUntilLock: 182 # Issues and pull requests with these labels will not be locked. Set to `[]` to disable exemptLabels: [] # Label to add before locking, such as `outdated`. Set to `false` to disable lockLabel: Outdated # Comment to post before locking. Set to `false` to disable lockComment: > This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. ================================================ FILE: .github/workflows/close-pull-request.yml ================================================ name: Close Pull Request on: pull_request_target: types: [opened] jobs: run: runs-on: ubuntu-latest steps: - uses: superbrothers/close-pull-request@v3 with: comment: 'Normalizr is no longer maintained and does not accept pull requests. Please maintain your own fork of this repository.' ================================================ FILE: .gitignore ================================================ .DS_Store node_modules/* dist/* *.log coverage/* .coveralls.yml .eslintcache ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - '10' - '12' - '14' script: - npm run lint:ci - npm run test:ci - npm run build - npm run typecheck after_success: - npm run test:coverage ================================================ FILE: CHANGELOG.md ================================================ # v3.6.1 - **Fixed** Add types for fallback strategy - **Chore** Upgraded development dependencies # v3.6.0 - **Added** `fallbackStrategy` for denormalization (#422) - **Fixed** entities can be `undefined` in TS defs if none found (#435) # v3.5.0 - **Added** ability to dynamically set nested schema type (#415) - **Changed** Enable loose transformation for object spread operator to improve performance (#431) - **Fixed** don't use schema to attribute mapping on singular array schemas (#387) - **Fixed** When normalize() receives null input, don't say it is an object (#411) - **Fixed** Improve performance of circular reference detection (#420) # v3.4.0 - **Changed** Now built with Babel 7 - **Added** Support for circular references (gh-335) - **Added** Symbols are valid keys for Entity keys (gh-369) - **Added/Changed** Typescript definitions include generics for `normalize` (gh-363) - **Fixed** denormalization skipping of falsy valued ids used in `Object` schemas (gh-345) - **Chore** Update dev dependencies - **Chore** Added Greenkeeper # v3.3.0 - **Added** ES Module builds - **Fixed** type error with typescript on array+object shorthand (gh-322) # v3.2.0 - **Added** Support denormalizing from Immutable entities (gh-228) - **Added** Brought back `get idAttribute()` to `schema.Entity` (gh-226) - **Fixed** Gracefully handle missing data in `denormalize` (gh-232) - **Fixed** Prevent infinite recursion in `denormalize` (gh-220) # v3.1.0 - **Added** `denormalize`. (gh-214) - **Changed** No longer requires all input in a polymorphic schema (`Array`, `Union`, `Values`) have a matching schema definition. (gh-208) - **Changed** Builds do both rollup and plain babel file conversions. `"main"` property in package.json points to babel-converted files. # v3.0.0 The entire normalizr package has been rewritten from v2.x for this version. Please refer to the [documentation](/docs) for all changes. ## Added - `schema.Entity` - `processStrategy` for modifying `Entity` objects before they're moved to the `entities` stack. - `mergeStrategy` for merging with multiple entities with the same ID. - Added `schema.Object`, with a shorthand of `{}` - Added `schema.Array`, with a shorthand of `[ schema ]` ## Changed - `Schema` has been moved to a `schema` namespace, available at `schema.Entity` - `arrayOf` has been replaced by `schema.Array` or `[]` - `unionOf` has been replaced by `schema.Union` - `valuesOf` has been replaced by `schema.Values` ## Removed - `normalize` no longer accepts an optional `options` argument. All options are assigned at the schema level. - Entity schema no longer accepts `defaults` as an option. Use a custom `processStrategy` option to apply defaults as needed. - `assignEntity` has been replaced by `processStrategy` - `meta` option. See `processStrategy` ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## Issues 1. Follow the [Issue Template](/.github/ISSUE_TEMPLATE.md) provided when opening a new Issue. 2. Provide a minimal, reproducible test-case. 3. Do not ask for help or usage questions in Issues. Use [StackOverflow](http://stackoverflow.com/questions/tagged/normalizr) for those. ## Pull Requests First, thank you so much for contributing to open source and the Normalizr project! Follow the instructions on the Pull Request Template (shown when you open a new PR) and make sure you've done the following: - [ ] Add & update tests - [ ] Ensure CI is passing (lint, tests) - [ ] Update relevant documentation ## Setup Normalizr uses [yarn](https://yarnpkg.com) for development dependency management. Ensure you have it installed before continuing. ```sh yarn ``` ## Running Tests ```sh npm run test ``` ## Lint 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: ```sh npm run lint ``` ## Building 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: ```sh npm run build ``` ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Dan Abramov, Paul Armstrong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # 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) # 📣 Normalizr is no longer maintained 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. ## FAQs ### Should I still use Normalizr? 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. ### What should I do if I want other features or found a bug? Fork [Normalizr on Github](https://github.com/paularmstrong/normalizr) and maintain a version yourself. ### Can I contribute back to Normalizr? 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. ================================================ FILE: docs/README.md ================================================ # 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) ## Install Install from the NPM repository using yarn or npm: ```shell yarn add normalizr ``` ```shell npm install normalizr ``` ## Motivation 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/). ## Solution Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries. ## Documentation - [Introduction](/docs/introduction.md) - [Build Files](/docs/introduction.md#build-files) - [Quick Start](/docs/quickstart.md) - [API](/docs/api.md) - [normalize](/docs/api.md#normalizedata-schema) - [denormalize](/docs/api.md#denormalizeinput-schema-entities) - [schema](/docs/api.md#schema) - [Using with JSONAPI](/docs/jsonapi.md) ## Examples - [Normalizing GitHub Issues](/examples/github) - [Relational Data](/examples/relationships) - [Interactive Redux](/examples/redux) ## Quick Start Consider a typical blog post. The API response for a single post might look something like this: ```json { "id": "123", "author": { "id": "1", "name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "commenter": { "id": "2", "name": "Nicole" } } ] } ``` We have two nested entity types within our `article`: `users` and `comments`. Using various `schema`, we can normalize all three entity types down: ```js import { normalize, schema } from 'normalizr'; // Define a users schema const user = new schema.Entity('users'); // Define your comments schema const comment = new schema.Entity('comments', { commenter: user, }); // Define your article const article = new schema.Entity('articles', { author: user, comments: [comment], }); const normalizedData = normalize(originalData, article); ``` Now, `normalizedData` will be: ```js { result: "123", entities: { "articles": { "123": { id: "123", author: "1", title: "My awesome blog post", comments: [ "324" ] } }, "users": { "1": { "id": "1", "name": "Paul" }, "2": { "id": "2", "name": "Nicole" } }, "comments": { "324": { id: "324", "commenter": "2" } } } } ``` ## Dependencies None. ## Credits 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). ================================================ FILE: docs/api.md ================================================ # API - [normalize](#normalizedata-schema) - [denormalize](#denormalizeinput-schema-entities) - [schema](#schema) - [Array](#arraydefinition-schemaattribute) - [Entity](#entitykey-definition---options--) - [Object](#objectdefinition) - [Union](#uniondefinition-schemaattribute) - [Values](#valuesdefinition-schemaattribute) ## `normalize(data, schema)` Normalizes input data per the schema definition provided. - `data`: **required** Input JSON (or plain JS object) data that needs normalization. - `schema`: **required** A schema definition ### Usage ```js import { normalize, schema } from 'normalizr'; const myData = { users: [{ id: 1 }, { id: 2 }] }; const user = new schema.Entity('users'); const mySchema = { users: [user] }; const normalizedData = normalize(myData, mySchema); ``` ### Output ```js { result: { users: [ 1, 2 ] }, entities: { users: { '1': { id: 1 }, '2': { id: 2 } } } } ``` ## `denormalize(input, schema, entities)` Denormalizes an input based on schema and provided entities from a plain object or Immutable data. The reverse of `normalize`. _Special Note:_ Be careful with denormalization. Prematurely reverting your data to large, nested objects could cause performance impacts in React (and other) applications. 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. - `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`. - `schema`: **required** A schema definition that was used to get the value for `input`. - `entities`: **required** An object, keyed by entity schema names that may appear in the denormalized output. Also accepts an object with Immutable data. ### Usage ```js import { denormalize, schema } from 'normalizr'; const user = new schema.Entity('users'); const mySchema = { users: [user] }; const entities = { users: { '1': { id: 1 }, '2': { id: 2 } } }; const denormalizedData = denormalize({ users: [1, 2] }, mySchema, entities); ``` ### Output ```js { users: [{ id: 1 }, { id: 2 }]; } ``` ## `schema` ### `Array(definition, schemaAttribute)` 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. _Note: The same behavior can be defined with shorthand syntax: `[ mySchema ]`_ - `definition`: **required** A singular schema that this array contains _or_ a mapping of schema to attribute values. - `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. Can be a string or a function. If given a function, accepts the following arguments: _ `value`: The input value of the entity. _ `parent`: The parent object of the input array. \* `key`: The key at which the input array appears on the parent object. #### Instance Methods - `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. #### Usage To describe a simple array of a singular entity type: ```js const data = [{ id: '123', name: 'Jim' }, { id: '456', name: 'Jane' }]; const userSchema = new schema.Entity('users'); const userListSchema = new schema.Array(userSchema); // or use shorthand syntax: const userListSchema = [userSchema]; const normalizedData = normalize(data, userListSchema); ``` #### Output ```js { entities: { users: { '123': { id: '123', name: 'Jim' }, '456': { id: '456', name: 'Jane' } } }, result: [ '123', '456' ] } ``` If your input data is an array of more than one type of entity, it is necessary to define a schema mapping. _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._ For example: ```js const data = [{ id: 1, type: 'admin' }, { id: 2, type: 'user' }]; const userSchema = new schema.Entity('users'); const adminSchema = new schema.Entity('admins'); const myArray = new schema.Array( { admins: adminSchema, users: userSchema }, (input, parent, key) => `${input.type}s` ); const normalizedData = normalize(data, myArray); ``` #### Output ```js { entities: { admins: { '1': { id: 1, type: 'admin' } }, users: { '2': { id: 2, type: 'user' } } }, result: [ { id: 1, schema: 'admins' }, { id: 2, schema: 'users' } ] } ``` ### `Entity(key, definition = {}, options = {})` - `key`: **required** The key name under which all entities of this type will be listed in the normalized response. Must be a string name. - `definition`: A definition of the nested entities found within this entity. Defaults to empty object. 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. - `options`: - `idAttribute`: The attribute where unique IDs for each of this entity type can be found. 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. As a function, accepts the following arguments, in order: - `value`: The input value of the entity. - `parent`: The parent object of the input array. - `key`: The key at which the input array appears on the parent object. - `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. - `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. _Note: It is recommended to always return a copy of your input and not modify the original._ The function accepts the following arguments, in order: - `value`: The input value of the entity. - `parent`: The parent object of the input array. - `key`: The key at which the input array appears on the parent object. - `fallbackStrategy(key, schema)`: Strategy to use when denormalizing data structures with id references to missing entities. - `key`: The key at which the input array appears on the parent object. - `schema`: The schema of the missing entity #### Instance Methods - `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. #### Instance Attributes - `key`: Returns the key provided to the constructor. - `idAttribute`: Returns the idAttribute provided to the constructor in options. #### Usage ```js const data = { id_str: '123', url: 'https://twitter.com', user: { id_str: '456', name: 'Jimmy' } }; const user = new schema.Entity('users', {}, { idAttribute: 'id_str' }); const tweet = new schema.Entity( 'tweets', { user: user }, { idAttribute: 'id_str', // Apply everything from entityB over entityA, except for "favorites" mergeStrategy: (entityA, entityB) => ({ ...entityA, ...entityB, favorites: entityA.favorites }), // Remove the URL field from the entity processStrategy: (entity) => omit(entity, 'url') } ); const normalizedData = normalize(data, tweet); ``` #### Output ```js { entities: { tweets: { '123': { id_str: '123', user: '456' } }, users: { '456': { id_str: '456', name: 'Jimmy' } } }, result: '123' } ``` #### `idAttribute` Usage When passing the `idAttribute` a function, it should return the IDs value. For Example: ```js const data = [{ id: '1', guest_id: null, name: 'Esther' }, { id: '1', guest_id: '22', name: 'Tom' }]; const patronsSchema = new schema.Entity('patrons', undefined, { // idAttribute *functions* must return the ids **value** (not key) idAttribute: (value) => (value.guest_id ? `${value.id}-${value.guest_id}` : value.id) }); normalize(data, [patronsSchema]); ``` #### Output ```js { entities: { patrons: { '1': { id: '1', guest_id: null, name: 'Esther' }, '1-22': { id: '1', guest_id: '22', name: 'Tom' }, } }, result: ['1', '1-22'] } ``` #### `fallbackStrategy` Usage ```js const users = { '1': { id: '1', name: "Emily", requestState: 'SUCCEEDED' }, '2': { id: '2', name: "Douglas", requestState: 'SUCCEEDED' } }; const books = { '1': {id: '1', name: "Book 1", author: 1 }, '2': {id: '2', name: "Book 2", author: 2 }, '3': {id: '3', name: "Book 3", author: 3 } }; const authorSchema = new schema.Entity('authors', {}, { fallbackStrategy: (key, schema) => { return { [schema.idAttribute]: key, name: 'Unknown', requestState: 'NONE' }; } }); const bookSchema = new schema.Entity('books', { author: authorSchema }); denormalize([1, 2, 3], [bookSchema], { books, authors: users }) ``` #### Output ```js [ { id: '1', name: "Book 1", author: { id: '1', name: "Emily", requestState: 'SUCCEEDED' } }, { id: '2', name: "Book 2", author: { id: '2', name: "Douglas", requestState: 'SUCCEEDED' }, }, { id: '3', name: "Book 3", author: { id: '3', name: "Unknown", requestState: 'NONE' }, } ] ``` ### `Object(definition)` Define a plain object mapping that has values needing to be normalized into Entities. _Note: The same behavior can be defined with shorthand syntax: `{ ... }`_ - `definition`: **required** A definition of the nested entities found within this object. Defaults to empty object. 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. #### Instance Methods - `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. #### Usage ```js // Example data response const data = { users: [{ id: '123', name: 'Beth' }] }; const user = new schema.Entity('users'); const responseSchema = new schema.Object({ users: new schema.Array(user) }); // or shorthand const responseSchema = { users: new schema.Array(user) }; const normalizedData = normalize(data, responseSchema); ``` #### Output ```js { entities: { users: { '123': { id: '123', name: 'Beth' } } }, result: { users: [ '123' ] } } ``` ### `Union(definition, schemaAttribute)` 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. - `definition`: **required** An object mapping the definition of the nested entities found within the input array - `schemaAttribute`: **required** The attribute on each entity found that defines what schema, per the definition mapping, to use when normalizing. Can be a string or a function. If given a function, accepts the following arguments: - `value`: The input value of the entity. - `parent`: The parent object of the input array. - `key`: The key at which the input array appears on the parent object. #### Instance Methods - `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. #### Usage _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._ ```js const data = { owner: { id: 1, type: 'user', name: 'Anne' } }; const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const unionSchema = new schema.Union( { user: user, group: group }, 'type' ); const normalizedData = normalize(data, { owner: unionSchema }); ``` #### Output ```js { entities: { users: { '1': { id: 1, type: 'user', name: 'Anne' } } }, result: { owner: { id: 1, schema: 'user' } } } ``` ### `Values(definition, schemaAttribute)` Describes a map whose values follow the given schema. - `definition`: **required** A singular schema that this array contains _or_ a mapping of schema to attribute values. - `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. Can be a string or a function. If given a function, accepts the following arguments: - `value`: The input value of the entity. - `parent`: The parent object of the input array. - `key`: The key at which the input array appears on the parent object. #### Instance Methods - `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. #### Usage ```js const data = { firstThing: { id: 1 }, secondThing: { id: 2 } }; const item = new schema.Entity('items'); const valuesSchema = new schema.Values(item); const normalizedData = normalize(data, valuesSchema); ``` #### Output ```js { entities: { items: { '1': { id: 1 }, '2': { id: 2 } } }, result: { firstThing: 1, secondThing: 2 } } ``` 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`. _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._ For example: ```js const data = { '1': { id: 1, type: 'admin' }, '2': { id: 2, type: 'user' } }; const userSchema = new schema.Entity('users'); const adminSchema = new schema.Entity('admins'); const valuesSchema = new schema.Values( { admins: adminSchema, users: userSchema }, (input, parent, key) => `${input.type}s` ); const normalizedData = normalize(data, valuesSchema); ``` #### Output ```js { entities: { admins: { '1': { id: 1, type: 'admin' } }, users: { '2': { id: 2, type: 'user' } } }, result: { '1': { id: 1, schema: 'admins' }, '2': { id: 2, schema: 'users' } } } ``` ================================================ FILE: docs/faqs.md ================================================ # Frequently Asked Questions 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. ================================================ FILE: docs/introduction.md ================================================ # Introduction ## Motivation 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/). ## Solution Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries. ### Example The following nested object: ```js [ { id: 1, title: 'Some Article', author: { id: 1, name: 'Dan' } }, { id: 2, title: 'Other Article', author: { id: 1, name: 'Dan' } } ]; ``` Can be normalized to: ```js { result: [1, 2], entities: { articles: { 1: { id: 1, title: 'Some Article', author: 1 }, 2: { id: 2, title: 'Other Article', author: 1 } }, users: { 1: { id: 1, name: 'Dan' } } } } ``` ## Build Files Normalizr is built for various environments - `src/*` - 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`. - `normalizr.js`, `normalizr.min.js` - [CommonJS](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) - `normalizr.amd.js`, `normalizr.amd.min.js` - [Asynchronous Module Definition](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) - `normalizr.umd.js`, `normalizr.umn.min.js` - [Universal Module Definition](http://davidbcalhoun.com/2014/what-is-amd-commonjs-and-umd/) - `normalizr.browser.js`, `normalizr.browser.min.js` - [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. - 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. ================================================ FILE: docs/jsonapi.md ================================================ # Normalizr and JSONAPI 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. 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\*: - [stevenpetryk/jsonapi-normalizer](https://github.com/stevenpetryk/jsonapi-normalizer) - [yury-dymov/json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer) - [JSONAPI client libraries](http://jsonapi.org/implementations/#client-libraries-javascript) **Note:** These are in no particular order. Review all libraries on your own before deciding which is best for your particular use-case. ================================================ FILE: docs/quickstart.md ================================================ # Quick Start Consider a typical blog post. The API response for a single post might look something like this: ```json { "id": "123", "author": { "id": "1", "name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "commenter": { "id": "2", "name": "Nicole" } } ] } ``` We have two nested entity types within our `article`: `users` and `comments`. Using various `schema`, we can normalize all three entity types down: ```js import { normalize, schema } from 'normalizr'; // Define a users schema const user = new schema.Entity('users'); // Define your comments schema const comment = new schema.Entity('comments', { commenter: user }); // Define your article const article = new schema.Entity('articles', { author: user, comments: [comment] }); const normalizedData = normalize(originalData, article); ``` Now, `normalizedData` will be: ```js { result: "123", entities: { "articles": { "123": { id: "123", author: "1", title: "My awesome blog post", comments: [ "324" ] } }, "users": { "1": { "id": "1", "name": "Paul" }, "2": { "id": "2", "name": "Nicole" } }, "comments": { "324": { id: "324", "commenter": "2" } } } } ``` ================================================ FILE: examples/.eslintrc ================================================ { "rules": { "no-console": "off" } } ================================================ FILE: examples/github/README.md ================================================ # Normalizing GitHub Issues This is a barebones example for node to illustrate how normalizing the GitHub Issues API endpoint could work. ## Running ```sh # from the root directory: yarn # from this directory: ../../node_modules/.bin/babel-node ./index.js ``` ## Files * [index.js](/examples/github/index.js): Pulls live data from the GitHub API for this project's issues and normalizes the JSON. * [output.json](/examples/github/output.json): A sample of the normalized output. * [schema.js](/examples/github/schema.js): The schema used to normalize the GitHub issues. ================================================ FILE: examples/github/index.js ================================================ import * as schema from './schema'; import fs from 'fs'; import https from 'https'; import { normalize } from '../../src'; import path from 'path'; let data = ''; const request = https.request( { host: 'api.github.com', path: '/repos/paularmstrong/normalizr/issues', method: 'get', headers: { 'user-agent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)', }, }, (res) => { res.on('data', (d) => { data += d; }); res.on('end', () => { const normalizedData = normalize(JSON.parse(data), schema.issueOrPullRequest); const out = JSON.stringify(normalizedData, null, 2); fs.writeFileSync(path.resolve(__dirname, './output.json'), out); }); res.on('error', (e) => { console.log(e); }); } ); request.end(); ================================================ FILE: examples/github/output.json ================================================ { "entities": { "users": { "14322": { "login": "whistlerbrk", "id": 14322, "avatar_url": "https://avatars.githubusercontent.com/u/14322?v=3", "gravatar_id": "", "url": "https://api.github.com/users/whistlerbrk", "html_url": "https://github.com/whistlerbrk", "followers_url": "https://api.github.com/users/whistlerbrk/followers", "following_url": "https://api.github.com/users/whistlerbrk/following{/other_user}", "gists_url": "https://api.github.com/users/whistlerbrk/gists{/gist_id}", "starred_url": "https://api.github.com/users/whistlerbrk/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/whistlerbrk/subscriptions", "organizations_url": "https://api.github.com/users/whistlerbrk/orgs", "repos_url": "https://api.github.com/users/whistlerbrk/repos", "events_url": "https://api.github.com/users/whistlerbrk/events{/privacy}", "received_events_url": "https://api.github.com/users/whistlerbrk/received_events", "type": "User", "site_admin": false }, "33297": { "login": "paularmstrong", "id": 33297, "avatar_url": "https://avatars.githubusercontent.com/u/33297?v=3", "gravatar_id": "", "url": "https://api.github.com/users/paularmstrong", "html_url": "https://github.com/paularmstrong", "followers_url": "https://api.github.com/users/paularmstrong/followers", "following_url": "https://api.github.com/users/paularmstrong/following{/other_user}", "gists_url": "https://api.github.com/users/paularmstrong/gists{/gist_id}", "starred_url": "https://api.github.com/users/paularmstrong/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/paularmstrong/subscriptions", "organizations_url": "https://api.github.com/users/paularmstrong/orgs", "repos_url": "https://api.github.com/users/paularmstrong/repos", "events_url": "https://api.github.com/users/paularmstrong/events{/privacy}", "received_events_url": "https://api.github.com/users/paularmstrong/received_events", "type": "User", "site_admin": false }, "69485": { "login": "unindented", "id": 69485, "avatar_url": "https://avatars.githubusercontent.com/u/69485?v=3", "gravatar_id": "", "url": "https://api.github.com/users/unindented", "html_url": "https://github.com/unindented", "followers_url": "https://api.github.com/users/unindented/followers", "following_url": "https://api.github.com/users/unindented/following{/other_user}", "gists_url": "https://api.github.com/users/unindented/gists{/gist_id}", "starred_url": "https://api.github.com/users/unindented/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/unindented/subscriptions", "organizations_url": "https://api.github.com/users/unindented/orgs", "repos_url": "https://api.github.com/users/unindented/repos", "events_url": "https://api.github.com/users/unindented/events{/privacy}", "received_events_url": "https://api.github.com/users/unindented/received_events", "type": "User", "site_admin": false }, "84749": { "login": "pcompassion", "id": 84749, "avatar_url": "https://avatars.githubusercontent.com/u/84749?v=3", "gravatar_id": "", "url": "https://api.github.com/users/pcompassion", "html_url": "https://github.com/pcompassion", "followers_url": "https://api.github.com/users/pcompassion/followers", "following_url": "https://api.github.com/users/pcompassion/following{/other_user}", "gists_url": "https://api.github.com/users/pcompassion/gists{/gist_id}", "starred_url": "https://api.github.com/users/pcompassion/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/pcompassion/subscriptions", "organizations_url": "https://api.github.com/users/pcompassion/orgs", "repos_url": "https://api.github.com/users/pcompassion/repos", "events_url": "https://api.github.com/users/pcompassion/events{/privacy}", "received_events_url": "https://api.github.com/users/pcompassion/received_events", "type": "User", "site_admin": false }, "655857": { "login": "adjohu", "id": 655857, "avatar_url": "https://avatars.githubusercontent.com/u/655857?v=3", "gravatar_id": "", "url": "https://api.github.com/users/adjohu", "html_url": "https://github.com/adjohu", "followers_url": "https://api.github.com/users/adjohu/followers", "following_url": "https://api.github.com/users/adjohu/following{/other_user}", "gists_url": "https://api.github.com/users/adjohu/gists{/gist_id}", "starred_url": "https://api.github.com/users/adjohu/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/adjohu/subscriptions", "organizations_url": "https://api.github.com/users/adjohu/orgs", "repos_url": "https://api.github.com/users/adjohu/repos", "events_url": "https://api.github.com/users/adjohu/events{/privacy}", "received_events_url": "https://api.github.com/users/adjohu/received_events", "type": "User", "site_admin": false }, "853220": { "login": "babsonmatt", "id": 853220, "avatar_url": "https://avatars.githubusercontent.com/u/853220?v=3", "gravatar_id": "", "url": "https://api.github.com/users/babsonmatt", "html_url": "https://github.com/babsonmatt", "followers_url": "https://api.github.com/users/babsonmatt/followers", "following_url": "https://api.github.com/users/babsonmatt/following{/other_user}", "gists_url": "https://api.github.com/users/babsonmatt/gists{/gist_id}", "starred_url": "https://api.github.com/users/babsonmatt/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/babsonmatt/subscriptions", "organizations_url": "https://api.github.com/users/babsonmatt/orgs", "repos_url": "https://api.github.com/users/babsonmatt/repos", "events_url": "https://api.github.com/users/babsonmatt/events{/privacy}", "received_events_url": "https://api.github.com/users/babsonmatt/received_events", "type": "User", "site_admin": false }, "969003": { "login": "YoruNoHikage", "id": 969003, "avatar_url": "https://avatars.githubusercontent.com/u/969003?v=3", "gravatar_id": "", "url": "https://api.github.com/users/YoruNoHikage", "html_url": "https://github.com/YoruNoHikage", "followers_url": "https://api.github.com/users/YoruNoHikage/followers", "following_url": "https://api.github.com/users/YoruNoHikage/following{/other_user}", "gists_url": "https://api.github.com/users/YoruNoHikage/gists{/gist_id}", "starred_url": "https://api.github.com/users/YoruNoHikage/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/YoruNoHikage/subscriptions", "organizations_url": "https://api.github.com/users/YoruNoHikage/orgs", "repos_url": "https://api.github.com/users/YoruNoHikage/repos", "events_url": "https://api.github.com/users/YoruNoHikage/events{/privacy}", "received_events_url": "https://api.github.com/users/YoruNoHikage/received_events", "type": "User", "site_admin": false }, "1499050": { "login": "poyannabati", "id": 1499050, "avatar_url": "https://avatars.githubusercontent.com/u/1499050?v=3", "gravatar_id": "", "url": "https://api.github.com/users/poyannabati", "html_url": "https://github.com/poyannabati", "followers_url": "https://api.github.com/users/poyannabati/followers", "following_url": "https://api.github.com/users/poyannabati/following{/other_user}", "gists_url": "https://api.github.com/users/poyannabati/gists{/gist_id}", "starred_url": "https://api.github.com/users/poyannabati/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/poyannabati/subscriptions", "organizations_url": "https://api.github.com/users/poyannabati/orgs", "repos_url": "https://api.github.com/users/poyannabati/repos", "events_url": "https://api.github.com/users/poyannabati/events{/privacy}", "received_events_url": "https://api.github.com/users/poyannabati/received_events", "type": "User", "site_admin": false }, "1591483": { "login": "arb", "id": 1591483, "avatar_url": "https://avatars.githubusercontent.com/u/1591483?v=3", "gravatar_id": "", "url": "https://api.github.com/users/arb", "html_url": "https://github.com/arb", "followers_url": "https://api.github.com/users/arb/followers", "following_url": "https://api.github.com/users/arb/following{/other_user}", "gists_url": "https://api.github.com/users/arb/gists{/gist_id}", "starred_url": "https://api.github.com/users/arb/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/arb/subscriptions", "organizations_url": "https://api.github.com/users/arb/orgs", "repos_url": "https://api.github.com/users/arb/repos", "events_url": "https://api.github.com/users/arb/events{/privacy}", "received_events_url": "https://api.github.com/users/arb/received_events", "type": "User", "site_admin": false }, "1690457": { "login": "TryingToImprove", "id": 1690457, "avatar_url": "https://avatars.githubusercontent.com/u/1690457?v=3", "gravatar_id": "", "url": "https://api.github.com/users/TryingToImprove", "html_url": "https://github.com/TryingToImprove", "followers_url": "https://api.github.com/users/TryingToImprove/followers", "following_url": "https://api.github.com/users/TryingToImprove/following{/other_user}", "gists_url": "https://api.github.com/users/TryingToImprove/gists{/gist_id}", "starred_url": "https://api.github.com/users/TryingToImprove/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/TryingToImprove/subscriptions", "organizations_url": "https://api.github.com/users/TryingToImprove/orgs", "repos_url": "https://api.github.com/users/TryingToImprove/repos", "events_url": "https://api.github.com/users/TryingToImprove/events{/privacy}", "received_events_url": "https://api.github.com/users/TryingToImprove/received_events", "type": "User", "site_admin": false }, "5119347": { "login": "lqzerogg", "id": 5119347, "avatar_url": "https://avatars.githubusercontent.com/u/5119347?v=3", "gravatar_id": "", "url": "https://api.github.com/users/lqzerogg", "html_url": "https://github.com/lqzerogg", "followers_url": "https://api.github.com/users/lqzerogg/followers", "following_url": "https://api.github.com/users/lqzerogg/following{/other_user}", "gists_url": "https://api.github.com/users/lqzerogg/gists{/gist_id}", "starred_url": "https://api.github.com/users/lqzerogg/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/lqzerogg/subscriptions", "organizations_url": "https://api.github.com/users/lqzerogg/orgs", "repos_url": "https://api.github.com/users/lqzerogg/repos", "events_url": "https://api.github.com/users/lqzerogg/events{/privacy}", "received_events_url": "https://api.github.com/users/lqzerogg/received_events", "type": "User", "site_admin": false }, "6775919": { "login": "BerkeleyTrue", "id": 6775919, "avatar_url": "https://avatars.githubusercontent.com/u/6775919?v=3", "gravatar_id": "", "url": "https://api.github.com/users/BerkeleyTrue", "html_url": "https://github.com/BerkeleyTrue", "followers_url": "https://api.github.com/users/BerkeleyTrue/followers", "following_url": "https://api.github.com/users/BerkeleyTrue/following{/other_user}", "gists_url": "https://api.github.com/users/BerkeleyTrue/gists{/gist_id}", "starred_url": "https://api.github.com/users/BerkeleyTrue/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/BerkeleyTrue/subscriptions", "organizations_url": "https://api.github.com/users/BerkeleyTrue/orgs", "repos_url": "https://api.github.com/users/BerkeleyTrue/repos", "events_url": "https://api.github.com/users/BerkeleyTrue/events{/privacy}", "received_events_url": "https://api.github.com/users/BerkeleyTrue/received_events", "type": "User", "site_admin": false }, "14313723": { "login": "vimalceg", "id": 14313723, "avatar_url": "https://avatars.githubusercontent.com/u/14313723?v=3", "gravatar_id": "", "url": "https://api.github.com/users/vimalceg", "html_url": "https://github.com/vimalceg", "followers_url": "https://api.github.com/users/vimalceg/followers", "following_url": "https://api.github.com/users/vimalceg/following{/other_user}", "gists_url": "https://api.github.com/users/vimalceg/gists{/gist_id}", "starred_url": "https://api.github.com/users/vimalceg/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/vimalceg/subscriptions", "organizations_url": "https://api.github.com/users/vimalceg/orgs", "repos_url": "https://api.github.com/users/vimalceg/repos", "events_url": "https://api.github.com/users/vimalceg/events{/privacy}", "received_events_url": "https://api.github.com/users/vimalceg/received_events", "type": "User", "site_admin": false } }, "labels": { "undefined": { "0": { "id": 373884166, "url": "https://api.github.com/repos/paularmstrong/normalizr/labels/Priority:%20Low", "name": "Priority: Low", "color": "009800", "default": false }, "1": { "id": 373884281, "url": "https://api.github.com/repos/paularmstrong/normalizr/labels/Status:%20Accepted", "name": "Status: Accepted", "color": "009800", "default": false }, "2": { "id": 373884447, "url": "https://api.github.com/repos/paularmstrong/normalizr/labels/Type:%20Bug", "name": "Type: Bug", "color": "e11d21", "default": false }, "3": { "id": 373884449, "url": "https://api.github.com/repos/paularmstrong/normalizr/labels/Type:%20Question", "name": "Type: Question", "color": "cc317c", "default": false } } }, "milestones": { "1883567": { "url": "https://api.github.com/repos/paularmstrong/normalizr/milestones/2", "html_url": "https://github.com/paularmstrong/normalizr/milestone/2", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/milestones/2/labels", "id": 1883567, "number": 2, "title": "2.3.0", "description": "", "creator": 33297, "open_issues": 2, "closed_issues": 1, "state": "closed", "created_at": "2016-07-14T15:24:52Z", "updated_at": "2016-12-21T21:54:06Z", "due_on": "2016-08-20T07:00:00Z", "closed_at": "2016-12-19T19:32:20Z" }, "2205635": { "url": "https://api.github.com/repos/paularmstrong/normalizr/milestones/3", "html_url": "https://github.com/paularmstrong/normalizr/milestone/3", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/milestones/3/labels", "id": 2205635, "number": 3, "title": "3.0.0", "description": "Fully re-written, simpler, faster, more extendable.", "creator": 33297, "open_issues": 3, "closed_issues": 0, "state": "open", "created_at": "2016-12-19T19:32:16Z", "updated_at": "2016-12-21T15:42:53Z", "due_on": "2017-01-03T08:00:00Z", "closed_at": null } }, "issues": { "127301608": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/54", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/54/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/54/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/54/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/54", "id": 127301608, "number": 54, "title": "Should empty arrays be preserved on their entity?", "user": 853220, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 5, "created_at": "2016-01-18T20:07:54Z", "updated_at": "2016-05-24T14:00:18Z", "closed_at": null, "body": "{ id: 1, name: 'test', tags: [] } normalizes as { id: 1, name: 'test' }\n\nWould it make sense to preserve the empty array or at least allow the option to do so?\n" }, "134547187": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/68", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/68/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/68/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/68/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/68", "id": 134547187, "number": 68, "title": "Dealing with collections keyed by ID without an ID in the entity itself", "user": 655857, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 4, "created_at": "2016-02-18T11:02:18Z", "updated_at": "2016-05-24T13:59:36Z", "closed_at": null, "body": "Is there any way I can deal with data in the following format?\n\n```\n{\n 1: {\n name: 'test user'\n }\n}\n```\n\nAs far as I can tell, I can only work with objects that themselves contain an id i.e.\n\n```\n{\n name: 'test user',\n id: 1\n}\n```\n" }, "134935104": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/70", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/70/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/70/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/70/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/70", "id": 134935104, "number": 70, "title": "Keep consistency for relations", "user": 969003, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 9, "created_at": "2016-02-19T18:10:17Z", "updated_at": "2016-05-24T13:45:14Z", "closed_at": null, "body": "I think it would be great to have consistency in relationship between entities when normalized.\nHere's an example : \n\nConsider you have two entities **user** and **project**. You define the relations like this (bidirectional) : \n\n``` js\nuserSchema.define({\n projects: arrayOf(projectSchema),\n})\n\nprojectSchema.define({\n contributors: arrayOf(userSchema),\n})\n```\n\nNow you want to create a project, you send some request to a server that replies with the project entity like this : \n\n``` js\n{\n id: 80,\n contributors: [1], // your userId for example\n // other fields\n}\n```\n\nYou normalize the response and merge with the rest of your entities (just like the real-world example for Redux), the projects' entities are updated but not the user.\n\nSo my proposal is to add a capability of telling what is the property in the foreign object and instead of normalizing like this : \n\n``` js\n{\n entities: {\n projects: {\n 80: {\n id: 80,\n contributors: [1],\n // other fields\n }\n }\n }\n}\n```\n\nI'm proposing something like this : \n\n``` js\n{\n entities: {\n projects: {\n 80: {\n id: 80,\n contributors: [1],\n // other fields\n }\n },\n users: {\n 1: {\n id: 1,\n projects: [80]\n }\n }\n }\n}\n```\n\nWe could do something like this : \n\n``` js\nuserSchema.define({\n projects: arrayOf(projectSchema).mappedBy('contributors'),\n})\n\nprojectSchema.define({\n contributors: arrayOf(userSchema).mappedBy('projects'),\n})\n```\n\nThis prevent any flaws in consistency while being simple (user side). I already tried a solution, it's not bulletproof but I can submit a cleaner PR if you're interested.\n" }, "154328039": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/105", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/105/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/105/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/105/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/105", "id": 154328039, "number": 105, "title": "Update & Organize Documentation", "user": 33297, "state": "open", "locked": false, "assignee": 33297, "assignees": [ 33297 ], "milestone": 1883567, "comments": 0, "created_at": "2016-05-11T19:52:34Z", "updated_at": "2016-07-14T15:24:58Z", "closed_at": null, "body": "" }, "154328158": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/106", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/106/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/106/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/106/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/106", "id": 154328158, "number": 106, "title": "Clean up tests", "user": 33297, "state": "open", "locked": false, "assignee": 33297, "assignees": [ 33297 ], "milestone": 1883567, "comments": 0, "created_at": "2016-05-11T19:53:07Z", "updated_at": "2016-07-14T15:25:19Z", "closed_at": null, "body": "Currently hard to read through. Should be better organized and easier to maintain.\n" }, "161111737": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/126", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/126/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/126/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/126/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/126", "id": 161111737, "number": 126, "title": "normalize is take some time if object has more number of keys.", "user": 14313723, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 3, "created_at": "2016-06-20T03:41:07Z", "updated_at": "2016-07-05T22:55:19Z", "closed_at": null, "body": "# Problem\n\nnormalization is take some time if object has more number of keys. I tried with array has 100 object and each object has 30 keys and mapping with two nested schema. It takes around ~25ms. I tried using schema based iteration it takes ~7ms. I don't know, Is any problem using schema based iteration? Please refer the link below.\n\nhttps://github.com/vimalceg/normalizr\n" }, "178773735": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/154", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/154/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/154/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/154/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/154", "id": 178773735, "number": 154, "title": "Will arrayOf support a parameter that not a schema?", "user": 5119347, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 2, "created_at": "2016-09-23T03:05:59Z", "updated_at": "2016-09-27T01:58:49Z", "closed_at": null, "body": "# Problem\n\nAfter normalizr process, I want something like this\n\n```\n{result: [{a: 'xxx', b: 'xxx', c: 'ccc'}]}\n```\n\nBut I don't want to defined a schema like:\n\n```\nconst dSchema = new Schema('d')\ndSchema.define({a: aSchema, b: bSchema, c: cSchema})\n```\n\nIs there any way to achieve this? If there is, please tell me. If not, will it possible to support it?\nI do feel sick of defining an extra schema.\n\n**Input**\n\n```\nlet input = [{a: {id: 'aid', name: 'object a'}, b: {id: 'bid', name: 'object b'}]\n```\n\n**schema**\n\n```\nconst aSchema = new Schema('as');\nconst bSchema = new Schema('bs');\nnormalize(input, arrayOf({a: aSchema, b: bSchema}));\n```\n\n**Output**\n\nHere's what I expect to see when I run the above:\n\n```\n{\n result: [{a: 'aid', b: 'bid'}],\n entities: {\n as: { aid: {id: 'aid', name: 'object a'}}\n bs: { bid: {id: 'bid', name: 'object b'}}\n }\n}\n```\n" }, "186469900": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/165", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/165/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/165/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/165/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/165", "id": 186469900, "number": 165, "title": "Django rest framework and normalizr ", "user": 84749, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 2, "created_at": "2016-11-01T06:46:33Z", "updated_at": "2016-11-02T06:30:40Z", "closed_at": null, "body": "For a following schema (expressed in django models)\r\n\r\n class A:\r\n b = foreigneKey(B)\r\n \r\n \r\n class B:\r\n pass\r\n\r\nDjango rest framework serializes A instance as\r\n\r\n { \r\n 'id': 1,\r\n 'b': 3\r\n }\r\n\r\n\r\nnormalizr seems to want a format of\r\n\r\n {\r\n 'id': 1,\r\n 'b': {\r\n 'id': 3\r\n }\r\n }\r\n\r\n\r\nI could change DRF 's default PrimaryKeyRelatedField's serialization, but I 'm wondering if there's another way? \r\nMaybe there's a way to tell normalizr that 'b' is itself an id attribute?\r\n\r\n" }, "188006036": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/172", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/172/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/172/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/172/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/172", "id": 188006036, "number": 172, "title": "Set field on child entity based on parent entity on normalization", "user": 1499050, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 2, "created_at": "2016-11-08T14:29:51Z", "updated_at": "2016-12-21T15:02:10Z", "closed_at": null, "body": "# Problem\r\n\r\nIs there anyway to set a field on the child entity based on the parent entity? I.e, if we have\r\n\r\n```\r\n[{\r\n id: 1,\r\n title: 'Some Article',\r\n comment: {\r\n id: 109,\r\n content: 'Hai'\r\n }\r\n}, {\r\n id: 2,\r\n title: 'Other Article',\r\n comment: {\r\n id: 110,\r\n content: 'Gee'\r\n }\r\n}]\r\n```\r\n\r\nCan we somehow get \r\n\r\n```\r\n{\r\n result: [1, 2],\r\n entities: {\r\n articles: {\r\n 1: {\r\n id: 1,\r\n title: 'Some Article',\r\n comments: [109]\r\n },\r\n 2: {\r\n id: 2,\r\n title: 'Other Article',\r\n comments: [110]\r\n }\r\n },\r\n comments: {\r\n 109: {\r\n id: 109,\r\n content: 'Hai'\r\n article: 1,\r\n },\r\n 110: {\r\n id: 110,\r\n content: 'Gee'\r\n article: 2,\r\n }\r\n }\r\n }\r\n}\r\n```\r\n?\r\n\r\nWould this be a desirable feature otherwise? " }, "191124603": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/176", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/176/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/176/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/176/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/176", "id": 191124603, "number": 176, "title": "bizarre uncaught reference when attempting import of normalizr", "user": 14322, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 3, "created_at": "2016-11-22T21:08:09Z", "updated_at": "2016-11-28T10:11:54Z", "closed_at": null, "body": "# Problem\r\n\r\nI have a perfectly working Redux app being served out via Express, I'm using webpack which I suspect may be the problem.\r\n\r\n**Input**\r\n\r\nHere's how I'm using normalizr, in schema.js\r\n\r\n```js\r\nimport { Schema, arrayOf, valuesOf } from 'normalizr';\r\n\r\nconst uploadSchema = new Schema('uploads');\r\nconst sheetSchema = new Schema('sheets');\r\nconst mappingSchema = new Schema('mappings');\r\nconst tableSchema = new Schema('tables');\r\nconst rowSchema = new Schema('rows');\r\n\r\nuploadSchema.define({\r\n sheets: valuesOf(sheetSchema)\r\n})\r\n\r\nsheetSchema.define({\r\n mapping: mappingSchema,\r\n rows: {\r\n collection: arrayOf(tableSchema)\r\n }\r\n})\r\n\r\nexport { uploadSchema, sheetSchema, mappingSchema, tableSchema, rowSchema }\r\n```\r\n\r\nand in a utility file I have for abstracting out my ajax calls via `whatwg-fetch`:\r\n\r\n```js\r\nimport { normalize } from 'normalizr';\r\nimport {uploadSchema, sheetSchema, mappingSchema, tableSchema, rowSchema} from \"./schema\";\r\nimport 'whatwg-fetch';\r\n\r\n... some functions ...\r\n\r\nfunction handleRequestAndResponse(url, request) {\r\n console.log(\"request:\");\r\n console.log(request);\r\n\r\n return fetch(url, request)\r\n .then(function (resp) {\r\n // check status and throw if unexpected...\r\n if (resp.status >= 200 && resp.status < 400) {\r\n return resp\r\n } else {\r\n\r\n console.log(\"uncaught error, but we will format a response from it...\")\r\n console.log(resp)\r\n\r\n var errorResponse = new Error(resp.statusText)\r\n errorResponse.error = true\r\n errorResponse.status = resp.status;\r\n errorResponse.url = resp.url;\r\n errorResponse.body = resp.body;\r\n\r\n throw errorResponse\r\n }\r\n }).then(function (resp) {\r\n // parse JSON\r\n return resp.json();\r\n }).then(function (resp) {\r\n // log and return\r\n console.log(\"response:\");\r\n console.log(resp);\r\n\r\n // boom, here, normalize is an uncaught ref, and if I insert a debugger anywhere else in this file, the same\r\n debugger\r\n\r\n return resp;\r\n }).catch(function(errorResp) {\r\n return errorResp;\r\n });\r\n}\r\n```\r\n\r\nI suspected TypeScript involvement and tried to remedy with ts-loader, no dice.\r\n\r\nI tried directly importing normalizr without success:\r\n\r\n`import 'normalizr'`\r\n\r\nI've tried adjusting my webpack in the endless esoteric ways it requires to no avail.\r\n\r\nI've tried directly importing functions from normalizr.min.js in the dist folder, also to no avail.\r\n\r\nMy webpack:\r\n\r\n```\r\nvar path = require('path');\r\nvar webpack = require('webpack');\r\n\r\nmodule.exports = {\r\n devtool: 'eval-source-map',\r\n entry:\r\n [\r\n 'whatwg-fetch',\r\n 'webpack-hot-middleware/client',\r\n './client/inventory-app'\r\n ]\r\n ,\r\n output: {\r\n path: path.join(__dirname, 'dist'),\r\n filename: 'bundle.js',\r\n publicPath: '/static/'\r\n },\r\n plugins: [\r\n new webpack.HotModuleReplacementPlugin(),\r\n new webpack.NoErrorsPlugin()\r\n ],\r\n resolve: {\r\n alias: {\r\n webworkify: 'webworkify-webpack'\r\n },\r\n extensions: ['', '.scss', '.css', '.js', '.json', '.ts'],\r\n modulesDirectories: [\r\n 'node_modules',\r\n path.resolve(__dirname, './node_modules')\r\n ]\r\n },\r\n module: {\r\n loaders: [\r\n {\r\n test: /\\.css$/,\r\n loader:'style!css!'\r\n },\r\n { test: /\\.tsx?$/, loader: \"ts-loader\" },\r\n {\r\n test: /\\.js$/,\r\n loaders: ['babel'],\r\n // loaders: ['babel?presets[]=es2015,presets[]=stage-0,presets[]=react,plugins[]=transform-runtime'],\r\n exclude: /node_modules/,\r\n include: path.join(__dirname, 'client')\r\n },\r\n {\r\n test: /\\.json$/,\r\n loader: 'json-loader'\r\n }\r\n ]\r\n }\r\n};\r\n```\r\n\r\nNote that the typescript stuff was added by me most recently and didn't help.\r\n\r\nAlso note that when in my schema.js when I insert a debugger statement below the const definitions, my consts *are* defined, but if I try to \"var foo = new Schema('foo')\" I'll get a reference error again.\r\n\r\nI'm at a loss. I hope this isn't my poor understanding of webpack, but this has only happened with this integration so I'm opening an issue after not finding success looking around on the web / asking in Reactiflux.\r\n\r\n\r\n\r\n" }, "196525159": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/185", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/185/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/185/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/185/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/185", "id": 196525159, "number": 185, "title": "Request: Add a changelog", "user": 6775919, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 1, "created_at": "2016-12-19T21:29:14Z", "updated_at": "2016-12-19T21:40:26Z", "closed_at": null, "body": "I'm attempting to debug in a breaking change introduced in 2.3\r\n\r\nMy first step is usually to run through the changelog, but to my surprise there is none to be found (At least to my eyes, but I have been known to miss things).\r\n\r\nAlso, want to say thanks for such an awesome project!" }, "196531836": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/186", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/186/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/186/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/186/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/186", "id": 196531836, "number": 186, "title": "results for arrayOf returns array instead of map as of version 2.3.0", "user": 6775919, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 0, "created_at": "2016-12-19T22:01:30Z", "updated_at": "2016-12-19T22:10:31Z", "closed_at": null, "body": "# Problem\r\n\r\nUpgrading to 2.3.0 introduces a breaking change to the way `results` are returned.\r\n\r\nIt looks like results are now normalized when both values are the same. https://github.com/paularmstrong/normalizr/blob/master/src/index.js#L116\r\n\r\nThis is a breaking change and should result in a major bump and a warning to those who expect a keyed map in return regardless of the values of that map.\r\n\r\n**Input**\r\n\r\nHere's how I'm using normalizr:\r\n\r\n```js\r\nconst challenge = new Schema('challenge', { idAttribute: 'dashedName' });\r\nconst block = new Schema('block', { idAttribute: 'dashedName' });\r\nconst superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });\r\n\r\nblock.define({\r\n challenges: arrayOf(challenge)\r\n});\r\n\r\nsuperBlock.define({\r\n blocks: arrayOf(block)\r\n});\r\n\r\nconst mapSchema = valuesOf(superBlock);\r\nconst map = {\r\n 'getting-started': {\r\n dashedName: 'getting-started',\r\n blocks: [\r\n {\r\n dashedName: 'some-block',\r\n challenges: [{\r\n dashedName: 'some-challenge',\r\n ...\r\n },\r\n ...\r\n ],\r\n ...\r\n },\r\n ...\r\n ],\r\n ...\r\n },\r\n ...\r\n};\r\nnormalize(map, mapSchema);\r\n```\r\n\r\n**Output**\r\nHere is what has been the output up to 2.2.1\r\n```js\r\n {\r\n result: {\r\n 'getting-started': 'getting-started',\r\n 'front-end-development-certification': 'front-end-development-certification',\r\n 'data-visualization-certification': 'data-visualization-certification',\r\n 'back-end-development-certification': 'back-end-development-certification',\r\n 'video-challenges': 'video-challenges',\r\n 'full-stack-development-certification': 'full-stack-development-certification',\r\n 'coding-interview-preparation': 'coding-interview-preparation'\r\n},\r\n entities: { ... }\r\n}\r\n```\r\n\r\nHere is what I actually as of 2.3.0\r\n\r\n\r\n```js\r\n{\r\n result: [ [ \r\n 'getting-started',\r\n 'front-end-development-certification',\r\n 'data-visualization-certification',\r\n 'back-end-development-certification',\r\n 'video-challenges',\r\n 'full-stack-development-certification',\r\n 'coding-interview-preparation'\r\n ],\r\n entities: { ...}\r\n}\r\n```\r\n" }, "196954427": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/187", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/187/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/187/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/187/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/187", "id": 196954427, "number": 187, "title": "v3 Docs", "user": 33297, "state": "open", "locked": false, "assignee": 33297, "assignees": [ 33297 ], "milestone": 2205635, "comments": 0, "created_at": "2016-12-21T15:04:39Z", "updated_at": "2016-12-21T21:53:28Z", "closed_at": null, "body": "# Problem\r\n\r\nDocumentation in <= 2.x has been a bit difficult to follow, with lots of really long examples in a single README.\r\n\r\n# Solution\r\n\r\nFor v3, create a `/docs` folder and separate out meaningful documentation by section/page, with shorter, easier to follow examples." }, "196954928": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/188", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/188/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/188/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/188/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/188", "id": 196954928, "number": 188, "title": "Add In-Repo Examples", "user": 33297, "state": "open", "locked": false, "assignee": 33297, "assignees": [ 33297 ], "milestone": 2205635, "comments": 0, "created_at": "2016-12-21T15:06:29Z", "updated_at": "2016-12-21T21:53:28Z", "closed_at": null, "body": "# Problem\r\n\r\nExamples are currently somewhere untracked by this project. They have moved or been lost in the past.\r\n\r\n# Solution\r\n\r\nAdd an `/examples` folder with clear and concise usage patterns." }, "196963939": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/189", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/189/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/189/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/189/events", "html_url": "https://github.com/paularmstrong/normalizr/issues/189", "id": 196963939, "number": 189, "title": "Wanted: Typescript definitions & tests for v3.0.0", "user": 33297, "state": "open", "locked": false, "assignee": 69485, "assignees": [ 69485 ], "milestone": 2205635, "comments": 2, "created_at": "2016-12-21T15:42:53Z", "updated_at": "2016-12-22T14:57:41Z", "closed_at": null, "body": "# Problem\r\n\r\n@paularmstrong doesn't know typescript and is too lazy to learn\r\n\r\n# Solution\r\n\r\nTry to get someone from the community donate a little time adding a typescript definition file and tests for the [v3.0.0 branch](https://github.com/paularmstrong/normalizr/tree/master) before launch" } }, "pullRequests": { "165986889": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/135", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/135/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/135/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/135/events", "html_url": "https://github.com/paularmstrong/normalizr/pull/135", "id": 165986889, "number": 135, "title": "It should be possible to use relations", "user": 1690457, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 1, "created_at": "2016-07-17T17:38:38Z", "updated_at": "2016-09-25T15:15:05Z", "closed_at": null, "pull_request": { "url": "https://api.github.com/repos/paularmstrong/normalizr/pulls/135", "html_url": "https://github.com/paularmstrong/normalizr/pull/135", "diff_url": "https://github.com/paularmstrong/normalizr/pull/135.diff", "patch_url": "https://github.com/paularmstrong/normalizr/pull/135.patch" }, "body": "# Problem\n\nSome objects have arrays with objects, which does not have a identifier, and is\ntightly coupled to the parent object.\n\nI created a issue here: #134\n# Solution\n\nIt should possible to mark those objects as a relation to the parent object\nand use the parent object id as the identifier when normalizing\n# TODO\n- [x] Add & Update Tests\n### Use case\n\nSee updated test\n" }, "183956234": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/161", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/161/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/161/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/161/events", "html_url": "https://github.com/paularmstrong/normalizr/pull/161", "id": 183956234, "number": 161, "title": "Pass parent object to idAttribute.", "user": 1591483, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 9, "created_at": "2016-10-19T13:06:03Z", "updated_at": "2016-11-29T18:51:34Z", "closed_at": null, "pull_request": { "url": "https://api.github.com/repos/paularmstrong/normalizr/pulls/161", "html_url": "https://github.com/paularmstrong/normalizr/pull/161", "diff_url": "https://github.com/paularmstrong/normalizr/pull/161.diff", "patch_url": "https://github.com/paularmstrong/normalizr/pull/161.patch" }, "body": "# Problem\n\nProvide a mechanism to use a parent object data in generating entity id.\n# Solution\n\nPass the `last` object through all of the traversal methods and make sure it gets passed specifically to `visitEntity` as that is where `getId` is called and pass the parent object into that function. \n\nCloses #160\n" }, "188066564": { "url": "https://api.github.com/repos/paularmstrong/normalizr/issues/173", "repository_url": "https://api.github.com/repos/paularmstrong/normalizr", "labels_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/173/labels{/name}", "comments_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/173/comments", "events_url": "https://api.github.com/repos/paularmstrong/normalizr/issues/173/events", "html_url": "https://github.com/paularmstrong/normalizr/pull/173", "id": 188066564, "number": 173, "title": "Added schema option for assigning the parent id on the child entity", "user": 1499050, "state": "open", "locked": false, "assignee": null, "assignees": [], "milestone": null, "comments": 2, "created_at": "2016-11-08T18:22:42Z", "updated_at": "2016-12-21T15:00:24Z", "closed_at": null, "pull_request": { "url": "https://api.github.com/repos/paularmstrong/normalizr/pulls/173", "html_url": "https://github.com/paularmstrong/normalizr/pull/173", "diff_url": "https://github.com/paularmstrong/normalizr/pull/173.diff", "patch_url": "https://github.com/paularmstrong/normalizr/pull/173.patch" }, "body": "# Problem\r\n\r\nCloses #172. Partially inspired by #135. \r\n\r\nBasically, we sometimes when normalizing, we would like to set the parent id on the child as well as setting the child ids on the parent. \r\n\r\nExample;\r\n\r\n```\r\n// INPUT \r\n\r\n[{\r\n id: 1,\r\n title: 'Some Article',\r\n comment: {\r\n id: 109,\r\n content: 'Hai'\r\n }\r\n}, {\r\n id: 2,\r\n title: 'Other Article',\r\n comment: {\r\n id: 110,\r\n content: 'Gee'\r\n }\r\n}]\r\n\r\n// OUTPUT\r\n\r\n{\r\n result: [1, 2],\r\n entities: {\r\n articles: {\r\n 1: {\r\n id: 1,\r\n title: 'Some Article',\r\n comments: [109]\r\n },\r\n 2: {\r\n id: 2,\r\n title: 'Other Article',\r\n comments: [110]\r\n }\r\n },\r\n comments: {\r\n 109: {\r\n id: 109,\r\n content: 'Hai'\r\n article: 1,\r\n },\r\n 110: {\r\n id: 110,\r\n content: 'Gee'\r\n article: 2,\r\n }\r\n }\r\n }\r\n}\r\n```\r\n\r\n# Solution\r\n\r\nWhen defining the schema, you can set assignParentId to true and this will result in the child entity getting the parent object's id set. \r\n\r\n# TODO\r\n- [ ] Didn't cover the UnionSchema case. Let me know if that is a requirement to proceed. \r\n- [x] Add & Update Tests\r\n\r\n" } } }, "result": [ { "id": 196963939, "schema": "issues" }, { "id": 196954928, "schema": "issues" }, { "id": 196954427, "schema": "issues" }, { "id": 196531836, "schema": "issues" }, { "id": 196525159, "schema": "issues" }, { "id": 191124603, "schema": "issues" }, { "id": 188066564, "schema": "pullRequests" }, { "id": 188006036, "schema": "issues" }, { "id": 186469900, "schema": "issues" }, { "id": 183956234, "schema": "pullRequests" }, { "id": 178773735, "schema": "issues" }, { "id": 165986889, "schema": "pullRequests" }, { "id": 161111737, "schema": "issues" }, { "id": 154328158, "schema": "issues" }, { "id": 154328039, "schema": "issues" }, { "id": 134935104, "schema": "issues" }, { "id": 134547187, "schema": "issues" }, { "id": 127301608, "schema": "issues" } ] } ================================================ FILE: examples/github/schema.js ================================================ import { schema } from '../../src'; export const user = new schema.Entity('users'); export const label = new schema.Entity('labels'); export const milestone = new schema.Entity('milestones', { creator: user, }); export const issue = new schema.Entity('issues', { assignee: user, assignees: [user], labels: label, milestone, user, }); export const pullRequest = new schema.Entity('pullRequests', { assignee: user, assignees: [user], labels: label, milestone, user, }); export const issueOrPullRequest = new schema.Array( { issues: issue, pullRequests: pullRequest, }, (entity) => (entity.pull_request ? 'pullRequests' : 'issues') ); ================================================ FILE: examples/redux/.babelrc ================================================ { "presets": ["es2015", "stage-1"] } ================================================ FILE: examples/redux/.gitignore ================================================ node_modules/* *.log ================================================ FILE: examples/redux/README.md ================================================ # Redux 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. ![redux example in use](/examples/redux/usage.gif) ## Running From this directory, run the following and follow the on-screen options: ```sh # from the root directory: yarn # or npm install # from this directory: yarn # or npm install npm run start ``` ================================================ FILE: examples/redux/index.js ================================================ import * as Action from './src/redux/actions'; import * as Selector from './src/redux/selectors'; import inquirer from 'inquirer'; import store from './src/redux'; const REPO = 'paularmstrong/normalizr'; const start = () => { inquirer .prompt([ { type: 'input', name: 'repo', message: 'What is the slug of the repo you wish to browseMain?', default: REPO, validate: (input) => { if (!/^[a-zA-Z0-9]+\/[a-zA-Z0-9]+/.test(input)) { return 'Repo slug must be in the form "user/project"'; } return true; }, }, ]) .then(({ repo }) => { store.dispatch(Action.setRepo(repo)); main(); }); }; const main = () => { return inquirer .prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', choices: ['Browse current state', 'Get new data', new inquirer.Separator(), 'Quit'], }, ]) .then(({ action }) => { switch (action) { case 'Browse current state': return browseMain(); case 'Get new data': return pull(); default: return process.exit(); } }); }; const browseMain = () => { return inquirer .prompt([ { type: 'list', name: 'browseMainAction', message: 'What would you like to do?', choices: () => { return [ { value: 'print', name: 'Print the entire state tree' }, new inquirer.Separator(), ...Object.keys(store.getState()).map((value) => ({ value, name: `Browse ${value}` })), new inquirer.Separator(), { value: 'main', name: 'Go Back to Main Menu' }, ]; }, }, ]) .then((answers) => { switch (answers.browseMainAction) { case 'main': return main(); case 'print': console.log(JSON.stringify(store.getState(), null, 2)); return browseMain(); default: return browse(answers.browseMainAction); } }); }; const browse = (stateKey) => { return inquirer .prompt([ { type: 'list', name: 'action', message: `Browse ${stateKey}`, choices: [ { value: 'count', name: 'Show # of Objects' }, { value: 'keys', name: 'List All Keys' }, { value: 'view', name: 'View by Key' }, { value: 'all', name: 'View All' }, { value: 'denormalize', name: 'Denormalize' }, new inquirer.Separator(), { value: 'browseMain', name: 'Go Back to Browse Menu' }, { value: 'main', name: 'Go Back to Main Menu' }, ], }, { type: 'list', name: 'list', message: `Select the ${stateKey} to view:`, choices: Object.keys(store.getState()[stateKey]), when: ({ action }) => action === 'view', }, ]) .then(({ action, list }) => { const state = store.getState()[stateKey]; if (list) { console.log(JSON.stringify(state[list], null, 2)); } switch (action) { case 'count': console.log(`-> ${Object.keys(state).length} items.`); return browse(stateKey); case 'keys': Object.keys(state).map((key) => console.log(key)); return browse(stateKey); case 'all': console.log(JSON.stringify(state, null, 2)); return browse(stateKey); case 'denormalize': return browseDenormalized(stateKey); case 'browseMain': return browseMain(); case 'main': return main(); default: return browse(stateKey); } }); }; const browseDenormalized = (stateKey) => { return inquirer .prompt([ { type: 'list', name: 'selector', message: `Denormalize a/and ${stateKey} entity`, choices: [ ...Object.keys(store.getState()[stateKey]), new inquirer.Separator(), { value: 'browse', name: 'Go Back to Browse Menu' }, { value: 'main', name: 'Go Back to Main Menu' }, ], }, ]) .then(({ selector }) => { switch (selector) { case 'browse': return browse(stateKey); case 'main': return main(); default: { const data = Selector[`select${stateKey.replace(/s$/, '')}`](store.getState(), selector); console.log(JSON.stringify(data, null, 2)); return browseDenormalized(stateKey); } } }); }; const pull = () => { return inquirer .prompt([ { type: 'list', name: 'pullAction', message: 'What data would you like to fetch?', choices: () => { return [ ...Object.keys(store.getState()).map((value) => ({ value, name: value })), new inquirer.Separator(), { value: 'main', name: 'Go Back to Main Menu' }, ]; }, }, ]) .then((answers) => { switch (answers.pullAction) { case 'commits': return store.dispatch(Action.getCommits()).then(pull); case 'issues': return store.dispatch(Action.getIssues()).then(pull); case 'labels': return store.dispatch(Action.getLabels()).then(pull); case 'milestones': return store.dispatch(Action.getMilestones()).then(pull); case 'pullRequests': return store.dispatch(Action.getPullRequests()).then(pull); case 'main': default: return main(); } }); }; start(); ================================================ FILE: examples/redux/package.json ================================================ { "name": "normalizr-redux-example", "version": "0.0.0", "description": "And example of using Normalizr with Redux", "main": "index.js", "author": "Paul Armstrong", "license": "MIT", "private": true, "scripts": { "start": "babel-node ./" }, "dependencies": { "babel-cli": "^6.18.0", "babel-preset-es2015": "^6.18.0", "babel-preset-stage-1": "^6.16.0", "github": "^14.0.0", "inquirer": "^6.3.1", "redux": "^4.0.1", "redux-thunk": "^2.1.0" } } ================================================ FILE: examples/redux/src/api/index.js ================================================ import GitHubApi from 'github'; export default new GitHubApi({ headers: { 'user-agent': 'Normalizr Redux Example', }, }); ================================================ FILE: examples/redux/src/api/schema.js ================================================ import { schema } from '../../../../src'; export const user = new schema.Entity('users'); export const commit = new schema.Entity( 'commits', { author: user, committer: user, }, { idAttribute: 'sha' } ); export const label = new schema.Entity('labels'); export const milestone = new schema.Entity('milestones', { creator: user, }); export const issue = new schema.Entity('issues', { assignee: user, assignees: [user], labels: [label], milestone, user, }); export const pullRequest = new schema.Entity('pullRequests', { assignee: user, assignees: [user], labels: [label], milestone, user, }); export const issueOrPullRequest = new schema.Array( { issues: issue, pullRequests: pullRequest, }, (entity) => (entity.pull_request ? 'pullRequests' : 'issues') ); ================================================ FILE: examples/redux/src/redux/actions.js ================================================ export { getCommits } from './modules/commits'; export { getIssues } from './modules/issues'; export { getLabels } from './modules/labels'; export { getMilestones } from './modules/milestones'; export { getPullRequests } from './modules/pull-requests'; export { setRepo } from './modules/repos'; export const ADD_ENTITIES = 'ADD_ENTITIES'; export const addEntities = (entities) => ({ type: ADD_ENTITIES, payload: entities, }); ================================================ FILE: examples/redux/src/redux/index.js ================================================ import * as schema from '../api/schema'; import api from '../api'; import reducer from './reducer'; import thunk from 'redux-thunk'; import { applyMiddleware, createStore } from 'redux'; export default createStore(reducer, applyMiddleware(thunk.withExtraArgument({ api, schema }))); ================================================ FILE: examples/redux/src/redux/modules/commits.js ================================================ import * as Repo from './repos'; import { commit } from '../../api/schema'; import { ADD_ENTITIES, addEntities } from '../actions'; import { denormalize, normalize } from '../../../../../src'; export const STATE_KEY = 'commits'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return { ...state, ...action.payload.commits, }; default: return state; } } export const getCommits = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { const state = getState(); const owner = Repo.selectOwner(state); const repo = Repo.selectRepo(state); return api.repos .getCommits({ owner, repo, }) .then((response) => { const data = normalize(response, [schema.commit]); dispatch(addEntities(data.entities)); return response; }) .catch((error) => { console.error(error); }); }; export const selectHydrated = (state, id) => denormalize(id, commit, state); ================================================ FILE: examples/redux/src/redux/modules/issues.js ================================================ import * as Repo from './repos'; import { issue } from '../../api/schema'; import { ADD_ENTITIES, addEntities } from '../actions'; import { denormalize, normalize } from '../../../../../src'; export const STATE_KEY = 'issues'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return { ...state, ...action.payload.issues, }; default: return state; } } export const getIssues = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { const state = getState(); const owner = Repo.selectOwner(state); const repo = Repo.selectRepo(state); return api.issues .getForRepo({ owner, repo, }) .then((response) => { const data = normalize(response, [schema.issue]); dispatch(addEntities(data.entities)); return response; }) .catch((error) => { console.error(error); }); }; export const selectHydrated = (state, id) => denormalize(id, issue, state); ================================================ FILE: examples/redux/src/redux/modules/labels.js ================================================ import * as Repo from './repos'; import { label } from '../../api/schema'; import { ADD_ENTITIES, addEntities } from '../actions'; import { denormalize, normalize } from '../../../../../src'; export const STATE_KEY = 'labels'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return { ...state, ...action.payload.labels, }; default: return state; } } export const getLabels = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { const state = getState(); const owner = Repo.selectOwner(state); const repo = Repo.selectRepo(state); return api.issues .getLabels({ owner, repo, }) .then((response) => { const data = normalize(response, [schema.label]); dispatch(addEntities(data.entities)); return response; }) .catch((error) => { console.error(error); }); }; export const selectHydrated = (state, id) => denormalize(id, label, state); ================================================ FILE: examples/redux/src/redux/modules/milestones.js ================================================ import * as Repo from './repos'; import { milestone } from '../../api/schema'; import { ADD_ENTITIES, addEntities } from '../actions'; import { denormalize, normalize } from '../../../../../src'; export const STATE_KEY = 'milestones'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return { ...state, ...action.payload.milestones, }; default: return state; } } export const getMilestones = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { const state = getState(); const owner = Repo.selectOwner(state); const repo = Repo.selectRepo(state); return api.issues .getMilestones({ owner, repo, }) .then((response) => { const data = normalize(response, [schema.milestone]); dispatch(addEntities(data.entities)); return response; }) .catch((error) => { console.error(error); }); }; export const selectHydrated = (state, id) => denormalize(id, milestone, state); ================================================ FILE: examples/redux/src/redux/modules/pull-requests.js ================================================ import * as Repo from './repos'; import { pullRequest } from '../../api/schema'; import { ADD_ENTITIES, addEntities } from '../actions'; import { denormalize, normalize } from '../../../../../src'; export const STATE_KEY = 'pullRequests'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return { ...state, ...action.payload.pullRequests, }; default: return state; } } export const getPullRequests = ({ page = 0 } = {}) => (dispatch, getState, { api, schema }) => { const state = getState(); const owner = Repo.selectOwner(state); const repo = Repo.selectRepo(state); return api.pullRequests .getAll({ owner, repo, }) .then((response) => { const data = normalize(response, [schema.pullRequest]); dispatch(addEntities(data.entities)); return response; }) .catch((error) => { console.error(error); }); }; export const selectHydrated = (state, id) => denormalize(id, pullRequest, state); ================================================ FILE: examples/redux/src/redux/modules/repos.js ================================================ export const STATE_KEY = 'repo'; export default function reducer(state = {}, action) { switch (action.type) { case Action.SET_REPO: return { ...state, ...action.payload, }; default: return state; } } const Action = { SET_REPO: 'SET_REPO', }; export const setRepo = (slug) => { const [owner, repo] = slug.split('/'); return { type: Action.SET_REPO, payload: { owner, repo }, }; }; export const selectOwner = (state) => state[STATE_KEY].owner; export const selectRepo = (state) => state[STATE_KEY].repo; ================================================ FILE: examples/redux/src/redux/modules/users.js ================================================ import { ADD_ENTITIES } from '../actions'; import { denormalize } from '../../../../../src'; import { user } from '../../api/schema'; export const STATE_KEY = 'users'; export default function reducer(state = {}, action) { switch (action.type) { case ADD_ENTITIES: return Object.entries(action.payload.users).reduce((mergedUsers, [id, user]) => { return { ...mergedUsers, [id]: { ...(mergedUsers[id] || {}), ...user, }, }; }, state); default: return state; } } export const selectHydrated = (state, id) => denormalize(id, user, state); ================================================ FILE: examples/redux/src/redux/reducer.js ================================================ import { combineReducers } from 'redux'; import commits, { STATE_KEY as COMMITS_STATE_KEY } from './modules/commits'; import issues, { STATE_KEY as ISSUES_STATE_KEY } from './modules/issues'; import labels, { STATE_KEY as LABELS_STATE_KEY } from './modules/labels'; import milestones, { STATE_KEY as MILESTONES_STATE_KEY } from './modules/milestones'; import pullRequests, { STATE_KEY as PULLREQUESTS_STATE_KEY } from './modules/pull-requests'; import repos, { STATE_KEY as REPO_STATE_KEY } from './modules/repos'; import users, { STATE_KEY as USERS_STATE_KEY } from './modules/users'; const reducer = combineReducers({ [COMMITS_STATE_KEY]: commits, [ISSUES_STATE_KEY]: issues, [LABELS_STATE_KEY]: labels, [MILESTONES_STATE_KEY]: milestones, [PULLREQUESTS_STATE_KEY]: pullRequests, [REPO_STATE_KEY]: repos, [USERS_STATE_KEY]: users, }); export default reducer; ================================================ FILE: examples/redux/src/redux/selectors.js ================================================ export { selectHydrated as selectcommit } from './modules/commits'; export { selectHydrated as selectissue } from './modules/issues'; export { selectHydrated as selectlabel } from './modules/labels'; export { selectHydrated as selectmilestone } from './modules/milestones'; export { selectHydrated as selectpullRequest } from './modules/pull-requests'; ================================================ FILE: examples/relationships/README.md ================================================ # Dealing with Relationships 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. ## Running ```sh # from the root directory: yarn # from this directory: ../../node_modules/.bin/babel-node ./index.js ``` ## Files * [index.js](/examples/relationships/index.js): Pulls live data from the GitHub API for this project's issues and normalizes the JSON. * [input.json](/examples/relationships/input.json): The raw JSON data before normalization. * [output.json](/examples/relationships/output.json): The normalized output. * [schema.js](/examples/relationships/schema.js): The schema used to normalize the GitHub issues. ================================================ FILE: examples/relationships/index.js ================================================ import fs from 'fs'; import input from './input.json'; import { normalize } from '../../src'; import path from 'path'; import postsSchema from './schema'; const normalizedData = normalize(input, postsSchema); const output = JSON.stringify(normalizedData, null, 2); fs.writeFileSync(path.resolve(__dirname, './output.json'), output); ================================================ FILE: examples/relationships/input.json ================================================ [ { "id": "1", "title": "My first post!", "author": { "id": "123", "name": "Paul" }, "comments": [ { "id": "249", "content": "Nice post!", "commenter": { "id": "245", "name": "Jane" } }, { "id": "250", "content": "Thanks!", "commenter": { "id": "123", "name": "Paul" } } ] }, { "id": "2", "title": "This other post", "author": { "id": "123", "name": "Paul" }, "comments": [ { "id": "251", "content": "Your other post was nicer", "commenter": { "id": "245", "name": "Jane" } }, { "id": "252", "content": "I am a spammer!", "commenter": { "id": "246", "name": "Spambot5000" } } ] } ] ================================================ FILE: examples/relationships/output.json ================================================ { "entities": { "users": { "123": { "id": "123", "name": "Paul", "posts": [ "1", "2" ], "comments": [ "250" ] }, "245": { "id": "245", "name": "Jane", "comments": [ "249", "251" ], "posts": [] }, "246": { "id": "246", "name": "Spambot5000", "comments": [ "252" ] } }, "comments": { "249": { "id": "249", "content": "Nice post!", "commenter": "245", "post": "1" }, "250": { "id": "250", "content": "Thanks!", "commenter": "123", "post": "1" }, "251": { "id": "251", "content": "Your other post was nicer", "commenter": "245", "post": "2" }, "252": { "id": "252", "content": "I am a spammer!", "commenter": "246", "post": "2" } }, "posts": { "1": { "id": "1", "title": "My first post!", "author": "123", "comments": [ "249", "250" ] }, "2": { "id": "2", "title": "This other post", "author": "123", "comments": [ "251", "252" ] } } }, "result": [ "1", "2" ] } ================================================ FILE: examples/relationships/schema.js ================================================ import { schema } from '../../src'; const userProcessStrategy = (value, parent, key) => { switch (key) { case 'author': return { ...value, posts: [parent.id] }; case 'commenter': return { ...value, comments: [parent.id] }; default: return { ...value }; } }; const userMergeStrategy = (entityA, entityB) => { return { ...entityA, ...entityB, posts: [...(entityA.posts || []), ...(entityB.posts || [])], comments: [...(entityA.comments || []), ...(entityB.comments || [])], }; }; const user = new schema.Entity( 'users', {}, { mergeStrategy: userMergeStrategy, processStrategy: userProcessStrategy, } ); const comment = new schema.Entity( 'comments', { commenter: user, }, { processStrategy: (value, parent, key) => { return { ...value, post: parent.id }; }, } ); const post = new schema.Entity('posts', { author: user, comments: [comment], }); export default [post]; ================================================ FILE: husky.config.js ================================================ const runYarnLock = 'yarn install --frozen-lockfile'; module.exports = { hooks: { 'post-checkout': `if [[ $HUSKY_GIT_PARAMS =~ 1$ ]]; then ${runYarnLock}; fi`, 'post-merge': runYarnLock, 'post-rebase': 'yarn install', 'pre-commit': 'yarn typecheck && yarn lint-staged', }, }; ================================================ FILE: index.d.ts ================================================ declare namespace schema { export type StrategyFunction = (value: any, parent: any, key: string) => T; export type SchemaFunction = (value: any, parent: any, key: string) => string; export type MergeFunction = (entityA: any, entityB: any) => any; export type FallbackFunction = (key: string, schema: schema.Entity) => T; export class Array { constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) define(definition: Schema): void } export interface EntityOptions { idAttribute?: string | SchemaFunction mergeStrategy?: MergeFunction processStrategy?: StrategyFunction fallbackStrategy?: FallbackFunction } export class Entity { constructor(key: string | symbol, definition?: Schema, options?: EntityOptions) define(definition: Schema): void key: string getId: SchemaFunction _processStrategy: StrategyFunction } export class Object { constructor(definition: SchemaObject) define(definition: Schema): void } export class Union { constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) define(definition: Schema): void } export class Values { constructor(definition: Schema, schemaAttribute?: string | SchemaFunction) define(definition: Schema): void } } export type Schema = | schema.Entity | schema.Object | schema.Union | schema.Values | SchemaObject | SchemaArray; export type SchemaValueFunction = (t: T) => Schema; export type SchemaValue = Schema | SchemaValueFunction; export interface SchemaObject { [key: string]: SchemaValue } export interface SchemaArray extends Array> {} export type NormalizedSchema = { entities: E, result: R }; export function normalize( data: any, schema: Schema ): NormalizedSchema; export function denormalize( input: any, schema: Schema, entities: any ): any; ================================================ FILE: jest.config.js ================================================ module.exports = { testMatch: ['**/__tests__/**/*.test.js'], }; ================================================ FILE: lint-staged.config.js ================================================ module.exports = { '*.{md}': ['prettier --write', 'git add'], '*.{js,jsx,json}': ['yarn lint', 'prettier --write', 'git add'], '*.{js,jsx,ts,tsx}': ['jest --bail --findRelatedTests'], }; ================================================ FILE: package.json ================================================ { "name": "normalizr", "version": "3.6.2", "description": "Normalizes and denormalizes JSON according to schema for Redux and Flux applications", "bugs": { "url": "https://github.com/paularmstrong/normalizr/issues" }, "homepage": "https://github.com/paularmstrong/normalizr", "repository": { "url": "https://github.com/paularmstrong/normalizr.git", "type": "git" }, "keywords": [ "flux", "redux", "normalize", "denormalize", "api", "json" ], "files": [ "dist/", "index.d.ts", "LICENSE", "README.md" ], "main": "dist/normalizr.js", "module": "dist/normalizr.es.js", "typings": "index.d.ts", "sideEffects": false, "scripts": { "build": "npm run clean && run-p build:*", "build:development": "NODE_ENV=development rollup -c", "build:production": "NODE_ENV=production rollup -c", "clean": "rimraf dist", "flow": "flow", "flow:ci": "flow check", "lint": "yarn lint:cmd --fix", "lint:ci": "yarn lint:cmd", "lint:cmd": "eslint . --ext '.js,.json,.snap' --cache", "prebuild": "npm run clean", "precommit": "flow check && lint-staged", "prepublishOnly": "npm run build", "test": "jest", "test:ci": "jest --ci", "test:coverage": "npm run test -- --coverage && cat ./coverage/lcov.info | coveralls", "tsc:ci": "tsc --noEmit typescript-tests/*", "typecheck": "run-p flow:ci tsc:ci" }, "author": "Paul Armstrong", "contributors": [ "Dan Abramov" ], "license": "MIT", "devDependencies": { "@babel/core": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.0.0", "@babel/preset-env": "^7.0.0", "@babel/preset-flow": "^7.0.0", "babel-eslint": "^10.0.1", "babel-jest": "^26.5.2", "coveralls": "^3.1.0", "eslint": "^7.11.0", "eslint-config-prettier": "^6.13.0", "eslint-plugin-jest": "^24.1.0", "eslint-plugin-json": "^2.1.2", "eslint-plugin-prettier": "^3.1.4", "flow-bin": "^0.136.0", "husky": "^2.3.0", "immutable": "^3.8.1", "jest": "^26.5.3", "lint-staged": "^8.1.7", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", "rimraf": "^3.0.2", "rollup": "^2.32.0", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-filesize": "^9.0.2", "rollup-plugin-terser": "^7.0.2", "typescript": "^3.4.5" }, "dependencies": {} } ================================================ FILE: prettier.config.js ================================================ module.exports = { 'arrowParens': 'always', 'printWidth': 120, 'singleQuote': true, 'quoteProps': 'preserve', }; ================================================ FILE: rollup.config.js ================================================ import babel from 'rollup-plugin-babel'; import filesize from 'rollup-plugin-filesize'; import { name } from './package.json'; import { terser } from 'rollup-plugin-terser'; const isProduction = process.env.NODE_ENV === 'production'; const destBase = 'dist/normalizr'; const destExtension = `${isProduction ? '.min' : ''}.js`; export default { input: 'src/index.js', output: [ { file: `${destBase}${destExtension}`, format: 'cjs' }, { file: `${destBase}.es${destExtension}`, format: 'es' }, { file: `${destBase}.umd${destExtension}`, format: 'umd', name }, { file: `${destBase}.amd${destExtension}`, format: 'amd', name }, { file: `${destBase}.browser${destExtension}`, format: 'iife', name }, ], plugins: [babel({}), isProduction && terser(), filesize()].filter(Boolean), }; ================================================ FILE: src/__tests__/__snapshots__/index.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`denormalize denormalizes entities 1`] = ` Array [ Object { "id": 1, "type": "foo", }, Object { "id": 2, "type": "bar", }, ] `; exports[`denormalize denormalizes nested entities 1`] = ` Object { "author": Object { "id": "8472", "name": "Paul", }, "body": "This article is great.", "comments": Array [ Object { "comment": "I like it!", "id": "comment-123-4738", "user": Object { "id": "10293", "name": "Jane", }, }, ], "id": "123", "title": "A Great Article", } `; exports[`denormalize denormalizes with function as idAttribute 1`] = ` Array [ Object { "guest": null, "id": "1", "name": "Esther", }, Object { "guest": Object { "guest_id": 1, }, "id": "2", "name": "Tom", }, ] `; exports[`denormalize set to undefined if schema key is not in entities 1`] = ` Object { "author": undefined, "comments": Array [ Object { "user": undefined, }, ], "id": "123", } `; exports[`normalize can normalize entity nested inside entity using property from parent 1`] = ` Object { "entities": Object { "linkables": Object { "1": Object { "data": 2, "id": 1, "module_type": "article", "schema_type": "media", }, }, "media": Object { "2": Object { "id": 2, "url": "catimage.jpg", }, }, }, "result": 1, } `; exports[`normalize can normalize entity nested inside object using property from parent 1`] = ` Object { "entities": Object { "media": Object { "2": Object { "id": 2, "url": "catimage.jpg", }, }, }, "result": Object { "data": 2, "id": 1, "module_type": "article", "schema_type": "media", }, } `; exports[`normalize can use fully custom entity classes 1`] = ` Object { "entities": Object { "children": Object { "4": Object { "id": 4, "name": "lettuce", }, }, "food": Object { "1234": Object { "children": Array [ 4, ], "name": "tacos", "uuid": "1234", }, }, }, "result": Object { "schema": "food", "uuid": "1234", }, } `; exports[`normalize ignores null values 1`] = ` Object { "entities": Object {}, "result": Array [ null, ], } `; exports[`normalize ignores null values 2`] = ` Object { "entities": Object {}, "result": Array [ undefined, ], } `; exports[`normalize ignores null values 3`] = ` Object { "entities": Object {}, "result": Array [ false, ], } `; exports[`normalize normalizes entities 1`] = ` Object { "entities": Object { "tacos": Object { "1": Object { "id": 1, "type": "foo", }, "2": Object { "id": 2, "type": "bar", }, }, }, "result": Array [ 1, 2, ], } `; exports[`normalize normalizes entities with circular references 1`] = ` Object { "entities": Object { "users": Object { "123": Object { "friends": Array [ 123, ], "id": 123, }, }, }, "result": 123, } `; exports[`normalize normalizes nested entities 1`] = ` Object { "entities": Object { "articles": Object { "123": Object { "author": "8472", "body": "This article is great.", "comments": Array [ "comment-123-4738", ], "id": "123", "title": "A Great Article", }, }, "comments": Object { "comment-123-4738": Object { "comment": "I like it!", "id": "comment-123-4738", "user": "10293", }, }, "users": Object { "10293": Object { "id": "10293", "name": "Jane", }, "8472": Object { "id": "8472", "name": "Paul", }, }, }, "result": "123", } `; exports[`normalize passes over pre-normalized values 1`] = ` Object { "entities": Object { "articles": Object { "123": Object { "author": 1, "id": "123", "title": "normalizr is great!", }, }, }, "result": "123", } `; exports[`normalize uses the non-normalized input when getting the ID for an entity 1`] = ` Object { "entities": Object { "recommendations": Object { "456": Object { "user": "456", }, }, "users": Object { "456": Object { "id": "456", }, }, }, "result": "456", } `; exports[`normalize uses the non-normalized input when getting the ID for an entity 2`] = ` Array [ Array [ Object { "user": Object { "id": "456", }, }, Object { "user": Object { "id": "456", }, }, null, ], Array [ Object { "user": Object { "id": "456", }, }, Object { "user": Object { "id": "456", }, }, null, ], ] `; ================================================ FILE: src/__tests__/index.test.js ================================================ // eslint-env jest import { denormalize, normalize, schema } from '../'; describe('normalize', () => { [42, null, undefined, '42', () => {}].forEach((input) => { test(`cannot normalize input that == ${input}`, () => { expect(() => normalize(input, new schema.Entity('test'))).toThrow(); }); }); test('cannot normalize without a schema', () => { expect(() => normalize({})).toThrow(); }); test('cannot normalize with null input', () => { const mySchema = new schema.Entity('tacos'); expect(() => normalize(null, mySchema)).toThrow(/null/); }); test('normalizes entities', () => { const mySchema = new schema.Entity('tacos'); expect( normalize( [ { id: 1, type: 'foo' }, { id: 2, type: 'bar' }, ], [mySchema] ) ).toMatchSnapshot(); }); test('normalizes entities with circular references', () => { const user = new schema.Entity('users'); user.define({ friends: [user], }); const input = { id: 123, friends: [] }; input.friends.push(input); expect(normalize(input, user)).toMatchSnapshot(); }); test('normalizes nested entities', () => { const user = new schema.Entity('users'); const comment = new schema.Entity('comments', { user: user, }); const article = new schema.Entity('articles', { author: user, comments: [comment], }); const input = { id: '123', title: 'A Great Article', author: { id: '8472', name: 'Paul', }, body: 'This article is great.', comments: [ { id: 'comment-123-4738', comment: 'I like it!', user: { id: '10293', name: 'Jane', }, }, ], }; expect(normalize(input, article)).toMatchSnapshot(); }); test('does not modify the original input', () => { const user = new schema.Entity('users'); const article = new schema.Entity('articles', { author: user }); const input = Object.freeze({ id: '123', title: 'A Great Article', author: Object.freeze({ id: '8472', name: 'Paul', }), }); expect(() => normalize(input, article)).not.toThrow(); }); test('ignores null values', () => { const myEntity = new schema.Entity('myentities'); expect(normalize([null], [myEntity])).toMatchSnapshot(); expect(normalize([undefined], [myEntity])).toMatchSnapshot(); expect(normalize([false], [myEntity])).toMatchSnapshot(); }); test('can use fully custom entity classes', () => { class MyEntity extends schema.Entity { schema = { children: [new schema.Entity('children')], }; getId(entity, parent, key) { return entity.uuid; } normalize(input, parent, key, visit, addEntity, visitedEntities) { const entity = { ...input }; Object.keys(this.schema).forEach((key) => { const schema = this.schema[key]; entity[key] = visit(input[key], input, key, schema, addEntity, visitedEntities); }); addEntity(this, entity, parent, key); return { uuid: this.getId(entity), schema: this.key, }; } } const mySchema = new MyEntity('food'); expect( normalize( { uuid: '1234', name: 'tacos', children: [{ id: 4, name: 'lettuce' }], }, mySchema ) ).toMatchSnapshot(); }); test('uses the non-normalized input when getting the ID for an entity', () => { const userEntity = new schema.Entity('users'); const idAttributeFn = jest.fn((nonNormalized, parent, key) => nonNormalized.user.id); const recommendation = new schema.Entity( 'recommendations', { user: userEntity }, { idAttribute: idAttributeFn, } ); expect(normalize({ user: { id: '456' } }, recommendation)).toMatchSnapshot(); expect(idAttributeFn.mock.calls).toMatchSnapshot(); expect(recommendation.idAttribute).toBe(idAttributeFn); }); test('passes over pre-normalized values', () => { const userEntity = new schema.Entity('users'); const articleEntity = new schema.Entity('articles', { author: userEntity }); expect(normalize({ id: '123', title: 'normalizr is great!', author: 1 }, articleEntity)).toMatchSnapshot(); }); test('can normalize object without proper object prototype inheritance', () => { const test = { id: 1, elements: [] }; test.elements.push( Object.assign(Object.create(null), { id: 18, name: 'test', }) ); const testEntity = new schema.Entity('test', { elements: [new schema.Entity('elements')], }); expect(() => normalize(test, testEntity)).not.toThrow(); }); test('can normalize entity nested inside entity using property from parent', () => { const linkablesSchema = new schema.Entity('linkables'); const mediaSchema = new schema.Entity('media'); const listsSchema = new schema.Entity('lists'); const schemaMap = { media: mediaSchema, lists: listsSchema, }; linkablesSchema.define({ data: (parent) => schemaMap[parent.schema_type], }); const input = { id: 1, module_type: 'article', schema_type: 'media', data: { id: 2, url: 'catimage.jpg', }, }; expect(normalize(input, linkablesSchema)).toMatchSnapshot(); }); test('can normalize entity nested inside object using property from parent', () => { const mediaSchema = new schema.Entity('media'); const listsSchema = new schema.Entity('lists'); const schemaMap = { media: mediaSchema, lists: listsSchema, }; const linkablesSchema = { data: (parent) => schemaMap[parent.schema_type], }; const input = { id: 1, module_type: 'article', schema_type: 'media', data: { id: 2, url: 'catimage.jpg', }, }; expect(normalize(input, linkablesSchema)).toMatchSnapshot(); }); }); describe('denormalize', () => { test('cannot denormalize without a schema', () => { expect(() => denormalize({})).toThrow(); }); test('returns the input if undefined', () => { expect(denormalize(undefined, {}, {})).toBeUndefined(); }); test('denormalizes entities', () => { const mySchema = new schema.Entity('tacos'); const entities = { tacos: { 1: { id: 1, type: 'foo' }, 2: { id: 2, type: 'bar' }, }, }; expect(denormalize([1, 2], [mySchema], entities)).toMatchSnapshot(); }); test('denormalizes nested entities', () => { const user = new schema.Entity('users'); const comment = new schema.Entity('comments', { user: user, }); const article = new schema.Entity('articles', { author: user, comments: [comment], }); const entities = { articles: { 123: { author: '8472', body: 'This article is great.', comments: ['comment-123-4738'], id: '123', title: 'A Great Article', }, }, comments: { 'comment-123-4738': { comment: 'I like it!', id: 'comment-123-4738', user: '10293', }, }, users: { 10293: { id: '10293', name: 'Jane', }, 8472: { id: '8472', name: 'Paul', }, }, }; expect(denormalize('123', article, entities)).toMatchSnapshot(); }); test('set to undefined if schema key is not in entities', () => { const user = new schema.Entity('users'); const comment = new schema.Entity('comments', { user: user, }); const article = new schema.Entity('articles', { author: user, comments: [comment], }); const entities = { articles: { 123: { id: '123', author: '8472', comments: ['1'], }, }, comments: { 1: { user: '123', }, }, }; expect(denormalize('123', article, entities)).toMatchSnapshot(); }); test('does not modify the original entities', () => { const user = new schema.Entity('users'); const article = new schema.Entity('articles', { author: user }); const entities = Object.freeze({ articles: Object.freeze({ 123: Object.freeze({ id: '123', title: 'A Great Article', author: '8472', }), }), users: Object.freeze({ 8472: Object.freeze({ id: '8472', name: 'Paul', }), }), }); expect(() => denormalize('123', article, entities)).not.toThrow(); }); test('denormalizes with function as idAttribute', () => { const normalizedData = { entities: { patrons: { 1: { id: '1', guest: null, name: 'Esther' }, 2: { id: '2', guest: 'guest-2-1', name: 'Tom' }, }, guests: { 'guest-2-1': { guest_id: 1 } }, }, result: ['1', '2'], }; const guestSchema = new schema.Entity( 'guests', {}, { idAttribute: (value, parent, key) => `${key}-${parent.id}-${value.guest_id}`, } ); const patronsSchema = new schema.Entity('patrons', { guest: guestSchema, }); expect(denormalize(normalizedData.result, [patronsSchema], normalizedData.entities)).toMatchSnapshot(); }); }); ================================================ FILE: src/index.js ================================================ import * as ImmutableUtils from './schemas/ImmutableUtils'; import EntitySchema from './schemas/Entity'; import UnionSchema from './schemas/Union'; import ValuesSchema from './schemas/Values'; import ArraySchema, * as ArrayUtils from './schemas/Array'; import ObjectSchema, * as ObjectUtils from './schemas/Object'; const visit = (value, parent, key, schema, addEntity, visitedEntities) => { if (typeof value !== 'object' || !value) { return value; } if (typeof schema === 'object' && (!schema.normalize || typeof schema.normalize !== 'function')) { const method = Array.isArray(schema) ? ArrayUtils.normalize : ObjectUtils.normalize; return method(schema, value, parent, key, visit, addEntity, visitedEntities); } return schema.normalize(value, parent, key, visit, addEntity, visitedEntities); }; const addEntities = (entities) => (schema, processedEntity, value, parent, key) => { const schemaKey = schema.key; const id = schema.getId(value, parent, key); if (!(schemaKey in entities)) { entities[schemaKey] = {}; } const existingEntity = entities[schemaKey][id]; if (existingEntity) { entities[schemaKey][id] = schema.merge(existingEntity, processedEntity); } else { entities[schemaKey][id] = processedEntity; } }; export const schema = { Array: ArraySchema, Entity: EntitySchema, Object: ObjectSchema, Union: UnionSchema, Values: ValuesSchema, }; export const normalize = (input, schema) => { if (!input || typeof input !== 'object') { throw new Error( `Unexpected input given to normalize. Expected type to be "object", found "${ input === null ? 'null' : typeof input }".` ); } const entities = {}; const addEntity = addEntities(entities); const visitedEntities = {}; const result = visit(input, input, null, schema, addEntity, visitedEntities); return { entities, result }; }; const unvisitEntity = (id, schema, unvisit, getEntity, cache) => { let entity = getEntity(id, schema); if (entity === undefined && schema instanceof EntitySchema) { entity = schema.fallback(id, schema); } if (typeof entity !== 'object' || entity === null) { return entity; } if (!cache[schema.key]) { cache[schema.key] = {}; } if (!cache[schema.key][id]) { // Ensure we don't mutate it non-immutable objects const entityCopy = ImmutableUtils.isImmutable(entity) ? entity : { ...entity }; // Need to set this first so that if it is referenced further within the // denormalization the reference will already exist. cache[schema.key][id] = entityCopy; cache[schema.key][id] = schema.denormalize(entityCopy, unvisit); } return cache[schema.key][id]; }; const getUnvisit = (entities) => { const cache = {}; const getEntity = getEntities(entities); return function unvisit(input, schema) { if (typeof schema === 'object' && (!schema.denormalize || typeof schema.denormalize !== 'function')) { const method = Array.isArray(schema) ? ArrayUtils.denormalize : ObjectUtils.denormalize; return method(schema, input, unvisit); } if (input === undefined || input === null) { return input; } if (schema instanceof EntitySchema) { return unvisitEntity(input, schema, unvisit, getEntity, cache); } return schema.denormalize(input, unvisit); }; }; const getEntities = (entities) => { const isImmutable = ImmutableUtils.isImmutable(entities); return (entityOrId, schema) => { const schemaKey = schema.key; if (typeof entityOrId === 'object') { return entityOrId; } if (isImmutable) { return entities.getIn([schemaKey, entityOrId.toString()]); } return entities[schemaKey] && entities[schemaKey][entityOrId]; }; }; export const denormalize = (input, schema, entities) => { if (typeof input !== 'undefined') { return getUnvisit(entities)(input, schema); } }; ================================================ FILE: src/schemas/Array.js ================================================ import PolymorphicSchema from './Polymorphic'; const validateSchema = (definition) => { const isArray = Array.isArray(definition); if (isArray && definition.length > 1) { throw new Error(`Expected schema definition to be a single schema, but found ${definition.length}.`); } return definition[0]; }; const getValues = (input) => (Array.isArray(input) ? input : Object.keys(input).map((key) => input[key])); export const normalize = (schema, input, parent, key, visit, addEntity, visitedEntities) => { schema = validateSchema(schema); const values = getValues(input); // Special case: Arrays pass *their* parent on to their children, since there // is not any special information that can be gathered from themselves directly return values.map((value, index) => visit(value, parent, key, schema, addEntity, visitedEntities)); }; export const denormalize = (schema, input, unvisit) => { schema = validateSchema(schema); return input && input.map ? input.map((entityOrId) => unvisit(entityOrId, schema)) : input; }; export default class ArraySchema extends PolymorphicSchema { normalize(input, parent, key, visit, addEntity, visitedEntities) { const values = getValues(input); return values .map((value, index) => this.normalizeValue(value, parent, key, visit, addEntity, visitedEntities)) .filter((value) => value !== undefined && value !== null); } denormalize(input, unvisit) { return input && input.map ? input.map((value) => this.denormalizeValue(value, unvisit)) : input; } } ================================================ FILE: src/schemas/Entity.js ================================================ import * as ImmutableUtils from './ImmutableUtils'; const getDefaultGetId = (idAttribute) => (input) => ImmutableUtils.isImmutable(input) ? input.get(idAttribute) : input[idAttribute]; export default class EntitySchema { constructor(key, definition = {}, options = {}) { if (!key || typeof key !== 'string') { throw new Error(`Expected a string key for Entity, but found ${key}.`); } const { idAttribute = 'id', mergeStrategy = (entityA, entityB) => { return { ...entityA, ...entityB }; }, processStrategy = (input) => ({ ...input }), fallbackStrategy = (key, schema) => undefined, } = options; this._key = key; this._getId = typeof idAttribute === 'function' ? idAttribute : getDefaultGetId(idAttribute); this._idAttribute = idAttribute; this._mergeStrategy = mergeStrategy; this._processStrategy = processStrategy; this._fallbackStrategy = fallbackStrategy; this.define(definition); } get key() { return this._key; } get idAttribute() { return this._idAttribute; } define(definition) { this.schema = Object.keys(definition).reduce((entitySchema, key) => { const schema = definition[key]; return { ...entitySchema, [key]: schema }; }, this.schema || {}); } getId(input, parent, key) { return this._getId(input, parent, key); } merge(entityA, entityB) { return this._mergeStrategy(entityA, entityB); } fallback(id, schema) { return this._fallbackStrategy(id, schema); } normalize(input, parent, key, visit, addEntity, visitedEntities) { const id = this.getId(input, parent, key); const entityType = this.key; if (!(entityType in visitedEntities)) { visitedEntities[entityType] = {}; } if (!(id in visitedEntities[entityType])) { visitedEntities[entityType][id] = []; } if (visitedEntities[entityType][id].some((entity) => entity === input)) { return id; } visitedEntities[entityType][id].push(input); const processedEntity = this._processStrategy(input, parent, key); Object.keys(this.schema).forEach((key) => { if (processedEntity.hasOwnProperty(key) && typeof processedEntity[key] === 'object') { const schema = this.schema[key]; const resolvedSchema = typeof schema === 'function' ? schema(input) : schema; processedEntity[key] = visit( processedEntity[key], processedEntity, key, resolvedSchema, addEntity, visitedEntities ); } }); addEntity(this, processedEntity, input, parent, key); return id; } denormalize(entity, unvisit) { if (ImmutableUtils.isImmutable(entity)) { return ImmutableUtils.denormalizeImmutable(this.schema, entity, unvisit); } Object.keys(this.schema).forEach((key) => { if (entity.hasOwnProperty(key)) { const schema = this.schema[key]; entity[key] = unvisit(entity[key], schema); } }); return entity; } } ================================================ FILE: src/schemas/ImmutableUtils.js ================================================ /** * Helpers to enable Immutable compatibility *without* bringing in * the 'immutable' package as a dependency. */ /** * Check if an object is immutable by checking if it has a key specific * to the immutable library. * * @param {any} object * @return {bool} */ export function isImmutable(object) { return !!( object && typeof object.hasOwnProperty === 'function' && (object.hasOwnProperty('__ownerID') || // Immutable.Map (object._map && object._map.hasOwnProperty('__ownerID'))) ); // Immutable.Record } /** * Denormalize an immutable entity. * * @param {Schema} schema * @param {Immutable.Map|Immutable.Record} input * @param {function} unvisit * @param {function} getDenormalizedEntity * @return {Immutable.Map|Immutable.Record} */ export function denormalizeImmutable(schema, input, unvisit) { return Object.keys(schema).reduce((object, key) => { // Immutable maps cast keys to strings on write so we need to ensure // we're accessing them using string keys. const stringKey = `${key}`; if (object.has(stringKey)) { return object.set(stringKey, unvisit(object.get(stringKey), schema[stringKey])); } else { return object; } }, input); } ================================================ FILE: src/schemas/Object.js ================================================ import * as ImmutableUtils from './ImmutableUtils'; export const normalize = (schema, input, parent, key, visit, addEntity, visitedEntities) => { const object = { ...input }; Object.keys(schema).forEach((key) => { const localSchema = schema[key]; const resolvedLocalSchema = typeof localSchema === 'function' ? localSchema(input) : localSchema; const value = visit(input[key], input, key, resolvedLocalSchema, addEntity, visitedEntities); if (value === undefined || value === null) { delete object[key]; } else { object[key] = value; } }); return object; }; export const denormalize = (schema, input, unvisit) => { if (ImmutableUtils.isImmutable(input)) { return ImmutableUtils.denormalizeImmutable(schema, input, unvisit); } const object = { ...input }; Object.keys(schema).forEach((key) => { if (object[key] != null) { object[key] = unvisit(object[key], schema[key]); } }); return object; }; export default class ObjectSchema { constructor(definition) { this.define(definition); } define(definition) { this.schema = Object.keys(definition).reduce((entitySchema, key) => { const schema = definition[key]; return { ...entitySchema, [key]: schema }; }, this.schema || {}); } normalize(...args) { return normalize(this.schema, ...args); } denormalize(...args) { return denormalize(this.schema, ...args); } } ================================================ FILE: src/schemas/Polymorphic.js ================================================ import { isImmutable } from './ImmutableUtils'; export default class PolymorphicSchema { constructor(definition, schemaAttribute) { if (schemaAttribute) { this._schemaAttribute = typeof schemaAttribute === 'string' ? (input) => input[schemaAttribute] : schemaAttribute; } this.define(definition); } get isSingleSchema() { return !this._schemaAttribute; } define(definition) { this.schema = definition; } getSchemaAttribute(input, parent, key) { return !this.isSingleSchema && this._schemaAttribute(input, parent, key); } inferSchema(input, parent, key) { if (this.isSingleSchema) { return this.schema; } const attr = this.getSchemaAttribute(input, parent, key); return this.schema[attr]; } normalizeValue(value, parent, key, visit, addEntity, visitedEntities) { const schema = this.inferSchema(value, parent, key); if (!schema) { return value; } const normalizedValue = visit(value, parent, key, schema, addEntity, visitedEntities); return this.isSingleSchema || normalizedValue === undefined || normalizedValue === null ? normalizedValue : { id: normalizedValue, schema: this.getSchemaAttribute(value, parent, key) }; } denormalizeValue(value, unvisit) { const schemaKey = isImmutable(value) ? value.get('schema') : value.schema; if (!this.isSingleSchema && !schemaKey) { return value; } const id = this.isSingleSchema ? undefined : isImmutable(value) ? value.get('id') : value.id; const schema = this.isSingleSchema ? this.schema : this.schema[schemaKey]; return unvisit(id || value, schema); } } ================================================ FILE: src/schemas/Union.js ================================================ import PolymorphicSchema from './Polymorphic'; export default class UnionSchema extends PolymorphicSchema { constructor(definition, schemaAttribute) { if (!schemaAttribute) { throw new Error('Expected option "schemaAttribute" not found on UnionSchema.'); } super(definition, schemaAttribute); } normalize(input, parent, key, visit, addEntity, visitedEntities) { return this.normalizeValue(input, parent, key, visit, addEntity, visitedEntities); } denormalize(input, unvisit) { return this.denormalizeValue(input, unvisit); } } ================================================ FILE: src/schemas/Values.js ================================================ import PolymorphicSchema from './Polymorphic'; export default class ValuesSchema extends PolymorphicSchema { normalize(input, parent, key, visit, addEntity, visitedEntities) { return Object.keys(input).reduce((output, key, index) => { const value = input[key]; return value !== undefined && value !== null ? { ...output, [key]: this.normalizeValue(value, input, key, visit, addEntity, visitedEntities), } : output; }, {}); } denormalize(input, unvisit) { return Object.keys(input).reduce((output, key) => { const entityOrId = input[key]; return { ...output, [key]: this.denormalizeValue(entityOrId, unvisit), }; }, {}); } } ================================================ FILE: src/schemas/__tests__/Array.test.js ================================================ // eslint-env jest import { fromJS } from 'immutable'; import { denormalize, normalize, schema } from '../../'; describe(`${schema.Array.name} normalization`, () => { describe('Object', () => { test(`normalizes plain arrays as shorthand for ${schema.Array.name}`, () => { const userSchema = new schema.Entity('user'); expect(normalize([{ id: 1 }, { id: 2 }], [userSchema])).toMatchSnapshot(); }); test('throws an error if created with more than one schema', () => { const userSchema = new schema.Entity('users'); const catSchema = new schema.Entity('cats'); expect(() => normalize([{ id: 1 }], [catSchema, userSchema])).toThrow(); }); test('passes its parent to its children when normalizing', () => { const processStrategy = (entity, parent, key) => { return { ...entity, parentId: parent.id, parentKey: key }; }; const childEntity = new schema.Entity('children', {}, { processStrategy }); const parentEntity = new schema.Entity('parents', { children: [childEntity], }); expect( normalize( { id: 1, content: 'parent', children: [{ id: 4, content: 'child' }], }, parentEntity ) ).toMatchSnapshot(); }); test('normalizes Objects using their values', () => { const userSchema = new schema.Entity('user'); expect(normalize({ foo: { id: 1 }, bar: { id: 2 } }, [userSchema])).toMatchSnapshot(); }); }); describe('Class', () => { test('normalizes a single entity', () => { const cats = new schema.Entity('cats'); const listSchema = new schema.Array(cats); expect(normalize([{ id: 1 }, { id: 2 }], listSchema)).toMatchSnapshot(); }); test('normalizes multiple entities', () => { const inferSchemaFn = jest.fn((input, parent, key) => input.type || 'dogs'); const catSchema = new schema.Entity('cats'); const peopleSchema = new schema.Entity('person'); const listSchema = new schema.Array( { cats: catSchema, people: peopleSchema, }, inferSchemaFn ); expect( normalize( [ { type: 'cats', id: '123' }, { type: 'people', id: '123' }, { id: '789', name: 'fido' }, { type: 'cats', id: '456' }, ], listSchema ) ).toMatchSnapshot(); expect(inferSchemaFn.mock.calls).toMatchSnapshot(); }); test('normalizes Objects using their values', () => { const userSchema = new schema.Entity('user'); const users = new schema.Array(userSchema); expect(normalize({ foo: { id: 1 }, bar: { id: 2 } }, users)).toMatchSnapshot(); }); test('filters out undefined and null normalized values', () => { const userSchema = new schema.Entity('user'); const users = new schema.Array(userSchema); expect(normalize([undefined, { id: 123 }, null], users)).toMatchSnapshot(); }); }); }); describe(`${schema.Array.name} denormalization`, () => { describe('Object', () => { test('denormalizes a single entity', () => { const cats = new schema.Entity('cats'); const entities = { cats: { 1: { id: 1, name: 'Milo' }, 2: { id: 2, name: 'Jake' }, }, }; expect(denormalize([1, 2], [cats], entities)).toMatchSnapshot(); expect(denormalize([1, 2], [cats], fromJS(entities))).toMatchSnapshot(); }); test('returns the input value if is not an array', () => { const filling = new schema.Entity('fillings'); const taco = new schema.Entity('tacos', { fillings: [filling] }); const entities = { tacos: { 123: { id: '123', fillings: null, }, }, }; expect(denormalize('123', taco, entities)).toMatchSnapshot(); expect(denormalize('123', taco, fromJS(entities))).toMatchSnapshot(); }); }); describe('Class', () => { test('denormalizes a single entity', () => { const cats = new schema.Entity('cats'); const entities = { cats: { 1: { id: 1, name: 'Milo' }, 2: { id: 2, name: 'Jake' }, }, }; const catList = new schema.Array(cats); expect(denormalize([1, 2], catList, entities)).toMatchSnapshot(); expect(denormalize([1, 2], catList, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes multiple entities', () => { const catSchema = new schema.Entity('cats'); const peopleSchema = new schema.Entity('person'); const listSchema = new schema.Array( { cats: catSchema, dogs: {}, people: peopleSchema, }, (input, parent, key) => input.type || 'dogs' ); const entities = { cats: { 123: { id: '123', type: 'cats', }, 456: { id: '456', type: 'cats', }, }, person: { 123: { id: '123', type: 'people', }, }, }; const input = [ { id: '123', schema: 'cats' }, { id: '123', schema: 'people' }, { id: { id: '789' }, schema: 'dogs' }, { id: '456', schema: 'cats' }, ]; expect(denormalize(input, listSchema, entities)).toMatchSnapshot(); expect(denormalize(input, listSchema, fromJS(entities))).toMatchSnapshot(); }); test('returns the input value if is not an array', () => { const filling = new schema.Entity('fillings'); const fillings = new schema.Array(filling); const taco = new schema.Entity('tacos', { fillings }); const entities = { tacos: { 123: { id: '123', fillings: {}, }, }, }; expect(denormalize('123', taco, entities)).toMatchSnapshot(); expect(denormalize('123', taco, fromJS(entities))).toMatchSnapshot(); }); test('does not assume mapping of schema to attribute values when schemaAttribute is not set', () => { const cats = new schema.Entity('cats'); const catRecord = new schema.Object({ cat: cats, }); const catList = new schema.Array(catRecord); const input = [ { cat: { id: 1 }, id: 5 }, { cat: { id: 2 }, id: 6 }, ]; const output = normalize(input, catList); expect(output).toMatchSnapshot(); expect(denormalize(output.result, catList, output.entities)).toEqual(input); }); }); }); ================================================ FILE: src/schemas/__tests__/Entity.test.js ================================================ // eslint-env jest import { denormalize, normalize, schema } from '../../'; import { fromJS, Record } from 'immutable'; const values = (obj) => Object.keys(obj).map((key) => obj[key]); describe(`${schema.Entity.name} normalization`, () => { test('normalizes an entity', () => { const entity = new schema.Entity('item'); expect(normalize({ id: 1 }, entity)).toMatchSnapshot(); }); describe('key', () => { test('must be created with a key name', () => { expect(() => new schema.Entity()).toThrow(); }); test('key name must be a string', () => { expect(() => new schema.Entity(42)).toThrow(); }); test('key getter should return key passed to constructor', () => { const user = new schema.Entity('users'); expect(user.key).toEqual('users'); }); }); describe('idAttribute', () => { test('can use a custom idAttribute string', () => { const user = new schema.Entity('users', {}, { idAttribute: 'id_str' }); expect(normalize({ id_str: '134351', name: 'Kathy' }, user)).toMatchSnapshot(); }); test('can normalize entity IDs based on their object key', () => { const user = new schema.Entity('users', {}, { idAttribute: (entity, parent, key) => key }); const inputSchema = new schema.Values({ users: user }, () => 'users'); expect(normalize({ 4: { name: 'taco' }, 56: { name: 'burrito' } }, inputSchema)).toMatchSnapshot(); }); test("can build the entity's ID from the parent object", () => { const user = new schema.Entity( 'users', {}, { idAttribute: (entity, parent, key) => `${parent.name}-${key}-${entity.id}`, } ); const inputSchema = new schema.Object({ user }); expect(normalize({ name: 'tacos', user: { id: '4', name: 'Jimmy' } }, inputSchema)).toMatchSnapshot(); }); }); describe('mergeStrategy', () => { test('defaults to plain merging', () => { const mySchema = new schema.Entity('tacos'); expect( normalize( [ { id: 1, name: 'foo' }, { id: 1, name: 'bar', alias: 'bar' }, ], [mySchema] ) ).toMatchSnapshot(); }); test('can use a custom merging strategy', () => { const mergeStrategy = (entityA, entityB) => { return { ...entityA, ...entityB, name: entityA.name }; }; const mySchema = new schema.Entity('tacos', {}, { mergeStrategy }); expect( normalize( [ { id: 1, name: 'foo' }, { id: 1, name: 'bar', alias: 'bar' }, ], [mySchema] ) ).toMatchSnapshot(); }); }); describe('processStrategy', () => { test('can use a custom processing strategy', () => { const processStrategy = (entity) => { return { ...entity, slug: `thing-${entity.id}` }; }; const mySchema = new schema.Entity('tacos', {}, { processStrategy }); expect(normalize({ id: 1, name: 'foo' }, mySchema)).toMatchSnapshot(); }); test('can use information from the parent in the process strategy', () => { const processStrategy = (entity, parent, key) => { return { ...entity, parentId: parent.id, parentKey: key }; }; const childEntity = new schema.Entity('children', {}, { processStrategy }); const parentEntity = new schema.Entity('parents', { child: childEntity, }); expect( normalize( { id: 1, content: 'parent', child: { id: 4, content: 'child' }, }, parentEntity ) ).toMatchSnapshot(); }); test('is run before and passed to the schema normalization', () => { const processStrategy = (input) => ({ ...values(input)[0], type: Object.keys(input)[0] }); const attachmentEntity = new schema.Entity('attachments'); // If not run before, this schema would require a parent object with key "message" const myEntity = new schema.Entity( 'entries', { data: { attachment: attachmentEntity }, }, { idAttribute: (input) => values(input)[0].id, processStrategy } ); expect(normalize({ message: { id: '123', data: { attachment: { id: '456' } } } }, myEntity)).toMatchSnapshot(); }); }); }); describe(`${schema.Entity.name} denormalization`, () => { test('denormalizes an entity', () => { const mySchema = new schema.Entity('tacos'); const entities = { tacos: { 1: { id: 1, type: 'foo' }, }, }; expect(denormalize(1, mySchema, entities)).toMatchSnapshot(); expect(denormalize(1, mySchema, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes deep entities', () => { const foodSchema = new schema.Entity('foods'); const menuSchema = new schema.Entity('menus', { food: foodSchema, }); const entities = { menus: { 1: { id: 1, food: 1 }, 2: { id: 2 }, }, foods: { 1: { id: 1 }, }, }; expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes to undefined for missing data', () => { const foodSchema = new schema.Entity('foods'); const menuSchema = new schema.Entity('menus', { food: foodSchema, }); const entities = { menus: { 1: { id: 1, food: 2 }, }, foods: { 1: { id: 1 }, }, }; expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes deep entities with records', () => { const foodSchema = new schema.Entity('foods'); const menuSchema = new schema.Entity('menus', { food: foodSchema, }); const Food = new Record({ id: null }); const Menu = new Record({ id: null, food: null }); const entities = { menus: { 1: new Menu({ id: 1, food: 1 }), 2: new Menu({ id: 2 }), }, foods: { 1: new Food({ id: 1 }), }, }; expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); expect(denormalize(2, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(2, menuSchema, fromJS(entities))).toMatchSnapshot(); }); test('can denormalize already partially denormalized data', () => { const foodSchema = new schema.Entity('foods'); const menuSchema = new schema.Entity('menus', { food: foodSchema, }); const entities = { menus: { 1: { id: 1, food: { id: 1 } }, }, foods: { 1: { id: 1 }, }, }; expect(denormalize(1, menuSchema, entities)).toMatchSnapshot(); expect(denormalize(1, menuSchema, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes recursive dependencies', () => { const user = new schema.Entity('users'); const report = new schema.Entity('reports'); user.define({ reports: [report], }); report.define({ draftedBy: user, publishedBy: user, }); const entities = { reports: { 123: { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, users: { 456: { id: '456', role: 'manager', reports: ['123'], }, }, }; expect(denormalize('123', report, entities)).toMatchSnapshot(); expect(denormalize('123', report, fromJS(entities))).toMatchSnapshot(); expect(denormalize('456', user, entities)).toMatchSnapshot(); expect(denormalize('456', user, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes entities with referential equality', () => { const user = new schema.Entity('users'); const report = new schema.Entity('reports'); user.define({ reports: [report], }); report.define({ draftedBy: user, publishedBy: user, }); const entities = { reports: { 123: { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, users: { 456: { id: '456', role: 'manager', reports: ['123'], }, }, }; const denormalizedReport = denormalize('123', report, entities); expect(denormalizedReport).toBe(denormalizedReport.draftedBy.reports[0]); expect(denormalizedReport.publishedBy).toBe(denormalizedReport.draftedBy); // NOTE: Given how immutable data works, referential equality can't be // maintained with nested denormalization. }); test('denormalizes with fallback strategy', () => { const user = new schema.Entity( 'users', {}, { idAttribute: 'userId', fallbackStrategy: (id, schema) => ({ [schema.idAttribute]: id, name: 'John Doe', }), } ); const report = new schema.Entity('reports', { draftedBy: user, publishedBy: user, }); const entities = { reports: { 123: { id: '123', title: 'Weekly report', draftedBy: '456', publishedBy: '456', }, }, users: {}, }; const denormalizedReport = denormalize('123', report, entities); expect(denormalizedReport.publishedBy).toBe(denormalizedReport.draftedBy); expect(denormalizedReport.publishedBy.name).toBe('John Doe'); expect(denormalizedReport.publishedBy.userId).toBe('456'); // }); }); ================================================ FILE: src/schemas/__tests__/Object.test.js ================================================ // eslint-env jest import { fromJS } from 'immutable'; import { denormalize, normalize, schema } from '../../'; describe(`${schema.Object.name} normalization`, () => { test('normalizes an object', () => { const userSchema = new schema.Entity('user'); const object = new schema.Object({ user: userSchema, }); expect(normalize({ user: { id: 1 } }, object)).toMatchSnapshot(); }); test(`normalizes plain objects as shorthand for ${schema.Object.name}`, () => { const userSchema = new schema.Entity('user'); expect(normalize({ user: { id: 1 } }, { user: userSchema })).toMatchSnapshot(); }); test('filters out undefined and null values', () => { const userSchema = new schema.Entity('user'); const users = { foo: userSchema, bar: userSchema, baz: userSchema }; expect(normalize({ foo: {}, bar: { id: '1' } }, users)).toMatchSnapshot(); }); }); describe(`${schema.Object.name} denormalization`, () => { test('denormalizes an object', () => { const userSchema = new schema.Entity('user'); const object = new schema.Object({ user: userSchema, }); const entities = { user: { 1: { id: 1, name: 'Nacho' }, }, }; expect(denormalize({ user: 1 }, object, entities)).toMatchSnapshot(); expect(denormalize({ user: 1 }, object, fromJS(entities))).toMatchSnapshot(); expect(denormalize(fromJS({ user: 1 }), object, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes plain object shorthand', () => { const userSchema = new schema.Entity('user'); const entities = { user: { 1: { id: 1, name: 'Jane' }, }, }; expect(denormalize({ user: 1 }, { user: userSchema, tacos: {} }, entities)).toMatchSnapshot(); expect(denormalize({ user: 1 }, { user: userSchema, tacos: {} }, fromJS(entities))).toMatchSnapshot(); expect(denormalize(fromJS({ user: 1 }), { user: userSchema, tacos: {} }, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes an object that contains a property representing a an object with an id of zero', () => { const userSchema = new schema.Entity('user'); const object = new schema.Object({ user: userSchema, }); const entities = { user: { 0: { id: 0, name: 'Chancho' }, }, }; expect(denormalize({ user: 0 }, object, entities)).toMatchSnapshot(); expect(denormalize({ user: 0 }, object, fromJS(entities))).toMatchSnapshot(); expect(denormalize(fromJS({ user: 0 }), object, fromJS(entities))).toMatchSnapshot(); }); }); ================================================ FILE: src/schemas/__tests__/Union.test.js ================================================ // eslint-env jest import { fromJS } from 'immutable'; import { denormalize, normalize, schema } from '../../'; describe(`${schema.Union.name} normalization`, () => { test('throws if not given a schemaAttribute', () => { expect(() => new schema.Union({})).toThrow(); }); test('normalizes an object using string schemaAttribute', () => { const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const union = new schema.Union( { users: user, groups: group, }, 'type' ); expect(normalize({ id: 1, type: 'users' }, union)).toMatchSnapshot(); expect(normalize({ id: 2, type: 'groups' }, union)).toMatchSnapshot(); }); test('normalizes an array of multiple entities using a function to infer the schemaAttribute', () => { const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const union = new schema.Union( { users: user, groups: group, }, (input) => { return input.username ? 'users' : input.groupname ? 'groups' : null; } ); expect(normalize({ id: 1, username: 'Janey' }, union)).toMatchSnapshot(); expect(normalize({ id: 2, groupname: 'People' }, union)).toMatchSnapshot(); expect(normalize({ id: 3, notdefined: 'yep' }, union)).toMatchSnapshot(); }); }); describe(`${schema.Union.name} denormalization`, () => { const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const entities = { users: { 1: { id: 1, username: 'Janey', type: 'users' }, }, groups: { 2: { id: 2, groupname: 'People', type: 'groups' }, }, }; test('denormalizes an object using string schemaAttribute', () => { const union = new schema.Union( { users: user, groups: group, }, 'type' ); expect(denormalize({ id: 1, schema: 'users' }, union, entities)).toMatchSnapshot(); expect(denormalize(fromJS({ id: 1, schema: 'users' }), union, fromJS(entities))).toMatchSnapshot(); expect(denormalize({ id: 2, schema: 'groups' }, union, entities)).toMatchSnapshot(); expect(denormalize(fromJS({ id: 2, schema: 'groups' }), union, fromJS(entities))).toMatchSnapshot(); }); test('denormalizes an array of multiple entities using a function to infer the schemaAttribute', () => { const union = new schema.Union( { users: user, groups: group, }, (input) => { return input.username ? 'users' : 'groups'; } ); expect(denormalize({ id: 1, schema: 'users' }, union, entities)).toMatchSnapshot(); expect(denormalize(fromJS({ id: 1, schema: 'users' }), union, fromJS(entities))).toMatchSnapshot(); expect(denormalize({ id: 2, schema: 'groups' }, union, entities)).toMatchSnapshot(); expect(denormalize(fromJS({ id: 2, schema: 'groups' }), union, fromJS(entities))).toMatchSnapshot(); }); test('returns the original value no schema is given', () => { const union = new schema.Union( { users: user, groups: group, }, (input) => { return input.username ? 'users' : 'groups'; } ); expect(denormalize({ id: 1 }, union, entities)).toMatchSnapshot(); expect(denormalize(fromJS({ id: 1 }), union, fromJS(entities))).toMatchSnapshot(); }); }); ================================================ FILE: src/schemas/__tests__/Values.test.js ================================================ // eslint-env jest import { fromJS } from 'immutable'; import { denormalize, normalize, schema } from '../../'; describe(`${schema.Values.name} normalization`, () => { test('normalizes the values of an object with the given schema', () => { const cat = new schema.Entity('cats'); const dog = new schema.Entity('dogs'); const valuesSchema = new schema.Values( { dogs: dog, cats: cat, }, (entity, key) => entity.type ); expect( normalize( { fido: { id: 1, type: 'dogs' }, fluffy: { id: 1, type: 'cats' }, }, valuesSchema ) ).toMatchSnapshot(); }); test('can use a function to determine the schema when normalizing', () => { const cat = new schema.Entity('cats'); const dog = new schema.Entity('dogs'); const valuesSchema = new schema.Values( { dogs: dog, cats: cat, }, (entity, key) => `${entity.type}s` ); expect( normalize( { fido: { id: 1, type: 'dog' }, fluffy: { id: 1, type: 'cat' }, jim: { id: 2, type: 'lizard' }, }, valuesSchema ) ).toMatchSnapshot(); }); test('filters out null and undefined values', () => { const cat = new schema.Entity('cats'); const dog = new schema.Entity('dogs'); const valuesSchema = new schema.Values( { dogs: dog, cats: cat, }, (entity, key) => entity.type ); expect( normalize( { fido: undefined, milo: null, fluffy: { id: 1, type: 'cats' }, }, valuesSchema ) ).toMatchSnapshot(); }); }); describe(`${schema.Values.name} denormalization`, () => { test('denormalizes the values of an object with the given schema', () => { const cat = new schema.Entity('cats'); const dog = new schema.Entity('dogs'); const valuesSchema = new schema.Values( { dogs: dog, cats: cat, }, (entity, key) => entity.type ); const entities = { cats: { 1: { id: 1, type: 'cats' } }, dogs: { 1: { id: 1, type: 'dogs' } }, }; expect( denormalize( { fido: { id: 1, schema: 'dogs' }, fluffy: { id: 1, schema: 'cats' }, }, valuesSchema, entities ) ).toMatchSnapshot(); expect( denormalize( { fido: { id: 1, schema: 'dogs' }, fluffy: { id: 1, schema: 'cats' }, }, valuesSchema, fromJS(entities) ) ).toMatchSnapshot(); }); }); ================================================ FILE: src/schemas/__tests__/__snapshots__/Array.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ArraySchema denormalization Class denormalizes a single entity 1`] = ` Array [ Object { "id": 1, "name": "Milo", }, Object { "id": 2, "name": "Jake", }, ] `; exports[`ArraySchema denormalization Class denormalizes a single entity 2`] = ` Array [ Immutable.Map { "id": 1, "name": "Milo", }, Immutable.Map { "id": 2, "name": "Jake", }, ] `; exports[`ArraySchema denormalization Class denormalizes multiple entities 1`] = ` Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", }, Object { "id": "456", "type": "cats", }, ] `; exports[`ArraySchema denormalization Class denormalizes multiple entities 2`] = ` Array [ Immutable.Map { "id": "123", "type": "cats", }, Immutable.Map { "id": "123", "type": "people", }, Object { "id": "789", }, Immutable.Map { "id": "456", "type": "cats", }, ] `; exports[`ArraySchema denormalization Class does not assume mapping of schema to attribute values when schemaAttribute is not set 1`] = ` Object { "entities": Object { "cats": Object { "1": Object { "id": 1, }, "2": Object { "id": 2, }, }, }, "result": Array [ Object { "cat": 1, "id": 5, }, Object { "cat": 2, "id": 6, }, ], } `; exports[`ArraySchema denormalization Class returns the input value if is not an array 1`] = ` Object { "fillings": Object {}, "id": "123", } `; exports[`ArraySchema denormalization Class returns the input value if is not an array 2`] = ` Immutable.Map { "id": "123", "fillings": Immutable.Map {}, } `; exports[`ArraySchema denormalization Object denormalizes a single entity 1`] = ` Array [ Object { "id": 1, "name": "Milo", }, Object { "id": 2, "name": "Jake", }, ] `; exports[`ArraySchema denormalization Object denormalizes a single entity 2`] = ` Array [ Immutable.Map { "id": 1, "name": "Milo", }, Immutable.Map { "id": 2, "name": "Jake", }, ] `; exports[`ArraySchema denormalization Object returns the input value if is not an array 1`] = ` Object { "fillings": null, "id": "123", } `; exports[`ArraySchema denormalization Object returns the input value if is not an array 2`] = ` Immutable.Map { "id": "123", "fillings": null, } `; exports[`ArraySchema normalization Class filters out undefined and null normalized values 1`] = ` Object { "entities": Object { "user": Object { "123": Object { "id": 123, }, }, }, "result": Array [ 123, ], } `; exports[`ArraySchema normalization Class normalizes Objects using their values 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": 1, }, "2": Object { "id": 2, }, }, }, "result": Array [ 1, 2, ], } `; exports[`ArraySchema normalization Class normalizes a single entity 1`] = ` Object { "entities": Object { "cats": Object { "1": Object { "id": 1, }, "2": Object { "id": 2, }, }, }, "result": Array [ 1, 2, ], } `; exports[`ArraySchema normalization Class normalizes multiple entities 1`] = ` Object { "entities": Object { "cats": Object { "123": Object { "id": "123", "type": "cats", }, "456": Object { "id": "456", "type": "cats", }, }, "person": Object { "123": Object { "id": "123", "type": "people", }, }, }, "result": Array [ Object { "id": "123", "schema": "cats", }, Object { "id": "123", "schema": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "schema": "cats", }, ], } `; exports[`ArraySchema normalization Class normalizes multiple entities 2`] = ` Array [ Array [ Object { "id": "123", "type": "cats", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "123", "type": "cats", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "123", "type": "people", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "123", "type": "people", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "789", "name": "fido", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "456", "type": "cats", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], Array [ Object { "id": "456", "type": "cats", }, Array [ Object { "id": "123", "type": "cats", }, Object { "id": "123", "type": "people", }, Object { "id": "789", "name": "fido", }, Object { "id": "456", "type": "cats", }, ], null, ], ] `; exports[`ArraySchema normalization Object normalizes Objects using their values 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": 1, }, "2": Object { "id": 2, }, }, }, "result": Array [ 1, 2, ], } `; exports[`ArraySchema normalization Object normalizes plain arrays as shorthand for ArraySchema 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": 1, }, "2": Object { "id": 2, }, }, }, "result": Array [ 1, 2, ], } `; exports[`ArraySchema normalization Object passes its parent to its children when normalizing 1`] = ` Object { "entities": Object { "children": Object { "4": Object { "content": "child", "id": 4, "parentId": 1, "parentKey": "children", }, }, "parents": Object { "1": Object { "children": Array [ 4, ], "content": "parent", "id": 1, }, }, }, "result": 1, } `; ================================================ FILE: src/schemas/__tests__/__snapshots__/Entity.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`EntitySchema denormalization can denormalize already partially denormalized data 1`] = ` Object { "food": Object { "id": 1, }, "id": 1, } `; exports[`EntitySchema denormalization can denormalize already partially denormalized data 2`] = ` Immutable.Map { "id": 1, "food": Immutable.Map { "id": 1, }, } `; exports[`EntitySchema denormalization denormalizes an entity 1`] = ` Object { "id": 1, "type": "foo", } `; exports[`EntitySchema denormalization denormalizes an entity 2`] = ` Immutable.Map { "id": 1, "type": "foo", } `; exports[`EntitySchema denormalization denormalizes deep entities 1`] = ` Object { "food": Object { "id": 1, }, "id": 1, } `; exports[`EntitySchema denormalization denormalizes deep entities 2`] = ` Immutable.Map { "id": 1, "food": Immutable.Map { "id": 1, }, } `; exports[`EntitySchema denormalization denormalizes deep entities 3`] = ` Object { "id": 2, } `; exports[`EntitySchema denormalization denormalizes deep entities 4`] = ` Immutable.Map { "id": 2, } `; exports[`EntitySchema denormalization denormalizes deep entities with records 1`] = ` Immutable.Record { "id": 1, "food": Immutable.Record { "id": 1, }, } `; exports[`EntitySchema denormalization denormalizes deep entities with records 2`] = ` Immutable.Record { "id": 1, "food": Immutable.Record { "id": 1, }, } `; exports[`EntitySchema denormalization denormalizes deep entities with records 3`] = ` Immutable.Record { "id": 2, "food": null, } `; exports[`EntitySchema denormalization denormalizes deep entities with records 4`] = ` Immutable.Record { "id": 2, "food": null, } `; exports[`EntitySchema denormalization denormalizes recursive dependencies 1`] = ` Object { "draftedBy": Object { "id": "456", "reports": Array [ [Circular], ], "role": "manager", }, "id": "123", "publishedBy": Object { "id": "456", "reports": Array [ [Circular], ], "role": "manager", }, "title": "Weekly report", } `; exports[`EntitySchema denormalization denormalizes recursive dependencies 2`] = ` Immutable.Map { "id": "123", "title": "Weekly report", "draftedBy": Immutable.Map { "id": "456", "role": "manager", "reports": Immutable.List [ Immutable.Map { "id": "123", "title": "Weekly report", "draftedBy": "456", "publishedBy": "456", }, ], }, "publishedBy": Immutable.Map { "id": "456", "role": "manager", "reports": Immutable.List [ Immutable.Map { "id": "123", "title": "Weekly report", "draftedBy": "456", "publishedBy": "456", }, ], }, } `; exports[`EntitySchema denormalization denormalizes recursive dependencies 3`] = ` Object { "id": "456", "reports": Array [ Object { "draftedBy": [Circular], "id": "123", "publishedBy": [Circular], "title": "Weekly report", }, ], "role": "manager", } `; exports[`EntitySchema denormalization denormalizes recursive dependencies 4`] = ` Immutable.Map { "id": "456", "role": "manager", "reports": Immutable.List [ Immutable.Map { "id": "123", "title": "Weekly report", "draftedBy": Immutable.Map { "id": "456", "role": "manager", "reports": Immutable.List [ "123", ], }, "publishedBy": Immutable.Map { "id": "456", "role": "manager", "reports": Immutable.List [ "123", ], }, }, ], } `; exports[`EntitySchema denormalization denormalizes to undefined for missing data 1`] = ` Object { "food": undefined, "id": 1, } `; exports[`EntitySchema denormalization denormalizes to undefined for missing data 2`] = ` Immutable.Map { "id": 1, "food": undefined, } `; exports[`EntitySchema denormalization denormalizes to undefined for missing data 3`] = `undefined`; exports[`EntitySchema denormalization denormalizes to undefined for missing data 4`] = `undefined`; exports[`EntitySchema normalization idAttribute can build the entity's ID from the parent object 1`] = ` Object { "entities": Object { "users": Object { "tacos-user-4": Object { "id": "4", "name": "Jimmy", }, }, }, "result": Object { "name": "tacos", "user": "tacos-user-4", }, } `; exports[`EntitySchema normalization idAttribute can normalize entity IDs based on their object key 1`] = ` Object { "entities": Object { "users": Object { "4": Object { "name": "taco", }, "56": Object { "name": "burrito", }, }, }, "result": Object { "4": Object { "id": "4", "schema": "users", }, "56": Object { "id": "56", "schema": "users", }, }, } `; exports[`EntitySchema normalization idAttribute can use a custom idAttribute string 1`] = ` Object { "entities": Object { "users": Object { "134351": Object { "id_str": "134351", "name": "Kathy", }, }, }, "result": "134351", } `; exports[`EntitySchema normalization mergeStrategy can use a custom merging strategy 1`] = ` Object { "entities": Object { "tacos": Object { "1": Object { "alias": "bar", "id": 1, "name": "foo", }, }, }, "result": Array [ 1, 1, ], } `; exports[`EntitySchema normalization mergeStrategy defaults to plain merging 1`] = ` Object { "entities": Object { "tacos": Object { "1": Object { "alias": "bar", "id": 1, "name": "bar", }, }, }, "result": Array [ 1, 1, ], } `; exports[`EntitySchema normalization normalizes an entity 1`] = ` Object { "entities": Object { "item": Object { "1": Object { "id": 1, }, }, }, "result": 1, } `; exports[`EntitySchema normalization processStrategy can use a custom processing strategy 1`] = ` Object { "entities": Object { "tacos": Object { "1": Object { "id": 1, "name": "foo", "slug": "thing-1", }, }, }, "result": 1, } `; exports[`EntitySchema normalization processStrategy can use information from the parent in the process strategy 1`] = ` Object { "entities": Object { "children": Object { "4": Object { "content": "child", "id": 4, "parentId": 1, "parentKey": "child", }, }, "parents": Object { "1": Object { "child": 4, "content": "parent", "id": 1, }, }, }, "result": 1, } `; exports[`EntitySchema normalization processStrategy is run before and passed to the schema normalization 1`] = ` Object { "entities": Object { "attachments": Object { "456": Object { "id": "456", }, }, "entries": Object { "123": Object { "data": Object { "attachment": "456", }, "id": "123", "type": "message", }, }, }, "result": "123", } `; ================================================ FILE: src/schemas/__tests__/__snapshots__/Object.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ObjectSchema denormalization denormalizes an object 1`] = ` Object { "user": Object { "id": 1, "name": "Nacho", }, } `; exports[`ObjectSchema denormalization denormalizes an object 2`] = ` Object { "user": Immutable.Map { "id": 1, "name": "Nacho", }, } `; exports[`ObjectSchema denormalization denormalizes an object 3`] = ` Immutable.Map { "user": Immutable.Map { "id": 1, "name": "Nacho", }, } `; exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 1`] = ` Object { "user": Object { "id": 0, "name": "Chancho", }, } `; exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 2`] = ` Object { "user": Immutable.Map { "id": 0, "name": "Chancho", }, } `; exports[`ObjectSchema denormalization denormalizes an object that contains a property representing a an object with an id of zero 3`] = ` Immutable.Map { "user": Immutable.Map { "id": 0, "name": "Chancho", }, } `; exports[`ObjectSchema denormalization denormalizes plain object shorthand 1`] = ` Object { "user": Object { "id": 1, "name": "Jane", }, } `; exports[`ObjectSchema denormalization denormalizes plain object shorthand 2`] = ` Object { "user": Immutable.Map { "id": 1, "name": "Jane", }, } `; exports[`ObjectSchema denormalization denormalizes plain object shorthand 3`] = ` Immutable.Map { "user": Immutable.Map { "id": 1, "name": "Jane", }, } `; exports[`ObjectSchema normalization filters out undefined and null values 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": "1", }, "undefined": Object {}, }, }, "result": Object { "bar": "1", }, } `; exports[`ObjectSchema normalization normalizes an object 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": 1, }, }, }, "result": Object { "user": 1, }, } `; exports[`ObjectSchema normalization normalizes plain objects as shorthand for ObjectSchema 1`] = ` Object { "entities": Object { "user": Object { "1": Object { "id": 1, }, }, }, "result": Object { "user": 1, }, } `; ================================================ FILE: src/schemas/__tests__/__snapshots__/Union.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 1`] = ` Object { "id": 1, "type": "users", "username": "Janey", } `; exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 2`] = ` Immutable.Map { "id": 1, "username": "Janey", "type": "users", } `; exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 3`] = ` Object { "groupname": "People", "id": 2, "type": "groups", } `; exports[`UnionSchema denormalization denormalizes an array of multiple entities using a function to infer the schemaAttribute 4`] = ` Immutable.Map { "id": 2, "groupname": "People", "type": "groups", } `; exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 1`] = ` Object { "id": 1, "type": "users", "username": "Janey", } `; exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 2`] = ` Immutable.Map { "id": 1, "username": "Janey", "type": "users", } `; exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 3`] = ` Object { "groupname": "People", "id": 2, "type": "groups", } `; exports[`UnionSchema denormalization denormalizes an object using string schemaAttribute 4`] = ` Immutable.Map { "id": 2, "groupname": "People", "type": "groups", } `; exports[`UnionSchema denormalization returns the original value no schema is given 1`] = ` Object { "id": 1, } `; exports[`UnionSchema denormalization returns the original value no schema is given 2`] = ` Immutable.Map { "id": 1, } `; exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 1`] = ` Object { "entities": Object { "users": Object { "1": Object { "id": 1, "username": "Janey", }, }, }, "result": Object { "id": 1, "schema": "users", }, } `; exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 2`] = ` Object { "entities": Object { "groups": Object { "2": Object { "groupname": "People", "id": 2, }, }, }, "result": Object { "id": 2, "schema": "groups", }, } `; exports[`UnionSchema normalization normalizes an array of multiple entities using a function to infer the schemaAttribute 3`] = ` Object { "entities": Object {}, "result": Object { "id": 3, "notdefined": "yep", }, } `; exports[`UnionSchema normalization normalizes an object using string schemaAttribute 1`] = ` Object { "entities": Object { "users": Object { "1": Object { "id": 1, "type": "users", }, }, }, "result": Object { "id": 1, "schema": "users", }, } `; exports[`UnionSchema normalization normalizes an object using string schemaAttribute 2`] = ` Object { "entities": Object { "groups": Object { "2": Object { "id": 2, "type": "groups", }, }, }, "result": Object { "id": 2, "schema": "groups", }, } `; ================================================ FILE: src/schemas/__tests__/__snapshots__/Values.test.js.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ValuesSchema denormalization denormalizes the values of an object with the given schema 1`] = ` Object { "fido": Object { "id": 1, "type": "dogs", }, "fluffy": Object { "id": 1, "type": "cats", }, } `; exports[`ValuesSchema denormalization denormalizes the values of an object with the given schema 2`] = ` Object { "fido": Immutable.Map { "id": 1, "type": "dogs", }, "fluffy": Immutable.Map { "id": 1, "type": "cats", }, } `; exports[`ValuesSchema normalization can use a function to determine the schema when normalizing 1`] = ` Object { "entities": Object { "cats": Object { "1": Object { "id": 1, "type": "cat", }, }, "dogs": Object { "1": Object { "id": 1, "type": "dog", }, }, }, "result": Object { "fido": Object { "id": 1, "schema": "dogs", }, "fluffy": Object { "id": 1, "schema": "cats", }, "jim": Object { "id": 2, "type": "lizard", }, }, } `; exports[`ValuesSchema normalization filters out null and undefined values 1`] = ` Object { "entities": Object { "cats": Object { "1": Object { "id": 1, "type": "cats", }, }, }, "result": Object { "fluffy": Object { "id": 1, "schema": "cats", }, }, } `; exports[`ValuesSchema normalization normalizes the values of an object with the given schema 1`] = ` Object { "entities": Object { "cats": Object { "1": Object { "id": 1, "type": "cats", }, }, "dogs": Object { "1": Object { "id": 1, "type": "dogs", }, }, }, "result": Object { "fido": Object { "id": 1, "schema": "dogs", }, "fluffy": Object { "id": 1, "schema": "cats", }, }, } `; ================================================ FILE: typescript-tests/array.ts ================================================ import { denormalize, normalize, schema } from '../index' const data = [{ id: '123', name: 'Jim' }, { id: '456', name: 'Jane' }]; const userSchema = new schema.Entity('users'); const userListSchema = new schema.Array(userSchema); const normalizedData = normalize(data, userListSchema); const userListSchemaAlt = [userSchema]; const normalizedDataAlt = normalize(data, userListSchemaAlt); const denormalizedData = denormalize(normalizedData.result, userListSchema, normalizedData.entities); ================================================ FILE: typescript-tests/array_schema.ts ================================================ import { denormalize, normalize, schema } from '../index' const data = [{ id: 1, type: 'admin' }, { id: 2, type: 'user' }]; const userSchema = new schema.Entity('users'); const adminSchema = new schema.Entity('admins'); const myArray = new schema.Array( { admins: adminSchema, users: userSchema }, (input, parent, key) => `${input.type}s` ); const normalizedData = normalize(data, myArray); const denormalizedData = denormalize(normalizedData.result, myArray, normalizedData.entities); ================================================ FILE: typescript-tests/entity.ts ================================================ import { denormalize, normalize, schema } from '../index' type User = { id_str: string; name: string; }; type Tweet = { id_str: string; url: string; user: User; }; const data = { /* ...*/ }; const user = new schema.Entity( 'users', {}, { idAttribute: 'id_str', fallbackStrategy: (key) => ({ id_str: key, name: 'Unknown' }) } ); const tweet = new schema.Entity( 'tweets', { user: user }, { idAttribute: 'id_str', // Apply everything from entityB over entityA, except for "favorites" mergeStrategy: (entityA, entityB) => ({ ...entityA, ...entityB, favorites: entityA.favorites }), // Remove the URL field from the entity processStrategy: (entity: Tweet, parent, key) => { const { url, ...entityWithoutUrl } = entity; return entityWithoutUrl; } } ); const normalizedData = normalize(data, tweet); const denormalizedData = denormalize(normalizedData.result, tweet, normalizedData.entities); const isTweet = tweet.key === 'tweets'; ================================================ FILE: typescript-tests/github.ts ================================================ import { normalize, schema } from '../index' const user = new schema.Entity('users'); const label = new schema.Entity('labels'); const milestone = new schema.Entity('milestones', { creator: user }); const issue = new schema.Entity('issues', { assignee: user, assignees: [user], labels: label, milestone, user }); const pullRequest = new schema.Entity('pullRequests', { assignee: user, assignees: [user], labels: label, milestone, user }); const issueOrPullRequest = new schema.Array( { issues: issue, pullRequests: pullRequest }, (entity: any) => (entity.pull_request ? 'pullRequests' : 'issues') ); const data = { /* ...*/ }; const normalizedData = normalize(data, issueOrPullRequest); console.log(normalizedData); ================================================ FILE: typescript-tests/object.ts ================================================ import { normalize, schema } from '../index' type Response = { users: Array<{ id: string }> } const data: Response = { users: [ { id: 'foo' } ] }; const user = new schema.Entity('users'); { const responseSchema = new schema.Object({ users: new schema.Array(user) }); const normalizedData = normalize(data, responseSchema); } { const responseSchema = new schema.Object({ users: (response: Response) => new schema.Array(user) }); const normalizedData = normalize(data, responseSchema); } { const responseSchema = { users: new schema.Array(user) }; const normalizedData = normalize(data, responseSchema); } ================================================ FILE: typescript-tests/relationships.ts ================================================ import { normalize, schema } from '../index' const userProcessStrategy = (value: any, parent: any, key: string) => { switch (key) { case 'author': return { ...value, posts: [parent.id] }; case 'commenter': return { ...value, comments: [parent.id] }; default: return { ...value }; } }; const userMergeStrategy = (entityA: any, entityB: any) => { return { ...entityA, ...entityB, posts: [...(entityA.posts || []), ...(entityB.posts || [])], comments: [...(entityA.comments || []), ...(entityB.comments || [])] }; }; const user = new schema.Entity( 'users', {}, { mergeStrategy: userMergeStrategy, processStrategy: userProcessStrategy } ); const comment = new schema.Entity( 'comments', { commenter: user }, { processStrategy: (value: any, parent: any, key: string) => { return { ...value, post: parent.id }; } } ); const post = new schema.Entity('posts', { author: user, comments: [comment] }); const data = { /* ...*/ }; const normalizedData = normalize(data, post); console.log(normalizedData); ================================================ FILE: typescript-tests/union.ts ================================================ import { normalize, schema } from '../index' const data = { owner: { id: 1, type: 'user' } }; const user = new schema.Entity('users'); const group = new schema.Entity('groups'); const unionSchema = new schema.Union( { user: user, group: group }, 'type' ); const normalizedData = normalize(data, { owner: unionSchema }); ================================================ FILE: typescript-tests/values.ts ================================================ import { normalize, schema } from '../index' const data = { firstThing: { id: 1 }, secondThing: { id: 2 } }; const item = new schema.Entity('items'); const valuesSchema = new schema.Values(item); const normalizedData = normalize(data, valuesSchema);