├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_0_2.md ├── package-lock.json ├── package.json ├── src ├── Jsona.ts ├── JsonaTypes.ts ├── builders │ ├── JsonDeserializer.ts │ ├── ModelsSerializer.ts │ └── ReduxObjectDenormalizer.ts ├── cache.ts ├── index.ts ├── simplePropertyMappers.ts ├── switchCasePropertyMappers.ts └── utils.ts ├── tests ├── Jsona.test.ts ├── ModelsSerializer.test.ts ├── ReduxObjectDenormalizer.test.ts ├── mocks.ts └── switchCasePropertyMappers.test.ts ├── tsconfig.json └── tsconfig.test.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | npm-debug.log 4 | .idea 5 | typings -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .gitignore 3 | .idea/ 4 | node_modules 5 | npm-debug.log 6 | tests/ 7 | src 8 | typings -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "node" 5 | install: 6 | - npm install 7 | script: 8 | - npm run test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present Sergei Solo 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jsona 2 | JSON API [v1.0 specification](http://jsonapi.org/format/1.0/) serializer and deserializer for use on the server and in the browser. 3 | * From JSON to simplified objects 4 | * Back from simplified objects to JSON (in according with [json:api specification](http://jsonapi.org/format/1.0/)) 5 | * Also from "reduxObject" to simplified objects (`reduxObject` is a result object of [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer)) 6 | 7 | [![NPM](https://img.shields.io/npm/v/jsona.svg)](https://www.npmjs.com/package/jsona/) 8 | [![dependencies](https://img.shields.io/static/v1?label=dependencies&message=none&color=success)](https://www.npmjs.com/package/jsona/) 9 | [![downloads](https://img.shields.io/npm/dm/jsona.svg)](https://www.npmjs.com/package/jsona/) 10 | 11 | #### What problem does it solve? 12 | When you work with API standardized to [json:api specification](http://jsonapi.org/format/1.0/), you're dealing with a special and optimized JSON data format in the request and response body. 13 | You can get data of several entities that are related to each other, but you'll receive it in array (included). 14 | You may need to send modified back to server (or new data) in accordance with specification. 15 | 16 | This may puzzle you with the following questions: 17 | 18 | * How to get necessary entity from `included` array many times more inconvenient and optimal? 19 | * How to describe data from server, working with typings (TypeScript, Flow)? 20 | * How to send JSON to the server without manually assembling JSON in accordance with specification? 21 | 22 | ### Installation 23 | 24 | ``` 25 | npm i jsona --save 26 | ``` 27 | or 28 | 29 | ``` 30 | yarn add jsona 31 | ``` 32 | 33 | 34 | ### How to use 35 | 36 | You need to instantiate Jsona once, then use its public methods to convert data. 37 | ```javascript 38 | import Jsona from 'jsona'; 39 | const dataFormatter = new Jsona(); 40 | ``` 41 | 42 | #### deserialize - creates simplified object(s) from json 43 | ```javascript 44 | const json = { 45 | data: { 46 | type: 'town', 47 | id: '123', 48 | attributes: { 49 | name: 'Barcelona' 50 | }, 51 | relationships: { 52 | country: { 53 | data: { 54 | type: 'country', 55 | id: '32' 56 | } 57 | } 58 | } 59 | }, 60 | included: [{ 61 | type: 'country', 62 | id: '32', 63 | attributes: { 64 | name: 'Spain' 65 | } 66 | }] 67 | }; 68 | 69 | const town = dataFormatter.deserialize(json); 70 | console.log(town); // will output: 71 | /* { 72 | type: 'town', 73 | id: '123', 74 | name: 'Barcelona', 75 | country: { 76 | type: 'country', 77 | id: '32', 78 | name: 'Spain' 79 | }, 80 | relationshipNames: ['country'] 81 | } */ 82 | ``` 83 | 84 | #### serialize - creates json from simplified object(s) 85 | ```javascript 86 | const user = { 87 | type: 'user', 88 | id: 1, 89 | categories: [{ type: 'category', id: '1', name: 'First category' }], 90 | town: { 91 | type: 'town', 92 | id: '123', 93 | name: 'Barcelona', 94 | country: { 95 | type: 'country', 96 | id: '32', 97 | name: 'Spain' 98 | }, 99 | relationshipNames: ['country'] 100 | }, 101 | relationshipNames: ['categories', 'town'] 102 | }; 103 | 104 | const newJson = dataFormatter.serialize({ 105 | stuff: user, // can handle array 106 | includeNames: ['categories', 'town.country'] // can include deep relations via dot 107 | }); 108 | 109 | console.log(newJson); // will output: 110 | /* { 111 | data: { 112 | type: 'user', 113 | id: 1, 114 | relationships: { 115 | categories: { 116 | data: [{ type: 'category', id: '1' }] 117 | }, 118 | town: { 119 | data: { type: 'town', id: '123' } 120 | } 121 | } 122 | }, 123 | included: [{ 124 | type: 'category', 125 | id: '1', 126 | attributes: { 127 | name: 'First category', 128 | } 129 | }, { 130 | type: 'town', 131 | id: '123', 132 | attributes: { 133 | name: 'Barcelona', 134 | }, 135 | relationships: { 136 | country: { 137 | data: { 138 | type: 'country', 139 | id: '32', 140 | } 141 | } 142 | } 143 | }, { 144 | type: 'country', 145 | id: '32', 146 | attributes: { 147 | name: 'Spain', 148 | } 149 | }] 150 | }*/ 151 | ``` 152 | 153 | #### denormalizeReduxObject - creates simplified object(s) from reduxObject 154 | "reduxObject" - result object of [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer) 155 | 156 | ```javascript 157 | const reduxObject = reduxStore.entities; // depends on where you store it 158 | const town = dataFormatter.denormalizeReduxObject({reduxObject, entityType: 'town', entityIds: '123'}); 159 | console.log(town); // if there is such town and country in reduxObject, it will output: 160 | /* { 161 | type: 'town', 162 | id: '123', 163 | name: 'Barcelona', 164 | country: { 165 | type: 'country', 166 | id: '34', 167 | name: 'Spain' 168 | }, 169 | relationshipNames: ['country'] 170 | } */ 171 | ``` 172 | 173 | ### Customize 174 | 175 | #### Build process and property names 176 | You can control process of building simplified objects, just use your own [propertyMappers](src/simplePropertyMappers.ts) when Jsona instantiates. 177 | 178 | With [IJsonPropertiesMapper](src/JsonaTypes.ts) you can implement your way of creation simplified objects (data models) from JSON, with [IModelPropertiesMapper](src/JsonaTypes.ts) implement how to give back values from data model to JSON. 179 | 180 | It gives unlimited possibilities to integrate Jsona with react, redux, angular2 181 | 182 | Example of passing your own [propertyMappers](src/simplePropertyMappers.ts) to Jsona: 183 | ```javascript 184 | import Jsona from 'jsona'; 185 | import {MyModelPropertiesMapper, MyJsonPropertiesMapper} from 'myPropertyMappers'; 186 | 187 | export const dataFormatter = new Jsona({ 188 | modelPropertiesMapper: MyModelPropertiesMapper, 189 | jsonPropertiesMapper: MyJsonPropertiesMapper 190 | }); 191 | ``` 192 | Also, there is built-in [switchCasePropertyMappers](src/switchCasePropertyMappers.ts), that you can use if need to automatically transform property names from kebab, snake, camel case and back. 193 | 194 | #### Cache 195 | For faster creation of simplified objects from json, it uses a cache for already processed json-entity, see [DeserializeCache](src/cache.ts) that uses by default. 196 | It possible to provide your own [IDeserializeCache](src/JsonaTypes.ts) manager: 197 | ```javascript 198 | import Jsona from 'jsona'; 199 | import {MyOwnDeserializeCache} from './index'; 200 | 201 | export const dataFormatter = new Jsona({ 202 | DeserializeCache: MyOwnDeserializeCache 203 | }); 204 | ``` 205 | 206 | ### License 207 | Jsona, examples provided in this repository and in the documentation are [MIT licensed](./LICENSE). 208 | -------------------------------------------------------------------------------- /README_0_2.md: -------------------------------------------------------------------------------- 1 | # Jsona 2 | Framework agnostic library that provide systemized way to work with JSON API [v1.0 spetification](http://jsonapi.org/format/1.0/) in your JavaScript / TypeScript code. 3 | 4 | [![NPM](https://nodei.co/npm/jsona.png?compact=true)](https://www.npmjs.com/package/jsona/) 5 | 6 | ### Why you may need it? 7 | 8 | If you use or build API, that specified by *json:api* and: 9 | - want to use more comfortable interface, than plain json:api object gives (such as array in `included`) to work with data in code; 10 | - dont't want to think how to build correct json in accordance with standard for POST/PUT/PATH requests; 11 | - like to have deal with typed data and you want to map API's entities to objects, that instantiated with unique constructors (classes); 12 | 13 | ### What it gives? 14 | 15 | Ability to automatically convert request body (json, formatted in accordance with specification *json:api*) to instances of predefined by you classes and back to correct json, easy. 16 | 17 | **Simple example** 18 | ```javascript 19 | import {Jsona} from 'jsona'; 20 | 21 | // it suppose that we already defined special classes, such as data models 22 | // and EntitiesFactory, that will help Jsona to map json:api entities to our data models and back 23 | import MyEntitiesFactory from './MyEntitiesFactory'; 24 | import TestEntity1 from './entities/TestEntity1'; 25 | import TestEntity2 from './entities/TestEntity2'; 26 | 27 | const testJson = { 28 | "data": { 29 | "id": 123, 30 | "type": "testentity1", 31 | "attributes": { 32 | "foo1": "bar1" 33 | }, 34 | "relationships": { 35 | "testrelation": { 36 | "data": { 37 | "id": 321, 38 | "type": "testentity2" 39 | } 40 | } 41 | } 42 | }, 43 | "included": [{ 44 | "id": 321, 45 | "type": "testentity2", 46 | "attributes": { 47 | "foo2": "bar2" 48 | } 49 | }] 50 | }; 51 | 52 | const dataFormatter = new Jsona(new MyEntitiesFactory()); 53 | 54 | // testJson may be stringified json or plain object 55 | const deserialized = dataFormatter.deserialize(testJson); 56 | 57 | console.log(deserialized); // will output something similar to: 58 | // { 59 | // hasCollection: false 60 | // hasItem: true 61 | // item: TestEntity1 { 62 | // foo1: "bar1" 63 | // id: 123 64 | // type: "testentity1" 65 | // testrelation: TestEntity2 { 66 | // foo2: "bar2" 67 | // id: 321 68 | // type: "testentity2" 69 | // } 70 | // } 71 | // collection: null 72 | // } 73 | 74 | ``` 75 | 76 | ### Examples of helpers and models that you may change and use in your project 77 | Library written in TypeScript, but examples below uses ES6-7 and partly Flow. 78 | 79 | **MyEntitiesFactory.js** 80 | ```javascript 81 | import Town from './entities/Town'; 82 | import Country from './entities/Country'; 83 | import Region from './entities/Region'; 84 | 85 | class MyEntitiesFactory { 86 | 87 | constructor() { 88 | this.map = { 89 | 'town': Town, 90 | 'country': Country, 91 | 'region': Region, 92 | }; 93 | } 94 | 95 | getClass(entityType) { 96 | return this.map[entityType]; 97 | } 98 | 99 | getModel(entityType) { 100 | var mappedClass = this.map[entityType]; 101 | 102 | if (!mappedClass) { 103 | throw new Error(`MyEntitiesFactory dont know about entity with type [${entityType}]`); 104 | } 105 | 106 | return new mappedClass(); 107 | } 108 | } 109 | 110 | export default MyEntitiesFactory; 111 | ``` 112 | 113 | **jsonaHelpers.js** 114 | ```javascript 115 | import {Jsona} from 'jsona'; 116 | import MyEntitiesFactory from './MyEntitiesFactory'; 117 | 118 | /** 119 | * @var {MyEntitiesFactory} entitiesFactory - factory, that will using in Jsona for instantiate entities for each defined type 120 | */ 121 | const entitiesFactory = new MyEntitiesFactory(); 122 | const dataFormatter = new Jsona(entitiesFactory); 123 | 124 | export function fromJsonToItem(jsonApiBody) { 125 | const deserialized = dataFormatter.deserialize(jsonApiBody); 126 | 127 | if (deserialized.hasItem) { 128 | return { 129 | item: deserialized.item, 130 | meta: deserialized.meta, 131 | }; 132 | } 133 | 134 | console.error('fromJsonToItem cant deserialize object', jsonApiBody); 135 | return {}; 136 | } 137 | 138 | export function fromJsonToCollection(jsonApiBody) { 139 | const deserialized = dataFormatter.deserialize(jsonApiBody); 140 | 141 | if (deserialized.hasCollection) { 142 | return { 143 | collection: deserialized.collection, 144 | meta: deserialized.meta, 145 | }; 146 | } 147 | 148 | console.error('fromJsonToCollection cant deserialize object', jsonApiBody); 149 | return {}; 150 | } 151 | 152 | export function fromCollectionToJson(collection, requestedIncludes, withAllIncludes = false) { 153 | return dataFormatter.serialize({collection, requestedIncludes, withAllIncludes}); 154 | } 155 | 156 | export function fromItemToJson(item, requestedIncludes, withAllIncludes = false) { 157 | return dataFormatter.serialize({item, requestedIncludes, withAllIncludes}); 158 | } 159 | 160 | export function fromJsonToItemOrCollection(jsonApiBody) { 161 | const deserialized = dataFormatter.deserialize(jsonApiBody); 162 | 163 | return { 164 | item: deserialized.item || null, 165 | collection: deserialized.collection || null, 166 | meta: deserialized.meta || null, 167 | }; 168 | } 169 | 170 | export default { 171 | fromJsonToCollection, 172 | fromJsonToItem, 173 | fromCollectionToJson, 174 | fromItemToJson, 175 | fromJsonToItemOrCollection, 176 | }; 177 | ``` 178 | 179 | **Town.js** (example of model) 180 | ```javascript 181 | import MyBaseEntity from './MyBaseEntity'; 182 | import Region from './Region'; 183 | import Country from './Country'; 184 | 185 | class Town extends MyBaseEntity { 186 | id: string; 187 | type: string; 188 | 189 | name: string; 190 | nameGenitive: string; 191 | timeZone: string; 192 | 193 | region: Region; 194 | country: Country; 195 | 196 | getRelationships() { 197 | return { 198 | region: this.region, 199 | country: this.country 200 | } 201 | } 202 | } 203 | 204 | export default Town; 205 | ``` 206 | 207 | **MyBaseEntity.js** 208 | ```javascript 209 | import {BaseJsonaModel} from 'jsona'; 210 | 211 | class MyBaseEntity extends BaseJsonaModel { 212 | constructor(props) { 213 | super(); 214 | 215 | Object.keys(params).forEach((k) => { 216 | this[k] = params[k]; 217 | }); 218 | } 219 | } 220 | 221 | export default MyBaseEntity; 222 | ``` 223 | 224 | ### License 225 | Jsona, examples provided in this repository and in the documentation are [MIT licensed](./LICENSE). -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsona", 3 | "version": "1.12.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "jsona", 9 | "version": "1.12.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.4.1" 13 | }, 14 | "devDependencies": { 15 | "@types/chai": "^4.2.4", 16 | "@types/mocha": "^5.2.7", 17 | "@types/node": "^12.11.5", 18 | "chai": "^4.3.4", 19 | "mocha": "^10.1.0", 20 | "ts-mocha": "^8.0.0", 21 | "typescript": "^4.9.3" 22 | } 23 | }, 24 | "node_modules/@types/chai": { 25 | "version": "4.2.4", 26 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.4.tgz", 27 | "integrity": "sha1-iTbP+tPJbsRwotwmo4w7qLm29hk=", 28 | "dev": true 29 | }, 30 | "node_modules/@types/json5": { 31 | "version": "0.0.29", 32 | "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", 33 | "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", 34 | "dev": true, 35 | "optional": true 36 | }, 37 | "node_modules/@types/mocha": { 38 | "version": "5.2.7", 39 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", 40 | "integrity": "sha1-MV1XDMtWxTRS/4Y4c432BybVtuo=", 41 | "dev": true 42 | }, 43 | "node_modules/@types/node": { 44 | "version": "12.11.5", 45 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.5.tgz", 46 | "integrity": "sha1-bDyNyEmIr/Ef0qY9e1+/Oeqqt7E=", 47 | "dev": true 48 | }, 49 | "node_modules/ansi-colors": { 50 | "version": "4.1.1", 51 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 52 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 53 | "dev": true, 54 | "engines": { 55 | "node": ">=6" 56 | } 57 | }, 58 | "node_modules/ansi-regex": { 59 | "version": "5.0.1", 60 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 61 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 62 | "dev": true, 63 | "engines": { 64 | "node": ">=8" 65 | } 66 | }, 67 | "node_modules/ansi-styles": { 68 | "version": "4.3.0", 69 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 70 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 71 | "dev": true, 72 | "dependencies": { 73 | "color-convert": "^2.0.1" 74 | }, 75 | "engines": { 76 | "node": ">=8" 77 | }, 78 | "funding": { 79 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 80 | } 81 | }, 82 | "node_modules/anymatch": { 83 | "version": "3.1.2", 84 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 85 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 86 | "dev": true, 87 | "dependencies": { 88 | "normalize-path": "^3.0.0", 89 | "picomatch": "^2.0.4" 90 | }, 91 | "engines": { 92 | "node": ">= 8" 93 | } 94 | }, 95 | "node_modules/argparse": { 96 | "version": "2.0.1", 97 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 98 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 99 | "dev": true 100 | }, 101 | "node_modules/arrify": { 102 | "version": "1.0.1", 103 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 104 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 105 | "dev": true, 106 | "engines": { 107 | "node": ">=0.10.0" 108 | } 109 | }, 110 | "node_modules/assertion-error": { 111 | "version": "1.1.0", 112 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 113 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 114 | "dev": true, 115 | "engines": { 116 | "node": "*" 117 | } 118 | }, 119 | "node_modules/balanced-match": { 120 | "version": "1.0.2", 121 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 122 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 123 | "dev": true 124 | }, 125 | "node_modules/binary-extensions": { 126 | "version": "2.2.0", 127 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 128 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 129 | "dev": true, 130 | "engines": { 131 | "node": ">=8" 132 | } 133 | }, 134 | "node_modules/brace-expansion": { 135 | "version": "1.1.11", 136 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 137 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 138 | "dev": true, 139 | "dependencies": { 140 | "balanced-match": "^1.0.0", 141 | "concat-map": "0.0.1" 142 | } 143 | }, 144 | "node_modules/braces": { 145 | "version": "3.0.3", 146 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 147 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 148 | "dev": true, 149 | "dependencies": { 150 | "fill-range": "^7.1.1" 151 | }, 152 | "engines": { 153 | "node": ">=8" 154 | } 155 | }, 156 | "node_modules/browser-stdout": { 157 | "version": "1.3.1", 158 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 159 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 160 | "dev": true 161 | }, 162 | "node_modules/buffer-from": { 163 | "version": "1.1.1", 164 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 165 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 166 | "dev": true 167 | }, 168 | "node_modules/camelcase": { 169 | "version": "6.3.0", 170 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 171 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 172 | "dev": true, 173 | "engines": { 174 | "node": ">=10" 175 | }, 176 | "funding": { 177 | "url": "https://github.com/sponsors/sindresorhus" 178 | } 179 | }, 180 | "node_modules/chai": { 181 | "version": "4.3.4", 182 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 183 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 184 | "dev": true, 185 | "dependencies": { 186 | "assertion-error": "^1.1.0", 187 | "check-error": "^1.0.2", 188 | "deep-eql": "^3.0.1", 189 | "get-func-name": "^2.0.0", 190 | "pathval": "^1.1.1", 191 | "type-detect": "^4.0.5" 192 | }, 193 | "engines": { 194 | "node": ">=4" 195 | } 196 | }, 197 | "node_modules/chalk": { 198 | "version": "4.1.2", 199 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 200 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 201 | "dev": true, 202 | "dependencies": { 203 | "ansi-styles": "^4.1.0", 204 | "supports-color": "^7.1.0" 205 | }, 206 | "engines": { 207 | "node": ">=10" 208 | }, 209 | "funding": { 210 | "url": "https://github.com/chalk/chalk?sponsor=1" 211 | } 212 | }, 213 | "node_modules/chalk/node_modules/supports-color": { 214 | "version": "7.2.0", 215 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 216 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 217 | "dev": true, 218 | "dependencies": { 219 | "has-flag": "^4.0.0" 220 | }, 221 | "engines": { 222 | "node": ">=8" 223 | } 224 | }, 225 | "node_modules/check-error": { 226 | "version": "1.0.2", 227 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 228 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 229 | "dev": true, 230 | "engines": { 231 | "node": "*" 232 | } 233 | }, 234 | "node_modules/chokidar": { 235 | "version": "3.5.3", 236 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 237 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 238 | "dev": true, 239 | "funding": [ 240 | { 241 | "type": "individual", 242 | "url": "https://paulmillr.com/funding/" 243 | } 244 | ], 245 | "dependencies": { 246 | "anymatch": "~3.1.2", 247 | "braces": "~3.0.2", 248 | "glob-parent": "~5.1.2", 249 | "is-binary-path": "~2.1.0", 250 | "is-glob": "~4.0.1", 251 | "normalize-path": "~3.0.0", 252 | "readdirp": "~3.6.0" 253 | }, 254 | "engines": { 255 | "node": ">= 8.10.0" 256 | }, 257 | "optionalDependencies": { 258 | "fsevents": "~2.3.2" 259 | } 260 | }, 261 | "node_modules/cliui": { 262 | "version": "7.0.4", 263 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 264 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 265 | "dev": true, 266 | "dependencies": { 267 | "string-width": "^4.2.0", 268 | "strip-ansi": "^6.0.0", 269 | "wrap-ansi": "^7.0.0" 270 | } 271 | }, 272 | "node_modules/color-convert": { 273 | "version": "2.0.1", 274 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 275 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 276 | "dev": true, 277 | "dependencies": { 278 | "color-name": "~1.1.4" 279 | }, 280 | "engines": { 281 | "node": ">=7.0.0" 282 | } 283 | }, 284 | "node_modules/color-name": { 285 | "version": "1.1.4", 286 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 287 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 288 | "dev": true 289 | }, 290 | "node_modules/concat-map": { 291 | "version": "0.0.1", 292 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 293 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 294 | "dev": true 295 | }, 296 | "node_modules/debug": { 297 | "version": "4.3.4", 298 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 299 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 300 | "dev": true, 301 | "dependencies": { 302 | "ms": "2.1.2" 303 | }, 304 | "engines": { 305 | "node": ">=6.0" 306 | }, 307 | "peerDependenciesMeta": { 308 | "supports-color": { 309 | "optional": true 310 | } 311 | } 312 | }, 313 | "node_modules/debug/node_modules/ms": { 314 | "version": "2.1.2", 315 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 316 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 317 | "dev": true 318 | }, 319 | "node_modules/decamelize": { 320 | "version": "4.0.0", 321 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 322 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 323 | "dev": true, 324 | "engines": { 325 | "node": ">=10" 326 | }, 327 | "funding": { 328 | "url": "https://github.com/sponsors/sindresorhus" 329 | } 330 | }, 331 | "node_modules/deep-eql": { 332 | "version": "3.0.1", 333 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 334 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 335 | "dev": true, 336 | "dependencies": { 337 | "type-detect": "^4.0.0" 338 | }, 339 | "engines": { 340 | "node": ">=0.12" 341 | } 342 | }, 343 | "node_modules/diff": { 344 | "version": "3.5.0", 345 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 346 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 347 | "dev": true, 348 | "engines": { 349 | "node": ">=0.3.1" 350 | } 351 | }, 352 | "node_modules/emoji-regex": { 353 | "version": "8.0.0", 354 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 355 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 356 | "dev": true 357 | }, 358 | "node_modules/escalade": { 359 | "version": "3.1.1", 360 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 361 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 362 | "dev": true, 363 | "engines": { 364 | "node": ">=6" 365 | } 366 | }, 367 | "node_modules/escape-string-regexp": { 368 | "version": "4.0.0", 369 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 370 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 371 | "dev": true, 372 | "engines": { 373 | "node": ">=10" 374 | }, 375 | "funding": { 376 | "url": "https://github.com/sponsors/sindresorhus" 377 | } 378 | }, 379 | "node_modules/fill-range": { 380 | "version": "7.1.1", 381 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 382 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 383 | "dev": true, 384 | "dependencies": { 385 | "to-regex-range": "^5.0.1" 386 | }, 387 | "engines": { 388 | "node": ">=8" 389 | } 390 | }, 391 | "node_modules/find-up": { 392 | "version": "5.0.0", 393 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 394 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 395 | "dev": true, 396 | "dependencies": { 397 | "locate-path": "^6.0.0", 398 | "path-exists": "^4.0.0" 399 | }, 400 | "engines": { 401 | "node": ">=10" 402 | }, 403 | "funding": { 404 | "url": "https://github.com/sponsors/sindresorhus" 405 | } 406 | }, 407 | "node_modules/flat": { 408 | "version": "5.0.2", 409 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 410 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 411 | "dev": true, 412 | "bin": { 413 | "flat": "cli.js" 414 | } 415 | }, 416 | "node_modules/fs.realpath": { 417 | "version": "1.0.0", 418 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 419 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 420 | "dev": true 421 | }, 422 | "node_modules/fsevents": { 423 | "version": "2.3.2", 424 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 425 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 426 | "dev": true, 427 | "hasInstallScript": true, 428 | "optional": true, 429 | "os": [ 430 | "darwin" 431 | ], 432 | "engines": { 433 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 434 | } 435 | }, 436 | "node_modules/get-caller-file": { 437 | "version": "2.0.5", 438 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 439 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 440 | "dev": true, 441 | "engines": { 442 | "node": "6.* || 8.* || >= 10.*" 443 | } 444 | }, 445 | "node_modules/get-func-name": { 446 | "version": "2.0.0", 447 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 448 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 449 | "dev": true, 450 | "engines": { 451 | "node": "*" 452 | } 453 | }, 454 | "node_modules/glob": { 455 | "version": "7.2.0", 456 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 457 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 458 | "dev": true, 459 | "dependencies": { 460 | "fs.realpath": "^1.0.0", 461 | "inflight": "^1.0.4", 462 | "inherits": "2", 463 | "minimatch": "^3.0.4", 464 | "once": "^1.3.0", 465 | "path-is-absolute": "^1.0.0" 466 | }, 467 | "engines": { 468 | "node": "*" 469 | }, 470 | "funding": { 471 | "url": "https://github.com/sponsors/isaacs" 472 | } 473 | }, 474 | "node_modules/glob-parent": { 475 | "version": "5.1.2", 476 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 477 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 478 | "dev": true, 479 | "dependencies": { 480 | "is-glob": "^4.0.1" 481 | }, 482 | "engines": { 483 | "node": ">= 6" 484 | } 485 | }, 486 | "node_modules/glob/node_modules/minimatch": { 487 | "version": "3.1.2", 488 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 489 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 490 | "dev": true, 491 | "dependencies": { 492 | "brace-expansion": "^1.1.7" 493 | }, 494 | "engines": { 495 | "node": "*" 496 | } 497 | }, 498 | "node_modules/has-flag": { 499 | "version": "4.0.0", 500 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 501 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 502 | "dev": true, 503 | "engines": { 504 | "node": ">=8" 505 | } 506 | }, 507 | "node_modules/he": { 508 | "version": "1.2.0", 509 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 510 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 511 | "dev": true, 512 | "bin": { 513 | "he": "bin/he" 514 | } 515 | }, 516 | "node_modules/inflight": { 517 | "version": "1.0.6", 518 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 519 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 520 | "dev": true, 521 | "dependencies": { 522 | "once": "^1.3.0", 523 | "wrappy": "1" 524 | } 525 | }, 526 | "node_modules/inherits": { 527 | "version": "2.0.4", 528 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 529 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 530 | "dev": true 531 | }, 532 | "node_modules/is-binary-path": { 533 | "version": "2.1.0", 534 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 535 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 536 | "dev": true, 537 | "dependencies": { 538 | "binary-extensions": "^2.0.0" 539 | }, 540 | "engines": { 541 | "node": ">=8" 542 | } 543 | }, 544 | "node_modules/is-extglob": { 545 | "version": "2.1.1", 546 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 547 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 548 | "dev": true, 549 | "engines": { 550 | "node": ">=0.10.0" 551 | } 552 | }, 553 | "node_modules/is-fullwidth-code-point": { 554 | "version": "3.0.0", 555 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 556 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 557 | "dev": true, 558 | "engines": { 559 | "node": ">=8" 560 | } 561 | }, 562 | "node_modules/is-glob": { 563 | "version": "4.0.3", 564 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 565 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 566 | "dev": true, 567 | "dependencies": { 568 | "is-extglob": "^2.1.1" 569 | }, 570 | "engines": { 571 | "node": ">=0.10.0" 572 | } 573 | }, 574 | "node_modules/is-number": { 575 | "version": "7.0.0", 576 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 577 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 578 | "dev": true, 579 | "engines": { 580 | "node": ">=0.12.0" 581 | } 582 | }, 583 | "node_modules/is-plain-obj": { 584 | "version": "2.1.0", 585 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 586 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 587 | "dev": true, 588 | "engines": { 589 | "node": ">=8" 590 | } 591 | }, 592 | "node_modules/is-unicode-supported": { 593 | "version": "0.1.0", 594 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 595 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 596 | "dev": true, 597 | "engines": { 598 | "node": ">=10" 599 | }, 600 | "funding": { 601 | "url": "https://github.com/sponsors/sindresorhus" 602 | } 603 | }, 604 | "node_modules/js-yaml": { 605 | "version": "4.1.0", 606 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 607 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 608 | "dev": true, 609 | "dependencies": { 610 | "argparse": "^2.0.1" 611 | }, 612 | "bin": { 613 | "js-yaml": "bin/js-yaml.js" 614 | } 615 | }, 616 | "node_modules/json5": { 617 | "version": "1.0.2", 618 | "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", 619 | "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", 620 | "dev": true, 621 | "optional": true, 622 | "dependencies": { 623 | "minimist": "^1.2.0" 624 | }, 625 | "bin": { 626 | "json5": "lib/cli.js" 627 | } 628 | }, 629 | "node_modules/locate-path": { 630 | "version": "6.0.0", 631 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 632 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 633 | "dev": true, 634 | "dependencies": { 635 | "p-locate": "^5.0.0" 636 | }, 637 | "engines": { 638 | "node": ">=10" 639 | }, 640 | "funding": { 641 | "url": "https://github.com/sponsors/sindresorhus" 642 | } 643 | }, 644 | "node_modules/log-symbols": { 645 | "version": "4.1.0", 646 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 647 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 648 | "dev": true, 649 | "dependencies": { 650 | "chalk": "^4.1.0", 651 | "is-unicode-supported": "^0.1.0" 652 | }, 653 | "engines": { 654 | "node": ">=10" 655 | }, 656 | "funding": { 657 | "url": "https://github.com/sponsors/sindresorhus" 658 | } 659 | }, 660 | "node_modules/make-error": { 661 | "version": "1.3.6", 662 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 663 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 664 | "dev": true 665 | }, 666 | "node_modules/minimatch": { 667 | "version": "5.0.1", 668 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", 669 | "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", 670 | "dev": true, 671 | "dependencies": { 672 | "brace-expansion": "^2.0.1" 673 | }, 674 | "engines": { 675 | "node": ">=10" 676 | } 677 | }, 678 | "node_modules/minimatch/node_modules/brace-expansion": { 679 | "version": "2.0.1", 680 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 681 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 682 | "dev": true, 683 | "dependencies": { 684 | "balanced-match": "^1.0.0" 685 | } 686 | }, 687 | "node_modules/minimist": { 688 | "version": "1.2.6", 689 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 690 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", 691 | "dev": true 692 | }, 693 | "node_modules/mkdirp": { 694 | "version": "0.5.5", 695 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 696 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 697 | "dev": true, 698 | "dependencies": { 699 | "minimist": "^1.2.5" 700 | }, 701 | "bin": { 702 | "mkdirp": "bin/cmd.js" 703 | } 704 | }, 705 | "node_modules/mocha": { 706 | "version": "10.1.0", 707 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", 708 | "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", 709 | "dev": true, 710 | "dependencies": { 711 | "ansi-colors": "4.1.1", 712 | "browser-stdout": "1.3.1", 713 | "chokidar": "3.5.3", 714 | "debug": "4.3.4", 715 | "diff": "5.0.0", 716 | "escape-string-regexp": "4.0.0", 717 | "find-up": "5.0.0", 718 | "glob": "7.2.0", 719 | "he": "1.2.0", 720 | "js-yaml": "4.1.0", 721 | "log-symbols": "4.1.0", 722 | "minimatch": "5.0.1", 723 | "ms": "2.1.3", 724 | "nanoid": "3.3.3", 725 | "serialize-javascript": "6.0.0", 726 | "strip-json-comments": "3.1.1", 727 | "supports-color": "8.1.1", 728 | "workerpool": "6.2.1", 729 | "yargs": "16.2.0", 730 | "yargs-parser": "20.2.4", 731 | "yargs-unparser": "2.0.0" 732 | }, 733 | "bin": { 734 | "_mocha": "bin/_mocha", 735 | "mocha": "bin/mocha.js" 736 | }, 737 | "engines": { 738 | "node": ">= 14.0.0" 739 | }, 740 | "funding": { 741 | "type": "opencollective", 742 | "url": "https://opencollective.com/mochajs" 743 | } 744 | }, 745 | "node_modules/mocha/node_modules/diff": { 746 | "version": "5.0.0", 747 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 748 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 749 | "dev": true, 750 | "engines": { 751 | "node": ">=0.3.1" 752 | } 753 | }, 754 | "node_modules/ms": { 755 | "version": "2.1.3", 756 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 757 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 758 | "dev": true 759 | }, 760 | "node_modules/nanoid": { 761 | "version": "3.3.3", 762 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", 763 | "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", 764 | "dev": true, 765 | "bin": { 766 | "nanoid": "bin/nanoid.cjs" 767 | }, 768 | "engines": { 769 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 770 | } 771 | }, 772 | "node_modules/normalize-path": { 773 | "version": "3.0.0", 774 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 775 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 776 | "dev": true, 777 | "engines": { 778 | "node": ">=0.10.0" 779 | } 780 | }, 781 | "node_modules/once": { 782 | "version": "1.4.0", 783 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 784 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 785 | "dev": true, 786 | "dependencies": { 787 | "wrappy": "1" 788 | } 789 | }, 790 | "node_modules/p-limit": { 791 | "version": "3.1.0", 792 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 793 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 794 | "dev": true, 795 | "dependencies": { 796 | "yocto-queue": "^0.1.0" 797 | }, 798 | "engines": { 799 | "node": ">=10" 800 | }, 801 | "funding": { 802 | "url": "https://github.com/sponsors/sindresorhus" 803 | } 804 | }, 805 | "node_modules/p-locate": { 806 | "version": "5.0.0", 807 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 808 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 809 | "dev": true, 810 | "dependencies": { 811 | "p-limit": "^3.0.2" 812 | }, 813 | "engines": { 814 | "node": ">=10" 815 | }, 816 | "funding": { 817 | "url": "https://github.com/sponsors/sindresorhus" 818 | } 819 | }, 820 | "node_modules/path-exists": { 821 | "version": "4.0.0", 822 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 823 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 824 | "dev": true, 825 | "engines": { 826 | "node": ">=8" 827 | } 828 | }, 829 | "node_modules/path-is-absolute": { 830 | "version": "1.0.1", 831 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 832 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 833 | "dev": true, 834 | "engines": { 835 | "node": ">=0.10.0" 836 | } 837 | }, 838 | "node_modules/pathval": { 839 | "version": "1.1.1", 840 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 841 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 842 | "dev": true, 843 | "engines": { 844 | "node": "*" 845 | } 846 | }, 847 | "node_modules/picomatch": { 848 | "version": "2.3.1", 849 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 850 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 851 | "dev": true, 852 | "engines": { 853 | "node": ">=8.6" 854 | }, 855 | "funding": { 856 | "url": "https://github.com/sponsors/jonschlinkert" 857 | } 858 | }, 859 | "node_modules/randombytes": { 860 | "version": "2.1.0", 861 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 862 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 863 | "dev": true, 864 | "dependencies": { 865 | "safe-buffer": "^5.1.0" 866 | } 867 | }, 868 | "node_modules/readdirp": { 869 | "version": "3.6.0", 870 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 871 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 872 | "dev": true, 873 | "dependencies": { 874 | "picomatch": "^2.2.1" 875 | }, 876 | "engines": { 877 | "node": ">=8.10.0" 878 | } 879 | }, 880 | "node_modules/require-directory": { 881 | "version": "2.1.1", 882 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 883 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 884 | "dev": true, 885 | "engines": { 886 | "node": ">=0.10.0" 887 | } 888 | }, 889 | "node_modules/safe-buffer": { 890 | "version": "5.2.1", 891 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 892 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 893 | "dev": true, 894 | "funding": [ 895 | { 896 | "type": "github", 897 | "url": "https://github.com/sponsors/feross" 898 | }, 899 | { 900 | "type": "patreon", 901 | "url": "https://www.patreon.com/feross" 902 | }, 903 | { 904 | "type": "consulting", 905 | "url": "https://feross.org/support" 906 | } 907 | ] 908 | }, 909 | "node_modules/serialize-javascript": { 910 | "version": "6.0.0", 911 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 912 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 913 | "dev": true, 914 | "dependencies": { 915 | "randombytes": "^2.1.0" 916 | } 917 | }, 918 | "node_modules/source-map": { 919 | "version": "0.6.1", 920 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 921 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 922 | "dev": true, 923 | "engines": { 924 | "node": ">=0.10.0" 925 | } 926 | }, 927 | "node_modules/source-map-support": { 928 | "version": "0.5.19", 929 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 930 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 931 | "dev": true, 932 | "dependencies": { 933 | "buffer-from": "^1.0.0", 934 | "source-map": "^0.6.0" 935 | } 936 | }, 937 | "node_modules/string-width": { 938 | "version": "4.2.3", 939 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 940 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 941 | "dev": true, 942 | "dependencies": { 943 | "emoji-regex": "^8.0.0", 944 | "is-fullwidth-code-point": "^3.0.0", 945 | "strip-ansi": "^6.0.1" 946 | }, 947 | "engines": { 948 | "node": ">=8" 949 | } 950 | }, 951 | "node_modules/strip-ansi": { 952 | "version": "6.0.1", 953 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 954 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 955 | "dev": true, 956 | "dependencies": { 957 | "ansi-regex": "^5.0.1" 958 | }, 959 | "engines": { 960 | "node": ">=8" 961 | } 962 | }, 963 | "node_modules/strip-bom": { 964 | "version": "3.0.0", 965 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 966 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 967 | "dev": true, 968 | "optional": true, 969 | "engines": { 970 | "node": ">=4" 971 | } 972 | }, 973 | "node_modules/strip-json-comments": { 974 | "version": "3.1.1", 975 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 976 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 977 | "dev": true, 978 | "engines": { 979 | "node": ">=8" 980 | }, 981 | "funding": { 982 | "url": "https://github.com/sponsors/sindresorhus" 983 | } 984 | }, 985 | "node_modules/supports-color": { 986 | "version": "8.1.1", 987 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 988 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 989 | "dev": true, 990 | "dependencies": { 991 | "has-flag": "^4.0.0" 992 | }, 993 | "engines": { 994 | "node": ">=10" 995 | }, 996 | "funding": { 997 | "url": "https://github.com/chalk/supports-color?sponsor=1" 998 | } 999 | }, 1000 | "node_modules/to-regex-range": { 1001 | "version": "5.0.1", 1002 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1003 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1004 | "dev": true, 1005 | "dependencies": { 1006 | "is-number": "^7.0.0" 1007 | }, 1008 | "engines": { 1009 | "node": ">=8.0" 1010 | } 1011 | }, 1012 | "node_modules/ts-mocha": { 1013 | "version": "8.0.0", 1014 | "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz", 1015 | "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==", 1016 | "dev": true, 1017 | "dependencies": { 1018 | "ts-node": "7.0.1" 1019 | }, 1020 | "bin": { 1021 | "ts-mocha": "bin/ts-mocha" 1022 | }, 1023 | "engines": { 1024 | "node": ">= 6.X.X" 1025 | }, 1026 | "optionalDependencies": { 1027 | "tsconfig-paths": "^3.5.0" 1028 | }, 1029 | "peerDependencies": { 1030 | "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X" 1031 | } 1032 | }, 1033 | "node_modules/ts-node": { 1034 | "version": "7.0.1", 1035 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", 1036 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 1037 | "dev": true, 1038 | "dependencies": { 1039 | "arrify": "^1.0.0", 1040 | "buffer-from": "^1.1.0", 1041 | "diff": "^3.1.0", 1042 | "make-error": "^1.1.1", 1043 | "minimist": "^1.2.0", 1044 | "mkdirp": "^0.5.1", 1045 | "source-map-support": "^0.5.6", 1046 | "yn": "^2.0.0" 1047 | }, 1048 | "bin": { 1049 | "ts-node": "dist/bin.js" 1050 | }, 1051 | "engines": { 1052 | "node": ">=4.2.0" 1053 | } 1054 | }, 1055 | "node_modules/tsconfig-paths": { 1056 | "version": "3.9.0", 1057 | "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", 1058 | "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", 1059 | "dev": true, 1060 | "optional": true, 1061 | "dependencies": { 1062 | "@types/json5": "^0.0.29", 1063 | "json5": "^1.0.1", 1064 | "minimist": "^1.2.0", 1065 | "strip-bom": "^3.0.0" 1066 | } 1067 | }, 1068 | "node_modules/tslib": { 1069 | "version": "2.4.1", 1070 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 1071 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 1072 | }, 1073 | "node_modules/type-detect": { 1074 | "version": "4.0.8", 1075 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1076 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1077 | "dev": true, 1078 | "engines": { 1079 | "node": ">=4" 1080 | } 1081 | }, 1082 | "node_modules/typescript": { 1083 | "version": "4.9.3", 1084 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 1085 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 1086 | "dev": true, 1087 | "bin": { 1088 | "tsc": "bin/tsc", 1089 | "tsserver": "bin/tsserver" 1090 | }, 1091 | "engines": { 1092 | "node": ">=4.2.0" 1093 | } 1094 | }, 1095 | "node_modules/workerpool": { 1096 | "version": "6.2.1", 1097 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", 1098 | "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", 1099 | "dev": true 1100 | }, 1101 | "node_modules/wrap-ansi": { 1102 | "version": "7.0.0", 1103 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1104 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1105 | "dev": true, 1106 | "dependencies": { 1107 | "ansi-styles": "^4.0.0", 1108 | "string-width": "^4.1.0", 1109 | "strip-ansi": "^6.0.0" 1110 | }, 1111 | "engines": { 1112 | "node": ">=10" 1113 | }, 1114 | "funding": { 1115 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1116 | } 1117 | }, 1118 | "node_modules/wrappy": { 1119 | "version": "1.0.2", 1120 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1121 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1122 | "dev": true 1123 | }, 1124 | "node_modules/y18n": { 1125 | "version": "5.0.8", 1126 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1127 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1128 | "dev": true, 1129 | "engines": { 1130 | "node": ">=10" 1131 | } 1132 | }, 1133 | "node_modules/yargs": { 1134 | "version": "16.2.0", 1135 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 1136 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 1137 | "dev": true, 1138 | "dependencies": { 1139 | "cliui": "^7.0.2", 1140 | "escalade": "^3.1.1", 1141 | "get-caller-file": "^2.0.5", 1142 | "require-directory": "^2.1.1", 1143 | "string-width": "^4.2.0", 1144 | "y18n": "^5.0.5", 1145 | "yargs-parser": "^20.2.2" 1146 | }, 1147 | "engines": { 1148 | "node": ">=10" 1149 | } 1150 | }, 1151 | "node_modules/yargs-parser": { 1152 | "version": "20.2.4", 1153 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 1154 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 1155 | "dev": true, 1156 | "engines": { 1157 | "node": ">=10" 1158 | } 1159 | }, 1160 | "node_modules/yargs-unparser": { 1161 | "version": "2.0.0", 1162 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1163 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1164 | "dev": true, 1165 | "dependencies": { 1166 | "camelcase": "^6.0.0", 1167 | "decamelize": "^4.0.0", 1168 | "flat": "^5.0.2", 1169 | "is-plain-obj": "^2.1.0" 1170 | }, 1171 | "engines": { 1172 | "node": ">=10" 1173 | } 1174 | }, 1175 | "node_modules/yn": { 1176 | "version": "2.0.0", 1177 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 1178 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 1179 | "dev": true, 1180 | "engines": { 1181 | "node": ">=4" 1182 | } 1183 | }, 1184 | "node_modules/yocto-queue": { 1185 | "version": "0.1.0", 1186 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1187 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1188 | "dev": true, 1189 | "engines": { 1190 | "node": ">=10" 1191 | }, 1192 | "funding": { 1193 | "url": "https://github.com/sponsors/sindresorhus" 1194 | } 1195 | } 1196 | }, 1197 | "dependencies": { 1198 | "@types/chai": { 1199 | "version": "4.2.4", 1200 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.4.tgz", 1201 | "integrity": "sha1-iTbP+tPJbsRwotwmo4w7qLm29hk=", 1202 | "dev": true 1203 | }, 1204 | "@types/json5": { 1205 | "version": "0.0.29", 1206 | "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", 1207 | "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", 1208 | "dev": true, 1209 | "optional": true 1210 | }, 1211 | "@types/mocha": { 1212 | "version": "5.2.7", 1213 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", 1214 | "integrity": "sha1-MV1XDMtWxTRS/4Y4c432BybVtuo=", 1215 | "dev": true 1216 | }, 1217 | "@types/node": { 1218 | "version": "12.11.5", 1219 | "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.5.tgz", 1220 | "integrity": "sha1-bDyNyEmIr/Ef0qY9e1+/Oeqqt7E=", 1221 | "dev": true 1222 | }, 1223 | "ansi-colors": { 1224 | "version": "4.1.1", 1225 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 1226 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 1227 | "dev": true 1228 | }, 1229 | "ansi-regex": { 1230 | "version": "5.0.1", 1231 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1232 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1233 | "dev": true 1234 | }, 1235 | "ansi-styles": { 1236 | "version": "4.3.0", 1237 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1238 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1239 | "dev": true, 1240 | "requires": { 1241 | "color-convert": "^2.0.1" 1242 | } 1243 | }, 1244 | "anymatch": { 1245 | "version": "3.1.2", 1246 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 1247 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 1248 | "dev": true, 1249 | "requires": { 1250 | "normalize-path": "^3.0.0", 1251 | "picomatch": "^2.0.4" 1252 | } 1253 | }, 1254 | "argparse": { 1255 | "version": "2.0.1", 1256 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 1257 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 1258 | "dev": true 1259 | }, 1260 | "arrify": { 1261 | "version": "1.0.1", 1262 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 1263 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 1264 | "dev": true 1265 | }, 1266 | "assertion-error": { 1267 | "version": "1.1.0", 1268 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 1269 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 1270 | "dev": true 1271 | }, 1272 | "balanced-match": { 1273 | "version": "1.0.2", 1274 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 1275 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 1276 | "dev": true 1277 | }, 1278 | "binary-extensions": { 1279 | "version": "2.2.0", 1280 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 1281 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 1282 | "dev": true 1283 | }, 1284 | "brace-expansion": { 1285 | "version": "1.1.11", 1286 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 1287 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 1288 | "dev": true, 1289 | "requires": { 1290 | "balanced-match": "^1.0.0", 1291 | "concat-map": "0.0.1" 1292 | } 1293 | }, 1294 | "braces": { 1295 | "version": "3.0.3", 1296 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 1297 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 1298 | "dev": true, 1299 | "requires": { 1300 | "fill-range": "^7.1.1" 1301 | } 1302 | }, 1303 | "browser-stdout": { 1304 | "version": "1.3.1", 1305 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 1306 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 1307 | "dev": true 1308 | }, 1309 | "buffer-from": { 1310 | "version": "1.1.1", 1311 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 1312 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", 1313 | "dev": true 1314 | }, 1315 | "camelcase": { 1316 | "version": "6.3.0", 1317 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 1318 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 1319 | "dev": true 1320 | }, 1321 | "chai": { 1322 | "version": "4.3.4", 1323 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", 1324 | "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", 1325 | "dev": true, 1326 | "requires": { 1327 | "assertion-error": "^1.1.0", 1328 | "check-error": "^1.0.2", 1329 | "deep-eql": "^3.0.1", 1330 | "get-func-name": "^2.0.0", 1331 | "pathval": "^1.1.1", 1332 | "type-detect": "^4.0.5" 1333 | } 1334 | }, 1335 | "chalk": { 1336 | "version": "4.1.2", 1337 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 1338 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 1339 | "dev": true, 1340 | "requires": { 1341 | "ansi-styles": "^4.1.0", 1342 | "supports-color": "^7.1.0" 1343 | }, 1344 | "dependencies": { 1345 | "supports-color": { 1346 | "version": "7.2.0", 1347 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1348 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1349 | "dev": true, 1350 | "requires": { 1351 | "has-flag": "^4.0.0" 1352 | } 1353 | } 1354 | } 1355 | }, 1356 | "check-error": { 1357 | "version": "1.0.2", 1358 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 1359 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 1360 | "dev": true 1361 | }, 1362 | "chokidar": { 1363 | "version": "3.5.3", 1364 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 1365 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 1366 | "dev": true, 1367 | "requires": { 1368 | "anymatch": "~3.1.2", 1369 | "braces": "~3.0.2", 1370 | "fsevents": "~2.3.2", 1371 | "glob-parent": "~5.1.2", 1372 | "is-binary-path": "~2.1.0", 1373 | "is-glob": "~4.0.1", 1374 | "normalize-path": "~3.0.0", 1375 | "readdirp": "~3.6.0" 1376 | } 1377 | }, 1378 | "cliui": { 1379 | "version": "7.0.4", 1380 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 1381 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 1382 | "dev": true, 1383 | "requires": { 1384 | "string-width": "^4.2.0", 1385 | "strip-ansi": "^6.0.0", 1386 | "wrap-ansi": "^7.0.0" 1387 | } 1388 | }, 1389 | "color-convert": { 1390 | "version": "2.0.1", 1391 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1392 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1393 | "dev": true, 1394 | "requires": { 1395 | "color-name": "~1.1.4" 1396 | } 1397 | }, 1398 | "color-name": { 1399 | "version": "1.1.4", 1400 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1401 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1402 | "dev": true 1403 | }, 1404 | "concat-map": { 1405 | "version": "0.0.1", 1406 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 1407 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 1408 | "dev": true 1409 | }, 1410 | "debug": { 1411 | "version": "4.3.4", 1412 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 1413 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 1414 | "dev": true, 1415 | "requires": { 1416 | "ms": "2.1.2" 1417 | }, 1418 | "dependencies": { 1419 | "ms": { 1420 | "version": "2.1.2", 1421 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1422 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1423 | "dev": true 1424 | } 1425 | } 1426 | }, 1427 | "decamelize": { 1428 | "version": "4.0.0", 1429 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 1430 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 1431 | "dev": true 1432 | }, 1433 | "deep-eql": { 1434 | "version": "3.0.1", 1435 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 1436 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 1437 | "dev": true, 1438 | "requires": { 1439 | "type-detect": "^4.0.0" 1440 | } 1441 | }, 1442 | "diff": { 1443 | "version": "3.5.0", 1444 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 1445 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 1446 | "dev": true 1447 | }, 1448 | "emoji-regex": { 1449 | "version": "8.0.0", 1450 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1451 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1452 | "dev": true 1453 | }, 1454 | "escalade": { 1455 | "version": "3.1.1", 1456 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 1457 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 1458 | "dev": true 1459 | }, 1460 | "escape-string-regexp": { 1461 | "version": "4.0.0", 1462 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1463 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1464 | "dev": true 1465 | }, 1466 | "fill-range": { 1467 | "version": "7.1.1", 1468 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 1469 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 1470 | "dev": true, 1471 | "requires": { 1472 | "to-regex-range": "^5.0.1" 1473 | } 1474 | }, 1475 | "find-up": { 1476 | "version": "5.0.0", 1477 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1478 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1479 | "dev": true, 1480 | "requires": { 1481 | "locate-path": "^6.0.0", 1482 | "path-exists": "^4.0.0" 1483 | } 1484 | }, 1485 | "flat": { 1486 | "version": "5.0.2", 1487 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 1488 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 1489 | "dev": true 1490 | }, 1491 | "fs.realpath": { 1492 | "version": "1.0.0", 1493 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1494 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 1495 | "dev": true 1496 | }, 1497 | "fsevents": { 1498 | "version": "2.3.2", 1499 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 1500 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 1501 | "dev": true, 1502 | "optional": true 1503 | }, 1504 | "get-caller-file": { 1505 | "version": "2.0.5", 1506 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 1507 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 1508 | "dev": true 1509 | }, 1510 | "get-func-name": { 1511 | "version": "2.0.0", 1512 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 1513 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 1514 | "dev": true 1515 | }, 1516 | "glob": { 1517 | "version": "7.2.0", 1518 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 1519 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 1520 | "dev": true, 1521 | "requires": { 1522 | "fs.realpath": "^1.0.0", 1523 | "inflight": "^1.0.4", 1524 | "inherits": "2", 1525 | "minimatch": "^3.0.4", 1526 | "once": "^1.3.0", 1527 | "path-is-absolute": "^1.0.0" 1528 | }, 1529 | "dependencies": { 1530 | "minimatch": { 1531 | "version": "3.1.2", 1532 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1533 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1534 | "dev": true, 1535 | "requires": { 1536 | "brace-expansion": "^1.1.7" 1537 | } 1538 | } 1539 | } 1540 | }, 1541 | "glob-parent": { 1542 | "version": "5.1.2", 1543 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 1544 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 1545 | "dev": true, 1546 | "requires": { 1547 | "is-glob": "^4.0.1" 1548 | } 1549 | }, 1550 | "has-flag": { 1551 | "version": "4.0.0", 1552 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1553 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1554 | "dev": true 1555 | }, 1556 | "he": { 1557 | "version": "1.2.0", 1558 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 1559 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 1560 | "dev": true 1561 | }, 1562 | "inflight": { 1563 | "version": "1.0.6", 1564 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1565 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 1566 | "dev": true, 1567 | "requires": { 1568 | "once": "^1.3.0", 1569 | "wrappy": "1" 1570 | } 1571 | }, 1572 | "inherits": { 1573 | "version": "2.0.4", 1574 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1575 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1576 | "dev": true 1577 | }, 1578 | "is-binary-path": { 1579 | "version": "2.1.0", 1580 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 1581 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 1582 | "dev": true, 1583 | "requires": { 1584 | "binary-extensions": "^2.0.0" 1585 | } 1586 | }, 1587 | "is-extglob": { 1588 | "version": "2.1.1", 1589 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1590 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1591 | "dev": true 1592 | }, 1593 | "is-fullwidth-code-point": { 1594 | "version": "3.0.0", 1595 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1596 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1597 | "dev": true 1598 | }, 1599 | "is-glob": { 1600 | "version": "4.0.3", 1601 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1602 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1603 | "dev": true, 1604 | "requires": { 1605 | "is-extglob": "^2.1.1" 1606 | } 1607 | }, 1608 | "is-number": { 1609 | "version": "7.0.0", 1610 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 1611 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 1612 | "dev": true 1613 | }, 1614 | "is-plain-obj": { 1615 | "version": "2.1.0", 1616 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 1617 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 1618 | "dev": true 1619 | }, 1620 | "is-unicode-supported": { 1621 | "version": "0.1.0", 1622 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 1623 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 1624 | "dev": true 1625 | }, 1626 | "js-yaml": { 1627 | "version": "4.1.0", 1628 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1629 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1630 | "dev": true, 1631 | "requires": { 1632 | "argparse": "^2.0.1" 1633 | } 1634 | }, 1635 | "json5": { 1636 | "version": "1.0.2", 1637 | "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", 1638 | "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", 1639 | "dev": true, 1640 | "optional": true, 1641 | "requires": { 1642 | "minimist": "^1.2.0" 1643 | } 1644 | }, 1645 | "locate-path": { 1646 | "version": "6.0.0", 1647 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1648 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1649 | "dev": true, 1650 | "requires": { 1651 | "p-locate": "^5.0.0" 1652 | } 1653 | }, 1654 | "log-symbols": { 1655 | "version": "4.1.0", 1656 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 1657 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 1658 | "dev": true, 1659 | "requires": { 1660 | "chalk": "^4.1.0", 1661 | "is-unicode-supported": "^0.1.0" 1662 | } 1663 | }, 1664 | "make-error": { 1665 | "version": "1.3.6", 1666 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 1667 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 1668 | "dev": true 1669 | }, 1670 | "minimatch": { 1671 | "version": "5.0.1", 1672 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", 1673 | "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", 1674 | "dev": true, 1675 | "requires": { 1676 | "brace-expansion": "^2.0.1" 1677 | }, 1678 | "dependencies": { 1679 | "brace-expansion": { 1680 | "version": "2.0.1", 1681 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 1682 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 1683 | "dev": true, 1684 | "requires": { 1685 | "balanced-match": "^1.0.0" 1686 | } 1687 | } 1688 | } 1689 | }, 1690 | "minimist": { 1691 | "version": "1.2.6", 1692 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 1693 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", 1694 | "dev": true 1695 | }, 1696 | "mkdirp": { 1697 | "version": "0.5.5", 1698 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 1699 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 1700 | "dev": true, 1701 | "requires": { 1702 | "minimist": "^1.2.5" 1703 | } 1704 | }, 1705 | "mocha": { 1706 | "version": "10.1.0", 1707 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.1.0.tgz", 1708 | "integrity": "sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==", 1709 | "dev": true, 1710 | "requires": { 1711 | "ansi-colors": "4.1.1", 1712 | "browser-stdout": "1.3.1", 1713 | "chokidar": "3.5.3", 1714 | "debug": "4.3.4", 1715 | "diff": "5.0.0", 1716 | "escape-string-regexp": "4.0.0", 1717 | "find-up": "5.0.0", 1718 | "glob": "7.2.0", 1719 | "he": "1.2.0", 1720 | "js-yaml": "4.1.0", 1721 | "log-symbols": "4.1.0", 1722 | "minimatch": "5.0.1", 1723 | "ms": "2.1.3", 1724 | "nanoid": "3.3.3", 1725 | "serialize-javascript": "6.0.0", 1726 | "strip-json-comments": "3.1.1", 1727 | "supports-color": "8.1.1", 1728 | "workerpool": "6.2.1", 1729 | "yargs": "16.2.0", 1730 | "yargs-parser": "20.2.4", 1731 | "yargs-unparser": "2.0.0" 1732 | }, 1733 | "dependencies": { 1734 | "diff": { 1735 | "version": "5.0.0", 1736 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 1737 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 1738 | "dev": true 1739 | } 1740 | } 1741 | }, 1742 | "ms": { 1743 | "version": "2.1.3", 1744 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1745 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1746 | "dev": true 1747 | }, 1748 | "nanoid": { 1749 | "version": "3.3.3", 1750 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", 1751 | "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", 1752 | "dev": true 1753 | }, 1754 | "normalize-path": { 1755 | "version": "3.0.0", 1756 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 1757 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 1758 | "dev": true 1759 | }, 1760 | "once": { 1761 | "version": "1.4.0", 1762 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1763 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 1764 | "dev": true, 1765 | "requires": { 1766 | "wrappy": "1" 1767 | } 1768 | }, 1769 | "p-limit": { 1770 | "version": "3.1.0", 1771 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1772 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1773 | "dev": true, 1774 | "requires": { 1775 | "yocto-queue": "^0.1.0" 1776 | } 1777 | }, 1778 | "p-locate": { 1779 | "version": "5.0.0", 1780 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1781 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1782 | "dev": true, 1783 | "requires": { 1784 | "p-limit": "^3.0.2" 1785 | } 1786 | }, 1787 | "path-exists": { 1788 | "version": "4.0.0", 1789 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1790 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1791 | "dev": true 1792 | }, 1793 | "path-is-absolute": { 1794 | "version": "1.0.1", 1795 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1796 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 1797 | "dev": true 1798 | }, 1799 | "pathval": { 1800 | "version": "1.1.1", 1801 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", 1802 | "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", 1803 | "dev": true 1804 | }, 1805 | "picomatch": { 1806 | "version": "2.3.1", 1807 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 1808 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 1809 | "dev": true 1810 | }, 1811 | "randombytes": { 1812 | "version": "2.1.0", 1813 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 1814 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 1815 | "dev": true, 1816 | "requires": { 1817 | "safe-buffer": "^5.1.0" 1818 | } 1819 | }, 1820 | "readdirp": { 1821 | "version": "3.6.0", 1822 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 1823 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 1824 | "dev": true, 1825 | "requires": { 1826 | "picomatch": "^2.2.1" 1827 | } 1828 | }, 1829 | "require-directory": { 1830 | "version": "2.1.1", 1831 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 1832 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 1833 | "dev": true 1834 | }, 1835 | "safe-buffer": { 1836 | "version": "5.2.1", 1837 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1838 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1839 | "dev": true 1840 | }, 1841 | "serialize-javascript": { 1842 | "version": "6.0.0", 1843 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 1844 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 1845 | "dev": true, 1846 | "requires": { 1847 | "randombytes": "^2.1.0" 1848 | } 1849 | }, 1850 | "source-map": { 1851 | "version": "0.6.1", 1852 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1853 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1854 | "dev": true 1855 | }, 1856 | "source-map-support": { 1857 | "version": "0.5.19", 1858 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 1859 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 1860 | "dev": true, 1861 | "requires": { 1862 | "buffer-from": "^1.0.0", 1863 | "source-map": "^0.6.0" 1864 | } 1865 | }, 1866 | "string-width": { 1867 | "version": "4.2.3", 1868 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1869 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1870 | "dev": true, 1871 | "requires": { 1872 | "emoji-regex": "^8.0.0", 1873 | "is-fullwidth-code-point": "^3.0.0", 1874 | "strip-ansi": "^6.0.1" 1875 | } 1876 | }, 1877 | "strip-ansi": { 1878 | "version": "6.0.1", 1879 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1880 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1881 | "dev": true, 1882 | "requires": { 1883 | "ansi-regex": "^5.0.1" 1884 | } 1885 | }, 1886 | "strip-bom": { 1887 | "version": "3.0.0", 1888 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1889 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1890 | "dev": true, 1891 | "optional": true 1892 | }, 1893 | "strip-json-comments": { 1894 | "version": "3.1.1", 1895 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1896 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1897 | "dev": true 1898 | }, 1899 | "supports-color": { 1900 | "version": "8.1.1", 1901 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1902 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1903 | "dev": true, 1904 | "requires": { 1905 | "has-flag": "^4.0.0" 1906 | } 1907 | }, 1908 | "to-regex-range": { 1909 | "version": "5.0.1", 1910 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 1911 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 1912 | "dev": true, 1913 | "requires": { 1914 | "is-number": "^7.0.0" 1915 | } 1916 | }, 1917 | "ts-mocha": { 1918 | "version": "8.0.0", 1919 | "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz", 1920 | "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==", 1921 | "dev": true, 1922 | "requires": { 1923 | "ts-node": "7.0.1", 1924 | "tsconfig-paths": "^3.5.0" 1925 | } 1926 | }, 1927 | "ts-node": { 1928 | "version": "7.0.1", 1929 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", 1930 | "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", 1931 | "dev": true, 1932 | "requires": { 1933 | "arrify": "^1.0.0", 1934 | "buffer-from": "^1.1.0", 1935 | "diff": "^3.1.0", 1936 | "make-error": "^1.1.1", 1937 | "minimist": "^1.2.0", 1938 | "mkdirp": "^0.5.1", 1939 | "source-map-support": "^0.5.6", 1940 | "yn": "^2.0.0" 1941 | } 1942 | }, 1943 | "tsconfig-paths": { 1944 | "version": "3.9.0", 1945 | "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", 1946 | "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", 1947 | "dev": true, 1948 | "optional": true, 1949 | "requires": { 1950 | "@types/json5": "^0.0.29", 1951 | "json5": "^1.0.1", 1952 | "minimist": "^1.2.0", 1953 | "strip-bom": "^3.0.0" 1954 | } 1955 | }, 1956 | "tslib": { 1957 | "version": "2.4.1", 1958 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", 1959 | "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" 1960 | }, 1961 | "type-detect": { 1962 | "version": "4.0.8", 1963 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1964 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1965 | "dev": true 1966 | }, 1967 | "typescript": { 1968 | "version": "4.9.3", 1969 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 1970 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 1971 | "dev": true 1972 | }, 1973 | "workerpool": { 1974 | "version": "6.2.1", 1975 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", 1976 | "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", 1977 | "dev": true 1978 | }, 1979 | "wrap-ansi": { 1980 | "version": "7.0.0", 1981 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1982 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1983 | "dev": true, 1984 | "requires": { 1985 | "ansi-styles": "^4.0.0", 1986 | "string-width": "^4.1.0", 1987 | "strip-ansi": "^6.0.0" 1988 | } 1989 | }, 1990 | "wrappy": { 1991 | "version": "1.0.2", 1992 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1993 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1994 | "dev": true 1995 | }, 1996 | "y18n": { 1997 | "version": "5.0.8", 1998 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1999 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 2000 | "dev": true 2001 | }, 2002 | "yargs": { 2003 | "version": "16.2.0", 2004 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 2005 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 2006 | "dev": true, 2007 | "requires": { 2008 | "cliui": "^7.0.2", 2009 | "escalade": "^3.1.1", 2010 | "get-caller-file": "^2.0.5", 2011 | "require-directory": "^2.1.1", 2012 | "string-width": "^4.2.0", 2013 | "y18n": "^5.0.5", 2014 | "yargs-parser": "^20.2.2" 2015 | } 2016 | }, 2017 | "yargs-parser": { 2018 | "version": "20.2.4", 2019 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 2020 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 2021 | "dev": true 2022 | }, 2023 | "yargs-unparser": { 2024 | "version": "2.0.0", 2025 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 2026 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 2027 | "dev": true, 2028 | "requires": { 2029 | "camelcase": "^6.0.0", 2030 | "decamelize": "^4.0.0", 2031 | "flat": "^5.0.2", 2032 | "is-plain-obj": "^2.1.0" 2033 | } 2034 | }, 2035 | "yn": { 2036 | "version": "2.0.0", 2037 | "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", 2038 | "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", 2039 | "dev": true 2040 | }, 2041 | "yocto-queue": { 2042 | "version": "0.1.0", 2043 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2044 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2045 | "dev": true 2046 | } 2047 | } 2048 | } 2049 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsona", 3 | "description": "Provide data formatters (data model builder & json builder) to work with JSON API specification v1.0 in your JavaScript / TypeScript code", 4 | "version": "1.12.1", 5 | "keywords": [ 6 | "json-api", 7 | "jsonapi", 8 | "json:api", 9 | "json-api-redux", 10 | "json api redux", 11 | "json-api angular", 12 | "json api typescript", 13 | "data formatter", 14 | "data converter", 15 | "data serializer", 16 | "formatter", 17 | "converter", 18 | "serializer", 19 | "json api json", 20 | "json", 21 | "redux-object", 22 | "rest", 23 | "rest api", 24 | "graphql", 25 | "graphql alternative", 26 | "json-api-normalizer" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/olosegres/jsona" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/olosegres/jsona/issues" 34 | }, 35 | "homepage": "https://github.com/olosegres/jsona#readme", 36 | "author": "Sergei Solo ", 37 | "license": "MIT", 38 | "main": "lib/index.js", 39 | "types": "lib/index.d.ts", 40 | "files": [ 41 | "lib", 42 | "src" 43 | ], 44 | "dependencies": { 45 | "tslib": "^2.4.1" 46 | }, 47 | "devDependencies": { 48 | "@types/chai": "^4.2.4", 49 | "@types/mocha": "^5.2.7", 50 | "@types/node": "^12.11.5", 51 | "chai": "^4.3.4", 52 | "mocha": "^10.1.0", 53 | "ts-mocha": "^8.0.0", 54 | "typescript": "^4.9.3" 55 | }, 56 | "scripts": { 57 | "clean": "rm -rf ./lib/*", 58 | "build": "./node_modules/.bin/tsc", 59 | "prebuild": "npm run clean", 60 | "preversion": "npm run build && npm test", 61 | "test-build": "tsc --p tsconfig.test.json", 62 | "test": "npm run test-build && env NODE_ENV=test ts-mocha ./**/*.test.ts" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Jsona.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IModelPropertiesMapper, 3 | IModelsSerializerConstructor, 4 | IJsonDeserializerConstructor, 5 | IJsonPropertiesMapper, 6 | TJsonaDenormalizedIncludeNames, 7 | TJsonaNormalizedIncludeNamesTree, 8 | TJsonaModel, 9 | TJsonApiBody, 10 | TReduxObject, 11 | IDeserializeCacheConstructor, 12 | TDeserializeOptions 13 | } from './JsonaTypes'; 14 | 15 | import {jsonParse} from './utils'; 16 | import ModelsSerializer from './builders/ModelsSerializer'; 17 | import JsonDeserializer from './builders/JsonDeserializer'; 18 | import ReduxObjectDenormalizer from './builders/ReduxObjectDenormalizer'; 19 | 20 | import { 21 | ModelPropertiesMapper, 22 | JsonPropertiesMapper 23 | } from './simplePropertyMappers'; 24 | 25 | import { DeserializeCache } from './cache'; 26 | 27 | class Jsona { 28 | 29 | public modelPropertiesMapper: IModelPropertiesMapper = new ModelPropertiesMapper(); 30 | public jsonPropertiesMapper: IJsonPropertiesMapper = new JsonPropertiesMapper(); 31 | public DeserializeCache: IDeserializeCacheConstructor = DeserializeCache; 32 | public ModelsSerializer: IModelsSerializerConstructor = ModelsSerializer; 33 | public JsonDeserializer: IJsonDeserializerConstructor = JsonDeserializer; 34 | 35 | constructor(params?: { 36 | modelPropertiesMapper?: IModelPropertiesMapper, 37 | jsonPropertiesMapper?: IJsonPropertiesMapper, 38 | DeserializeCache?: IDeserializeCacheConstructor, 39 | ModelsSerializer?: IModelsSerializerConstructor, 40 | JsonDeserializer?: IJsonDeserializerConstructor, 41 | }) { 42 | if (params && params.modelPropertiesMapper) { 43 | this.modelPropertiesMapper = params.modelPropertiesMapper; 44 | } 45 | if (params && params.jsonPropertiesMapper) { 46 | this.jsonPropertiesMapper = params.jsonPropertiesMapper; 47 | } 48 | if (params && params.DeserializeCache) { 49 | this.DeserializeCache = params.DeserializeCache; 50 | } 51 | if (params && params.ModelsSerializer) { 52 | this.ModelsSerializer = params.ModelsSerializer; 53 | } 54 | if (params && params.JsonDeserializer) { 55 | this.JsonDeserializer = params.JsonDeserializer; 56 | } 57 | } 58 | 59 | /** 60 | * serialize 61 | * Creates JSON, compatible with json:api specification from Jsona model(s). 62 | */ 63 | serialize({ 64 | stuff, includeNames}: { 65 | stuff: TJsonaModel | Array, 66 | includeNames?: TJsonaDenormalizedIncludeNames | TJsonaNormalizedIncludeNamesTree 67 | } 68 | ): TJsonApiBody { 69 | if (!stuff) { 70 | throw new Error('Jsona can not serialize, stuff is not passed'); 71 | } 72 | 73 | const jsonBuilder = new this.ModelsSerializer(this.modelPropertiesMapper); 74 | 75 | jsonBuilder.setStuff(stuff); 76 | 77 | if (includeNames) { 78 | jsonBuilder.setIncludeNames(includeNames); 79 | } 80 | 81 | return jsonBuilder.build(); 82 | } 83 | 84 | /** 85 | * deserialize 86 | * Creates Jsona model(s) from JSON, compatible with json:api specification. 87 | */ 88 | deserialize(body: TJsonApiBody | string, options?: TDeserializeOptions): TJsonaModel | Array { 89 | if (!body) { 90 | throw new Error('Jsona can not deserialize, body is not passed'); 91 | } 92 | 93 | const deserializeCache = new this.DeserializeCache(); 94 | const modelBuilder = new this.JsonDeserializer(this.jsonPropertiesMapper, deserializeCache, options); 95 | 96 | if (typeof body === 'string') { 97 | modelBuilder.setJsonParsedObject(jsonParse(body)); 98 | } else { 99 | modelBuilder.setJsonParsedObject(body); 100 | } 101 | 102 | return modelBuilder.build(); 103 | } 104 | 105 | /** 106 | * denormalizeReduxObject 107 | * Creates Jsona model(s) from ReduxObject, that creates by json-api-normalizer 108 | * https://github.com/yury-dymov/json-api-normalizer 109 | * 110 | */ 111 | denormalizeReduxObject( 112 | {reduxObject, entityType, entityIds, returnBuilderInRelations = false}: { 113 | reduxObject: TReduxObject, 114 | entityType: string, 115 | entityIds?: string | Array, 116 | returnBuilderInRelations?: boolean, 117 | } 118 | ): null | TJsonaModel | Array { 119 | 120 | if (!reduxObject) { 121 | throw new Error('Jsona can not denormalize ReduxObject, incorrect reduxObject passed'); 122 | } 123 | if (!entityType) { 124 | throw new Error('Jsona can not denormalize ReduxObject, entityType is not passed'); 125 | } 126 | 127 | if (!reduxObject[entityType]) { 128 | return null; 129 | } 130 | 131 | const modelBuilder = new ReduxObjectDenormalizer(this.jsonPropertiesMapper); 132 | 133 | modelBuilder.setReduxObject(reduxObject); 134 | modelBuilder.setEntityType(entityType); 135 | modelBuilder.setReturnBuilderInRelations(returnBuilderInRelations); 136 | 137 | if (entityIds) { 138 | modelBuilder.setEntityIds( 139 | Array.isArray(entityIds) ? entityIds : entityIds.toString() 140 | ); 141 | } 142 | 143 | return modelBuilder.build(); 144 | } 145 | 146 | } 147 | 148 | export default Jsona; 149 | -------------------------------------------------------------------------------- /src/JsonaTypes.ts: -------------------------------------------------------------------------------- 1 | type AtLeastOneProperty }> = Partial & U[keyof U]; 2 | 3 | export interface IModelPropertiesMapper { 4 | getId(model: TJsonaModel): string | number; 5 | getType(model: TJsonaModel): string; 6 | getAttributes(model: TJsonaModel): TAnyKeyValueObject; 7 | getRelationships(model: TJsonaModel): TJsonaRelationships; 8 | } 9 | 10 | export interface IJsonPropertiesMapper { 11 | createModel(type: string): TJsonaModel; 12 | setId(model: TJsonaModel, id: string | number): void; 13 | setAttributes(model: TJsonaModel, attributes: TAnyKeyValueObject): void; 14 | setMeta(model: TJsonaModel, meta: TAnyKeyValueObject): void; 15 | setLinks(model: TJsonaModel, links: TAnyKeyValueObject): void; 16 | setResourceIdObjMeta(model: TJsonaModel, meta: unknown): void; 17 | setRelationships(model: TJsonaModel, relationships: TJsonaRelationships): void; 18 | setRelationshipLinks(parentModel: TJsonaModel, relationName: string, links: TJsonApiLinks): void; 19 | setRelationshipMeta(parentModel: TJsonaModel, relationName: string, meta: TAnyKeyValueObject): void; 20 | } 21 | 22 | export interface IJsonaModelBuilder { 23 | build(): TJsonaModel | Array; 24 | } 25 | 26 | export interface IDeserializeCache { 27 | getCachedModel(data: TJsonApiData, resourceIdObject: TResourceIdObj): TJsonaModel | null; 28 | handleModel(model: TJsonaModel, data: TJsonApiData, resourceIdObject: TResourceIdObj): void; 29 | createCacheKey(data: TJsonApiData, resourceIdObject: TResourceIdObj): string; 30 | } 31 | 32 | export interface IDeserializeCacheConstructor { 33 | new(): IDeserializeCache; 34 | } 35 | 36 | export interface IJsonaDeserializer extends IJsonaModelBuilder { 37 | setDeserializeCache(dc: IDeserializeCache): void; 38 | setPropertiesMapper(pm: IJsonPropertiesMapper): void; 39 | setJsonParsedObject(body: TJsonApiBody): void; 40 | buildModelByData(data: TJsonApiData): TJsonaModel; 41 | buildRelationsByData(data: TJsonApiData, model: TJsonaModel): TJsonaRelationships | null; 42 | buildDataFromIncludedOrData(id: string | number, type: string): TJsonApiData; 43 | buildDataInObject(): { [key: string]: TJsonApiData }; 44 | buildIncludedInObject(): { [key: string]: TJsonApiData }; 45 | } 46 | 47 | export interface IJsonDeserializerConstructor { 48 | new(propertiesMapper: IJsonPropertiesMapper, deserializeCache: IDeserializeCache, options?: TDeserializeOptions): IJsonaDeserializer; 49 | } 50 | 51 | export interface IModelsSerializer { 52 | setPropertiesMapper(propertiesMapper: IModelPropertiesMapper): void; 53 | setStuff(stuff: TJsonaModel | TJsonaModel[]): void; 54 | setIncludeNames(includeNames: TJsonaDenormalizedIncludeNames | TJsonaNormalizedIncludeNamesTree): void; 55 | build(): TJsonApiBody; 56 | buildDataByModel(model: TJsonaModel | null): TJsonApiData; 57 | buildRelationshipsByModel(model: TJsonaModel): TJsonApiRelationships; 58 | buildIncludedByModel( 59 | model: TJsonaModel, 60 | includeTree: TJsonaNormalizedIncludeNamesTree, 61 | builtIncluded: TJsonaUniqueIncluded 62 | ): void; 63 | buildIncludedItem( 64 | relationModel: TJsonaModel, 65 | subIncludeTree: TJsonaNormalizedIncludeNamesTree, 66 | builtIncluded: TJsonaUniqueIncluded 67 | ): void; 68 | } 69 | 70 | export interface IModelsSerializerConstructor { 71 | new(propertiesMapper?: IModelPropertiesMapper): IModelsSerializer; 72 | } 73 | 74 | export type TDeserializeOptions = { 75 | preferNestedDataFromData?: boolean, 76 | } 77 | 78 | export type TAnyKeyValueObject = { 79 | [key: string]: any 80 | }; 81 | 82 | export type TJsonApiBody = { 83 | data?: TJsonApiData | TJsonApiData[]; 84 | included?: Array; 85 | }; 86 | 87 | 88 | export type TJsonApiData = { 89 | type: string; 90 | id?: string|number; 91 | attributes?: TAnyKeyValueObject; 92 | meta?: TAnyKeyValueObject; 93 | links?: TJsonApiLinks; 94 | relationships?: TJsonApiRelationships; 95 | }; 96 | 97 | export type TJsonApiRelationshipData = { 98 | type: string; 99 | id: string | number; 100 | meta?: TAnyKeyValueObject 101 | [propertyName: string]: any 102 | }; 103 | 104 | type FullTJsonApiRelation = { 105 | data: TJsonApiRelationshipData | Array | null 106 | links: TJsonApiLinks 107 | meta: TAnyKeyValueObject 108 | } 109 | 110 | export type TJsonApiRelation = AtLeastOneProperty; 111 | 112 | type LinkKey = "self" | "related" | "first" | "prev" | "next" | "last"; 113 | 114 | // https://jsonapi.org/format/#document-links 115 | type LinkObjectMember = string | { href?: string; meta?: TAnyKeyValueObject } | null; 116 | 117 | export type TJsonApiLinks = { 118 | [key in LinkKey]?: LinkObjectMember; 119 | }; 120 | 121 | export type TJsonApiRelationships = { 122 | [relationName: string]: TJsonApiRelation 123 | }; 124 | 125 | export type TJsonaUniqueIncluded = { 126 | [entityTypeId: string]: TJsonApiData 127 | }; 128 | 129 | /** 130 | * TJsonaDenormalizedIncludeNames example: 131 | * 'user.town.country' 132 | */ 133 | export type TJsonaIncludeNamesChain = string; 134 | 135 | /** 136 | * TJsonaDenormalizedIncludeNames example: 137 | * ['user', 'user.town', 'user.town.country', 'comments', 'comments.author'] 138 | */ 139 | export type TJsonaDenormalizedIncludeNames = Array; 140 | 141 | /** 142 | * TJsonaNormalizedIncludeNamesTree example: 143 | * { 144 | * user: { 145 | * town: { 146 | * country: null 147 | * } 148 | * comments: { 149 | * author: null 150 | * } 151 | */ 152 | export type TJsonaNormalizedIncludeNamesTree = { 153 | [relationName: string]: null | TJsonaNormalizedIncludeNamesTree 154 | }; 155 | 156 | export type TJsonaModel = { 157 | [propertyName: string]: any 158 | }; 159 | 160 | export type TResourceIdObj = TJsonApiRelationshipData & { 161 | type: string, 162 | }; 163 | 164 | export type TJsonaRelationshipBuild = () => (TJsonaModel | Array); 165 | 166 | export type TJsonaRelationships = { 167 | [relationName: string]: TJsonaRelationshipBuild | TJsonaModel | Array 168 | }; 169 | 170 | export type TReduxObject = { 171 | [entityType: string]: { 172 | [entityId: string]: TReduxObjectModel 173 | } 174 | }; 175 | 176 | export type TReduxObjectModel = { 177 | id: number | string, 178 | attributes?: TAnyKeyValueObject, 179 | relationships?: TJsonApiRelationships 180 | }; 181 | 182 | export type TReduxObjectRelation = { 183 | data: { 184 | // '1' or something like '1,12,44' for one-to-many relationships, ['1', '12', '44'] is reserved for future 185 | id: string | Array, 186 | type: string, 187 | } 188 | } 189 | 190 | export type SwitchCaseModelMapperOptionsType = { 191 | switchAttributes?: boolean, 192 | switchRelationships?: boolean, 193 | switchType?: boolean, 194 | switchChar?: string, 195 | }; 196 | export type SwitchCaseJsonMapperOptionsType = { 197 | camelizeAttributes?: boolean, 198 | camelizeRelationships?: boolean, 199 | camelizeType?: boolean, 200 | camelizeMeta?: boolean, 201 | switchChar?: string, 202 | }; 203 | -------------------------------------------------------------------------------- /src/builders/JsonDeserializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IJsonPropertiesMapper, 3 | TJsonaModel, 4 | TJsonaRelationships, 5 | TJsonApiBody, 6 | TJsonApiData, 7 | IJsonaDeserializer, 8 | IDeserializeCache, 9 | TResourceIdObj, 10 | } from '../JsonaTypes'; 11 | 12 | export class JsonDeserializer implements IJsonaDeserializer { 13 | 14 | protected pm: IJsonPropertiesMapper; 15 | protected dc: IDeserializeCache; 16 | protected body; 17 | protected dataInObject; 18 | protected preferNestedDataFromData = false; 19 | protected includedInObject; 20 | 21 | constructor(propertiesMapper, deserializeCache, options) { 22 | this.setPropertiesMapper(propertiesMapper); 23 | this.setDeserializeCache(deserializeCache); 24 | 25 | if (!options) { 26 | return; 27 | } 28 | 29 | if (options.preferNestedDataFromData) { 30 | this.preferNestedDataFromData = true; 31 | } 32 | } 33 | 34 | setDeserializeCache(dc): void { 35 | this.dc = dc; 36 | } 37 | 38 | setPropertiesMapper(pm): void { 39 | this.pm = pm; 40 | } 41 | 42 | setJsonParsedObject(body: TJsonApiBody): void { 43 | this.body = body; 44 | } 45 | 46 | build(): TJsonaModel | Array { 47 | const {data} = this.body; 48 | let stuff; 49 | 50 | if (Array.isArray(data)) { 51 | stuff = []; 52 | const collectionLength = data.length; 53 | 54 | for (let i = 0; i < collectionLength; i++) { 55 | if (data[i]) { 56 | const model = this.buildModelByData(data[i]); 57 | 58 | if (model) { 59 | stuff.push(model); 60 | } 61 | } 62 | } 63 | } else if (data) { 64 | stuff = this.buildModelByData(data); 65 | } 66 | 67 | return stuff; 68 | } 69 | 70 | buildModelByData(data: TJsonApiData, resourceIdObj?: TResourceIdObj): TJsonaModel { 71 | const cachedModel = this.dc.getCachedModel(data, resourceIdObj); 72 | 73 | if (cachedModel) { 74 | return cachedModel; 75 | } 76 | 77 | const model = this.pm.createModel(data.type); 78 | 79 | this.dc.handleModel(model, data, resourceIdObj); // should be called before this.pm.setRelationships(model, relationships); 80 | 81 | if (model) { 82 | this.pm.setId(model, data.id); 83 | 84 | if (data.attributes) { 85 | this.pm.setAttributes(model, data.attributes); 86 | } 87 | 88 | if (data.meta && this.pm.setMeta) { 89 | this.pm.setMeta(model, data.meta); 90 | } 91 | 92 | if (data.links && this.pm.setLinks) { 93 | this.pm.setLinks(model, data.links); 94 | } 95 | 96 | if (resourceIdObj?.meta) { 97 | this.pm.setResourceIdObjMeta(model, resourceIdObj.meta); 98 | } 99 | 100 | const relationships: null | TJsonaRelationships = this.buildRelationsByData(data, model); 101 | 102 | if (relationships) { 103 | this.pm.setRelationships(model, relationships); 104 | } 105 | } 106 | 107 | return model; 108 | } 109 | 110 | buildRelationsByData(data: TJsonApiData, model: TJsonaModel): TJsonaRelationships | null { 111 | const readyRelations = {}; 112 | 113 | if (data.relationships) { 114 | for (let k in data.relationships) { 115 | const relation = data.relationships[k]; 116 | 117 | if (Array.isArray(relation.data)) { 118 | readyRelations[k] = []; 119 | 120 | const relationDataLength = relation.data.length; 121 | let resourceIdObj; 122 | 123 | for (let i = 0; i < relationDataLength; i++) { 124 | resourceIdObj = relation.data[i]; 125 | 126 | if (!resourceIdObj) { 127 | continue; 128 | } 129 | 130 | let dataItem = this.buildDataFromIncludedOrData( 131 | resourceIdObj.id, 132 | resourceIdObj.type 133 | ); 134 | readyRelations[k].push( 135 | this.buildModelByData(dataItem, resourceIdObj) 136 | ); 137 | } 138 | } else if (relation.data) { 139 | let dataItem = this.buildDataFromIncludedOrData(relation.data.id, relation.data.type); 140 | readyRelations[k] = this.buildModelByData(dataItem, relation.data); 141 | } else if (relation.data === null) { 142 | readyRelations[k] = null; 143 | } 144 | 145 | if (relation.links) { 146 | const {setRelationshipLinks} = this.pm; 147 | if (setRelationshipLinks) { 148 | setRelationshipLinks(model, k, relation.links); 149 | } 150 | } 151 | 152 | if (relation.meta) { 153 | const {setRelationshipMeta} = this.pm; 154 | if (setRelationshipMeta) { 155 | setRelationshipMeta(model, k, relation.meta); 156 | } 157 | } 158 | } 159 | } 160 | 161 | if (Object.keys(readyRelations).length) { 162 | return readyRelations; 163 | } 164 | 165 | return null; 166 | } 167 | 168 | buildDataFromIncludedOrData(id: string | number, type: string): TJsonApiData { 169 | 170 | if (this.preferNestedDataFromData) { 171 | const dataObject = this.buildDataInObject(); 172 | const dataItemFromData = dataObject[type + id]; 173 | 174 | if (dataItemFromData) { 175 | return dataItemFromData; 176 | } 177 | } 178 | 179 | const includedObject = this.buildIncludedInObject(); 180 | const dataItemFromIncluded = includedObject[type + id]; 181 | 182 | if (dataItemFromIncluded) { 183 | return dataItemFromIncluded; 184 | } 185 | 186 | if (!this.preferNestedDataFromData) { 187 | const dataObject = this.buildDataInObject(); 188 | const dataItemFromData = dataObject[type + id]; 189 | 190 | if (dataItemFromData) { 191 | return dataItemFromData; 192 | } 193 | } 194 | 195 | return { id: id, type: type }; 196 | } 197 | 198 | buildDataInObject(): { [key: string]: TJsonApiData } { 199 | if (!this.dataInObject) { 200 | this.dataInObject = {}; 201 | 202 | const { data } = this.body; 203 | const dataLength = data.length; 204 | 205 | if (data && dataLength) { 206 | for (let i = 0; i < dataLength; i++) { 207 | let item = data[i]; 208 | this.dataInObject[item.type + item.id] = item; 209 | } 210 | } else if (data) { 211 | this.dataInObject[data.type + data.id] = data; 212 | } 213 | } 214 | 215 | return this.dataInObject; 216 | } 217 | 218 | buildIncludedInObject(): { [key: string]: TJsonApiData } { 219 | if (!this.includedInObject) { 220 | this.includedInObject = {}; 221 | 222 | if (this.body.included) { 223 | let includedLength = this.body.included.length; 224 | for (let i = 0; i < includedLength; i++) { 225 | let item = this.body.included[i]; 226 | this.includedInObject[item.type + item.id] = item; 227 | } 228 | } 229 | } 230 | 231 | return this.includedInObject; 232 | } 233 | 234 | } 235 | 236 | export default JsonDeserializer; -------------------------------------------------------------------------------- /src/builders/ModelsSerializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TJsonaModel, 3 | TJsonApiBody, 4 | TJsonApiData, 5 | TJsonaDenormalizedIncludeNames, 6 | TJsonaNormalizedIncludeNamesTree, 7 | TJsonaUniqueIncluded, 8 | IModelPropertiesMapper, 9 | IModelsSerializer, 10 | } from '../JsonaTypes'; 11 | 12 | import {createIncludeNamesTree} from '../utils'; 13 | 14 | export class ModelsSerializer implements IModelsSerializer { 15 | 16 | protected propertiesMapper: IModelPropertiesMapper; 17 | protected stuff: TJsonaModel | Array; 18 | protected includeNamesTree: TJsonaNormalizedIncludeNamesTree; 19 | private buildIncludedIndex: number; 20 | 21 | constructor(propertiesMapper?: IModelPropertiesMapper) { 22 | propertiesMapper && this.setPropertiesMapper(propertiesMapper); 23 | this.buildIncludedIndex = 0; 24 | } 25 | 26 | setPropertiesMapper(propertiesMapper: IModelPropertiesMapper) { 27 | this.propertiesMapper = propertiesMapper; 28 | } 29 | 30 | setStuff(stuff: TJsonaModel | TJsonaModel[]) { 31 | this.stuff = stuff; 32 | } 33 | 34 | setIncludeNames(includeNames: TJsonaDenormalizedIncludeNames | TJsonaNormalizedIncludeNamesTree) { 35 | if (Array.isArray(includeNames)) { 36 | const includeNamesTree = {}; 37 | includeNames.forEach((namesChain) => { 38 | createIncludeNamesTree(namesChain, includeNamesTree); 39 | }); 40 | this.includeNamesTree = includeNamesTree; 41 | } else { 42 | this.includeNamesTree = includeNames; 43 | } 44 | } 45 | 46 | build(): TJsonApiBody { 47 | const {stuff, propertiesMapper} = this; 48 | 49 | if (!propertiesMapper || typeof propertiesMapper !== 'object') { 50 | throw new Error('ModelsSerializer cannot build, propertiesMapper is not set'); 51 | } else if (!stuff || typeof stuff !== 'object') { 52 | throw new Error('ModelsSerializer cannot build, stuff is not set'); 53 | } 54 | 55 | const body: TJsonApiBody = {}; 56 | const uniqueIncluded: TJsonaUniqueIncluded = {}; 57 | 58 | if (stuff && Array.isArray(stuff)) { 59 | const collectionLength = stuff.length; 60 | const data = []; 61 | 62 | for (let i = 0; i < collectionLength; i++) { 63 | data.push( 64 | this.buildDataByModel(stuff[i]) 65 | ); 66 | 67 | this.buildIncludedByModel( 68 | stuff[i], 69 | this.includeNamesTree, 70 | uniqueIncluded 71 | ); 72 | } 73 | 74 | body['data'] = data; 75 | 76 | } else if (stuff) { 77 | body['data'] = this.buildDataByModel(stuff); 78 | 79 | this.buildIncludedByModel( 80 | stuff, 81 | this.includeNamesTree, 82 | uniqueIncluded 83 | ); 84 | } else if (stuff === null) { 85 | body['data'] = null; 86 | } 87 | 88 | if (Object.keys(uniqueIncluded).length) { 89 | body['included'] = Object.values(uniqueIncluded); 90 | } 91 | 92 | return body; 93 | } 94 | 95 | buildDataByModel(model: TJsonaModel | null): TJsonApiData { 96 | const id = this.propertiesMapper.getId(model); 97 | const type = this.propertiesMapper.getType(model); 98 | const attributes = this.propertiesMapper.getAttributes(model); 99 | const data = { type, 100 | ...(typeof id !== 'undefined' ? { id } : {}), 101 | ...(typeof attributes !== 'undefined' ? { attributes } : {}), 102 | }; 103 | 104 | if (typeof data.type !== 'string' || !data.type) { 105 | console.warn('ModelsSerializer cannot buildDataByModel, type is not set or incorrect', model); 106 | throw new Error('ModelsSerializer cannot buildDataByModel, type is not set or incorrect'); 107 | } 108 | 109 | const relationships = this.buildRelationshipsByModel(model); 110 | 111 | if (relationships && Object.keys(relationships).length) { 112 | data['relationships'] = relationships; 113 | } 114 | 115 | return data; 116 | } 117 | 118 | buildResourceObjectPart(relation: TJsonaModel) { 119 | const id = this.propertiesMapper.getId(relation); 120 | const type = this.propertiesMapper.getType(relation); 121 | 122 | return { 123 | type, 124 | ...(typeof id === 'undefined' ? {} : { id }), 125 | }; 126 | } 127 | 128 | buildRelationshipsByModel(model: TJsonaModel) { 129 | const relations = this.propertiesMapper.getRelationships(model); 130 | 131 | if (!relations || !Object.keys(relations).length) { 132 | return; 133 | } 134 | 135 | const relationships = {}; 136 | 137 | Object.keys(relations).forEach((k) => { 138 | const relation = relations[k]; 139 | 140 | if (Array.isArray(relation)) { 141 | const relationshipData = []; 142 | 143 | for (const relationItem of relation) { 144 | const relationshipDataItem = this.buildResourceObjectPart(relationItem); 145 | 146 | if ('type' in relationshipDataItem) { 147 | relationshipData.push(relationshipDataItem); 148 | } else { 149 | console.error( 150 | `Can't create data item for relationship ${k}, 151 | it doesn't have 'id' or 'type', it was skipped`, 152 | relationItem 153 | ); 154 | } 155 | } 156 | 157 | relationships[k] = { 158 | data: relationshipData 159 | }; 160 | } else if (relation) { 161 | const item = this.buildResourceObjectPart(relation); 162 | 163 | if ('type' in item) { 164 | relationships[k] = { 165 | data: item 166 | }; 167 | } else { 168 | console.error( 169 | `Can't create data for relationship ${k}, it doesn't have 'type', it was skipped`, 170 | relation 171 | ); 172 | } 173 | } else { 174 | relationships[k] = { 175 | data: relation 176 | }; 177 | } 178 | }); 179 | 180 | return relationships; 181 | } 182 | 183 | buildIncludedByModel( 184 | model: TJsonaModel, 185 | includeTree: TJsonaNormalizedIncludeNamesTree, 186 | builtIncluded: TJsonaUniqueIncluded = {} 187 | ): void { 188 | if (!includeTree || !Object.keys(includeTree).length) { 189 | return; 190 | } 191 | 192 | const modelRelationships = this.propertiesMapper.getRelationships(model); 193 | if (!modelRelationships || !Object.keys(modelRelationships).length) { 194 | return; 195 | } 196 | 197 | const includeNames = Object.keys(includeTree); 198 | const includeNamesLength = includeNames.length; 199 | 200 | for (let i = 0; i < includeNamesLength; i++) { 201 | const currentRelationName = includeNames[i]; 202 | const relation: TJsonaModel | Array = modelRelationships[currentRelationName]; 203 | 204 | if (relation) { 205 | if (Array.isArray(relation)) { 206 | let relationModelsLength = relation.length; 207 | 208 | for (let r = 0; r < relationModelsLength; r++) { 209 | const relationModel: TJsonaModel = relation[r]; 210 | this.buildIncludedItem(relationModel, includeTree[currentRelationName], builtIncluded); 211 | } 212 | } else { 213 | this.buildIncludedItem(relation, includeTree[currentRelationName], builtIncluded); 214 | } 215 | } 216 | } 217 | } 218 | 219 | buildIncludedItem( 220 | relationModel: TJsonaModel, 221 | subIncludeTree: TJsonaNormalizedIncludeNamesTree, 222 | builtIncluded: TJsonaUniqueIncluded 223 | ) { 224 | const id = this.propertiesMapper.getId(relationModel); 225 | const type = this.propertiesMapper.getType(relationModel); 226 | let includeKey = type + id; 227 | 228 | if (!id || !builtIncluded[includeKey]) { 229 | // create data by current entity if such included is not yet created 230 | if (includeKey in builtIncluded) { 231 | includeKey += this.buildIncludedIndex; 232 | this.buildIncludedIndex += 1; 233 | } 234 | builtIncluded[includeKey] = this.buildDataByModel(relationModel); 235 | 236 | if (subIncludeTree) { 237 | this.buildIncludedByModel(relationModel, subIncludeTree, builtIncluded); 238 | } 239 | } 240 | } 241 | 242 | } 243 | 244 | export default ModelsSerializer; -------------------------------------------------------------------------------- /src/builders/ReduxObjectDenormalizer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IJsonaModelBuilder, 3 | IJsonPropertiesMapper, 4 | TJsonaModel, 5 | TJsonaRelationships, 6 | TReduxObject, 7 | TReduxObjectModel, 8 | TJsonApiRelationships, 9 | TJsonApiRelationshipData 10 | } from '../JsonaTypes'; 11 | 12 | class ReduxObjectDenormalizer implements IJsonaModelBuilder { 13 | 14 | protected propertiesMapper: IJsonPropertiesMapper; 15 | protected reduxObject: TReduxObject; 16 | protected entityType: string; 17 | protected ids?: string | Array; 18 | protected returnBuilderInRelations: boolean; 19 | protected cachedModels = {}; 20 | 21 | constructor(propertiesMapper) { 22 | this.setPropertiesMapper(propertiesMapper); 23 | } 24 | 25 | setPropertiesMapper(propertiesMapper: IJsonPropertiesMapper) { 26 | this.propertiesMapper = propertiesMapper; 27 | } 28 | 29 | setReduxObject(reduxObject: TReduxObject) { 30 | this.reduxObject = reduxObject; 31 | } 32 | setEntityType(entityType: string) { 33 | this.entityType = entityType; 34 | } 35 | 36 | setEntityIds(ids: string | Array) { 37 | this.ids = ids; 38 | } 39 | 40 | setReturnBuilderInRelations(returnBuilderInRelations: boolean) { 41 | this.returnBuilderInRelations = returnBuilderInRelations; 42 | } 43 | 44 | build(): null | TJsonaModel | Array { 45 | const {reduxObject, entityType, propertiesMapper} = this; 46 | 47 | if (!propertiesMapper || typeof propertiesMapper !== 'object') { 48 | throw new Error('ReduxObjectDenormalizer cannot build, propertiesMapper is not set'); 49 | } else if (!reduxObject || typeof reduxObject !== 'object') { 50 | throw new Error('ReduxObjectDenormalizer cannot build, reduxObject is not set'); 51 | } else if (!entityType) { 52 | throw new Error('ReduxObjectDenormalizer cannot build, entityType is not set'); 53 | } 54 | 55 | if (!reduxObject[entityType]) { 56 | return null; 57 | } 58 | 59 | let {ids} = this; 60 | 61 | if (!ids) { 62 | ids = Object.keys(reduxObject[entityType]); 63 | } 64 | 65 | if (Array.isArray(ids)) { 66 | 67 | if (!ids.length) { 68 | return null; 69 | } 70 | 71 | const models = []; 72 | 73 | ids.forEach((id) => { 74 | const model = this.buildModel(entityType, id); 75 | if (model) { 76 | models.push(model); 77 | } 78 | }); 79 | 80 | return models; 81 | } 82 | 83 | return this.buildModel(entityType, ids); 84 | } 85 | 86 | buildModel(type: string, id: string | number): null | TJsonaModel { 87 | const {reduxObject} = this; 88 | 89 | if (!reduxObject[type]) { 90 | return null; 91 | } 92 | 93 | const reduxObjectModel: TReduxObjectModel = reduxObject[type][id]; 94 | 95 | if (!reduxObjectModel) { 96 | return null; 97 | } 98 | 99 | // checks for built model in cachedModels is a protection from creating models on recursive relationships 100 | const entityKey = `${type}-${id}`; 101 | let model = this.cachedModels[entityKey]; 102 | 103 | if (!model) { 104 | model = this.propertiesMapper.createModel(type); 105 | 106 | if (model) { 107 | this.cachedModels[entityKey] = model; 108 | 109 | this.propertiesMapper.setId(model, reduxObjectModel.id); 110 | 111 | if (reduxObjectModel.attributes) { 112 | this.propertiesMapper.setAttributes(model, reduxObjectModel.attributes); 113 | } 114 | 115 | const relationships = this.buildRelationships(model, reduxObjectModel.relationships); 116 | 117 | if (relationships) { 118 | this.propertiesMapper.setRelationships(model, relationships) 119 | } 120 | } 121 | } 122 | 123 | return model; 124 | } 125 | 126 | buildRelationships(model: TJsonaModel, reduxObjectRelationships: TJsonApiRelationships): null | TJsonaRelationships { 127 | 128 | if (!reduxObjectRelationships) { 129 | return null; 130 | } 131 | const relationNames = Object.keys(reduxObjectRelationships); 132 | 133 | if (!relationNames.length) { 134 | return null; 135 | } 136 | 137 | const relations = {}; 138 | 139 | relationNames.forEach((relationName) => { 140 | const relation = reduxObjectRelationships[relationName]; 141 | 142 | if (relation && relation.data) { 143 | if (this.returnBuilderInRelations) { 144 | relations[relationName] = this.buildRelationModels.bind(this, relation.data); 145 | } else { 146 | relations[relationName] = this.buildRelationModels(relation.data); 147 | } 148 | } 149 | 150 | if (relation && relation.links) { 151 | this.propertiesMapper.setRelationshipLinks(model, relationName, relation.links); 152 | } 153 | 154 | if (relation && relation.meta) { 155 | const {setRelationshipMeta} = this.propertiesMapper; 156 | if (setRelationshipMeta) { // support was added in patch release 157 | setRelationshipMeta(model, relationName, relation.meta); 158 | } 159 | } 160 | }); 161 | 162 | return Object.keys(relations).length ? relations : null; 163 | } 164 | 165 | buildRelationModels( 166 | data: TJsonApiRelationshipData | Array 167 | ): null | TJsonaModel | Array { 168 | 169 | if (Array.isArray(data)) { 170 | const relationModels = []; 171 | 172 | data.forEach((dataItem) => { 173 | const model = this.buildModel(dataItem.type, dataItem.id); 174 | relationModels.push(model || dataItem); 175 | }); 176 | 177 | return relationModels; 178 | } else if (data.id && data.type) { 179 | return this.buildModel(data.type, data.id) || data; 180 | } 181 | 182 | return null; 183 | } 184 | } 185 | 186 | export default ReduxObjectDenormalizer; 187 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IDeserializeCache, TAnyKeyValueObject, TJsonaModel, TJsonApiData, TResourceIdObj 3 | } from './JsonaTypes'; 4 | 5 | 6 | export function jsonStringify(json: TAnyKeyValueObject): string { 7 | let stringified; 8 | 9 | try { 10 | stringified = JSON.stringify(json); 11 | } catch (e) { 12 | stringified = ''; 13 | console.warn(e); 14 | } 15 | 16 | return stringified; 17 | } 18 | 19 | export class DeserializeCache implements IDeserializeCache { 20 | 21 | protected cachedModels = {}; 22 | 23 | getCachedModel(data:TJsonApiData, resourceIdObject: TResourceIdObj) { 24 | const entityKey = this.createCacheKey(data, resourceIdObject); 25 | return this.cachedModels[entityKey] || null; 26 | } 27 | 28 | handleModel(model: TJsonaModel, data: TJsonApiData, resourceIdObject: TResourceIdObj) { 29 | const entityKey = this.createCacheKey(data, resourceIdObject); 30 | const dataWithPayload = data.attributes || data.relationships; 31 | 32 | if (entityKey && dataWithPayload) { 33 | this.cachedModels[entityKey] = model; 34 | } 35 | } 36 | 37 | createCacheKey(data, resourceIdObject?: TResourceIdObj) { 38 | // resourceIdObject.meta sets to model in simplePropertyMappers.ts, so it should be used here too 39 | // cache in this case probably will be redundant 40 | if (!data.id || !data.type) { 41 | return; 42 | } 43 | 44 | let resourcePart = resourceIdObject ? `${resourceIdObject.type}-${resourceIdObject.id}` : ''; 45 | 46 | if (resourceIdObject?.meta) { 47 | resourcePart += `-${jsonStringify(resourceIdObject.meta)}`; 48 | } 49 | 50 | if (data.meta) { 51 | return `${data.type}-${data.id}-${jsonStringify(data.meta)}-${resourcePart}`; 52 | } 53 | 54 | return `${data.type}-${data.id}-${resourcePart}`; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Jsona from './Jsona'; 2 | import ModelsSerializer from './builders/ModelsSerializer'; 3 | import JsonDeserializer from './builders/JsonDeserializer'; 4 | import {ModelPropertiesMapper, JsonPropertiesMapper} from './simplePropertyMappers'; 5 | import {SwitchCaseModelMapper, SwitchCaseJsonMapper} from './switchCasePropertyMappers'; 6 | 7 | export { 8 | Jsona, 9 | ModelsSerializer, 10 | JsonDeserializer, 11 | ModelPropertiesMapper, 12 | JsonPropertiesMapper, 13 | SwitchCaseModelMapper, 14 | SwitchCaseJsonMapper, 15 | }; 16 | 17 | export default Jsona; 18 | -------------------------------------------------------------------------------- /src/simplePropertyMappers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IModelPropertiesMapper, 3 | IJsonPropertiesMapper, 4 | TAnyKeyValueObject, 5 | TJsonaModel, 6 | TJsonaRelationships, TJsonaRelationshipBuild, TJsonApiLinks, TResourceIdObj 7 | } from './JsonaTypes'; 8 | 9 | export const RELATIONSHIP_NAMES_PROP = 'relationshipNames'; 10 | 11 | export class ModelPropertiesMapper implements IModelPropertiesMapper { 12 | 13 | getId(model: TJsonaModel) { 14 | return model.id; 15 | } 16 | 17 | getType(model: TJsonaModel) { 18 | return model.type; 19 | } 20 | 21 | getAttributes(model: TJsonaModel) { 22 | let exceptProps = ['id', 'type', RELATIONSHIP_NAMES_PROP]; 23 | 24 | if (Array.isArray(model[RELATIONSHIP_NAMES_PROP])) { 25 | 26 | exceptProps.push(...model[RELATIONSHIP_NAMES_PROP]); 27 | 28 | } else if (model[RELATIONSHIP_NAMES_PROP]) { 29 | console.warn( 30 | `Can't getAttributes correctly, '${RELATIONSHIP_NAMES_PROP}' property of ${model.type}-${model.id} model 31 | isn't array of relationship names`, 32 | model[RELATIONSHIP_NAMES_PROP] 33 | ); 34 | } 35 | 36 | const attributes = {}; 37 | Object.keys(model).forEach((attrName) => { 38 | if (exceptProps.indexOf(attrName) === -1) { 39 | attributes[attrName] = model[attrName]; 40 | } 41 | }); 42 | 43 | return attributes; 44 | } 45 | 46 | getRelationships(model: TJsonaModel) { 47 | const relationshipNames = model[RELATIONSHIP_NAMES_PROP]; 48 | 49 | if (!relationshipNames || Array.isArray(relationshipNames) && !relationshipNames.length) { 50 | return; 51 | } else if (relationshipNames && !Array.isArray(relationshipNames)) { 52 | console.warn( 53 | `Can't getRelationships correctly, 54 | '${RELATIONSHIP_NAMES_PROP}' property of ${model.type}-${model.id} model 55 | isn't array of relationship names`, 56 | model[RELATIONSHIP_NAMES_PROP] 57 | ); 58 | return; 59 | } 60 | 61 | const relationships = {}; 62 | relationshipNames.forEach((relationName) => { 63 | if (model[relationName] !== undefined) { 64 | relationships[relationName] = model[relationName]; 65 | } 66 | }); 67 | return relationships; 68 | } 69 | } 70 | 71 | export function defineRelationGetter( 72 | model, 73 | relationName, 74 | buildRelation: TJsonaRelationshipBuild 75 | ) { 76 | Object.defineProperty( 77 | model, 78 | relationName, 79 | { 80 | enumerable: true, 81 | configurable: true, 82 | set: (value) => { 83 | delete model[relationName]; 84 | model[relationName] = value; 85 | }, 86 | get: () => { 87 | delete model[relationName]; 88 | return model[relationName] = buildRelation(); 89 | }, 90 | }, 91 | ); 92 | } 93 | 94 | export class JsonPropertiesMapper implements IJsonPropertiesMapper { 95 | 96 | createModel(type: string): TJsonaModel { 97 | return {type}; 98 | } 99 | 100 | setId(model: TJsonaModel, id: string | number) { 101 | model.id = id; 102 | } 103 | 104 | setAttributes(model: TJsonaModel, attributes: TAnyKeyValueObject) { 105 | Object.keys(attributes).forEach((propName) => { 106 | model[propName] = attributes[propName]; 107 | }); 108 | } 109 | 110 | setMeta(model: TJsonaModel, meta: TAnyKeyValueObject) { 111 | model.meta = meta; 112 | } 113 | 114 | setLinks(model: TJsonaModel, links: TJsonApiLinks) { 115 | model.links = links; 116 | } 117 | 118 | setResourceIdObjMeta(model: TJsonaModel, meta: TResourceIdObj) { 119 | model.resourceIdObjMeta = meta; 120 | } 121 | 122 | setRelationships(model: TJsonaModel, relationships: TJsonaRelationships) { 123 | 124 | Object.keys(relationships).forEach((propName) => { 125 | if (typeof relationships[propName] === 'function') { 126 | defineRelationGetter(model, propName, relationships[propName]); 127 | } else { 128 | model[propName] = relationships[propName]; 129 | } 130 | }); 131 | 132 | const newNames = Object.keys(relationships); 133 | const currentNames = model[RELATIONSHIP_NAMES_PROP]; 134 | 135 | if (currentNames && currentNames.length) { 136 | model[RELATIONSHIP_NAMES_PROP] = [...currentNames, ...newNames].filter((value, i, self) => self.indexOf(value) === i); 137 | } else { 138 | model[RELATIONSHIP_NAMES_PROP] = newNames; 139 | } 140 | } 141 | 142 | setRelationshipLinks(parentModel: TJsonaModel, relationName: string, links: TJsonApiLinks) { 143 | // inherit your IJsonPropertiesMapper and overload this method, if you want to handle relationship links 144 | } 145 | 146 | setRelationshipMeta(parentModel: TJsonaModel, relationName: string, links: TAnyKeyValueObject) { 147 | // inherit your IJsonPropertiesMapper and overload this method, if you want to handle relationship meta 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/switchCasePropertyMappers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IModelPropertiesMapper, 3 | IJsonPropertiesMapper, 4 | TAnyKeyValueObject, 5 | TJsonaModel, TJsonaRelationships, 6 | SwitchCaseModelMapperOptionsType, 7 | SwitchCaseJsonMapperOptionsType, 8 | } from './JsonaTypes'; 9 | import { 10 | ModelPropertiesMapper, 11 | JsonPropertiesMapper, 12 | RELATIONSHIP_NAMES_PROP, 13 | } from './simplePropertyMappers'; 14 | import { 15 | isPlainObject 16 | } from './utils'; 17 | 18 | export class SwitchCaseModelMapper extends ModelPropertiesMapper implements IModelPropertiesMapper { 19 | 20 | switchAttributes: boolean; 21 | switchRelationships: boolean; 22 | switchType: boolean; 23 | switchChar: string; 24 | regex: RegExp; 25 | 26 | constructor(options?: SwitchCaseModelMapperOptionsType) { 27 | super(); 28 | 29 | const { 30 | switchAttributes = true, 31 | switchRelationships = true, 32 | switchType = true, 33 | switchChar = '-', 34 | } = options || {}; 35 | 36 | this.switchAttributes = switchAttributes; 37 | this.switchRelationships = switchRelationships; 38 | this.switchType = switchType; 39 | this.switchChar = switchChar; 40 | this.regex = new RegExp(/([a-z][A-Z0-9])/g); 41 | } 42 | 43 | getType(model: TJsonaModel) { 44 | const type = super.getType(model); 45 | 46 | if (!this.switchType || !type) { 47 | return type; 48 | } 49 | 50 | return this.convertFromCamelCaseString(type); 51 | } 52 | 53 | getAttributes(model: TJsonaModel) { 54 | const camelCasedAttributes = super.getAttributes(model); 55 | 56 | if (!this.switchAttributes || !camelCasedAttributes) { 57 | return camelCasedAttributes; 58 | } 59 | 60 | return this.convertFromCamelCase(camelCasedAttributes); 61 | } 62 | 63 | getRelationships(model: TJsonaModel) { 64 | const camelCasedRelationships = super.getRelationships(model); 65 | 66 | if (!this.switchRelationships || !camelCasedRelationships) { 67 | return camelCasedRelationships; 68 | } 69 | 70 | return this.convertFromCamelCase(camelCasedRelationships); 71 | } 72 | 73 | private convertFromCamelCase(stuff: unknown) { 74 | if (Array.isArray(stuff)) { 75 | return stuff.map(item => this.convertFromCamelCase(item)); 76 | } 77 | 78 | if(isPlainObject(stuff)) { 79 | const converted = {}; 80 | Object.entries(stuff).forEach(([propName, value]) => { 81 | const kebabName = this.convertFromCamelCaseString(propName); 82 | converted[kebabName] = this.convertFromCamelCase(value); 83 | }) 84 | return converted; 85 | } 86 | 87 | return stuff; 88 | } 89 | 90 | private convertFromCamelCaseString(camelCaseString: string) { 91 | return camelCaseString.replace(this.regex, g => g[0] + this.switchChar + g[1].toLowerCase()); 92 | } 93 | } 94 | 95 | export class SwitchCaseJsonMapper extends JsonPropertiesMapper implements IJsonPropertiesMapper { 96 | 97 | camelizeAttributes: boolean; 98 | camelizeRelationships: boolean; 99 | camelizeType: boolean; 100 | camelizeMeta: boolean; 101 | switchChar: string; 102 | regex: RegExp; 103 | 104 | constructor(options?: SwitchCaseJsonMapperOptionsType) { 105 | super(); 106 | 107 | const { 108 | camelizeAttributes = true, 109 | camelizeRelationships = true, 110 | camelizeType = true, 111 | camelizeMeta = false, 112 | switchChar = '-' 113 | } = options || {}; 114 | 115 | this.camelizeAttributes = camelizeAttributes; 116 | this.camelizeRelationships = camelizeRelationships; 117 | this.camelizeType = camelizeType; 118 | this.camelizeMeta = camelizeMeta; 119 | this.switchChar = switchChar; 120 | this.regex = new RegExp(`${this.switchChar}([a-z0-9])`, 'g'); 121 | } 122 | 123 | createModel(type: string): TJsonaModel { 124 | if (!this.camelizeType) { 125 | return {type}; 126 | } 127 | 128 | const camelizedType = this.convertToCamelCaseString(type); 129 | return {type: camelizedType}; 130 | } 131 | 132 | setAttributes(model: TJsonaModel, attributes: TAnyKeyValueObject) { 133 | if (!this.camelizeAttributes) { 134 | return super.setAttributes(model, attributes); 135 | } 136 | 137 | Object.assign(model, this.convertToCamelCase(attributes)); 138 | } 139 | 140 | setMeta(model: TJsonaModel, meta: TAnyKeyValueObject) { 141 | if (!this.camelizeMeta) { 142 | return super.setMeta(model, meta); 143 | } 144 | 145 | model.meta = this.convertToCamelCase(meta); 146 | } 147 | 148 | setRelationships(model: TJsonaModel, relationships: TJsonaRelationships) { 149 | // call super.setRelationships first, just for not to copy paste setRelationships logic 150 | super.setRelationships(model, relationships); 151 | 152 | if (!this.camelizeRelationships) { 153 | return; 154 | } 155 | 156 | // then change relationship names case if needed 157 | model[RELATIONSHIP_NAMES_PROP].forEach((kebabName, i) => { 158 | const camelName = this.convertToCamelCaseString(kebabName); 159 | if (camelName !== kebabName) { 160 | model[camelName] = model[kebabName]; 161 | delete model[kebabName]; 162 | model[RELATIONSHIP_NAMES_PROP][i] = camelName; 163 | } 164 | }); 165 | } 166 | 167 | private convertToCamelCase(stuff: unknown) { 168 | if (Array.isArray(stuff)) { 169 | return stuff.map(item => this.convertToCamelCase(item)); 170 | } 171 | 172 | if(isPlainObject(stuff)) { 173 | const converted = {}; 174 | Object.entries(stuff).forEach(([propName, value]) => { 175 | const camelName = this.convertToCamelCaseString(propName); 176 | converted[camelName] = this.convertToCamelCase(value); 177 | }); 178 | return converted; 179 | } 180 | 181 | return stuff; 182 | } 183 | 184 | convertToCamelCaseString(notCamelCaseString: string) { 185 | return notCamelCaseString.replace(this.regex, g => g[1].toUpperCase()); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TJsonaIncludeNamesChain, 3 | TJsonaNormalizedIncludeNamesTree 4 | } from './JsonaTypes'; 5 | 6 | export function createIncludeNamesTree( 7 | namesChain: TJsonaIncludeNamesChain, 8 | includeTree: TJsonaNormalizedIncludeNamesTree, 9 | ): void { 10 | const namesArray = namesChain.split('.'); 11 | const currentIncludeName = namesArray.shift(); 12 | const chainHasMoreNames = namesArray.length; 13 | 14 | let subTree = null; 15 | 16 | if (chainHasMoreNames) { 17 | subTree = includeTree[currentIncludeName] || {}; 18 | createIncludeNamesTree(namesArray.join('.'), subTree); 19 | } 20 | 21 | includeTree[currentIncludeName] = subTree; 22 | } 23 | 24 | export function jsonParse(stringified: string): Object { 25 | let parsed; 26 | 27 | try { 28 | parsed = JSON.parse(stringified); 29 | } catch (e) { 30 | parsed = {}; 31 | console.warn(e); 32 | } 33 | 34 | return parsed; 35 | } 36 | 37 | // https://github.com/30-seconds/30-seconds-of-code/blob/master/snippets/isPlainObject.md 38 | export const isPlainObject = (val: unknown): val is Object => !!val && typeof val === 'object' && val.constructor === Object; -------------------------------------------------------------------------------- /tests/Jsona.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {expect} from 'chai'; 3 | import Jsona from '../src'; 4 | 5 | import { 6 | town1, 7 | town2, 8 | user1, 9 | user2, 10 | specialty1, 11 | specialty2, 12 | country1, 13 | country2, 14 | reduxObject1, 15 | circular, 16 | duplicate, 17 | reduxObjectWithCircular, 18 | withoutRootIdsMock, 19 | withNullRelationsMock, 20 | resourceIdObjMetaMock, 21 | differentAttrsInDataAndIncludedMock, 22 | circularWithMeta, 23 | userWithIdlessSpecialties, 24 | idlessSpecialty1, 25 | idlessSpecialty2, 26 | } from './mocks'; 27 | 28 | chai.config.showDiff = true; 29 | chai.config.truncateThreshold = 0; 30 | 31 | describe('Jsona', () => { 32 | const jsona = new Jsona(); 33 | 34 | it('should instantiate with fallback property mappers', () => { 35 | let jsona = new Jsona(); 36 | }); 37 | 38 | describe('serialize', () => { 39 | it('should throw Error if stuff is not passed', () => { 40 | expect(jsona.serialize.bind(jsona, null)).to.throw(Error); 41 | expect(jsona.serialize.bind(jsona, undefined)).to.throw(Error); 42 | expect(jsona.serialize.bind(jsona, {stuff: null})).to.throw(Error); 43 | }); 44 | 45 | it('should build json with item, without included', () => { 46 | const jsonBody = jsona.serialize({stuff: town1.model}); 47 | expect(jsonBody.data).to.be.deep.equal(town1.json); 48 | expect(jsonBody.included).to.be.equal(undefined); 49 | }); 50 | 51 | it('should build json with collection, with included', () => { 52 | const jsonBody = jsona.serialize({stuff: user2.model, includeNames: ['specialty', 'town.country']}); 53 | expect(jsonBody.data).to.be.deep.equal(user2.json); 54 | expect(jsonBody.included).to.be.deep.equal([specialty1.json, specialty2.json, town2.json, country2.json]); 55 | }); 56 | 57 | it('should build json with collection, with included resources that do not have ids', () => { 58 | const jsonBody = jsona.serialize({stuff: userWithIdlessSpecialties.model, includeNames: ['specialtyWithoutIds']}); 59 | expect(jsonBody.included).to.be.deep.equal([idlessSpecialty1.json, idlessSpecialty2.json]); 60 | }); 61 | 62 | it('should build json and save null relationships', () => { 63 | const jsonBody = jsona.serialize({stuff: withNullRelationsMock.collection}); 64 | expect(jsonBody.data).to.be.deep.equal(withNullRelationsMock.json); 65 | }); 66 | }); 67 | 68 | describe('deserialize', () => { 69 | it('should throw Error if body is not passed', () => { 70 | expect(jsona.deserialize.bind(jsona, null)).to.throw(Error); 71 | expect(jsona.deserialize.bind(jsona, undefined)).to.throw(Error); 72 | }); 73 | 74 | it('should deserialize item without included', () => { 75 | const userModel = jsona.deserialize({data: user2.json}); 76 | expect(userModel).to.be.deep.equal(user2.modelWithoutIncluded); 77 | }); 78 | 79 | it('should deserialize collection with included', () => { 80 | const townsCollection = jsona.deserialize({ 81 | data: [town1.json, town2.json], 82 | included: [country1.json, country2.json] 83 | }); 84 | expect(townsCollection).to.be.deep.equal([town1.model, town2.model]); 85 | }); 86 | 87 | it('should deserialize json with circular relationships', () => { 88 | const recursiveItem = jsona.deserialize(circular.json); 89 | expect(recursiveItem).to.be.deep.equal(circular.model); 90 | }); 91 | 92 | it('should deserialize json with meta & circular relationships', () => { 93 | const recursiveItem = jsona.deserialize(circularWithMeta.json); 94 | expect(recursiveItem).to.be.deep.equal(circularWithMeta.model); 95 | }); 96 | 97 | it('should deserialize json with duplicate relationships', () => { 98 | const duplicateItem = jsona.deserialize(duplicate.json, { preferNestedDataFromData: true }); 99 | expect(duplicateItem).to.be.deep.equal(duplicate.model); 100 | }); 101 | 102 | it('should deserialize json with data without root ids', () => { 103 | const collectionWithoutRootIds = jsona.deserialize({data: withoutRootIdsMock.json}); 104 | expect(collectionWithoutRootIds).to.be.deep.equal(withoutRootIdsMock.collection); 105 | }); 106 | 107 | it('should deserialize json and save null relationships', () => { 108 | const collectionWithNullRelations = jsona.deserialize({data: withNullRelationsMock.json}); 109 | expect(collectionWithNullRelations).to.be.deep.equal(withNullRelationsMock.collection); 110 | }); 111 | 112 | it('should deserialize resource id object meta field into resourceIdObjMeta', () => { 113 | const stuff = jsona.deserialize(resourceIdObjMetaMock.json); 114 | expect(stuff).to.be.deep.equal(resourceIdObjMetaMock.collection); 115 | }); 116 | 117 | it('should deserialize with different attrs for root object and related', () => { 118 | const stuff = jsona.deserialize(differentAttrsInDataAndIncludedMock.json); 119 | expect(stuff).to.be.deep.equal(differentAttrsInDataAndIncludedMock.collection); 120 | }); 121 | 122 | it('should new', () => { 123 | const stuff = jsona.deserialize(differentAttrsInDataAndIncludedMock.json); 124 | expect(stuff).to.be.deep.equal(differentAttrsInDataAndIncludedMock.collection); 125 | }); 126 | 127 | }); 128 | 129 | describe('denormalizeReduxObject', () => { 130 | 131 | it('should throw Error if reduxObject is not passed', () => { 132 | const denormalizeReduxObject = jsona.denormalizeReduxObject.bind(jsona, { 133 | reduxObject: null, 134 | }); 135 | expect(denormalizeReduxObject).to.throw(Error, 'reduxObject'); 136 | }); 137 | 138 | it('should throw Error if entityType is not passed', () => { 139 | const denormalizeReduxObject = jsona.denormalizeReduxObject.bind(jsona, { 140 | reduxObject: {}, 141 | }); 142 | expect(denormalizeReduxObject).to.throw(Error, 'entityType'); 143 | }); 144 | 145 | it('should return null if no such entityType in reduxObject', () => { 146 | const model = jsona.denormalizeReduxObject({ 147 | reduxObject: {}, 148 | entityType: 'myEntity' 149 | }); 150 | expect(model).to.be.equal(null); 151 | }); 152 | 153 | it('should return null if no such entityId in reduxObject', () => { 154 | const model = jsona.denormalizeReduxObject({ 155 | reduxObject: reduxObject1, 156 | entityType: 'article', 157 | entityIds: '1029' 158 | }); 159 | expect(model).to.be.equal(null); 160 | }); 161 | 162 | it('should return collection of models if entityIds is Array', () => { 163 | const models = jsona.denormalizeReduxObject({ 164 | reduxObject: reduxObject1, 165 | returnBuilderInRelations: false, 166 | entityType: 'town', 167 | entityIds: ['21', '80'] 168 | }); 169 | expect(models).to.be.an('array'); 170 | expect(models).to.be.deep.equal([town1.model, town2.model]); 171 | }); 172 | 173 | it('should return collection of models if entityIds is not passed', () => { 174 | const models = jsona.denormalizeReduxObject({ 175 | reduxObject: reduxObject1, 176 | returnBuilderInRelations: false, 177 | entityType: 'user', 178 | }); 179 | expect(models).to.be.an('array'); 180 | expect(models).to.be.deep.equal([user1.model, user2.model]); 181 | }); 182 | 183 | 184 | it('should denormalize json with circular relationships', () => { 185 | const model = jsona.denormalizeReduxObject({ 186 | reduxObject: reduxObjectWithCircular, 187 | returnBuilderInRelations: false, 188 | entityType: 'model', 189 | entityIds: '1' 190 | }); 191 | expect(model).to.be.deep.equal(circular.model); 192 | }); 193 | 194 | it('should denormalize model with relationships', () => { 195 | const model1 = jsona.denormalizeReduxObject({ 196 | reduxObject: reduxObject1, 197 | returnBuilderInRelations: false, 198 | entityType: 'town', 199 | entityIds: '21' 200 | }); 201 | const model2 = jsona.denormalizeReduxObject({ 202 | reduxObject: reduxObject1, 203 | returnBuilderInRelations: true, 204 | entityType: 'town', 205 | entityIds: '21' 206 | }); 207 | expect(model1['country']).to.be.deep.equal(country1.model); 208 | expect(model2['country']).to.be.deep.equal(country1.model); 209 | }); 210 | 211 | it('should allow to set relationships before denormalization', () => { 212 | const model = jsona.denormalizeReduxObject({ 213 | reduxObject: reduxObject1, 214 | returnBuilderInRelations: true, 215 | entityType: 'town', 216 | entityIds: '21' 217 | }); 218 | model['country']= country1.model; 219 | expect(model['country']).to.be.deep.equal(country1.model); 220 | }); 221 | 222 | }); 223 | 224 | }); 225 | -------------------------------------------------------------------------------- /tests/ModelsSerializer.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {expect} from 'chai'; 3 | import ModelsSerializer from "../src/builders/ModelsSerializer"; 4 | import {ModelPropertiesMapper} from "../src/simplePropertyMappers"; 5 | 6 | import { 7 | includeNames1, 8 | user2, 9 | user1, 10 | article1, 11 | article2, 12 | articleWithoutAuthor, 13 | country1, 14 | country2, 15 | town2, 16 | town1, 17 | specialty1, 18 | specialty2, 19 | } from './mocks'; 20 | 21 | chai.config.truncateThreshold = 0; 22 | 23 | describe('ModelsSerializer', () => { 24 | let builder; 25 | let propertiesMapper; 26 | 27 | it('should throw Error if jsonPropertiesMapper is not passed', () => { 28 | builder = new ModelsSerializer(); 29 | builder.setStuff(user2.model); 30 | expect(builder.build.bind(builder)).to.throw(Error, 'propertiesMapper'); 31 | }); 32 | 33 | it('should instantiate without errors', () => { 34 | propertiesMapper = new ModelPropertiesMapper(); 35 | builder = new ModelsSerializer(propertiesMapper); 36 | expect(builder).to.be.an.instanceof(ModelsSerializer); 37 | }); 38 | 39 | it('should handle one model in setStuff', () => { 40 | builder.setStuff(user2.model); 41 | expect(builder.stuff).to.be.deep.equal(user2.model); 42 | }); 43 | 44 | it('should handle collection of models in setStuff', () => { 45 | builder.setStuff([user2.model, user1.model]); 46 | expect(builder.stuff).to.be.deep.equal([user2.model, user1.model]); 47 | }); 48 | 49 | it('should setIncludeNames with convertation to TJsonaNormalizedIncludeNamesTree', () => { 50 | builder.setIncludeNames(includeNames1.denormalized); 51 | expect(builder.includeNamesTree).to.be.deep.equal(includeNames1.normalized); 52 | }); 53 | 54 | it('should setIncludeNames as they are', () => { 55 | builder.setIncludeNames(includeNames1.normalized); 56 | expect(builder.includeNamesTree).to.be.deep.equal(includeNames1.normalized); 57 | }); 58 | 59 | it('should build correct json with one data-item, without included', () => { 60 | builder = new ModelsSerializer(propertiesMapper); 61 | 62 | builder.setStuff(article1.model); 63 | const json = builder.build(); 64 | expect(json).to.be.deep.equal({data: article1.json}); 65 | }); 66 | 67 | it('should build correct json with one data-item, with included', () => { 68 | builder = new ModelsSerializer(propertiesMapper); 69 | 70 | builder.setStuff(user2.model); 71 | builder.setIncludeNames(user2.includeNames.townOnly); 72 | const json = builder.build(); 73 | expect(json).to.be.deep.equal({data: user2.json, included: user2.included.townOnly}); 74 | }); 75 | 76 | it('should build correct json with collection of data items, with included', () => { 77 | builder = new ModelsSerializer(propertiesMapper); 78 | 79 | builder.setStuff([article1.model, article2.model]); 80 | 81 | builder.setIncludeNames([ 82 | 'author.town.contry', 83 | 'author.specialty', 84 | 'country' 85 | ]); 86 | 87 | const json = builder.build(); 88 | 89 | expect(json).to.be.deep.equal({ 90 | data: [ 91 | article1.json, article2.json 92 | ], 93 | included: [ // sorting order make sense 94 | user1.json, 95 | town1.json, 96 | specialty1.json, 97 | country1.json, 98 | user2.json, 99 | town2.json, 100 | specialty2.json, 101 | country2.json, 102 | ] 103 | }); 104 | 105 | }); 106 | 107 | it('should build json with null data for nulled relation', () => { 108 | builder = new ModelsSerializer(propertiesMapper); 109 | 110 | builder.setStuff(articleWithoutAuthor.model); 111 | builder.setIncludeNames(articleWithoutAuthor.includeNames); 112 | const json = builder.build(); 113 | expect(json).to.be.deep.equal({ data: articleWithoutAuthor.json }); 114 | }); 115 | }); -------------------------------------------------------------------------------- /tests/ReduxObjectDenormalizer.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {expect} from 'chai'; 3 | import ReduxObjectDenormalizer from "../src/builders/ReduxObjectDenormalizer"; 4 | import {JsonPropertiesMapper} from "../src/simplePropertyMappers"; 5 | 6 | import { 7 | reduxObject1, 8 | article1, 9 | country2, 10 | specialty1, 11 | specialty2, 12 | user2 13 | } from './mocks'; 14 | 15 | chai.config.showDiff = true; 16 | chai.config.truncateThreshold = 0; 17 | 18 | describe('ReduxObjectDenormalizer', () => { 19 | let builder; 20 | let propertiesMapper; 21 | 22 | it('should instantiate without errors', () => { 23 | propertiesMapper = new JsonPropertiesMapper(); 24 | builder = new ReduxObjectDenormalizer(propertiesMapper); 25 | }); 26 | 27 | it('should have public setters', () => { 28 | expect(typeof builder.setReduxObject).to.be.equal('function'); 29 | expect(typeof builder.setEntityType).to.be.equal('function'); 30 | expect(typeof builder.setEntityIds).to.be.equal('function'); 31 | expect(typeof builder.setReturnBuilderInRelations).to.be.equal('function'); 32 | }); 33 | 34 | describe('build', () => { 35 | 36 | it('should throw Error if propertiesMapper is not set', () => { 37 | builder = new ReduxObjectDenormalizer(null); 38 | const build = builder.build.bind(builder); 39 | expect(build).to.throw(Error, 'propertiesMapper'); 40 | }); 41 | 42 | it('should throw Error if reduxObject is not set', () => { 43 | builder.setPropertiesMapper(propertiesMapper); 44 | const build = builder.build.bind(builder); 45 | expect(build).to.throw(Error, 'reduxObject'); 46 | }); 47 | 48 | it('should throw Error if entityType is not set', () => { 49 | builder.setPropertiesMapper(propertiesMapper); 50 | builder.setReduxObject({}); 51 | const build = builder.build.bind(builder); 52 | expect(build).to.throw(Error, 'entityType'); 53 | }); 54 | 55 | it('should return null if no needed type in ReduxObject', () => { 56 | builder.setPropertiesMapper(propertiesMapper); 57 | builder.setReduxObject({}); 58 | builder.setEntityType('myEntity'); 59 | expect(builder.build()).to.be.equal(null); 60 | }); 61 | 62 | it('should return null if no needed id in ReduxObject', () => { 63 | builder.setPropertiesMapper(propertiesMapper); 64 | builder.setReduxObject(reduxObject1); 65 | builder.setEntityType('town'); 66 | builder.setEntityIds('21343'); 67 | expect(builder.build()).to.be.equal(null); 68 | }); 69 | 70 | it('should return null if there is empty object for some type in ReduxObject', () => { 71 | builder = new ReduxObjectDenormalizer(propertiesMapper); 72 | builder.setReduxObject({town: {}}); 73 | builder.setEntityType('town'); 74 | expect(builder.build()).to.be.equal(null); 75 | }); 76 | 77 | it('should return one model with correct number type of id', () => { 78 | builder.setPropertiesMapper(propertiesMapper); 79 | builder.setReduxObject(reduxObject1); 80 | builder.setEntityType('article'); 81 | builder.setEntityIds('1'); 82 | builder.setReturnBuilderInRelations(false); 83 | expect(builder.build()).to.be.deep.equal(article1.model); 84 | }); 85 | 86 | it('should return collection of models', () => { 87 | builder.setPropertiesMapper(propertiesMapper); 88 | builder.setReduxObject(reduxObject1); 89 | builder.setEntityType('specialty'); 90 | builder.setEntityIds(['1', '2']); 91 | expect(builder.build()).to.be.deep.equal([specialty1.model, specialty2.model]); 92 | }); 93 | 94 | it('should return collection of one model with multiple relations', () => { 95 | builder.setPropertiesMapper(propertiesMapper); 96 | builder.setReduxObject(reduxObject1); 97 | builder.setEntityType('user'); 98 | builder.setEntityIds(['2']); 99 | expect(builder.build()).to.be.deep.equal([user2.model]); 100 | }); 101 | }); 102 | 103 | 104 | describe('buildModel', () => { 105 | it('should return null if no such type in reduxObject', () => { 106 | builder.setPropertiesMapper(propertiesMapper); 107 | builder.setReduxObject({}); 108 | expect(builder.buildModel('user', '123')).to.be.equal(null); 109 | }); 110 | 111 | it('should return null if no such id in reduxObject', () => { 112 | builder.setPropertiesMapper(propertiesMapper); 113 | builder.setReduxObject({user: {}}); 114 | expect(builder.buildModel('user', '123')).to.be.equal(null); 115 | }); 116 | }); 117 | 118 | describe('buildRelationships', () => { 119 | it('should return null', () => { 120 | const model = {}; 121 | expect(builder.buildRelationships()).to.be.equal(null); 122 | expect(builder.buildRelationships({})).to.be.equal(null); 123 | expect(builder.buildRelationships(model, {someRelation: null})).to.be.equal(null); 124 | expect(builder.buildRelationships(model, {someRelation: {data: null}})).to.be.equal(null); 125 | }); 126 | 127 | it('should return relations with builders', () => { 128 | builder.setPropertiesMapper(propertiesMapper); 129 | builder.setReduxObject(reduxObject1); 130 | builder.setReturnBuilderInRelations(true); 131 | const model = {}; 132 | const relations1 = builder.buildRelationships(model, reduxObject1.town['80'].relationships); 133 | expect(typeof relations1.country).to.be.equal('function'); 134 | expect(relations1.country()).to.be.deep.equal(country2.model); 135 | }); 136 | 137 | it('should return relations without builders', () => { 138 | builder.setPropertiesMapper(propertiesMapper); 139 | builder.setReduxObject(reduxObject1); 140 | builder.setReturnBuilderInRelations(false); 141 | const model = {}; 142 | const relations1 = builder.buildRelationships(model, reduxObject1.town['80'].relationships); 143 | expect(relations1.country).to.be.deep.equal(country2.model); 144 | }); 145 | }); 146 | 147 | describe('buildRelationModels', () => { 148 | 149 | it('should return null', () => { 150 | builder.setPropertiesMapper(propertiesMapper); 151 | builder.setReduxObject(reduxObject1); 152 | builder.setReturnBuilderInRelations(false); 153 | 154 | const builtModel = builder.buildRelationModels({}); 155 | expect(builtModel).to.be.equal(null); 156 | }); 157 | 158 | it('should return one model', () => { 159 | builder.setPropertiesMapper(propertiesMapper); 160 | builder.setReduxObject(reduxObject1); 161 | builder.setReturnBuilderInRelations(false); 162 | 163 | const builtModel = builder.buildRelationModels(reduxObject1.town['80'].relationships.country.data); 164 | expect(builtModel).to.be.deep.equal(country2.model); 165 | }); 166 | 167 | it('should return collection of models', () => { 168 | builder.setPropertiesMapper(propertiesMapper); 169 | builder.setReduxObject(reduxObject1); 170 | builder.setReturnBuilderInRelations(false); 171 | 172 | const builtModels = builder.buildRelationModels(reduxObject1.user['2'].relationships.specialty.data); 173 | expect(builtModels).to.be.deep.equal([specialty1.model, specialty2.model]); 174 | }); 175 | }); 176 | 177 | }); -------------------------------------------------------------------------------- /tests/mocks.ts: -------------------------------------------------------------------------------- 1 | import {RELATIONSHIP_NAMES_PROP} from '../src/simplePropertyMappers'; 2 | 3 | export const country2 = { 4 | model: { 5 | type: 'country', 6 | id: '34', 7 | name: 'Spain', 8 | }, 9 | json: { 10 | type: 'country', 11 | id: '34', 12 | attributes: { 13 | name: 'Spain', 14 | } 15 | } 16 | }; 17 | 18 | export const country1 = { 19 | model: { 20 | type: 'country', 21 | id: '86', 22 | name: 'China', 23 | }, 24 | json: { 25 | type: 'country', 26 | id: '86', 27 | attributes: { 28 | name: 'China', 29 | } 30 | } 31 | }; 32 | 33 | export const town1 = { 34 | model: { 35 | type: 'town', 36 | id: '21', 37 | name: 'Shanghai', 38 | country: country1.model, 39 | [RELATIONSHIP_NAMES_PROP]: ['country'] 40 | }, 41 | json: { 42 | type: 'town', 43 | id: '21', 44 | attributes: { 45 | name: 'Shanghai', 46 | }, 47 | relationships: { 48 | country: { 49 | data: { 50 | type: 'country', 51 | id: country1.model.id, 52 | } 53 | } 54 | } 55 | } 56 | }; 57 | 58 | export const town2 = { 59 | model: { 60 | type: 'town', 61 | id: '80', 62 | name: 'Barcelona', 63 | country: country2.model, 64 | [RELATIONSHIP_NAMES_PROP]: ['country'] 65 | }, 66 | json: { 67 | type: 'town', 68 | id: '80', 69 | attributes: { 70 | name: 'Barcelona', 71 | }, 72 | relationships: { 73 | country: { 74 | data: { 75 | type: 'country', 76 | id: country2.model.id, 77 | } 78 | } 79 | } 80 | } 81 | }; 82 | 83 | export const specialty1 = { 84 | model: { 85 | type: 'specialty', 86 | id: '1', 87 | title: 'mycategory1' 88 | }, 89 | json: { 90 | type: 'specialty', 91 | id: '1', 92 | attributes: { 93 | title: 'mycategory1' 94 | } 95 | } 96 | }; 97 | 98 | export const specialty2 = { 99 | model: { 100 | type: 'specialty', 101 | id: '2', 102 | title: 'mycategory2' 103 | }, 104 | json: { 105 | type: 'specialty', 106 | id: '2', 107 | attributes: { 108 | title: 'mycategory2' 109 | } 110 | } 111 | }; 112 | 113 | export const idlessSpecialty1 = { 114 | model: { 115 | type: 'specialty', 116 | title: 'mycategory1' 117 | }, 118 | json: { 119 | type: 'specialty', 120 | attributes: { 121 | title: 'mycategory1' 122 | } 123 | } 124 | }; 125 | 126 | export const idlessSpecialty2 = { 127 | model: { 128 | type: 'specialty', 129 | title: 'mycategory2' 130 | }, 131 | json: { 132 | type: 'specialty', 133 | attributes: { 134 | title: 'mycategory2' 135 | } 136 | } 137 | }; 138 | 139 | export const user1 = { 140 | model: { 141 | type: 'user', 142 | id: '1', 143 | name: 'myName1', 144 | active: false, 145 | town: town1.model, 146 | specialty: [specialty1.model], 147 | [RELATIONSHIP_NAMES_PROP]: ['town', 'specialty'] 148 | }, 149 | json: { 150 | type: 'user', 151 | id: '1', 152 | attributes: { 153 | name: 'myName1', 154 | active: false, 155 | }, 156 | relationships: { 157 | town: { 158 | data: { 159 | type: 'town', 160 | id: town1.model.id, 161 | } 162 | }, 163 | specialty: { 164 | data: [{ 165 | type: 'specialty', 166 | id: specialty1.model.id 167 | }] 168 | } 169 | } 170 | }, 171 | included: { 172 | townOnly: [ 173 | town1.json 174 | ] 175 | } 176 | }; 177 | 178 | export const user2 = { 179 | model: { 180 | type: 'user', 181 | id: '2', 182 | name: 'myName2', 183 | active: true, 184 | town: town2.model, 185 | specialty: [specialty1.model, specialty2.model], 186 | [RELATIONSHIP_NAMES_PROP]: ['town', 'specialty'] 187 | }, 188 | modelWithoutIncluded: { 189 | type: 'user', 190 | id: '2', 191 | name: 'myName2', 192 | active: true, 193 | town: { 194 | id: town2.model.id, 195 | type: town2.model.type, 196 | }, 197 | specialty: [{ 198 | id: specialty1.model.id, 199 | type: specialty1.model.type, 200 | }, { 201 | id: specialty2.model.id, 202 | type: specialty2.model.type, 203 | }], 204 | [RELATIONSHIP_NAMES_PROP]: ['town', 'specialty'] 205 | }, 206 | json: { 207 | type: 'user', 208 | id: '2', 209 | attributes: { 210 | name: 'myName2', 211 | active: true, 212 | }, 213 | relationships: { 214 | town: { 215 | data: { 216 | type: 'town', 217 | id: town2.model.id, 218 | } 219 | }, 220 | specialty: { 221 | data: [{ 222 | type: 'specialty', 223 | id: specialty1.model.id 224 | }, { 225 | type: 'specialty', 226 | id: specialty2.model.id 227 | }] 228 | } 229 | } 230 | }, 231 | includeNames: { 232 | townOnly: ['town'] 233 | }, 234 | included: { 235 | townOnly: [ 236 | town2.json 237 | ] 238 | } 239 | }; 240 | 241 | export const userWithIdlessSpecialties = { 242 | model: { 243 | type: 'user', 244 | id: '2', 245 | name: 'myName2', 246 | active: true, 247 | specialtyWithoutIds: [idlessSpecialty1.model, idlessSpecialty2.model], 248 | [RELATIONSHIP_NAMES_PROP]: ['specialtyWithoutIds'] 249 | }, 250 | }; 251 | 252 | export const article1 = { 253 | model: { 254 | type: 'article', 255 | id: 1, 256 | likes: 5550, 257 | author: user1.model, 258 | country: country1.model, 259 | [RELATIONSHIP_NAMES_PROP]: ['author', 'country'] 260 | }, 261 | json: { 262 | type: 'article', 263 | id: 1, 264 | attributes: { 265 | likes: 5550 266 | }, 267 | relationships: { 268 | author: { 269 | data: { 270 | type: 'user', 271 | id: user1.model.id 272 | } 273 | }, 274 | country: { 275 | data: { 276 | type: 'country', 277 | id: country1.model.id 278 | } 279 | } 280 | } 281 | } 282 | }; 283 | 284 | export const article2 = { 285 | model: { 286 | type: 'article', 287 | id: 2, 288 | likes: 100, 289 | author: user2.model, 290 | country: country2.model, 291 | [RELATIONSHIP_NAMES_PROP]: ['author', 'country'] 292 | }, 293 | json: { 294 | type: 'article', 295 | id: 2, 296 | attributes: { 297 | likes: 100 298 | }, 299 | relationships: { 300 | author: { 301 | data: { 302 | type: 'user', 303 | id: user2.model.id, 304 | } 305 | }, 306 | country: { 307 | data: { 308 | type: 'country', 309 | id: country2.model.id, 310 | } 311 | } 312 | } 313 | }, 314 | includeNames: [ 315 | 'author.town.contry', 316 | 'author.specialty', 317 | 'country' 318 | ], 319 | }; 320 | 321 | export const articleWithoutAuthor = { 322 | model: { 323 | type: 'article', 324 | id: 3, 325 | likes: 0, 326 | author: null, 327 | [RELATIONSHIP_NAMES_PROP]: ['author'] 328 | }, 329 | json: { 330 | type: 'article', 331 | id: 3, 332 | attributes: { 333 | likes: 0, 334 | }, 335 | relationships: { 336 | author: { 337 | data: null, 338 | }, 339 | }, 340 | }, 341 | includeNames: [ 342 | 'author' 343 | ], 344 | }; 345 | 346 | const circularModel = { 347 | type: 'model', 348 | id: '1', 349 | relationshipNames: ['simpleRelation'], 350 | }; 351 | 352 | const circularSubmodel = { 353 | type: 'subModel', 354 | id: '1', 355 | relationshipNames: ['circularRelation'], 356 | }; 357 | 358 | circularModel['simpleRelation'] = circularSubmodel; 359 | circularSubmodel['circularRelation'] = circularModel; 360 | 361 | export const circular = { 362 | model: circularModel, 363 | json: { 364 | "data": { 365 | "type": "model", 366 | "id": "1", 367 | "relationships": { 368 | "simpleRelation": { 369 | "data": { 370 | "type": "subModel", 371 | "id": "1" 372 | } 373 | }, 374 | } 375 | }, 376 | "included": [ 377 | { 378 | "type": "subModel", 379 | "id": "1", 380 | "relationships": { 381 | "circularRelation": { 382 | "data": { 383 | "type": "model", 384 | "id": "1" 385 | } 386 | } 387 | } 388 | } 389 | ] 390 | }, 391 | }; 392 | 393 | const circularWithMetaRootModel = { 394 | type: 'model', 395 | id: '1', 396 | relationshipNames: ['simpleRelation'], 397 | }; 398 | 399 | const circularWithMetaRootNestedModel = { 400 | type: 'model', 401 | id: '1', 402 | resourceIdObjMeta: { 403 | foo2: 'bar2', 404 | }, 405 | relationshipNames: ['simpleRelation'], 406 | }; 407 | 408 | const circularWithMetaSubmodel = { 409 | type: 'subModel', 410 | id: '1', 411 | resourceIdObjMeta: { 412 | foo: 'bar', 413 | }, 414 | relationshipNames: ['circularRelation'], 415 | }; 416 | 417 | circularWithMetaRootModel['simpleRelation'] = circularWithMetaSubmodel; 418 | circularWithMetaRootNestedModel['simpleRelation'] = circularWithMetaSubmodel; 419 | circularWithMetaSubmodel['circularRelation'] = circularWithMetaRootNestedModel; 420 | 421 | export const circularWithMeta = { 422 | model: circularWithMetaRootModel, 423 | json: { 424 | "data": { 425 | "type": "model", 426 | "id": "1", 427 | "relationships": { 428 | "simpleRelation": { 429 | "data": { 430 | "type": "subModel", 431 | "id": "1", 432 | "meta": { 433 | "foo": "bar" 434 | } 435 | } 436 | }, 437 | } 438 | }, 439 | "included": [ 440 | { 441 | "type": "subModel", 442 | "id": "1", 443 | "relationships": { 444 | "circularRelation": { 445 | "data": { 446 | "type": "model", 447 | "id": "1", 448 | "meta": { 449 | "foo2": "bar2" 450 | } 451 | } 452 | } 453 | } 454 | } 455 | ] 456 | }, 457 | }; 458 | 459 | const duplicateModels = [ 460 | { 461 | type: 'model', 462 | id: '1', 463 | 'relationshipNames': [ 464 | 'simpleRelation' 465 | ], 466 | }, 467 | { 468 | type: 'model', 469 | id: '2', 470 | 'relationshipNames': [ 471 | 'simpleRelation' 472 | ], 473 | }, 474 | ]; 475 | 476 | const duplicateSubModel = { 477 | 'type': 'subModel', 478 | 'id': '1', 479 | 'relationshipNames': [ 480 | 'simpleRelation2' 481 | ], 482 | }; 483 | 484 | duplicateModels[0]['simpleRelation'] = duplicateSubModel; 485 | duplicateModels[1]['simpleRelation'] = duplicateSubModel; 486 | duplicateSubModel['simpleRelation2'] = [ 487 | duplicateModels[0], 488 | duplicateModels[1], 489 | ]; 490 | 491 | export const duplicate = { 492 | model: duplicateModels, 493 | json: { 494 | 'data': [ 495 | { 496 | 'type': 'model', 497 | 'id': '1', 498 | 'relationships': { 499 | 'simpleRelation': { 500 | 'data': { 501 | 'type': 'subModel', 502 | 'id': '1' 503 | } 504 | }, 505 | } 506 | }, 507 | { 508 | 'type': 'model', 509 | 'id': '2', 510 | 'relationships': { 511 | 'simpleRelation': { 512 | 'data': { 513 | 'type': 'subModel', 514 | 'id': '1' 515 | } 516 | }, 517 | } 518 | } 519 | ], 520 | 'included': [ 521 | { 522 | 'type': 'subModel', 523 | 'id': '1', 524 | 'relationships': { 525 | 'simpleRelation2': { 526 | 'data': [ 527 | { 528 | 'type': 'model', 529 | 'id': '1' 530 | }, 531 | { 532 | 'type': 'model', 533 | 'id': '2' 534 | } 535 | ] 536 | } 537 | } 538 | }, 539 | { 540 | 'type': 'model', 541 | 'id': '1', 542 | }, 543 | { 544 | 'type': 'model', 545 | 'id': '2', 546 | } 547 | ] 548 | }, 549 | }; 550 | 551 | export const includeNames1 = { 552 | denormalized: [ 553 | 'articles.author.town.country', 554 | 'articles.country', 555 | 'country', 556 | ], 557 | normalized: { 558 | country: null, 559 | articles: { 560 | author: { 561 | town: { 562 | country: null 563 | } 564 | }, 565 | country: null 566 | } 567 | } 568 | }; 569 | 570 | export const reduxObject1 = { 571 | "article": { 572 | "1": { 573 | "id": 1, 574 | "attributes": {"likes": 5550}, 575 | "relationships": { 576 | "author": {"data": {"id": '1', "type": "user"}}, 577 | "country": {"data": {"id": '86', "type": "country"}} 578 | } 579 | }, 580 | "2": { 581 | "id": 2, 582 | "attributes": {"likes": 100}, 583 | "relationships": { 584 | "author": {"data": {"id": '2', "type": "user"}}, 585 | "country": {"data": {"id": '34', "type": "country"}} 586 | } 587 | } 588 | }, 589 | "country": { 590 | "34": {"id": '34', "attributes": {"name": "Spain"}}, 591 | "86": {"id": '86', "attributes": {"name": "China"}} 592 | }, 593 | "specialty": { 594 | "1": {"id": "1", "attributes": {"title": "mycategory1"}}, 595 | "2": {"id": "2", "attributes": {"title": "mycategory2"}} 596 | }, 597 | "town": { 598 | "21": { 599 | "id": '21', 600 | "attributes": {"name": "Shanghai"}, 601 | "relationships": {"country": {"data": {"id": '86', "type": "country"}}} 602 | }, 603 | "80": { 604 | "id": '80', 605 | "attributes": {"name": "Barcelona"}, 606 | "relationships": {"country": {"data": {"id": '34', "type": "country"}}} 607 | } 608 | }, 609 | "user": { 610 | "1": { 611 | "id": '1', 612 | "attributes": {"name": "myName1", "active": false}, 613 | "relationships": { 614 | "town": {"data": {"id": '21', "type": "town"}}, 615 | "specialty": { 616 | "data": [{"id": "1", "type": "specialty"}] 617 | } 618 | } 619 | }, 620 | "2": { 621 | "id": '2', 622 | "attributes": {"name": "myName2", "active": true}, 623 | "relationships": { 624 | "town": { 625 | "data": {"id": '80', "type": "town"} 626 | }, 627 | "specialty": { 628 | "data": [ 629 | {"id": "1", "type": "specialty"}, 630 | {"id": "2", "type": "specialty"} 631 | ] 632 | } 633 | } 634 | } 635 | } 636 | }; 637 | 638 | export const reduxObjectWithCircular = { 639 | model: { 640 | '1': { 641 | "id": "1", 642 | "relationships": { 643 | "simpleRelation": { 644 | "data": { 645 | "type": "subModel", 646 | "id": "1" 647 | } 648 | }, 649 | } 650 | } 651 | }, 652 | subModel: { 653 | '1': { 654 | "id": "1", 655 | "relationships": { 656 | "circularRelation": { 657 | "data": { 658 | "type": "model", 659 | "id": "1" 660 | } 661 | }, 662 | } 663 | } 664 | }, 665 | }; 666 | 667 | 668 | export const withoutRootIdsMock = { 669 | 670 | json: [{ 671 | "type": "language-knowledges", 672 | "relationships": { 673 | "sourceLanguage": { 674 | "data": { 675 | "type": "source-languages", 676 | "id": "11" 677 | } 678 | }, 679 | "workArea": { 680 | "data": { 681 | "type": "work-areas", 682 | "id": "22" 683 | } 684 | } 685 | } 686 | }, { 687 | "type": "language-knowledges", 688 | "relationships": { 689 | "sourceLanguage": { 690 | "data": { 691 | "type": "source-languages", 692 | "id": "22" 693 | } 694 | }, 695 | "workArea": { 696 | "data": { 697 | "type": "work-areas", 698 | "id": "22" 699 | } 700 | } 701 | } 702 | }], 703 | 704 | collection: [{ 705 | type: 'language-knowledges', 706 | id: undefined, 707 | sourceLanguage: { 708 | type: 'source-languages', 709 | id: '11' 710 | }, 711 | workArea: { 712 | type: 'work-areas', 713 | id: '22' 714 | }, 715 | relationshipNames: ['sourceLanguage', 'workArea'] 716 | }, { 717 | type: 'language-knowledges', 718 | id: undefined, 719 | sourceLanguage: { 720 | type: 'source-languages', 721 | id: '22' 722 | }, 723 | workArea: { 724 | type: 'work-areas', 725 | id: '22' 726 | }, 727 | relationshipNames: ['sourceLanguage', 'workArea'] 728 | }], 729 | 730 | }; 731 | 732 | export const withNullRelationsMock = { 733 | 734 | json: [{ 735 | "type": "category", 736 | "id": "3", 737 | "attributes": { 738 | "slug": "ya" 739 | }, 740 | "relationships": { 741 | "parent": { 742 | "data": { 743 | "type": "category", 744 | "id": "0" 745 | } 746 | } 747 | } 748 | }, { 749 | "type": "category", 750 | "id": "0", 751 | "attributes": { 752 | "slug": "home" 753 | }, 754 | "relationships": { 755 | "parent": { 756 | "data": null 757 | } 758 | } 759 | }], 760 | 761 | collection: [{ 762 | "type": "category", 763 | "id": "3", 764 | "slug": "ya", 765 | "parent": { 766 | "type": "category", 767 | "id": "0", 768 | "slug": "home", 769 | "parent": null, 770 | "relationshipNames": [ 771 | "parent" 772 | ] 773 | }, 774 | "relationshipNames": [ 775 | "parent" 776 | ] 777 | }, { 778 | "type": "category", 779 | "id": "0", 780 | "slug": "home", 781 | "parent": null, 782 | "relationshipNames": [ 783 | "parent" 784 | ] 785 | }], 786 | 787 | }; 788 | 789 | export const resourceIdObjMetaMock = { 790 | 791 | json: { 792 | "data": [{ 793 | "type": "node--site_configuration", 794 | "id": "f8895943-7f51-451b-bb8f-a479853f1b4b", 795 | "attributes": { 796 | "langcode": "en", 797 | "title": "Site Configuration" 798 | }, 799 | "relationships": { 800 | "field_logo": { 801 | "data": { 802 | "type": "file--file", 803 | "id": "551ec1b9-b0c6-4649-bb7c-b6ebb09354ff", 804 | "meta": { 805 | "alt": "ACME Corp Logo", 806 | "title": "", 807 | "width": 206, 808 | "height": 278 809 | } 810 | } 811 | } 812 | } 813 | },{ 814 | "type": "node--site_configuration", 815 | "id": "2", 816 | "attributes": { 817 | "title": "Site Configuration 2" 818 | }, 819 | "relationships": { 820 | "field_logo": { 821 | "data": { 822 | "type": "file--file", 823 | "id": "551ec1b9-b0c6-4649-bb7c-b6ebb09354ff", 824 | "meta": { 825 | "alt": "ACME Corp Logo 2", 826 | } 827 | } 828 | } 829 | } 830 | }], 831 | "included": [{ 832 | "type": "file--file", 833 | "id": "551ec1b9-b0c6-4649-bb7c-b6ebb09354ff", 834 | "attributes": { 835 | "langcode": "en", 836 | "uri": { 837 | "value": "public://2020-07/acmecorp-logo-colour-2x.png", 838 | "url": "http://acmecorp.oss-cn-hongkong.aliyuncs.com/s3fs-public/2020-07/acmecorp-logo-colour-2x.png" 839 | }, 840 | "filemime": "image/png", 841 | "filesize": 54952 842 | } 843 | }] 844 | }, 845 | 846 | collection: [ 847 | { 848 | "type": "node--site_configuration", 849 | "id": "f8895943-7f51-451b-bb8f-a479853f1b4b", 850 | "langcode": "en", 851 | "title": "Site Configuration", 852 | "field_logo": { 853 | "type": "file--file", 854 | "id": "551ec1b9-b0c6-4649-bb7c-b6ebb09354ff", 855 | "langcode": "en", 856 | "uri": { 857 | "value": "public://2020-07/acmecorp-logo-colour-2x.png", 858 | "url": "http://acmecorp.oss-cn-hongkong.aliyuncs.com/s3fs-public/2020-07/acmecorp-logo-colour-2x.png" 859 | }, 860 | "filemime": "image/png", 861 | "filesize": 54952, 862 | "resourceIdObjMeta": { 863 | "alt": "ACME Corp Logo", 864 | "title": "", 865 | "width": 206, 866 | "height": 278 867 | } 868 | }, 869 | "relationshipNames": [ 870 | "field_logo" 871 | ] 872 | }, 873 | { 874 | "type": "node--site_configuration", 875 | "id": "2", 876 | "title": "Site Configuration 2", 877 | "field_logo": { 878 | "type": "file--file", 879 | "id": "551ec1b9-b0c6-4649-bb7c-b6ebb09354ff", 880 | "langcode": "en", 881 | "uri": { 882 | "value": "public://2020-07/acmecorp-logo-colour-2x.png", 883 | "url": "http://acmecorp.oss-cn-hongkong.aliyuncs.com/s3fs-public/2020-07/acmecorp-logo-colour-2x.png" 884 | }, 885 | "filemime": "image/png", 886 | "filesize": 54952, 887 | "resourceIdObjMeta": { 888 | "alt": "ACME Corp Logo 2" 889 | } 890 | }, 891 | "relationshipNames": [ 892 | "field_logo" 893 | ] 894 | } 895 | ], 896 | 897 | }; 898 | 899 | export const differentAttrsInDataAndIncludedMock = { 900 | json: { 901 | data: [{ 902 | "id": "1", 903 | "type": "box", 904 | "relationships": {"revision": {"data": {"id": "1", "type": "revision"}}} 905 | }, { 906 | "id": "1", 907 | "type": "process", 908 | "attributes": {"prop-1": "hello"}, 909 | "relationships": {"revision": {"data": {"id": "1", "type": "revision"}}} 910 | }], 911 | included: [{ 912 | "id": "1", 913 | "type": "revision", 914 | "relationships": {"link": {"data": {"id": "1", "type": "link"}}} 915 | }, { 916 | "id": "1", 917 | "type": "link", 918 | "relationships": {"process": {"data": {"id": "1", "type": "process"}}} 919 | }, { 920 | "id": "1", 921 | "type": "process", 922 | "attributes": {"prop-2": "prop2"} 923 | }] 924 | }, 925 | collection: [{ 926 | "type": "box", 927 | "id": "1", 928 | "revision": { 929 | "type": "revision", 930 | "id": "1", 931 | "link": { 932 | "type": "link", 933 | "id": "1", 934 | "process": {"type": "process", "id": "1", "prop-2": "prop2"}, 935 | "relationshipNames": ["process"] 936 | }, 937 | "relationshipNames": ["link"] 938 | }, 939 | "relationshipNames": ["revision"] 940 | }, { 941 | "type": "process", 942 | "id": "1", 943 | "prop-1": "hello", 944 | "revision": { 945 | "type": "revision", 946 | "id": "1", 947 | "link": { 948 | "type": "link", 949 | "id": "1", 950 | "process": {"type": "process", "id": "1", "prop-2": "prop2"}, 951 | "relationshipNames": ["process"] 952 | }, 953 | "relationshipNames": ["link"] 954 | }, 955 | "relationshipNames": ["revision"] 956 | }] 957 | } 958 | -------------------------------------------------------------------------------- /tests/switchCasePropertyMappers.test.ts: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import {expect} from 'chai'; 3 | import {SwitchCaseJsonMapper, SwitchCaseModelMapper} from "../src/switchCasePropertyMappers"; 4 | import {RELATIONSHIP_NAMES_PROP} from "../src/simplePropertyMappers"; 5 | import Jsona from '../src'; 6 | 7 | chai.config.truncateThreshold = 0; 8 | 9 | describe('switchCasePropertyMappers', () => { 10 | 11 | describe('SwitchCaseModelMapper', () => { 12 | let propertiesMapper; 13 | 14 | it(`should transform camelized model's attribute names to kebab case`, () => { 15 | propertiesMapper = new SwitchCaseModelMapper(); 16 | const testModel = { 17 | fooBar: 1, 18 | fooFooBar: 123, 19 | 'bar-foo': 2, 20 | 'bar-foo-foo': 'foo', 21 | foo: 3, 22 | foo_bar: 5, 23 | foo234: 234, 24 | nestedFooBar: { 25 | nestedFooBar: { 26 | nestedFooBar: 1 27 | } 28 | }, 29 | arrayNestedFooBar: { 30 | nestedFooBar: [ 31 | { nestedFooBar: 1 }, { nestedFooBar: 2 }, { nestedFooBar: 3 } 32 | ] 33 | } 34 | }; 35 | const kebabAttributes = propertiesMapper.getAttributes(testModel); 36 | expect(kebabAttributes['foo-bar']).to.be.equal(1); 37 | expect(kebabAttributes['foo-foo-bar']).to.be.equal(123); 38 | expect(kebabAttributes['bar-foo']).to.be.equal(2); 39 | expect(kebabAttributes['bar-foo-foo']).to.be.equal('foo'); 40 | expect(kebabAttributes['foo']).to.be.equal(3); 41 | expect(kebabAttributes['foo_bar']).to.be.equal(5); 42 | expect(kebabAttributes['foo-234']).to.be.equal(234); 43 | expect(kebabAttributes['nested-foo-bar']).to.be.deep.equal({ 'nested-foo-bar': { 'nested-foo-bar': 1 } }); 44 | expect(kebabAttributes['array-nested-foo-bar']).to.be.deep.equal({ 'nested-foo-bar': [{ 'nested-foo-bar': 1 }, { 'nested-foo-bar': 2 }, { 'nested-foo-bar': 3 }] }); 45 | }); 46 | 47 | it(`should transform camelized model's relationship names to kebab case`, () => { 48 | propertiesMapper = new SwitchCaseModelMapper(); 49 | const relationOne = {}; 50 | const relation2 = {}; 51 | const testModel = { 52 | relationOne, relation2, 53 | [RELATIONSHIP_NAMES_PROP]: ['relationOne', 'relation2'], 54 | }; 55 | 56 | 57 | const kebabRelationships = propertiesMapper.getRelationships(testModel); 58 | const kebabRelationshipNames = Object.keys(kebabRelationships); 59 | expect(kebabRelationshipNames.indexOf('relation-one') !== -1).to.be.true; 60 | expect(kebabRelationshipNames.indexOf('relation-2') !== -1).to.be.true; 61 | }); 62 | 63 | it(`should transform camelized model's relationship attributes to kebab case`, () => { 64 | const relationOne = { type: 'relatedModel', id: 1, relatedCamelizedAttr: true }; 65 | const testModel = { type: 'testModel', id: 1, relationOne, [RELATIONSHIP_NAMES_PROP]: ['relationOne'] }; 66 | 67 | const dataFormatter = new Jsona({ 68 | modelPropertiesMapper: new SwitchCaseModelMapper(), 69 | jsonPropertiesMapper: new SwitchCaseJsonMapper(), 70 | }); 71 | 72 | const json = dataFormatter.serialize({ stuff: testModel, includeNames: ['relation-one'] }); 73 | 74 | expect(json.included[0].attributes['related-camelized-attr']).to.be.true; 75 | }); 76 | 77 | it(`can be configured to transform camelized model's relationship names to snake case`, () => { 78 | propertiesMapper = new SwitchCaseModelMapper({ switchChar: '_'}); 79 | const relationOne = {}; 80 | const relation2 = {}; 81 | const testModel = { 82 | relationOne, relation2, 83 | [RELATIONSHIP_NAMES_PROP]: ['relationOne', 'relation2'], 84 | }; 85 | 86 | 87 | const snakeRelationships = propertiesMapper.getRelationships(testModel); 88 | const snakeRelationshipNames = Object.keys(snakeRelationships); 89 | expect(snakeRelationshipNames.indexOf('relation_one') !== -1).to.be.true; 90 | expect(snakeRelationshipNames.indexOf('relation_2') !== -1).to.be.true; 91 | }); 92 | 93 | it(`can be configured to transform camelized model's relationship attributes to snake case`, () => { 94 | const relationOne = { type: 'relatedModel', id: 1, relatedCamelizedAttr: true }; 95 | const testModel = { type: 'testModel', id: 1, relationOne, [RELATIONSHIP_NAMES_PROP]: ['relationOne'] }; 96 | 97 | const dataFormatter = new Jsona({ 98 | modelPropertiesMapper: new SwitchCaseModelMapper({ switchChar: '_'}), 99 | jsonPropertiesMapper: new SwitchCaseJsonMapper(), 100 | }); 101 | 102 | const json = dataFormatter.serialize({ stuff: testModel, includeNames: ['relation_one'] }); 103 | 104 | expect(json.included[0].attributes['related_camelized_attr']).to.be.true; 105 | }); 106 | 107 | it(`can be configured to transform camelized model's attribute names to snake case`, () => { 108 | propertiesMapper = new SwitchCaseModelMapper({switchChar: '_'}); 109 | const testModel = { 110 | fooBar: 1, 111 | fooFooBar: 123, 112 | bar_foo: 2, 113 | bar_foo_foo: 'foo', 114 | foo: 3, 115 | 'foo-bar': 5, 116 | foo234: 234, 117 | nestedFooBar: { 118 | nestedFooBar: { 119 | nestedFooBar: 1. 120 | } 121 | } 122 | }; 123 | const snakeAttributes = propertiesMapper.getAttributes(testModel); 124 | expect(snakeAttributes.foo_bar).to.be.equal(1); 125 | expect(snakeAttributes.foo_foo_bar).to.be.equal(123); 126 | expect(snakeAttributes.bar_foo).to.be.equal(2); 127 | expect(snakeAttributes.bar_foo_foo).to.be.equal('foo'); 128 | expect(snakeAttributes.foo).to.be.equal(3); 129 | expect(snakeAttributes['foo-bar']).to.be.equal(5); 130 | expect(snakeAttributes.foo_234).to.be.equal(234); 131 | expect(snakeAttributes['nested_foo_bar']).to.be.deep.equal({ 'nested_foo_bar': { 'nested_foo_bar': 1 } }); 132 | }); 133 | }); 134 | 135 | describe('SwitchCaseJsonMapper', () => { 136 | let propertiesMapper; 137 | 138 | it(`should transform kebabized json's attribute names to camel case`, () => { 139 | propertiesMapper = new SwitchCaseJsonMapper(); 140 | const model = {}; 141 | const testAttributes = { 142 | fooBar: 1, 143 | fooFooBar: 123, 144 | 'bar-foo': 2, 145 | 'bar-foo-foo': 'foo', 146 | foo: 3, 147 | foo_bar: 5, 148 | 'foo-234': 234, 149 | nestedFooBar: { 150 | nestedFooBar: { 151 | nestedFooBar: 1 152 | } 153 | }, 154 | 'nested-bar-foo': { 155 | 'nested-bar-foo': { 156 | 'nested-bar-foo': 2 157 | } 158 | }, 159 | 'array-nested-bar-foo': { 160 | 'nested-bar-foo': [{ 'nested-bar-foo': 2 }, { 'nested-bar-foo': 3 }, { 'nested-bar-foo': 4 }] 161 | }, 162 | }; 163 | propertiesMapper.setAttributes(model, testAttributes); 164 | 165 | expect(model['fooBar']).to.be.equal(1); 166 | expect(model['fooFooBar']).to.be.equal(123); 167 | expect(model['barFoo']).to.be.equal(2); 168 | expect(model['barFooFoo']).to.be.equal('foo'); 169 | expect(model['foo']).to.be.equal(3); 170 | expect(model['foo_bar']).to.be.equal(5); 171 | expect(model['foo234']).to.be.equal(234); 172 | expect(model['nestedFooBar']).to.be.deep.equal({ nestedFooBar: { nestedFooBar: 1 } }); 173 | expect(model['nestedBarFoo']).to.be.deep.equal({ nestedBarFoo: { nestedBarFoo: 2 } }); 174 | expect(model['arrayNestedBarFoo']).to.be.deep.equal({ nestedBarFoo: [{ nestedBarFoo: 2 }, { nestedBarFoo: 3 }, { nestedBarFoo: 4 }] }); 175 | }); 176 | 177 | it(`should transform kebabized json's relationship names to camel case`, () => { 178 | propertiesMapper = new SwitchCaseJsonMapper(); 179 | const model = propertiesMapper.createModel('testModelType'); 180 | const relation1 = {some: 'relation'}; 181 | const relation2 = [relation1]; 182 | const relation3 = () => { 183 | return 123; 184 | }; 185 | const testRelations1 = {'relation-one': relation1, 'relation-2': relation2}; 186 | const testRelations2 = {relation3}; 187 | propertiesMapper.setRelationships(model, testRelations1); 188 | propertiesMapper.setRelationships(model, testRelations2); 189 | 190 | const relation3Descriptor = Object.getOwnPropertyDescriptor(model, 'relation3'); 191 | 192 | expect(model.relationOne).to.be.equal(relation1); 193 | expect(model.relation2).to.be.equal(relation2); 194 | expect(typeof relation3Descriptor.get).to.be.equal('function'); 195 | expect(model.relation3).to.be.equal(123); 196 | }); 197 | 198 | it(`should transform kebabized json's relationship attributes to camel case`, () => { 199 | propertiesMapper = new SwitchCaseModelMapper(); 200 | 201 | type TestModelType = { 202 | relation1: { 203 | kebabAttr1: boolean, 204 | } 205 | }; 206 | 207 | const textJson = { 208 | data: { 209 | type: 'model-type', 210 | id: 1, 211 | relationships: { 212 | 'relation-1': { 213 | data: { 214 | type: 'related-model-1', 215 | id: 1, 216 | } 217 | } 218 | } 219 | }, 220 | included: [{ 221 | type: 'related-model-1', 222 | id: 1, 223 | attributes: { 224 | 'kebab-attr-1': true, 225 | } 226 | }], 227 | }; 228 | 229 | const dataFormatter = new Jsona({ 230 | modelPropertiesMapper: new SwitchCaseModelMapper(), 231 | jsonPropertiesMapper: new SwitchCaseJsonMapper(), 232 | }); 233 | 234 | const model = dataFormatter.deserialize(textJson); 235 | expect(model.relation1.kebabAttr1).to.be.true; 236 | }); 237 | 238 | it(`can be configured to transform snake case json's attribute names to camel case`, () => { 239 | propertiesMapper = new SwitchCaseJsonMapper({switchChar: '_'}); 240 | const model = {}; 241 | const testAttributes = { 242 | fooBar: 1, 243 | fooFooBar: 123, 244 | bar_foo: 2, 245 | bar_foo_foo: 'foo', 246 | foo: 3, 247 | 'foo-bar': 5, 248 | foo_234: 234, 249 | nested_foo_bar: { 250 | nested_foo_bar: { 251 | nested_foo_bar: 123 252 | } 253 | }, 254 | }; 255 | propertiesMapper.setAttributes(model, testAttributes); 256 | 257 | expect(model['fooBar']).to.be.equal(1); 258 | expect(model['fooFooBar']).to.be.equal(123); 259 | expect(model['barFoo']).to.be.equal(2); 260 | expect(model['barFooFoo']).to.be.equal('foo'); 261 | expect(model['foo']).to.be.equal(3); 262 | expect(model['foo-bar']).to.be.equal(5); 263 | expect(model['foo234']).to.be.equal(234); 264 | expect(model['nestedFooBar']).to.be.deep.equal({ nestedFooBar: { nestedFooBar: 123 } }); 265 | }); 266 | 267 | it(`can be configured to transform snaked json's relationship names to camel case`, () => { 268 | propertiesMapper = new SwitchCaseJsonMapper({switchChar: '_'}); 269 | const model = propertiesMapper.createModel('testModelType'); 270 | const relation1 = {some: 'relation'}; 271 | const relation2 = [relation1]; 272 | const relation3 = () => { 273 | return 123; 274 | }; 275 | const testRelations1 = {'relation_one': relation1, 'relation_2': relation2}; 276 | const testRelations2 = {relation3}; 277 | propertiesMapper.setRelationships(model, testRelations1); 278 | propertiesMapper.setRelationships(model, testRelations2); 279 | 280 | const relation3Descriptor = Object.getOwnPropertyDescriptor(model, 'relation3'); 281 | 282 | expect(model.relationOne).to.be.equal(relation1); 283 | expect(model.relation2).to.be.equal(relation2); 284 | expect(typeof relation3Descriptor.get).to.be.equal('function'); 285 | expect(model.relation3).to.be.equal(123); 286 | }); 287 | 288 | it(`should transform kebabized json's relationship attributes to camel case`, () => { 289 | propertiesMapper = new SwitchCaseModelMapper({switchChar: '_'}); 290 | 291 | type TestModelType = { 292 | relation1: { 293 | kebabAttr1: boolean, 294 | } 295 | }; 296 | 297 | const textJson = { 298 | data: { 299 | type: 'model_type', 300 | id: 1, 301 | relationships: { 302 | 'relation_1': { 303 | data: { 304 | type: 'related_model_1', 305 | id: 1, 306 | } 307 | } 308 | } 309 | }, 310 | included: [{ 311 | type: 'related_model_1', 312 | id: 1, 313 | attributes: { 314 | 'kebab_attr_1': true, 315 | } 316 | }], 317 | }; 318 | 319 | const dataFormatter = new Jsona({ 320 | modelPropertiesMapper: new SwitchCaseModelMapper(), 321 | jsonPropertiesMapper: new SwitchCaseJsonMapper({ switchChar: '_'}), 322 | }); 323 | 324 | const model = dataFormatter.deserialize(textJson); 325 | expect(model.relation1.kebabAttr1).to.be.true; 326 | }); 327 | 328 | 329 | it(`should transform kebabized json's meta names to camel case`, () => { 330 | propertiesMapper = new SwitchCaseJsonMapper({ camelizeMeta: true }); 331 | const model = { meta: {} }; 332 | const testMeta = { 333 | fooBar: 1, 334 | 'bar-foo': 2, 335 | foo: 3, 336 | foo_bar: 5, 337 | 'foo-234': 234, 338 | }; 339 | propertiesMapper.setMeta(model, testMeta); 340 | 341 | expect(model.meta['fooBar']).to.be.equal(1); 342 | expect(model.meta['barFoo']).to.be.equal(2); 343 | expect(model.meta['foo']).to.be.equal(3); 344 | expect(model.meta['foo_bar']).to.be.equal(5); 345 | expect(model.meta['foo234']).to.be.equal(234); 346 | }); 347 | 348 | it(`should not transform kebabized json's meta names to camel case`, () => { 349 | propertiesMapper = new SwitchCaseJsonMapper(); 350 | const model = { meta: {} }; 351 | const testMeta = { 352 | fooBar: 1, 353 | 'bar-foo': 2, 354 | foo: 3, 355 | foo_bar: 5, 356 | 'foo-234': 234, 357 | }; 358 | propertiesMapper.setMeta(model, testMeta); 359 | 360 | expect(model.meta['fooBar']).to.be.equal(1); 361 | expect(model.meta['bar-foo']).to.be.equal(2); 362 | expect(model.meta['foo']).to.be.equal(3); 363 | expect(model.meta['foo_bar']).to.be.equal(5); 364 | expect(model.meta['foo-234']).to.be.equal(234); 365 | }); 366 | }); 367 | 368 | }); 369 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.6.2", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "outDir": "lib/", 9 | "strict": false, 10 | "removeComments": true, 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "skipLibCheck": true 15 | }, 16 | "files": [ 17 | "./src/index.ts" 18 | ], 19 | "lib": [ 20 | "es6", 21 | "dom" 22 | ], 23 | "include": ["./src"] 24 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "./tests/**.ts" 5 | ], 6 | "compilerOptions": { 7 | "noEmit": true, 8 | "lib": ["es6"] 9 | } 10 | } 11 | --------------------------------------------------------------------------------