├── .npmignore ├── src ├── index.js ├── Schema.js ├── dewormalize.js └── wormalize.js ├── .eslintrc ├── .gitignore ├── LICENSE ├── index.d.ts ├── package.json ├── test ├── wormalize.js └── dewormalize.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | src 3 | bin 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | import wormalize from './wormalize' 3 | import dewormalize from './dewormalize' 4 | 5 | export { Schema, wormalize, dewormalize } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "rackt", 3 | "rules": { 4 | "no-console": 0, 5 | "arrow-parens": 2, 6 | "react/jsx-uses-react": 1, 7 | "react/jsx-no-undef": 2, 8 | "react/wrap-multilines": 2, 9 | "indent": ["error", 2, { "SwitchCase": 0 }], 10 | "array-bracket-spacing": ["error", "never"] 11 | }, 12 | "plugins": [ 13 | "react" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/Schema.js: -------------------------------------------------------------------------------- 1 | export default class Schema { 2 | constructor(name, idProperty = 'id') { 3 | this._name = name 4 | this._idProperty = idProperty 5 | this._nestedSchemas = [] 6 | } 7 | 8 | get name() { 9 | return this._name 10 | } 11 | 12 | get idProperty() { 13 | return this._idProperty 14 | } 15 | 16 | get isPlain() { 17 | return this._nestedSchemas.length === 0 18 | } 19 | 20 | define(nestedSchemas) { 21 | Object.keys(nestedSchemas).forEach((property) => { 22 | const schema = nestedSchemas[property] 23 | this._nestedSchemas.push([property, schema]) 24 | }) 25 | } 26 | 27 | forEachNestedSchema(iter) { 28 | this._nestedSchemas.forEach(iter) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Shimo Docs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function wormalize(): any; 2 | /*! 3 | * Copyright 2018 yangjunbao . All rights reserved. 4 | * @since 2018-08-14 14:58:08 5 | */ 6 | 7 | declare module 'wormalize' { 8 | export class Schema { 9 | readonly name: string; 10 | readonly idProperty: string; 11 | readonly isPlain: boolean; 12 | 13 | constructor(name: string, idProperty?: string); 14 | 15 | define(properties: Record): void; 16 | 17 | forEachNestedSchema( 18 | iter: ( 19 | value: [string, Schema | [Schema]], 20 | index: number, 21 | context: this, 22 | ) => void, 23 | ): void; 24 | } 25 | 26 | export type TSchema = { [P: string]: TSchema } | [Schema] | Schema; 27 | 28 | export interface Entity { 29 | result: K[]; 30 | entities: Record>; 31 | } 32 | 33 | export function wormalize( 34 | input: any, 35 | schema: TSchema, 36 | ): Entity; 37 | 38 | export function dewormalize( 39 | result: K[], 40 | schema: TSchema, 41 | entities: 42 | | Record> 43 | | ((name: string, id: number | string) => any), 44 | ): any; 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wormalize", 3 | "version": "0.0.4", 4 | "description": "Normalizes nested JSON according to a schema", 5 | "main": "lib/index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "ava": "NODE_ENV=test ava", 9 | "lint": "eslint src", 10 | "test": "npm run lint && npm run ava", 11 | "build": "babel src --presets es2015-without-symbol --out-dir lib", 12 | "build-test": "npm run build && npm test", 13 | "doc": "./bin/generate_doc.sh", 14 | "prepublish": "rm -rf lib && npm run build-test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/shimohq/wormalize.git" 19 | }, 20 | "keywords": [ 21 | "normalize" 22 | ], 23 | "devDependencies": { 24 | "ava": "^0.16.0", 25 | "babel-cli": "^6.16.0", 26 | "babel-eslint": "^7.0.0", 27 | "babel-preset-es2015-without-symbol": "^6.14.5", 28 | "babel-register": "^6.16.0", 29 | "eslint": "^3.6.1", 30 | "eslint-config-rackt": "^1.1.1", 31 | "eslint-plugin-react": "^6.3.0" 32 | }, 33 | "author": "Zihua Li ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/shimohq/wormalize/issues" 37 | }, 38 | "homepage": "https://github.com/shimohq/wormalize#readme", 39 | "dependencies": { 40 | "deep-assign": "^2.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/dewormalize.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | 3 | export default function dewormalize(data, schema, entities) { 4 | if (!data) { 5 | return data 6 | } 7 | if (Array.isArray(schema)) { 8 | return dewormalizeArray(data, schema, entities) 9 | } 10 | if (schema instanceof Schema) { 11 | return dewormalizeSchema(data, schema, entities) 12 | } 13 | if (schema !== null && typeof schema === 'object') { 14 | return dewormalizeObject(data, schema, entities) 15 | } 16 | 17 | throw new Error(`Invalid schema: ${schema}`) 18 | } 19 | 20 | function dewormalizeObject(data, schema, entities) { 21 | const result = Object.assign({}, data) 22 | Object.keys(schema).forEach((key) => { 23 | result[key] = dewormalize(data[key], schema[key], entities) 24 | }) 25 | return result 26 | } 27 | 28 | function dewormalizeSchema(data, schema, entities) { 29 | const entity = getEntity(entities, schema.name, data) 30 | if (schema.isPlain || !entity) { 31 | return entity 32 | } 33 | const override = Object.assign({}, entity) 34 | schema.forEachNestedSchema(([property, nestedSchema]) => { 35 | override[property] = dewormalize(override[property], nestedSchema, entities) 36 | }) 37 | 38 | return override 39 | } 40 | 41 | function dewormalizeArray(data, [schema], entities) { 42 | return data.map((item) => dewormalize(item, schema, entities)) 43 | } 44 | 45 | function getEntity(entities, schemaName, id) { 46 | if (typeof entities === 'function') { 47 | return entities(schemaName, id) 48 | } 49 | if (entities[schemaName] && entities[schemaName][id]) { 50 | return entities[schemaName][id] 51 | } 52 | return null 53 | } 54 | -------------------------------------------------------------------------------- /src/wormalize.js: -------------------------------------------------------------------------------- 1 | import Schema from './Schema' 2 | import deepAssign from 'deep-assign' 3 | 4 | export default function wormalize(data, schema) { 5 | if (!data) { 6 | return { result: data, entities: {} } 7 | } 8 | if (Array.isArray(schema)) { 9 | return wormalizeArray(data, schema) 10 | } 11 | if (schema instanceof Schema) { 12 | return wormalizeSchema(data, schema) 13 | } 14 | if (schema !== null && typeof schema === 'object') { 15 | return wormalizeObject(data, schema) 16 | } 17 | 18 | throw new Error(`Invalid schema: ${schema}`) 19 | } 20 | 21 | function wormalizeObject(data, schema) { 22 | const result = {} 23 | const entities = {} 24 | Object.keys(schema).forEach((key) => { 25 | const nested = wormalize(data[key], schema[key]) 26 | deepAssign(entities, nested.entities) 27 | result[key] = nested.result 28 | }) 29 | 30 | Object.keys(data).forEach((key) => { 31 | if (!schema.hasOwnProperty(key)) { 32 | result[key] = data[key] 33 | } 34 | }) 35 | 36 | return { result, entities } 37 | } 38 | 39 | function wormalizeSchema(data, schema) { 40 | const id = data[schema.idProperty] 41 | if (typeof id === 'undefined') { 42 | return { result: null, entities: {} } 43 | } 44 | if (schema.isPlain) { 45 | return { result: id, entities: setEntity(data, schema.name, id) } 46 | } 47 | const override = Object.assign({}, data) 48 | const entities = {} 49 | schema.forEachNestedSchema(([property, nestedSchema]) => { 50 | if (typeof override[property] !== 'undefined') { 51 | const nested = wormalize(override[property], nestedSchema) 52 | override[property] = nested.result 53 | deepAssign(entities, nested.entities) 54 | } 55 | }) 56 | setEntity(override, schema.name, id, entities) 57 | 58 | return { result: id, entities } 59 | } 60 | 61 | function wormalizeArray(data, [schema]) { 62 | const result = [] 63 | const entities = {} 64 | data.forEach((item) => { 65 | const nested = wormalize(item, schema) 66 | result.push(nested.result) 67 | deepAssign(entities, nested.entities) 68 | }) 69 | 70 | return { result, entities } 71 | } 72 | 73 | function setEntity(data, schemaName, id, entities = {}) { 74 | if (typeof entities[schemaName] === 'undefined') { 75 | entities[schemaName] = {} 76 | } 77 | entities[schemaName][id] = data 78 | 79 | return entities 80 | } 81 | -------------------------------------------------------------------------------- /test/wormalize.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { Schema, wormalize } from '../lib' 3 | 4 | const Person = new Schema('Person') 5 | const Book = new Schema('Book') 6 | 7 | Book.define({ 8 | author: Person, 9 | readers: [Person] 10 | }) 11 | 12 | test('simple schema', (t) => { 13 | t.deepEqual( 14 | wormalize({ id: 1, name: 'Bob' } , Person), 15 | { result: 1, entities: { Person: { 1: { id: 1, name: 'Bob' } } } } 16 | ) 17 | }) 18 | 19 | test('nested schema', (t) => { 20 | t.deepEqual( 21 | wormalize({ 22 | id: 1, 23 | author: { id: 1, name: 'Bob' }, 24 | readers: [ 25 | { id: 2, name: 'Jeff' }, 26 | { id: 3, name: 'Tom' } 27 | ], 28 | } , Book), 29 | { 30 | result: 1, 31 | entities: { 32 | Person: { 33 | 1: { id: 1, name: 'Bob' }, 34 | 2: { id: 2, name: 'Jeff' }, 35 | 3: { id: 3, name: 'Tom' } 36 | }, 37 | Book: { 38 | 1: { id: 1, author: 1, readers: [2, 3] } 39 | } 40 | } 41 | } 42 | ) 43 | }) 44 | 45 | test('ignore undefined property', (t) => { 46 | t.deepEqual( 47 | wormalize({ id: 1 } , Book), 48 | { result: 1, entities: { Book: { 1: { id: 1 } } } } 49 | ) 50 | }) 51 | 52 | test('array of nested schema', (t) => { 53 | t.deepEqual( 54 | wormalize([{ 55 | id: 1, 56 | author: { id: 1, name: 'Bob' }, 57 | readers: [ { id: 2, name: 'Jeff' }, { id: 3, name: 'Tom' } ], 58 | }, { 59 | id: 2, 60 | author: { id: 2, name: 'Jeff' }, 61 | readers: [ { id: 3, name: 'Tom' } ], 62 | }], [Book]), 63 | { 64 | result: [ 1, 2 ], 65 | entities: { 66 | Person: { 67 | 1: { id: 1, name: 'Bob' }, 68 | 2: { id: 2, name: 'Jeff' }, 69 | 3: { id: 3, name: 'Tom' } 70 | }, 71 | Book: { 72 | 1: { id: 1, author: 1, readers: [2, 3] }, 73 | 2: { id: 2, author: 2, readers: [3] } 74 | } 75 | } 76 | } 77 | ) 78 | }) 79 | 80 | test('object of nested schema', (t) => { 81 | t.deepEqual( 82 | wormalize({ 83 | books: [{ 84 | id: 1, 85 | author: { id: 1, name: 'Bob' }, 86 | readers: [ { id: 2, name: 'Jeff' }, { id: 3, name: 'Tom' } ], 87 | }, { 88 | id: 2, 89 | author: { id: 2, name: 'Jeff' }, 90 | readers: [ { id: 3, name: 'Tom' } ], 91 | }], 92 | plain: 'plain' 93 | }, { books: [Book] }), 94 | { 95 | result: { books: [ 1, 2 ], plain: 'plain' }, 96 | entities: { 97 | Person: { 98 | 1: { id: 1, name: 'Bob' }, 99 | 2: { id: 2, name: 'Jeff' }, 100 | 3: { id: 3, name: 'Tom' } 101 | }, 102 | Book: { 103 | 1: { id: 1, author: 1, readers: [2, 3] }, 104 | 2: { id: 2, author: 2, readers: [3] } 105 | } 106 | } 107 | } 108 | ) 109 | }) 110 | -------------------------------------------------------------------------------- /test/dewormalize.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { Schema, dewormalize } from '../lib' 3 | 4 | const Person = new Schema('Person') 5 | const Book = new Schema('Book') 6 | 7 | Book.define({ 8 | author: Person, 9 | readers: [Person] 10 | }) 11 | 12 | test('simple schema', (t) => { 13 | t.deepEqual( 14 | dewormalize(1 , Person, { Person: { 1: { id: 1, name: 'Bob' } } }), 15 | { id: 1, name: 'Bob' } 16 | ) 17 | }) 18 | 19 | test('nested schema', (t) => { 20 | t.deepEqual( 21 | dewormalize(1, Book, { 22 | Person: { 23 | 1: { id: 1, name: 'Bob' }, 24 | 2: { id: 2, name: 'Jeff' }, 25 | 3: { id: 3, name: 'Tom' } 26 | }, 27 | Book: { 28 | 1: { id: 1, author: 1, readers: [2, 3] } 29 | } 30 | }), 31 | { 32 | id: 1, 33 | author: { id: 1, name: 'Bob' }, 34 | readers: [ 35 | { id: 2, name: 'Jeff' }, 36 | { id: 3, name: 'Tom' } 37 | ], 38 | } 39 | ) 40 | }) 41 | 42 | test('array of nested schema', (t) => { 43 | t.deepEqual( 44 | dewormalize([1, 2], [Book], { 45 | Person: { 46 | 1: { id: 1, name: 'Bob' }, 47 | 2: { id: 2, name: 'Jeff' }, 48 | 3: { id: 3, name: 'Tom' } 49 | }, 50 | Book: { 51 | 1: { id: 1, author: 1, readers: [2, 3] }, 52 | 2: { id: 2, author: 2, readers: [3] } 53 | } 54 | }), 55 | [{ 56 | id: 1, 57 | author: { id: 1, name: 'Bob' }, 58 | readers: [ { id: 2, name: 'Jeff' }, { id: 3, name: 'Tom' } ], 59 | }, { 60 | id: 2, 61 | author: { id: 2, name: 'Jeff' }, 62 | readers: [ { id: 3, name: 'Tom' } ], 63 | }] 64 | ) 65 | }) 66 | 67 | test('object of nested schema', (t) => { 68 | t.deepEqual( 69 | dewormalize({ books: [ 1, 2 ], plain: 'plain' }, { books: [Book] }, { 70 | Person: { 71 | 1: { id: 1, name: 'Bob' }, 72 | 2: { id: 2, name: 'Jeff' }, 73 | 3: { id: 3, name: 'Tom' } 74 | }, 75 | Book: { 76 | 1: { id: 1, author: 1, readers: [2, 3] }, 77 | 2: { id: 2, author: 2, readers: [3] } 78 | } 79 | }), 80 | { 81 | books: [{ 82 | id: 1, 83 | author: { id: 1, name: 'Bob' }, 84 | readers: [ { id: 2, name: 'Jeff' }, { id: 3, name: 'Tom' } ], 85 | }, { 86 | id: 2, 87 | author: { id: 2, name: 'Jeff' }, 88 | readers: [ { id: 3, name: 'Tom' } ], 89 | }], 90 | plain: 'plain' 91 | } 92 | ) 93 | }) 94 | 95 | test('fill with null when the entity is absent', (t) => { 96 | t.deepEqual( 97 | dewormalize({ books: [ 1, 2 ] }, { books: [Book] }, { 98 | Person: { 99 | 1: { id: 1, name: 'Bob' }, 100 | 2: { id: 2, name: 'Jeff' }, 101 | 3: { id: 3, name: 'Tom' } 102 | }, 103 | Book: { 104 | 1: { id: 1, author: 1, readers: [2, 3] } 105 | } 106 | }), 107 | { 108 | books: [{ 109 | id: 1, 110 | author: { id: 1, name: 'Bob' }, 111 | readers: [ { id: 2, name: 'Jeff' }, { id: 3, name: 'Tom' } ], 112 | }, null] 113 | } 114 | ) 115 | }) 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Wormalize

4 |

Normalizes nested JSON according to a schema

5 |
6 | 7 | ## Install 8 | 9 | ```shell 10 | $ npm install wormalize 11 | ``` 12 | 13 | ## Usage 14 | 15 | Using `Schema` to defines schemas that responding to your model definitions. All the following 16 | operations are based on these schemas. 17 | 18 | ```javascript 19 | import { Schema } from 'wormalize' 20 | 21 | const Person = new Schema('Person') 22 | const Book = new Schema('Book') 23 | 24 | Book.define({ 25 | author: Person, 26 | readers: [Person] 27 | }) 28 | ``` 29 | 30 | Given the following API response consisting of a list of the Book entity (which has already 31 | been defined above), the User entity is nested in the `author` and `readers` properties, 32 | which makes it non-trivial to resolve them into your Redux state. 33 | 34 | ```javascript 35 | { 36 | id: 1, 37 | author: { id: 1, name: 'Bob' }, 38 | readers: [ 39 | { id: 2, name: 'Jeff' }, 40 | { id: 3, name: 'Tom' } 41 | ], 42 | }, { 43 | id: 2, 44 | author: { id: 2, name: 'Jeff' }, 45 | readers: [ 46 | { id: 3, name: 'Tom' } 47 | ], 48 | } 49 | ``` 50 | 51 | `wormalize` comes to rescue in this case. By providing a schema corresponding to the structure 52 | of data, `wormalize` is able to resolve them to the `result` and `entities` properties: 53 | 54 | ```javascript 55 | import { wormalize } from 'wormalize' 56 | 57 | wormalize([{ 58 | id: 1, 59 | author: { id: 1, name: 'Bob' }, 60 | readers: [ 61 | { id: 2, name: 'Jeff' }, 62 | { id: 3, name: 'Tom' } 63 | ], 64 | }, { 65 | id: 2, 66 | author: { id: 2, name: 'Jeff' }, 67 | readers: [ 68 | { id: 3, name: 'Tom' } 69 | ], 70 | }], [Book]) 71 | ``` 72 | 73 | The code above returns: 74 | 75 | ```javascript 76 | { 77 | result: [1, 2], 78 | entities: { 79 | Person: { 80 | 1: { id: 1, name: 'Bob' }, 81 | 2: { id: 2, name: 'Jeff' }, 82 | 3: { id: 3, name: 'Tom' } 83 | }, 84 | Book: { 85 | 1: { id: 1, author: 1, readers: [2, 3] }, 86 | 2: { id: 2, author: 2, readers: [3] } 87 | } 88 | } 89 | } 90 | ``` 91 | 92 | Correspondingly, `dewormalize` is provided to do the opposite: 93 | 94 | ```javascript 95 | import { dewormalize } from 'wormalize' 96 | 97 | dewormalize([1, 2], [Book], { 98 | Person: { 99 | 1: { id: 1, name: 'Bob' }, 100 | 2: { id: 2, name: 'Jeff' }, 101 | 3: { id: 3, name: 'Tom' } 102 | }, 103 | Book: { 104 | 1: { id: 1, author: 1, readers: [2, 3] }, 105 | 2: { id: 2, author: 2, readers: [3] } 106 | } 107 | }) 108 | ``` 109 | 110 | The code above returns: 111 | 112 | ```javascript 113 | [{ 114 | id: 1, 115 | author: { id: 1, name: 'Bob' }, 116 | readers: [ 117 | { id: 2, name: 'Jeff' }, 118 | { id: 3, name: 'Tom' } 119 | ], 120 | }, { 121 | id: 2, 122 | author: { id: 2, name: 'Jeff' }, 123 | readers: [ 124 | { id: 3, name: 'Tom' } 125 | ], 126 | } 127 | ``` 128 | 129 | The third argument of `dewormalize` can also be a function, which will be called with 130 | two arguments `schemaName` and `id` when resolving each data. 131 | 132 | --- 133 | 134 |
Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY
135 | --------------------------------------------------------------------------------