├── .browserslistrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg ├── post-commit ├── pre-commit └── pre-push ├── .lintstagedrc ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.js ├── AUTHORS.md ├── README.md ├── babel.config.js ├── commitlint.config.js ├── configurations ├── babel │ ├── config.js │ └── index.js ├── commitlint │ ├── config.js │ └── index.js ├── eslint │ ├── config.js │ └── index.js ├── jest │ ├── config.js │ └── index.js ├── prettier │ ├── config.js │ └── index.js ├── rollup │ ├── config.js │ └── index.js ├── semantic-release │ ├── config.js │ └── index.js └── webpack │ ├── index.js │ ├── rules.js │ ├── setup.common.js │ └── setup.dev.js ├── demo ├── application.tsx ├── assets │ ├── css │ │ └── default.css │ └── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ └── favicon.ico ├── components │ └── formatter │ │ ├── formatter.tsx │ │ └── index.ts ├── index.html ├── screens │ ├── playground │ │ ├── demos │ │ │ ├── mapper.tsx │ │ │ └── mapping.ts │ │ ├── index.ts │ │ └── playground.tsx │ └── shared │ │ ├── menu │ │ ├── index.ts │ │ └── menu.tsx │ │ ├── source-code │ │ ├── index.ts │ │ └── source-code.tsx │ │ └── styles.ts └── tsconfig.json ├── jest.config.js ├── mapper-js.png ├── package.json ├── release.config.js ├── rollup.config.js ├── scripts └── authors.js ├── src ├── __mocks__ │ └── data.js ├── __snapshots__ │ ├── index.test.ts.snap │ ├── map.test.ts.snap │ └── mapper.test.ts.snap ├── index.test.ts ├── index.ts ├── map.test.ts ├── map.ts ├── mapper.test.ts ├── mapper.ts ├── options.ts └── utils │ ├── __snapshots__ │ ├── is.test.ts.snap │ └── to-array.test.ts.snap │ ├── is.test.ts │ ├── is.ts │ ├── to-array.test.ts │ ├── to-array.ts │ ├── type-of.test.ts │ └── type-of.ts ├── tsconfig.json ├── tsconfig.test.json ├── webpack.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 versions 2 | not android < 100 3 | not and_qq < 100 4 | not and_uc < 100 5 | not baidu < 100 6 | not bb < 100 7 | not opera < 1000 8 | not op_mini all 9 | not op_mob < 100 10 | not samsung < 100 11 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # list with all available versions of node.js images 2 | # https://github.com/CircleCI-Public/circleci-dockerfiles/tree/master/node/images 3 | version: 2.1 4 | 5 | orbs: 6 | node: circleci/node@3.0.1 7 | 8 | defaults: &build-image 9 | docker: 10 | - image: node:14 11 | environment: 12 | ## this enables colors in the output 13 | TERM: xterm 14 | commands: 15 | install_package_dependencies: 16 | description: 'Install library dependencies' 17 | steps: 18 | - checkout 19 | - run: 20 | name: Installing dependencies 21 | command: yarn install --frozen-lockfile 22 | 23 | unit_test: 24 | description: 'Run unit tests' 25 | parameters: 26 | node_version: 27 | type: integer 28 | default: 10 29 | coverage: 30 | type: boolean 31 | default: false 32 | steps: 33 | - run: 34 | name: Running unit tests on Node.js v<< parameters.node_version >> <<# parameters.coverage >>with coverage<> 35 | command: yarn test<<# parameters.coverage >>:coverage<> 36 | 37 | jobs: 38 | install: 39 | <<: *build-image 40 | steps: 41 | - checkout 42 | - restore_cache: 43 | keys: 44 | - c00k-b00k-{{ .Branch }}-{{ checksum "yarn.lock" }} 45 | # fallback to using the latest cache if no exact match is found 46 | - c00k-b00k- 47 | - run: 48 | name: Installing dependencies 49 | command: yarn install --frozen-lockfile 50 | - save_cache: 51 | key: c00k-b00k-{{ .Branch }}-{{ checksum "yarn.lock" }} 52 | paths: 53 | - node_modules 54 | - ~/.cache 55 | - store_artifacts: 56 | path: yarn-error.log 57 | - persist_to_workspace: 58 | root: . 59 | paths: 60 | - node_modules 61 | 62 | lint: 63 | <<: *build-image 64 | steps: 65 | - checkout 66 | - attach_workspace: 67 | at: . 68 | - run: 69 | name: Linting library 70 | command: yarn lint 71 | 72 | audit: 73 | <<: *build-image 74 | steps: 75 | - checkout 76 | - attach_workspace: 77 | at: . 78 | - run: 79 | name: Auditing all dependencies 80 | command: yarn audit 81 | 82 | test_node_12: 83 | docker: 84 | - image: node:12 85 | steps: 86 | - checkout 87 | - install_package_dependencies 88 | - unit_test: 89 | node_version: 12 90 | coverage: true 91 | 92 | test_node_14: 93 | docker: 94 | - image: node:14 95 | steps: 96 | - checkout 97 | - install_package_dependencies 98 | - unit_test: 99 | node_version: 14 100 | 101 | build: 102 | <<: *build-image 103 | steps: 104 | - checkout 105 | - attach_workspace: 106 | at: . 107 | - run: 108 | name: Building the library 109 | command: yarn build 110 | - persist_to_workspace: 111 | root: . 112 | paths: 113 | - lib 114 | - store_artifacts: 115 | path: lib 116 | 117 | release: 118 | <<: *build-image 119 | steps: 120 | - checkout 121 | - attach_workspace: 122 | at: . 123 | - add_ssh_keys: 124 | fingerprints: 125 | - $GITHUB_FINGERPRINT 126 | - run: 127 | name: Set git upstream 128 | command: git branch --set-upstream-to origin/${CIRCLE_BRANCH} 129 | - run: 130 | name: Running semantic-release workflow 131 | command: npx semantic-release 132 | - store_artifacts: 133 | path: release 134 | 135 | workflows: 136 | version: 2 137 | 'Test, Build & Maybe Deploy': 138 | jobs: 139 | - install 140 | - lint: 141 | requires: 142 | - install 143 | # - audit: 144 | # requires: 145 | # - install 146 | - test_node_12 147 | - test_node_14 148 | - build: 149 | requires: 150 | - lint 151 | - test_node_12 152 | - test_node_14 153 | - release: 154 | requires: 155 | - build 156 | filters: 157 | branches: 158 | only: 159 | - master 160 | - next 161 | - pre/rc 162 | - beta 163 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/eslint'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | coverage/ 4 | node_modules/ 5 | cypress/screenshots/ 6 | cypress/videos/ 7 | build/ 8 | public/ 9 | lib/ 10 | .stylelintcache 11 | .env 12 | .envrc 13 | report*.json 14 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/post-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | git update-index --again 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn test 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "./src/**/*.{ts,tsx}": [ 3 | "eslint", 4 | "jest --passWithNoTests", 5 | "prettier" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea 3 | .circleci 4 | .husky 5 | .github 6 | configurations/ 7 | demo/ 8 | release/ 9 | scripts/ 10 | src/ 11 | .browserslistrc 12 | .editorconfig 13 | .eslintignore 14 | .eslintrc.js 15 | .gitignore 16 | .huskyrc 17 | .lintstagedrc 18 | .nvmrc 19 | .prettierignore 20 | .prettierrc.js 21 | babel.config.js 22 | commitlint.config.js 23 | jest.config.js 24 | mapper-js.png 25 | release.config.js 26 | rollup.config.js 27 | tsconfig.* 28 | webpack.config.js 29 | yarn-error.log 30 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | src/**/*.snap 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/prettier'); 2 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | ### @cookbook/mapper-js is authored by: 2 | * Marcos 3 | * Marcos Gonçalves 4 | * semantic-release-bot 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @cookbook/mapper-js 2 | 3 | > Fast, reliable and intuitive object mapping. 4 | 5 | [![NPM Version][npm-image]][npm-url] 6 | [![CI Status][circleci-image]][circleci-url] 7 | [![Downloads Stats][npm-downloads]][npm-url] 8 | [![GitHub stars][stars-image]][stars-url] 9 | [![Known Vulnerabilities][vulnerabilities-image]][vulnerabilities-url] 10 | [![GitHub issues][issues-image]][issues-url] 11 | [![Awesome][awesome-image]][awesome-url] 12 | [![install size][install-size-image]][install-size-url] 13 | [![gzip size][gzip-size-image]][gzip-size-url] 14 | 15 | ![](mapper-js.png) 16 | 17 | ## Demo 18 | 19 | Play around with _mapper-js_ and experience **the magic**! 20 | 21 | [![Edit @cookbook/mapper-js](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/cookbookmapper-js-jo7gf?fontsize=14&hidenavigation=1&theme=dark) 22 | 23 | ## Installation 24 | 25 | ```sh 26 | npm install @cookbook/mapper-js --save 27 | #or 28 | yarn add @cookbook/mapper-js 29 | ``` 30 | 31 | ## How to use 32 | 33 | ### 1) Know the structure from your source data 34 | 35 | Before we start, it is essential that we know your data structure 36 | so we can map it accordingly. 37 | 38 | For this demo case, let's assume that we have the following object: 39 | 40 | ```js 41 | const source = { 42 | person: { 43 | name: { 44 | firstName: 'John', 45 | lastName: 'Doe', 46 | }, 47 | age: 32, 48 | drinks: ['beer', 'whiskey'], 49 | address: [ 50 | { 51 | street: 'Infinite Loop', 52 | city: 'Cupertino', 53 | state: 'CA', 54 | postalCode: 95014, 55 | country: 'United States', 56 | }, 57 | { 58 | street: '1600 Amphitheatre', 59 | city: 'Mountain View', 60 | state: 'CA', 61 | postalCode: 94043, 62 | country: 'United States', 63 | }, 64 | ], 65 | }, 66 | }; 67 | ``` 68 | 69 | ### 2) Create your mapping using dot notation 70 | 71 | At this step, we need to create our `mapping` against our data `source`. 72 | 73 | We will be using `dot notation` to create our `final structure`. 74 | 75 | > For more info about `dot notation` API, check out the [documentation](https://github.com/the-cookbook/dot-notation) 76 | 77 | With `mapper`, it is possible to `get` _one_ or _several_ values from our `source` 78 | and even `transform` it in the way we need. 79 | 80 | For that, `map()` accepts `single dot notation` path or 81 | `an array of dot notation paths`. E.g.: `map('person.name.firstName')`, `map([person.name.firstName, person.name.lastName]);`' 82 | 83 | Those values can be _transformed_ by using the `.transform()` method, which expects a `function` as argument and provides 84 | the selected values as array in the `parameter`. 85 | 86 | > For more information about the usage, check the [API Documentation](#api-documentation). 87 | 88 | Now let's create our `mapping`! 89 | 90 | ```js 91 | // mapping.ts 92 | import mapper from '@cookbook/mapper-js'; 93 | 94 | ... 95 | 96 | const mapping = mapper((map) => ({ 97 | 'person.name': map('person.name') 98 | .transform(({ firstName, lastName }) => `${firstName} ${lastName}`) 99 | .value, 100 | 'person.lastName': map('person.lastName').value, 101 | 'person.isAllowedToDrive': map(['person.age', 'person.drinks']) 102 | .transform((age, drinks) => age > 18 && drinks.includes('soft-drink')) 103 | .value, 104 | address: map('person.address').value, 105 | defaultAddress: map('person.address[0]').value, 106 | })); 107 | ``` 108 | 109 | ### 3) Create your mapped object 110 | 111 | ```js 112 | import mapping from './mapping'; 113 | ... 114 | 115 | const result = mapping(source); 116 | /* outputs 117 | { 118 | person: { 119 | name: 'John Doe', 120 | isAllowedToDrive: false, 121 | }, 122 | address: [ 123 | { 124 | street: 'Infinite Loop', 125 | city: 'Cupertino', 126 | state: 'CA', 127 | postalCode: 95014, 128 | country: 'United States' 129 | }, 130 | ... 131 | ], 132 | defaultAddress: { 133 | street: 'Infinite Loop', 134 | city: 'Cupertino', 135 | state: 'CA', 136 | postalCode: 95014, 137 | country: 'United States' 138 | } 139 | } 140 | */ 141 | 142 | ``` 143 | 144 | # API Documentation 145 | 146 | ## mapper 147 | 148 | **Type:** `function()` 149 | **Parameter:** `mapping: Mapping` 150 | **Return:** `(source: object | object[], options?: Options) => T extends [] ? T[] : T`, 151 | **Signature:** `(mapping: Mapping) => (source: object | object[], options?: Options) => T extends [] ? T[] : T` 152 | 153 | **Description:** 154 | 155 | `mapper()` is the main method and responsible for mapping the _values_ from your _data source_ against the _mapping instructions_. 156 | It accepts `dot notation` path(s) as `key(s)`. 157 | 158 | Example: 159 | 160 | ```ts 161 | // raw definition 162 | const mapping = mapper((map) => ({ 163 | ... 164 | })); 165 | 166 | // with map() query 167 | const mapping = mapper((map) => ({ 168 | 'employee.name': map('person.name.firstName').value, 169 | 'employee.age': map('person.name.age').value, 170 | 'employee.address': map('person.address').value, 171 | })); 172 | ``` 173 | 174 | As a result from the above implementation, `mapper()` return a new `function` to map and compile your _source data_ against your _mapping_. 175 | 176 | It accepts an extra (_optional_) argument defining the [_global mapping options_](#mapper-options). 177 | 178 | Example: 179 | 180 | ```ts 181 | ... 182 | 183 | mapping(source, options); 184 | 185 | /* outputs 186 | { 187 | employee: { 188 | name: 'John', 189 | age: 32, 190 | address: [ 191 | { 192 | street: 'Infinite Loop', 193 | city: 'Cupertino', 194 | state: 'CA', 195 | postalCode: 95014, 196 | country: 'United States' 197 | }, 198 | ... 199 | ], 200 | }, 201 | } 202 | */ 203 | ``` 204 | 205 | --- 206 | 207 | ## map 208 | 209 | **Type:** `function` 210 | **Parameter:** `keys: string | string[], options?: Options` 211 | **Return:** `MapMethods`, 212 | **Signature:** `(keys: string | string[], options?: Options) => MapMethods` 213 | 214 | **Description:** 215 | 216 | `root` method retrieves values from your _source data_ using `dot notation` path, it accepts a string or array of string. 217 | 218 | It accepts an extra (_optional_) argument to define the [_mapping options for current entry_](#mapper-options), _overriding_ the _global mapping options_. 219 | 220 | Example: 221 | 222 | ```ts 223 | map('person.name.firstName'); 224 | map(['person.name.firstName', 'person.name.lastName']); 225 | map(['person.name.firstName', 'person.name.lastName'], options); 226 | ``` 227 | 228 | #### `transform` 229 | 230 | **Type:** `function` 231 | **Parameter:** `...unknown[]` 232 | **Return:** `unknown | unknown[]`, 233 | **Signature:** `(...args: unknown[]) => unknown | unknown[]` 234 | 235 | **Description:** 236 | 237 | `.transform` method provides you the ability to _transform_ the retrieved value(s) from `map()` according to your needs, and for that, it expects a return value. 238 | 239 | `.transform` provides you as _parameter_, the retrieved value(s) in the **same order** as defined in the `map()` method, otherwise 240 | 241 | Example: 242 | 243 | ```ts 244 | // single value 245 | map('person.name.firstName').transform((firstName) => firstName.toLoweCase()); 246 | 247 | // multiple values 248 | map(['person.name.firstName', 'person.name.lastName']).transform((firstName, lastName) => `${firstName} ${lastName}`); 249 | ``` 250 | 251 | #### `value` 252 | 253 | **Type:** `readonly` 254 | **Return:** `T` 255 | **Description:** 256 | 257 | `.value` returns the value of your `dot notation` query. If transformed, returns the transformed value. 258 | 259 | Example: 260 | 261 | ```ts 262 | // single value 263 | map('person.name.firstName').transform((firstName) => firstName.toLoweCase()).value; 264 | 265 | // multiple values 266 | map(['person.name.firstName', 'person.name.lastName']).transform((firstName, lastName) => `${firstName} ${lastName}`) 267 | .value; 268 | ``` 269 | 270 | ## Mapper Options 271 | 272 | ### defaults 273 | 274 | ```js 275 | { 276 | omitNullUndefined: false, 277 | omitStrategy: () => false, 278 | } 279 | ``` 280 | 281 | ### Details 282 | 283 | **`omitNullUndefined`** 284 | 285 | **Type:** `boolean` 286 | **default value:** `false` 287 | 288 | **Description:** 289 | 290 | Removes `null` or `undefined` entries from the _mapped_ object. 291 | 292 | Example: 293 | 294 | ```ts 295 | /* source object 296 | { 297 | person: { 298 | name: 'John', 299 | lastName: 'Doe', 300 | age: 32, 301 | }, 302 | } 303 | */ 304 | const mapping = mapper((map) => ({ 305 | name: map('person.name').value, 306 | age: map('person.age').value, 307 | // source doesn't have property 'address', 308 | // therefore will return "undefined" 309 | address: map('person.address').value, 310 | })); 311 | 312 | mapping(source, { omitNullUndefined: true }); 313 | /* outputs 314 | { 315 | name: 'John', 316 | age: 32, 317 | } 318 | */ 319 | ``` 320 | 321 | **`omitStrategy`** 322 | 323 | **Type:** `function` 324 | **Parameter:** `value: unknown | unknown[]` 325 | **Return:** `boolean` 326 | **Signature:** `(value: unknown | unknown[]) => boolean` 327 | 328 | **Description:** 329 | 330 | Defines a _custom strategy_ to omit (_suppress_) entries from the _mapped object_. 331 | 332 | Example: 333 | 334 | ```tsx 335 | /* source object 336 | { 337 | person: { 338 | name: 'John', 339 | lastName: 'Doe', 340 | age: 32, 341 | address: { 342 | street: 'Infinite Loop', 343 | city: 'Cupertino', 344 | state: 'CA', 345 | postalCode: 95014, 346 | country: 'United States', 347 | } 348 | }, 349 | } 350 | */ 351 | 352 | const customOmitStrategy = (address: Record): boolean => address && address.city === 'Cupertino'; 353 | 354 | const mapping = mapper((map) => ({ 355 | name: map('person.name').value, 356 | age: map('person.age').value, 357 | address: map('person.address').value, 358 | })); 359 | 360 | mapping(source, { omitStrategy: customOmitStrategy }); 361 | /* outputs 362 | { 363 | name: 'John', 364 | age: 32, 365 | } 366 | */ 367 | ``` 368 | 369 | 370 | 371 | [npm-image]: https://img.shields.io/npm/v/@cookbook/mapper-js.svg?style=flat-square 372 | [npm-url]: https://npmjs.org/package/@cookbook/mapper-js 373 | [npm-downloads]: https://img.shields.io/npm/dm/@cookbook/mapper-js.svg?style=flat-square 374 | [circleci-image]: https://circleci.com/gh/the-cookbook/mapper-js.svg?style=svg 375 | [circleci-url]: https://circleci.com/gh/the-cookbook/mapper-js 376 | [stars-image]: https://img.shields.io/github/stars/the-cookbook/mapper-js.svg 377 | [stars-url]: https://github.com/the-cookbook/mapper-js/stargazers 378 | [vulnerabilities-image]: https://snyk.io/test/github/the-cookbook/mapper-js/badge.svg 379 | [vulnerabilities-url]: https://snyk.io/test/github/the-cookbook/mapper-js 380 | [issues-image]: https://img.shields.io/github/issues/the-cookbook/mapper-js.svg 381 | [issues-url]: https://github.com/the-cookbook/mapper-js/issues 382 | [awesome-image]: https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg 383 | [awesome-url]: https://github.com/the-cookbook/mapper-js 384 | [install-size-image]: https://packagephobia.now.sh/badge?p=@cookbook/mapper-js 385 | [install-size-url]: https://packagephobia.now.sh/result?p=@cookbook/mapper-js 386 | [gzip-size-image]: http://img.badgesize.io/https://unpkg.com/@cookbook/mapper-js/lib/mapper.min.js?compression=gzip 387 | [gzip-size-url]: https://unpkg.com/@cookbook/mapper-js/lib/mapper.min.js 388 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/babel'); 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/commitlint'); 2 | -------------------------------------------------------------------------------- /configurations/babel/config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | 4 | return { 5 | presets: [ 6 | [ 7 | '@babel/preset-env', 8 | { 9 | debug: process.env.NODE_ENV === 'development', 10 | }, 11 | ], 12 | '@babel/preset-typescript', 13 | '@babel/preset-react', 14 | ], 15 | plugins: [ 16 | [ 17 | 'module-resolver', 18 | { 19 | root: ['./src'], 20 | }, 21 | ], 22 | '@babel/plugin-transform-react-jsx', 23 | ['@babel/plugin-proposal-class-properties', { loose: false }], 24 | '@babel/plugin-proposal-export-default-from', 25 | ], 26 | env: { 27 | test: { 28 | plugins: ['dynamic-import-node'], 29 | }, 30 | production: { 31 | comments: false, 32 | plugins: [ 33 | [ 34 | 'transform-remove-console', 35 | { 36 | exclude: ['error', 'warn'], 37 | }, 38 | ], 39 | [ 40 | 'transform-react-remove-prop-types', 41 | { 42 | mode: 'remove', 43 | removeImport: true, 44 | additionalLibraries: [/\/prop-types$/u], 45 | }, 46 | ], 47 | ], 48 | }, 49 | }, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /configurations/babel/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/commitlint/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'chore', 10 | 'ci', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'release', 17 | 'revert', 18 | 'style', 19 | 'test', 20 | ], 21 | ], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /configurations/commitlint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/eslint/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | browser: true, 7 | es6: true, 8 | node: true, 9 | }, 10 | globals: { 11 | VERSION: false, 12 | ENVIRONMENT: false, 13 | }, 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: path.resolve(__dirname, '../../', 'tsconfig.test.json'), 17 | ecmaVersion: 2020, 18 | sourceType: 'module', 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | }, 23 | plugins: [ 24 | '@typescript-eslint', 25 | 'prettier', 26 | 'filenames', 27 | 'jest', 28 | 'optimize-regex', 29 | 'no-only-tests', 30 | 'testing-library', 31 | ], 32 | extends: [ 33 | 'airbnb', 34 | 'eslint:recommended', 35 | 'plugin:@typescript-eslint/eslint-recommended', 36 | 'plugin:@typescript-eslint/recommended', 37 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 38 | 'plugin:eslint-comments/recommended', 39 | 'plugin:import/errors', 40 | 'plugin:import/typescript', 41 | 'plugin:import/warnings', 42 | 'plugin:jest/recommended', 43 | 'plugin:no-unsanitized/DOM', 44 | 'plugin:optimize-regex/all', 45 | 'plugin:prettier/recommended', 46 | 'plugin:react/recommended', 47 | 'eslint-config-prettier', 48 | 'prettier', 49 | ], 50 | rules: { 51 | '@typescript-eslint/explicit-function-return-type': [ 52 | 'error', 53 | { allowExpressions: true, allowTypedFunctionExpressions: true }, 54 | ], 55 | '@typescript-eslint/indent': ['error', 2], 56 | '@typescript-eslint/interface-name-prefix': 'off', 57 | '@typescript-eslint/member-ordering': [ 58 | 'error', 59 | { 60 | default: [ 61 | 'field', 62 | 'private-static-field', 63 | 'public-static-field', 64 | 'constructor', 65 | 'private-instance-method', 66 | 'protected-instance-method', 67 | 'public-instance-method', 68 | ], 69 | }, 70 | ], 71 | '@typescript-eslint/no-explicit-any': ['error', { ignoreRestArgs: true }], 72 | '@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }], 73 | '@typescript-eslint/no-shadow': 'error', 74 | '@typescript-eslint/no-unused-vars': 'error', 75 | '@typescript-eslint/no-useless-constructor': 'error', 76 | '@typescript-eslint/no-unsafe-call': 'off', 77 | '@typescript-eslint/no-unsafe-member-access': 'off', 78 | '@typescript-eslint/unbound-method': 'off', 79 | '@typescript-eslint/restrict-template-expressions': [ 80 | 'error', 81 | { 82 | allowNumber: true, 83 | allowBoolean: true, 84 | allowAny: false, 85 | allowNullish: false, 86 | }, 87 | ], 88 | 'array-callback-return': 'error', 89 | 'block-scoped-var': 'error', 90 | camelcase: ['error', { properties: 'always' }], 91 | 'consistent-return': 'off', 92 | 'default-case': 'error', 93 | 'filenames/match-regex': ['error', '^[a-z0-9-]+(.test|.spec|.d)?$', true], 94 | 'filenames/match-exported': ['error', 'kebab', null, false], 95 | 'filenames/no-index': 'off', 96 | // https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules 97 | 'import/default': 'error', 98 | 'import/export': 'error', 99 | 'import/exports-last': 'off', 100 | 'import/extensions': ['error', 'never', { json: 'always', scss: 'always', graphql: 'always', mapping: 'always' }], 101 | 'import/first': 'error', 102 | 'import/max-dependencies': ['error', { max: 30 }], 103 | 'import/named': 'error', 104 | 'import/namespace': ['error', { allowComputed: true }], 105 | 'import/newline-after-import': 'error', 106 | 'import/no-absolute-path': 'error', 107 | 'import/no-amd': 'error', 108 | 'import/no-anonymous-default-export': 'off', 109 | 'import/no-commonjs': 'error', 110 | 'import/no-default-export': 'off', 111 | 'import/no-duplicates': 'error', 112 | 'import/no-dynamic-require': 'error', 113 | 'import/no-extraneous-dependencies': [ 114 | 'error', 115 | { devDependencies: true, optionalDependencies: true, peerDependencies: false }, 116 | ], 117 | 'import/no-internal-modules': 'off', 118 | 'import/no-mutable-exports': 'error', 119 | 'import/no-named-as-default': 'off', 120 | 'import/no-named-as-default-member': 'error', 121 | 'import/no-named-default': 'error', 122 | 'import/no-namespace': 'off', 123 | 'import/no-nodejs-modules': 'off', 124 | 'import/no-unassigned-import': ['error', { allow: ['**/*.scss'] }], 125 | 'import/no-unresolved': 'off', 126 | 'import/no-useless-path-segments': 'off', 127 | 'import/no-webpack-loader-syntax': 'error', 128 | 'import/order': [ 129 | 'error', 130 | { 131 | groups: [ 132 | ['builtin', 'external'], 133 | ['internal', 'parent'], 134 | ['sibling', 'index'], 135 | ], 136 | 'newlines-between': 'always', 137 | }, 138 | ], 139 | 'import/prefer-default-export': 'off', 140 | 'import/unambiguous': 'off', 141 | 'max-depth': ['error', 4], 142 | 'max-len': ['error', { code: 120, tabWidth: 2, comments: 120 }], 143 | 'no-cond-assign': ['error', 'except-parens'], 144 | 'no-continue': 'off', 145 | 'no-duplicate-imports': 'error', 146 | 'no-only-tests/no-only-tests': 'error', 147 | 'no-plusplus': 'error', 148 | 'no-shadow': 'off', 149 | 'no-template-curly-in-string': 'error', 150 | 'no-use-before-define': 'off', 151 | 'no-unused-vars': 'off', 152 | 'no-unsanitized/method': 'error', 153 | 'no-unsanitized/property': 'error', 154 | 'no-void': ['error', { allowAsStatement: true }], 155 | 'prefer-rest-params': 'error', 156 | 'prettier/prettier': ['error'], 157 | 'react/display-name': 'off', 158 | 'react/jsx-filename-extension': ['error', { extensions: ['.tsx'] }], 159 | 'react/jsx-fragments': ['error', 'element'], 160 | 'react/jsx-handler-names': 'off', 161 | 'react/jsx-indent': ['error', 2], 162 | 'react/jsx-indent-props': ['error', 2], 163 | 'react/jsx-max-depth': ['error', { max: 6 }], 164 | 'react/jsx-max-props-per-line': ['error', { maximum: 1, when: 'multiline' }], 165 | 'react/jsx-no-bind': ['error', { ignoreRefs: true, allowArrowFunctions: true }], 166 | 'react/jsx-no-literals': ['error', { ignoreProps: true }], 167 | 'react/jsx-pascal-case': 'off', 168 | 'react/jsx-props-no-spreading': ['error', { exceptions: ['Component'] }], 169 | 'react/no-multi-comp': 'off', 170 | 'react/prefer-stateless-function': 'error', 171 | 'react/prop-types': 'off', 172 | 'react/require-default-props': 'off', 173 | 'testing-library/consistent-data-testid': [ 174 | 'error', 175 | { 176 | testIdAttribute: ['data-testid'], 177 | testIdPattern: '^([a-z-]+)$', 178 | }, 179 | ], 180 | 'optimize-regex/optimize-regex': 'error', 181 | }, 182 | overrides: [ 183 | { 184 | files: ['src/**/**.test.ts', 'src/**/**.test.tsx'], 185 | rules: { 186 | 'react/jsx-props-no-spreading': 'off', 187 | '@typescript-eslint/explicit-function-return-type': 'off', 188 | }, 189 | }, 190 | ], 191 | settings: { 192 | react: { 193 | version: 'detect', 194 | }, 195 | }, 196 | }; 197 | -------------------------------------------------------------------------------- /configurations/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/jest/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | verbose: true, 3 | roots: ['/src'], 4 | transform: { 5 | '.*': 'babel-jest', 6 | }, 7 | testPathIgnorePatterns: ['/node_modules/'], 8 | moduleDirectories: ['node_modules', 'src'], 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'], 10 | moduleNameMapper: { 11 | '^configurations(.*)$': '/configurations$1', 12 | }, 13 | collectCoverageFrom: [ 14 | 'src/**/*.{tsx,ts,js,jsx}', 15 | '!src/**/__test__/**/*.{tsx,ts,js,jsx}', 16 | '!**/node_modules/**', 17 | '!**/coverage/**', 18 | '!configurations/**', 19 | ], 20 | coverageReporters: ['json', 'lcov', 'text-summary', 'html'], 21 | preset: 'ts-jest', 22 | globals: { 23 | 'ts-jest': { 24 | tsConfig: './tsconfig.test.json', 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /configurations/jest/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/prettier/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | semi: true, 4 | trailingComma: 'all', 5 | arrowParens: 'always', 6 | singleQuote: true, 7 | tabWidth: 2, 8 | }; 9 | -------------------------------------------------------------------------------- /configurations/prettier/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/rollup/config.js: -------------------------------------------------------------------------------- 1 | const resolve = require('rollup-plugin-node-resolve'); 2 | const commonjs = require('rollup-plugin-commonjs'); 3 | const { terser } = require('rollup-plugin-terser'); 4 | 5 | const pkg = require('../../package.json'); 6 | const pkgName = 'mapper'; 7 | const global = [...Object.keys(pkg.dependencies)]; 8 | 9 | module.exports = [ 10 | { 11 | input: './lib/index.js', 12 | output: { 13 | file: `lib/${pkgName}.min.js`, 14 | format: 'umd', 15 | name: pkgName, 16 | sourcemap: true, 17 | interop: false, 18 | }, 19 | plugins: [ 20 | resolve({ 21 | mainFields: ['module', 'main'], 22 | }), 23 | commonjs(), 24 | terser(), 25 | ], 26 | }, 27 | { 28 | input: './lib/index.js', 29 | output: { 30 | file: `lib/${pkgName}.pure.min.js`, 31 | format: 'umd', 32 | name: pkgName, 33 | sourcemap: true, 34 | interop: false, 35 | }, 36 | plugins: [ 37 | resolve({ 38 | mainFields: ['module', 'main'], 39 | }), 40 | commonjs(), 41 | terser(), 42 | ], 43 | external: global, 44 | } 45 | ]; 46 | -------------------------------------------------------------------------------- /configurations/rollup/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/semantic-release/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: [ 3 | { name: 'master' }, 4 | { name: 'next' }, 5 | { name: 'pre/rc', channel: 'pre/rc', prerelease: 'rc' }, 6 | { name: 'beta', channel: 'beta', prerelease: 'beta' }, 7 | ], 8 | plugins: [ 9 | '@semantic-release/commit-analyzer', 10 | '@semantic-release/release-notes-generator', 11 | [ 12 | '@semantic-release/npm', 13 | { 14 | tarballDir: 'release', 15 | }, 16 | ], 17 | // documentation https://github.com/semantic-release/github#readme 18 | [ 19 | '@semantic-release/github', 20 | { 21 | assets: [ 22 | { path: 'release/*.tgz' }, 23 | { path: `lib/mapper.min.js*(.map)`, label: 'UMD build minified' }, 24 | { path: `lib/mapper.pure.min.js*(.map)`, label: 'UMD build minified - without dependencies' } 25 | ], 26 | }, 27 | ], 28 | [ 29 | '@semantic-release/git', 30 | { 31 | message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', 32 | assets: ['CHANGELOG.md', 'AUTHORS.md', 'package.json', 'yarn.lock', 'npm-shrinkwrap.json'] 33 | }, 34 | ], 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /configurations/semantic-release/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./config'); 2 | -------------------------------------------------------------------------------- /configurations/webpack/index.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge').merge; 2 | 3 | module.exports = merge([require('./setup.dev'), require('./setup.common')]); 4 | -------------------------------------------------------------------------------- /configurations/webpack/rules.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | test: /\.tsx?$/, 4 | exclude: /node_modules/, 5 | use: ['react-hot-loader/webpack', 'babel-loader'], 6 | }, 7 | { 8 | enforce: 'pre', 9 | test: /\.js$/, 10 | loader: 'source-map-loader', 11 | exclude: /(node_modules)/, 12 | }, 13 | { 14 | test: /\.mjs$/, 15 | include: /node_modules/, 16 | type: 'javascript/auto', 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /configurations/webpack/setup.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const app = require('../../package.json'); 3 | const TerserJSPlugin = require('terser-webpack-plugin'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | const appSource = 'demo'; 8 | 9 | module.exports = { 10 | entry: { 11 | app: ['core-js', 'react-hot-loader/patch', `./${appSource}/application.tsx`], 12 | }, 13 | resolve: { 14 | mainFields: ['browser', 'module', 'main'], 15 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json'], 16 | modules: [path.resolve(__dirname, '../../', appSource), 'node_modules'], 17 | alias: { 18 | '@cookbook/mapper-js': path.resolve(__dirname, '../../src'), 19 | }, 20 | }, 21 | target: 'web', 22 | optimization: { 23 | namedModules: true, 24 | noEmitOnErrors: true, 25 | concatenateModules: true, 26 | minimizer: [ 27 | new TerserJSPlugin({ 28 | terserOptions: { 29 | warnings: false, 30 | compress: { 31 | toplevel: true, 32 | }, 33 | mangle: { 34 | toplevel: true, 35 | }, 36 | output: { 37 | beautify: false, 38 | }, 39 | }, 40 | sourceMap: true, 41 | parallel: true, 42 | cache: false, 43 | }), 44 | ], 45 | splitChunks: { 46 | chunks: 'all', 47 | cacheGroups: { 48 | commons: { 49 | test: /[\\/]node_modules[\\/]/, 50 | name: 'vendors', 51 | chunks: 'all', 52 | minChunks: 2, 53 | }, 54 | default: { 55 | minChunks: 2, 56 | reuseExistingChunk: true, 57 | }, 58 | }, 59 | }, 60 | }, 61 | plugins: [ 62 | new CopyWebpackPlugin({ 63 | patterns: [ 64 | { from: `./${appSource}/assets/favicon/*`, to: '', flatten: true }, 65 | { from: `./${appSource}/assets/css/*`, to: 'css', flatten: true }, 66 | ], 67 | }), 68 | new HtmlWebpackPlugin({ 69 | template: './demo/index.html', 70 | title: app.name, 71 | minify: { 72 | collapseWhitespace: true, 73 | preserveLineBreaks: false, 74 | }, 75 | }), 76 | ], 77 | }; 78 | -------------------------------------------------------------------------------- /configurations/webpack/setup.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | 5 | const rules = require('./rules'); 6 | 7 | const HOST = process.env.HOST || '127.0.0.1'; 8 | const PORT = process.env.PORT || '3300'; 9 | 10 | module.exports = { 11 | mode: 'development', 12 | devtool: 'cheap-module-eval-source-map', 13 | output: { 14 | publicPath: '/', 15 | path: path.join(__dirname, '../../public'), 16 | filename: 'js/[name].bundle.js', 17 | chunkFilename: 'js/[name].chunk.js', 18 | }, 19 | module: { 20 | rules, 21 | }, 22 | devServer: { 23 | contentBase: './public', 24 | noInfo: true, 25 | hot: true, 26 | inline: true, 27 | watchContentBase: true, 28 | historyApiFallback: true, 29 | disableHostCheck: true, 30 | open: true, 31 | port: PORT, 32 | host: HOST, 33 | }, 34 | performance: { 35 | hints: 'warning', 36 | maxAssetSize: 512000, 37 | maxEntrypointSize: 8500000, 38 | assetFilter: function (assetFilename) { 39 | return assetFilename.endsWith('.js'); 40 | }, 41 | }, 42 | optimization: { 43 | nodeEnv: 'development', 44 | }, 45 | plugins: [ 46 | new webpack.HotModuleReplacementPlugin(), 47 | new webpack.optimize.OccurrenceOrderPlugin(), 48 | new webpack.DefinePlugin({ 49 | 'process.env': { NODE_ENV: '"development"' }, 50 | }), 51 | new BundleAnalyzerPlugin({ 52 | analyzerHost: HOST, 53 | openAnalyzer: process.env.NODE_ENV === 'development', 54 | }), 55 | ], 56 | }; 57 | -------------------------------------------------------------------------------- /demo/application.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Playground from './screens/playground'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /demo/assets/css/default.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | width: 100%; 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 7 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 8 | } 9 | 10 | .bg-pan-left { 11 | -webkit-animation: bg-pan-left 8s both infinite; 12 | animation: bg-pan-left 8s infinite alternate both; 13 | background-size: 400% 100% !important; 14 | } 15 | 16 | .btn { 17 | box-shadow: inset 0px 1px 0px 0px #ffffff; 18 | background: linear-gradient(to bottom, #ffffff 5%, #f6f6f6 100%); 19 | background-color: #ffffff; 20 | border-radius: 6px; 21 | border: 1px solid #dcdcdc; 22 | display: inline-block; 23 | cursor: pointer; 24 | color: #666666; 25 | font-family: Arial; 26 | font-size: 15px; 27 | font-weight: bold; 28 | padding: 6px 24px; 29 | text-decoration: none; 30 | text-shadow: 0px 1px 0px #ffffff; 31 | } 32 | 33 | .btn:hover { 34 | background: linear-gradient(to bottom, #f6f6f6 5%, #ffffff 100%); 35 | background-color: #f6f6f6; 36 | } 37 | 38 | .btn:active { 39 | position: relative; 40 | top: 1px; 41 | } 42 | 43 | .menu-container { 44 | position: relative; 45 | background-color: #fff; 46 | width: 100%; 47 | z-index: 2; 48 | } 49 | 50 | .menu-container > ul { 51 | position: relative; 52 | background-color: #fff; 53 | padding: 0; 54 | margin: 0; 55 | list-style: none; 56 | display: block; 57 | border-radius: 0; 58 | box-shadow: none; 59 | } 60 | 61 | .menu-container span { 62 | display: block; 63 | padding: 16px; 64 | } 65 | 66 | .menu-container > ul > li, 67 | .menu-category > li { 68 | display: inline-block; 69 | transition: all ease-in 0.3s; 70 | cursor: pointer; 71 | } 72 | 73 | .menu-container > ul > li.active, 74 | .menu-container > ul > li:hover, 75 | .menu-category > li:hover, 76 | .menu-category > li.active { 77 | background-color: thistle; 78 | } 79 | 80 | .menu-container > ul > li:hover > ul { 81 | display: block; 82 | } 83 | .menu-category { 84 | position: absolute; 85 | background-color: #fff; 86 | padding: 0; 87 | margin: -8px 0 0 10px; 88 | list-style: none; 89 | display: none; 90 | border-radius: 4px; 91 | box-shadow: 0 6px 12px -4px #1b1919; 92 | } 93 | 94 | .menu-category > li { 95 | display: block; 96 | } 97 | .menu-category > li:first-child { 98 | border-radius: 4px 4px 0 0; 99 | } 100 | .menu-category > li:last-child { 101 | border-radius: 0 0 4px 4px; 102 | } 103 | 104 | @-webkit-keyframes bg-pan-left { 105 | 0% { 106 | background-position: 100% 50%; 107 | } 108 | 100% { 109 | background-position: 0% 50%; 110 | } 111 | } 112 | @keyframes bg-pan-left { 113 | 0% { 114 | background-position: 100% 50%; 115 | } 116 | 100% { 117 | background-position: 0% 50%; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /demo/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /demo/assets/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /demo/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /demo/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /demo/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /demo/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/demo/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /demo/components/formatter/formatter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import is from '@cookbook/mapper-js/utils/is'; 4 | 5 | interface Formatter { 6 | title?: string; 7 | source: Record; 8 | } 9 | 10 | const Formatter: React.FunctionComponent = (props: Formatter) => { 11 | const { title, source } = props; 12 | 13 | const shouldFormat = is.array(source) || is.object(source); 14 | 15 | return ( 16 |
17 | {title &&

{title}

} 18 |
26 | {!shouldFormat && source} 27 | {shouldFormat && } 28 |
29 |
30 | ); 31 | }; 32 | 33 | export type { Formatter }; 34 | export default Formatter; 35 | -------------------------------------------------------------------------------- /demo/components/formatter/index.ts: -------------------------------------------------------------------------------- 1 | import Formatter from './formatter'; 2 | 3 | export type { Formatter } from './formatter'; 4 | export default Formatter; 5 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/screens/playground/demos/mapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import data from '@cookbook/mapper-js/__mocks__/data' 3 | 4 | import Formatter from '../../../components/formatter'; 5 | import styles from '../../shared/styles'; 6 | 7 | import mapping from './mapping'; 8 | 9 | const Playground: React.FunctionComponent> = () => { 10 | 11 | return ( 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default Playground; 26 | -------------------------------------------------------------------------------- /demo/screens/playground/demos/mapping.ts: -------------------------------------------------------------------------------- 1 | import mapper from '@cookbook/mapper-js'; 2 | 3 | const mapping = mapper((map) => ({ 4 | active: map('isActive').value, 5 | 'account.balance': map('balance').value, 6 | 'person.age': map('age').value, 7 | 'person.address': map('address').value, 8 | commonFriends: map('commonFriends').value, 9 | })); 10 | 11 | export default mapping; 12 | -------------------------------------------------------------------------------- /demo/screens/playground/index.ts: -------------------------------------------------------------------------------- 1 | import Playground from './playground'; 2 | 3 | export default Playground; 4 | -------------------------------------------------------------------------------- /demo/screens/playground/playground.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Menu from '../shared/menu'; 4 | 5 | import Mapper from './demos/mapper'; 6 | 7 | const selectView = (state: string, action: string ): React.ReactNode => { 8 | switch (action) { 9 | case 'mapper': 10 | default: 11 | return Mapper; 12 | } 13 | } 14 | 15 | const Playground: React.FunctionComponent> = () => { 16 | const [View, dispatch] = React.useReducer(selectView, Mapper); 17 | 18 | const handleOnClick = (value: string): void => { 19 | dispatch(value); 20 | } 21 | 22 | return ( 23 | 24 | 25 |
36 |

Playground

37 | 38 |
39 | 40 | ); 41 | }; 42 | 43 | export default Playground; 44 | -------------------------------------------------------------------------------- /demo/screens/shared/menu/index.ts: -------------------------------------------------------------------------------- 1 | import Menu from './menu'; 2 | 3 | export default Menu; 4 | -------------------------------------------------------------------------------- /demo/screens/shared/menu/menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface Category { 4 | label: string; 5 | value:string, 6 | categories?: Category[]; 7 | } 8 | 9 | const menu: Category[] = [ 10 | { 11 | label: 'Default', 12 | value: 'mapper' 13 | }, 14 | ]; 15 | 16 | interface Menu { 17 | onClick: (value: string) => void; 18 | } 19 | 20 | const Menu: React.FunctionComponent = (props: Menu) => { 21 | const [selected, updateSelection] = React.useState(menu[0].value); 22 | const { onClick } = props; 23 | 24 | const handleOnClick = (value: string): void => { 25 | updateSelection(value); 26 | onClick(value) 27 | }; 28 | 29 | const renderCategories = (categories: Category[]): React.ReactNode => ( 30 |
    31 | {categories.map(({ label, value }, idx) => ( 32 | 33 |
  • 34 | { 36 | e.preventDefault(); 37 | handleOnClick(value); 38 | }} 39 | > 40 | {label} 41 | 42 |
  • 43 |
    44 | ))} 45 |
46 | ); 47 | 48 | return
{renderCategories(menu)}
; 49 | }; 50 | 51 | export type { Menu }; 52 | export default Menu; 53 | -------------------------------------------------------------------------------- /demo/screens/shared/source-code/index.ts: -------------------------------------------------------------------------------- 1 | import SourceCode from './source-code'; 2 | 3 | export default SourceCode; 4 | -------------------------------------------------------------------------------- /demo/screens/shared/source-code/source-code.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from '../styles'; 3 | 4 | const validateJSON = (source: string): boolean => { 5 | try { 6 | JSON.parse(source); 7 | } catch (e) { 8 | return false; 9 | } 10 | 11 | return true; 12 | } 13 | 14 | 15 | interface SourceCode { 16 | source: string | Record; 17 | onChange: (value: string) => void; 18 | } 19 | 20 | const SourceCode: React.FunctionComponent = (props: SourceCode) => { 21 | const { source, onChange } = props; 22 | const [isJSONValid, updateJSONValidation] = React.useState(true); 23 | 24 | const handleOnChange = ({ target: { value }}: React.ChangeEvent) => { 25 | let isValid = validateJSON(value); 26 | 27 | updateJSONValidation(isValid); 28 | 29 | if (isValid) { 30 | onChange(JSON.parse(value)) 31 | } 32 | } 33 | 34 | 35 | return ( 36 | 37 | {!isJSONValid && ( 38 |

Invalid JSON format

39 | )} 40 |
49 | 50 |
51 |
52 | ) 53 | }; 54 | 55 | export default SourceCode; 56 | -------------------------------------------------------------------------------- /demo/screens/shared/styles.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default { 4 | row: { 5 | display: 'flex', 6 | flexWrap: 'wrap', 7 | marginRight: -15, 8 | marginLeft: -15, 9 | }, 10 | col: { 11 | position: 'relative', 12 | width: '100%', 13 | padding: 15, 14 | flexBasis: 0, 15 | flexGrow: 1, 16 | maxWidth: '100%', 17 | }, 18 | title: { 19 | minWidth: '100%', 20 | }, 21 | textField: { 22 | width: '100%', 23 | padding: 11, 24 | marginBottom:' 26px', 25 | border: 'none', 26 | borderRadius: '5px', 27 | }, 28 | textArea: { 29 | backgroundColor: 'transparent', 30 | color: 'white', 31 | resize: 'none', 32 | display: 'flex', 33 | height: '100%', 34 | width: '100%', 35 | margin: 0, 36 | padding: 20, 37 | border: 'none', 38 | boxSizing: 'border-box', 39 | }, 40 | error: { 41 | width: '100%', 42 | fontStyle: 'italic', 43 | backgroundColor: '#fff', 44 | color: 'red', 45 | padding: 4, 46 | } 47 | } as Record; 48 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "rootDir": "./", 6 | "outDir": "./build", 7 | "paths": { 8 | "@cookbook/mapper-js": ["../src/*"] 9 | } 10 | }, 11 | "include": ["**/*.ts", "**/*.tsx"] 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/jest'); 2 | -------------------------------------------------------------------------------- /mapper-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the-cookbook/mapper-js/c76b0b130dbcd94e34321b67ac9e29475dd7d711/mapper-js.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cookbook/mapper-js", 3 | "version": "1.0.0", 4 | "description": "Fast, reliable and intuitive object mapping.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/the-cookbook/mapper-js.git" 10 | }, 11 | "engines": { 12 | "node": ">=12" 13 | }, 14 | "author": "Marcos Gonçalves ", 15 | "license": "MIT", 16 | "private": false, 17 | "bugs": { 18 | "url": "https://github.com/the-cookbook/mapper-js/issues" 19 | }, 20 | "homepage": "https://github.com/the-cookbook/mapper-js#readme", 21 | "keywords": [ 22 | "library", 23 | "javascript", 24 | "typescript", 25 | "mapper", 26 | "mapping", 27 | "data-mapping", 28 | "object-mapping", 29 | "dot-notation" 30 | ], 31 | "scripts": { 32 | "authors": "node ./scripts/authors.js", 33 | "prepare": "husky install && npm run build && npm run authors", 34 | "prepublishOnly": "npm test && npm run lint", 35 | "preversion": "npm run lint", 36 | "postversion": "git push && git push --tags", 37 | "release": "standard-version", 38 | "prestart": "npm run clean", 39 | "start": "webpack serve --mode development --progress --profile", 40 | "new": "plop", 41 | "commit": "npx git-cz", 42 | "build": "npm run clean && npx babel src/ -d lib --extensions '.ts' --ignore '**/*.test.ts,**/__mocks__/**/*' && tsc --project tsconfig.json --declaration --emitDeclarationOnly", 43 | "postbuild": "rollup --config", 44 | "clean": "rm -rf lib && rm -rf .*cache", 45 | "test": "jest --no-cache", 46 | "test:watch": "jest --watch", 47 | "test:coverage": "jest --coverage --maxWorkers=2", 48 | "lint": "npm-run-all lint:*", 49 | "lint:js": "eslint './src/**/*.{ts,tsx}' || exit 0", 50 | "lint-fix": "run-p lint-fix:*", 51 | "lint-fix:js": "npm run lint:js -- --fix || exit 0", 52 | "prettier": "prettier -c --write 'src/**/*'" 53 | }, 54 | "devDependencies": { 55 | "@babel/cli": "7.15.7", 56 | "@babel/core": "7.15.5", 57 | "@babel/plugin-proposal-class-properties": "7.14.5", 58 | "@babel/plugin-proposal-export-default-from": "7.14.5", 59 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 60 | "@babel/preset-env": "7.15.6", 61 | "@babel/preset-react": "7.14.5", 62 | "@babel/preset-typescript": "7.15.0", 63 | "@commitlint/cli": "13.2.0", 64 | "@commitlint/config-conventional": "13.2.0", 65 | "@semantic-release/git": "v9.0.1", 66 | "@semantic-release/github": "7.2.3", 67 | "@types/chai": "4.2.22", 68 | "@types/core-js": "2.5.5", 69 | "@types/expect": "24.3.0", 70 | "@types/jest": "27.0.2", 71 | "@types/react": "17.0.26", 72 | "@types/react-dom": "17.0.9", 73 | "@types/react-test-renderer": "17.0.1", 74 | "@types/webpack-env": "1.16.2", 75 | "@typescript-eslint/eslint-plugin": "4.32.0", 76 | "@typescript-eslint/parser": "4.32.0", 77 | "babel-loader": "8.2.2", 78 | "babel-plugin-dynamic-import-node": "2.3.3", 79 | "babel-plugin-module-resolver": "4.1.0", 80 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 81 | "babel-plugin-transform-remove-console": "6.9.4", 82 | "chai": "4.3.4", 83 | "commitizen": "4.2.4", 84 | "commitlint": "13.2.0", 85 | "copy-webpack-plugin": "9.0.1", 86 | "cz-conventional-changelog": "3.3.0", 87 | "deepmerge": "4.2.2", 88 | "eslint": "7.32.0", 89 | "eslint-config-airbnb": "18.2.1", 90 | "eslint-config-prettier": "8.3.0", 91 | "eslint-plugin-eslint-comments": "3.2.0", 92 | "eslint-plugin-filenames": "1.3.2", 93 | "eslint-plugin-import": "2.24.2", 94 | "eslint-plugin-jest": "24.5.0", 95 | "eslint-plugin-jsx-a11y": "6.4.1", 96 | "eslint-plugin-no-only-tests": "2.6.0", 97 | "eslint-plugin-no-unsanitized": "3.1.5", 98 | "eslint-plugin-optimize-regex": "1.2.1", 99 | "eslint-plugin-prettier": "4.0.0", 100 | "eslint-plugin-react": "7.26.1", 101 | "eslint-plugin-react-hooks": "4.2.0", 102 | "eslint-plugin-testing-library": "4.12.4", 103 | "html-webpack-plugin": "5.3.2", 104 | "husky": "7.0.2", 105 | "jest": "27.2.4", 106 | "lint-staged": "11.1.2", 107 | "npm-run-all": "4.1.5", 108 | "prettier": "2.4.1", 109 | "prop-types": "15.7.2", 110 | "react": "17.0.2", 111 | "react-docgen": "5.4.0", 112 | "react-docgen-typescript": "2.1.0", 113 | "react-dom": "17.0.2", 114 | "react-hot-loader": "4.13.0", 115 | "react-json-view": "1.21.3", 116 | "react-test-renderer": "17.0.2", 117 | "require-all": "3.0.0", 118 | "rollup": "2.58.0", 119 | "rollup-plugin-commonjs": "10.1.0", 120 | "rollup-plugin-node-resolve": "5.2.0", 121 | "rollup-plugin-terser": "7.0.2", 122 | "source-map-loader": "3.0.0", 123 | "standard-version": "9.3.1", 124 | "start-server-and-test": "1.14.0", 125 | "terser-webpack-plugin": "5.2.4", 126 | "ts-jest": "27.0.5", 127 | "typescript": "4.4.3", 128 | "webpack": "5.56.0", 129 | "webpack-bundle-analyzer": "4.4.2", 130 | "webpack-cli": "4.8.0", 131 | "webpack-dev-server": "4.3.0", 132 | "webpack-merge": "5.8.0" 133 | }, 134 | "peerDependencies": {}, 135 | "dependencies": { 136 | "@cookbook/dot-notation": "1.1.0" 137 | }, 138 | "config": { 139 | "commitizen": { 140 | "path": "./node_modules/cz-conventional-changelog" 141 | } 142 | }, 143 | "publishConfig": { 144 | "access": "public" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/semantic-release'); 2 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/rollup'); 2 | -------------------------------------------------------------------------------- /scripts/authors.js: -------------------------------------------------------------------------------- 1 | const { name } = require('../package.json'); 2 | const { exec } = require('child_process'); 3 | 4 | const scripts = `echo '### ${name} is authored by: \n\n' 5 | > AUTHORS.md 6 | | git log --format='* %aN <%aE>' 7 | | sort -u >> AUTHORS.md`; 8 | 9 | exec(scripts.replace(/\n/gi, ''), function (err, stdout) { 10 | if (err) { 11 | console.error(err); 12 | return; 13 | } 14 | 15 | console.log(stdout); 16 | }); 17 | -------------------------------------------------------------------------------- /src/__mocks__/data.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | _id: '5f298c3a9de3ab8556c084db', 4 | index: 0, 5 | guid: '1ee5f65e-d6d3-4cb4-b529-61ef47a152a7', 6 | isActive: true, 7 | balance: '$1,273.07', 8 | picture: 'http://placehold.it/32x32', 9 | age: 32, 10 | eyeColor: 'blue', 11 | name: 'Gay Landry', 12 | gender: 'female', 13 | company: 'XANIDE', 14 | email: 'gaylandry@xanide.com', 15 | phone: '+1 (955) 541-2541', 16 | address: '740 Debevoise Avenue, Silkworth, Oregon, 2792', 17 | about: 18 | 'Cillum est cillum incididunt laborum Lorem ad sit et enim exercitation laboris pariatur. Sit labore occaecat et reprehenderit labore dolore exercitation cupidatat labore esse eu eiusmod. Amet ipsum qui ex id.\r\n', 19 | registered: '2014-11-05T04:54:40 -01:00', 20 | latitude: -21.237512, 21 | longitude: -79.117184, 22 | tags: ['amet', 'labore', 'irure', 'in', 'proident', 'qui', 'nulla'], 23 | friends: [ 24 | { 25 | id: 0, 26 | name: 'Courtney Medina', 27 | }, 28 | { 29 | id: 1, 30 | name: 'Hodges Avery', 31 | }, 32 | { 33 | id: 2, 34 | name: 'Blanchard Hyde', 35 | }, 36 | ], 37 | greeting: 'Hello, Gay Landry! You have 2 unread messages.', 38 | favoriteFruit: 'banana', 39 | }, 40 | { 41 | _id: '5f298c3a79466b7d76789046', 42 | index: 1, 43 | guid: 'acc54e37-8bbc-49a4-ba41-3a57aa415f4a', 44 | isActive: false, 45 | balance: '$3,134.54', 46 | picture: 'http://placehold.it/32x32', 47 | age: 24, 48 | eyeColor: 'blue', 49 | name: 'Harris Bryant', 50 | gender: 'male', 51 | company: 'KINDALOO', 52 | email: 'harrisbryant@kindaloo.com', 53 | phone: '+1 (881) 504-2864', 54 | address: '406 Macdougal Street, Katonah, Arkansas, 2252', 55 | about: 56 | 'Dolore nostrud mollit esse sint sint nostrud elit reprehenderit. Nulla mollit in ullamco quis est. Nulla adipisicing ad deserunt veniam consectetur nostrud irure aute. Et do veniam non esse irure sunt ut aliqua anim pariatur ad velit irure culpa. Adipisicing non veniam anim eu laborum. Excepteur deserunt enim consequat consequat laboris officia excepteur dolore.\r\n', 57 | registered: '2018-12-07T01:13:53 -01:00', 58 | latitude: 21.020802, 59 | longitude: 162.663481, 60 | tags: ['consequat', 'culpa', 'pariatur', 'incididunt', 'sunt', 'enim', 'dolor'], 61 | friends: [ 62 | { 63 | id: 0, 64 | name: 'Ruby Conley', 65 | }, 66 | { 67 | id: 1, 68 | name: 'Morgan Ramos', 69 | }, 70 | { 71 | id: 2, 72 | name: 'Jeanne Woodward', 73 | }, 74 | ], 75 | greeting: 'Hello, Harris Bryant! You have 7 unread messages.', 76 | favoriteFruit: 'banana', 77 | }, 78 | { 79 | _id: '5f298c3abbbc5c3e156cee62', 80 | index: 2, 81 | guid: '0b1b7bb7-4ac1-4c75-8fbe-bdc1ca8854e0', 82 | isActive: true, 83 | balance: '$1,871.29', 84 | picture: 'http://placehold.it/32x32', 85 | age: 32, 86 | eyeColor: 'blue', 87 | name: 'Rosario Delacruz', 88 | gender: 'female', 89 | company: 'ENTOGROK', 90 | email: 'rosariodelacruz@entogrok.com', 91 | phone: '+1 (813) 508-3145', 92 | address: '848 College Place, Glenshaw, New York, 1218', 93 | about: 94 | 'Est tempor et id officia occaecat ad. In fugiat sint do velit in veniam laboris laboris consectetur labore. Eu qui velit sunt laborum. Quis laboris ut enim nisi irure id dolor dolor commodo aliqua non in. Cillum aliqua adipisicing ex nostrud eu ipsum in dolore commodo aliquip ut.\r\n', 95 | registered: '2020-04-29T04:59:36 -02:00', 96 | latitude: 62.267437, 97 | longitude: 4.248991, 98 | tags: ['ut', 'irure', 'nulla', 'esse', 'labore', 'occaecat', 'nisi'], 99 | friends: [ 100 | { 101 | id: 0, 102 | name: 'Lesley Hebert', 103 | }, 104 | { 105 | id: 1, 106 | name: 'Ramos Booth', 107 | }, 108 | { 109 | id: 2, 110 | name: 'Decker Hubbard', 111 | }, 112 | ], 113 | greeting: 'Hello, Rosario Delacruz! You have 9 unread messages.', 114 | favoriteFruit: 'strawberry', 115 | }, 116 | { 117 | _id: '5f298c3abad7589e14b8ea22', 118 | index: 3, 119 | guid: '4297fcc6-ee7c-4ff5-ae62-768977358359', 120 | isActive: false, 121 | balance: '$3,377.96', 122 | picture: 'http://placehold.it/32x32', 123 | age: 36, 124 | eyeColor: 'blue', 125 | name: 'Lorene Hays', 126 | gender: 'female', 127 | company: 'ECOSYS', 128 | email: 'lorenehays@ecosys.com', 129 | phone: '+1 (852) 436-2074', 130 | address: '683 Cornelia Street, Edgar, Pennsylvania, 201', 131 | about: 132 | 'Magna Lorem magna nulla ipsum aute in velit cupidatat occaecat. Est dolor cillum ad aliquip in reprehenderit enim. Voluptate dolore minim voluptate nisi nisi ipsum duis nisi.\r\n', 133 | registered: '2014-07-02T04:22:37 -02:00', 134 | latitude: -14.832106, 135 | longitude: 97.156766, 136 | tags: ['excepteur', 'dolor', 'voluptate', 'ea', 'ea', 'esse', 'sit'], 137 | friends: [ 138 | { 139 | id: 0, 140 | name: 'Lillie Holloway', 141 | }, 142 | { 143 | id: 1, 144 | name: 'Jenna Chandler', 145 | }, 146 | { 147 | id: 2, 148 | name: 'Nguyen Shannon', 149 | }, 150 | ], 151 | greeting: 'Hello, Lorene Hays! You have 1 unread messages.', 152 | favoriteFruit: 'apple', 153 | }, 154 | { 155 | _id: '5f298c3ae53e4116a9df500f', 156 | index: 4, 157 | guid: 'ef899c53-cbee-4ee0-be36-a84ed418a33b', 158 | isActive: true, 159 | balance: '$1,697.40', 160 | picture: 'http://placehold.it/32x32', 161 | age: 38, 162 | eyeColor: 'green', 163 | name: 'Fern Holcomb', 164 | gender: 'female', 165 | company: 'BUNGA', 166 | email: 'fernholcomb@bunga.com', 167 | phone: '+1 (886) 586-3726', 168 | address: '565 Hinsdale Street, Chicopee, Federated States Of Micronesia, 5333', 169 | about: 170 | 'Id ea cillum cupidatat ullamco exercitation commodo elit sint id dolore velit officia. Non sunt dolor amet aute voluptate ipsum eiusmod. Amet consequat voluptate do amet culpa ea.\r\n', 171 | registered: '2017-10-20T12:02:15 -02:00', 172 | latitude: 36.949756, 173 | longitude: -63.13798, 174 | tags: ['ea', 'ipsum', 'dolore', 'elit', 'exercitation', 'ullamco', 'dolor'], 175 | friends: [ 176 | { 177 | id: 0, 178 | name: 'Latisha Cochran', 179 | }, 180 | { 181 | id: 1, 182 | name: 'Ramsey Duke', 183 | }, 184 | { 185 | id: 2, 186 | name: 'Terrell Dickerson', 187 | }, 188 | ], 189 | greeting: 'Hello, Fern Holcomb! You have 8 unread messages.', 190 | favoriteFruit: 'apple', 191 | }, 192 | { 193 | _id: '5f298c3a7c82300acb59f925', 194 | index: 5, 195 | guid: 'acf11664-0349-4967-962d-ebf80bbfa498', 196 | isActive: false, 197 | balance: '$2,188.03', 198 | picture: 'http://placehold.it/32x32', 199 | age: 34, 200 | eyeColor: 'blue', 201 | name: 'Sadie Sloan', 202 | gender: 'female', 203 | company: 'CAXT', 204 | email: 'sadiesloan@caxt.com', 205 | phone: '+1 (996) 460-2068', 206 | address: '105 Lafayette Walk, Galesville, Texas, 7311', 207 | about: 208 | 'Dolor nisi culpa magna et minim cillum reprehenderit incididunt elit qui excepteur. In ad dolor culpa occaecat id voluptate quis amet consequat. Sit elit excepteur velit aliquip id quis deserunt officia duis. Tempor sunt do quis velit elit in commodo consectetur officia sunt ipsum.\r\n', 209 | registered: '2016-12-11T09:46:02 -01:00', 210 | latitude: 45.161667, 211 | longitude: 72.818628, 212 | tags: ['velit', 'adipisicing', 'amet', 'anim', 'labore', 'tempor', 'ea'], 213 | friends: [ 214 | { 215 | id: 0, 216 | name: 'Jennings Mays', 217 | }, 218 | { 219 | id: 1, 220 | name: 'Mendoza Stone', 221 | }, 222 | { 223 | id: 2, 224 | name: 'Yang Espinoza', 225 | }, 226 | ], 227 | greeting: 'Hello, Sadie Sloan! You have 2 unread messages.', 228 | favoriteFruit: 'apple', 229 | }, 230 | { 231 | _id: '5f298c3aff8283a5ca3aa82d', 232 | index: 6, 233 | guid: '36b9df6a-b846-4940-80cc-c9d163d2128c', 234 | isActive: false, 235 | balance: '$3,222.06', 236 | picture: 'http://placehold.it/32x32', 237 | age: 38, 238 | eyeColor: 'green', 239 | name: 'Doris Mcfarland', 240 | gender: 'female', 241 | company: 'MAGNINA', 242 | email: 'dorismcfarland@magnina.com', 243 | phone: '+1 (835) 486-2425', 244 | address: '367 Drew Street, Kiskimere, Arizona, 3542', 245 | about: 246 | 'Aute ut sit aliquip mollit irure. Tempor officia ut laboris anim minim sit ipsum quis exercitation anim proident ipsum voluptate. Nisi anim ad consequat velit culpa deserunt dolore sit incididunt. Sint aliquip enim irure laboris duis. Officia aute irure ut nisi aute elit mollit culpa eiusmod consequat laboris adipisicing.\r\n', 247 | registered: '2014-04-25T04:58:14 -02:00', 248 | latitude: 21.082733, 249 | longitude: -89.945592, 250 | tags: ['nulla', 'et', 'pariatur', 'nisi', 'aliquip', 'irure', 'deserunt'], 251 | friends: [ 252 | { 253 | id: 0, 254 | name: 'Marcy Lucas', 255 | }, 256 | { 257 | id: 1, 258 | name: 'Christensen Schwartz', 259 | }, 260 | { 261 | id: 2, 262 | name: 'Amanda Valencia', 263 | }, 264 | ], 265 | greeting: 'Hello, Doris Mcfarland! You have 2 unread messages.', 266 | favoriteFruit: 'apple', 267 | }, 268 | ]; 269 | -------------------------------------------------------------------------------- /src/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapper should match snapshot 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/__snapshots__/map.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`map() should match snapshot 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/__snapshots__/mapper.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapper() should match snapshot 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import mapper from './'; 2 | 3 | describe('mapper', () => { 4 | it('should match snapshot', () => { 5 | expect(mapper).toMatchSnapshot(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable filenames/match-exported */ 2 | import mapper, { Mapper, Mapping } from './mapper'; 3 | import type { Map, MapMethods, TransformCallback } from './map'; 4 | 5 | export type { Mapper, Mapping, Map, MapMethods, TransformCallback }; 6 | export default mapper; 7 | /* eslint-enable filenames/match-exported */ 8 | -------------------------------------------------------------------------------- /src/map.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any, jest/no-mocks-import */ 2 | import map from './map'; 3 | import data from './__mocks__/data'; 4 | 5 | describe('map()', () => { 6 | it('should match snapshot', () => { 7 | expect(map).toMatchSnapshot(); 8 | }); 9 | 10 | describe('exceptions', () => { 11 | it('should throw an error when source is not an object', () => { 12 | [true, '', 0, null, undefined, [0]].forEach((source) => { 13 | expect(() => map(source as any)('person.address[0]').value).toThrow(TypeError); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('default', () => { 19 | it('should map single keys', () => { 20 | expect(map(data[0])('address[0]').value).toEqual(data[0].address[0]); 21 | expect(map(data[0])('email').value).toEqual(data[0].email); 22 | expect(map(data[0])('name').transform((name: string) => name.replace(/\s/gm, '-')).value).toEqual( 23 | data[0].name.replace(/\s/gm, '-'), 24 | ); 25 | }); 26 | 27 | it('should map multiple keys', () => { 28 | expect( 29 | map(data[2])(['latitude', 'longitude']).transform((latitude: number, longitude: number) => [ 30 | latitude, 31 | longitude, 32 | ]).value, 33 | ).toEqual([data[2].latitude, data[2].longitude]); 34 | 35 | expect( 36 | map(data[2])(['phone', 'email']).transform((phone: number, email: number) => ({ 37 | phone, 38 | email, 39 | })).value, 40 | ).toEqual({ 41 | phone: data[2].phone, 42 | email: data[2].email, 43 | }); 44 | }); 45 | }); 46 | 47 | describe('options', () => { 48 | describe('omitNullUndefined', () => { 49 | it('should omit "null" and "undefined" entries', () => { 50 | expect(map(data[0])('closeFriends', { omitNullUndefined: true }).value).toEqual(map.suppress); 51 | expect(map(data[0])('non-existing', { omitNullUndefined: true }).value).toEqual(map.suppress); 52 | }); 53 | }); 54 | 55 | describe('omitStrategy', () => { 56 | it('should omit entries through custom strategy', () => { 57 | const omitStrategy = (value: unknown | unknown[]): boolean => Array.isArray(value); 58 | 59 | expect(map(data[0])('friends', { omitStrategy }).value).toEqual(map.suppress); 60 | expect(map(data[0])('tags', { omitStrategy }).value).toEqual(map.suppress); 61 | }); 62 | }); 63 | }); 64 | }); 65 | /* eslint-enable @typescript-eslint/no-explicit-any, jest/no-mocks-import */ 66 | -------------------------------------------------------------------------------- /src/map.ts: -------------------------------------------------------------------------------- 1 | import { pick } from '@cookbook/dot-notation'; 2 | 3 | import { defaultOptions, Options } from './options'; 4 | import toArray from './utils/to-array'; 5 | import is from './utils/is'; 6 | import typeOf from './utils/type-of'; 7 | 8 | type TransformCallback = (...args: unknown[]) => unknown | unknown[]; 9 | 10 | type MapMethods = { 11 | transform(callback: TransformCallback): MapMethods; 12 | readonly value: T extends [] ? T[] : T; 13 | }; 14 | 15 | type Map = (keys: string | string[], options?: Options) => MapMethods; 16 | 17 | const suppress = Symbol('map-suppress'); 18 | 19 | const map = (source: Record, mapperOptions: Options = {}): Map => { 20 | if (!is.object(source)) { 21 | throw new TypeError(`Instance of "source" must be an object, but instead got "${typeOf(source)}"`); 22 | } 23 | 24 | return (keys: string | string[], options: Options = {}) => { 25 | const OPTIONS = { ...defaultOptions, ...mapperOptions, ...options }; 26 | 27 | let result: unknown | unknown[] = is.array(keys) ? keys.map((key) => pick(source, key)) : pick(source, keys); 28 | 29 | return { 30 | transform(callback: TransformCallback) { 31 | result = is.array(keys) ? callback(...(result)) : callback(result); 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return 34 | return this; 35 | }, 36 | get value(): T extends [] ? T[] : T { 37 | const { omitNullUndefined, omitStrategy } = OPTIONS; 38 | 39 | if ((is.nullOrUndefined(result) && omitNullUndefined) || (omitStrategy && omitStrategy(result))) { 40 | return suppress as unknown as T extends [] ? T[] : T; 41 | } 42 | 43 | return result as T extends [] ? T[] : T; 44 | }, 45 | }; 46 | }; 47 | }; 48 | 49 | map.omitEntries = >(entries: T | T[]): T => { 50 | const result = toArray(entries).map((entry) => { 51 | const values: Record = {}; 52 | const keys = Object.keys(entry); 53 | 54 | for (let i = 0; i < keys.length; i += 1) { 55 | const key = keys[i]; 56 | 57 | if (entry[key] !== suppress) { 58 | values[key] = entry[key]; 59 | } 60 | } 61 | 62 | return values; 63 | }); 64 | 65 | return (is.array(entries) ? result : result[0]) as T; 66 | }; 67 | 68 | map.suppress = suppress; 69 | 70 | export type { Map, MapMethods, TransformCallback }; 71 | export default map; 72 | -------------------------------------------------------------------------------- /src/mapper.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any, jest/no-mocks-import */ 2 | import mapper from './mapper'; 3 | import data from './__mocks__/data'; 4 | 5 | const kebabCase = (str: string) => 6 | str 7 | .replace(/([a-z])([A-Z])/g, '$1-$2') 8 | .replace(/\s+/g, '-') 9 | .toLowerCase(); 10 | 11 | describe('mapper()', () => { 12 | it('should match snapshot', () => { 13 | expect(mapper).toMatchSnapshot(); 14 | }); 15 | 16 | describe('exceptions', () => { 17 | it('should throw an error when source is not an object', () => { 18 | [true, '', 0, null, undefined, [0]].forEach((source) => { 19 | expect(() => 20 | mapper((map) => ({ 21 | address: map('person.address').value, 22 | active: true, 23 | }))(source as any), 24 | ).toThrow(TypeError); 25 | }); 26 | }); 27 | }); 28 | 29 | describe('data source', () => { 30 | describe('object', () => { 31 | it('should perform simple mappings accordingly', () => { 32 | const mapping = mapper((map) => ({ 33 | active: map('isActive').value, 34 | 'account.balance': map('balance').value, 35 | 'person.age': map('age').value, 36 | 'person.address': map('address').value, 37 | friends: map('friends').value, 38 | })); 39 | 40 | const expectation = { 41 | active: true, 42 | account: { 43 | balance: data[0].balance, 44 | }, 45 | person: { 46 | age: data[0].age, 47 | address: data[0].address, 48 | }, 49 | friends: data[0].friends, 50 | }; 51 | 52 | expect(mapping(data[0])).toStrictEqual(expectation); 53 | }); 54 | 55 | it('should perform mappings with transformations accordingly', () => { 56 | const mapping = mapper((map) => ({ 57 | username: map('name').transform((name: string) => kebabCase(name)).value, 58 | active: map('isActive').value, 59 | 'account.balance': map('balance').transform((balance: string) => 60 | balance.substring(0, balance.length - 4).padEnd(balance.length, '*'), 61 | ).value, 62 | 'person.age': map('age').value, 63 | 'person.address': map('address').value, 64 | closeFriends: map('friends').transform((friends: unknown[]) => friends.slice(0, 2)).value, 65 | })); 66 | 67 | const expectation = { 68 | username: kebabCase(data[0].name), 69 | active: data[0].isActive, 70 | account: { 71 | balance: data[0].balance.substring(0, data[0].balance.length - 4).padEnd(data[0].balance.length, '*'), 72 | }, 73 | person: { 74 | age: data[0].age, 75 | address: data[0].address, 76 | }, 77 | closeFriends: data[0].friends.slice(0, 2), 78 | }; 79 | 80 | expect(mapping(data[0])).toStrictEqual(expectation); 81 | }); 82 | 83 | it('should perform mappings with multiple keys accordingly', () => { 84 | const mapping = mapper((map) => ({ 85 | coords: map(['latitude', 'longitude']).transform((latitude: number, longitude: number) => [ 86 | latitude, 87 | longitude, 88 | ]).value, 89 | contact: map(['phone', 'email']).transform((phone: number, email: number) => ({ 90 | phone, 91 | email, 92 | })).value, 93 | })); 94 | 95 | const expectation = { 96 | coords: [data[0].latitude, data[0].longitude], 97 | contact: { 98 | phone: data[0].phone, 99 | email: data[0].email, 100 | }, 101 | }; 102 | 103 | expect(mapping(data[0])).toStrictEqual(expectation); 104 | }); 105 | }); 106 | 107 | describe('array', () => { 108 | it('should perform simple mappings accordingly', () => { 109 | const mapping = mapper((map) => ({ 110 | active: map('isActive').value, 111 | 'account.balance': map('balance').value, 112 | 'person.age': map('age').value, 113 | 'person.address': map('address').value, 114 | friends: map('friends').value, 115 | })); 116 | 117 | const expectation = [ 118 | { 119 | active: data[1].isActive, 120 | account: { 121 | balance: data[1].balance, 122 | }, 123 | person: { 124 | age: data[1].age, 125 | address: data[1].address, 126 | }, 127 | friends: data[1].friends, 128 | }, 129 | { 130 | active: data[2].isActive, 131 | account: { 132 | balance: data[2].balance, 133 | }, 134 | person: { 135 | age: data[2].age, 136 | address: data[2].address, 137 | }, 138 | friends: data[2].friends, 139 | }, 140 | ]; 141 | 142 | expect(mapping([data[1], data[2]])).toStrictEqual(expectation); 143 | }); 144 | 145 | it('should perform mappings with transformations accordingly', () => { 146 | const mapping = mapper((map) => ({ 147 | username: map('name').transform((name: string) => kebabCase(name)).value, 148 | active: map('isActive').value, 149 | 'account.balance': map('balance').transform((balance: string) => 150 | balance.substring(0, balance.length - 4).padEnd(balance.length, '*'), 151 | ).value, 152 | 'person.age': map('age').value, 153 | 'person.address': map('address').value, 154 | closeFriends: map('friends').transform((friends: unknown[]) => friends.slice(0, 2)).value, 155 | })); 156 | 157 | const expectation = [ 158 | { 159 | username: kebabCase(data[1].name), 160 | active: data[1].isActive, 161 | account: { 162 | balance: data[1].balance.substring(0, data[1].balance.length - 4).padEnd(data[1].balance.length, '*'), 163 | }, 164 | person: { 165 | age: data[1].age, 166 | address: data[1].address, 167 | }, 168 | closeFriends: data[1].friends.slice(0, 2), 169 | }, 170 | { 171 | username: kebabCase(data[2].name), 172 | active: data[2].isActive, 173 | account: { 174 | balance: data[2].balance.substring(0, data[2].balance.length - 4).padEnd(data[2].balance.length, '*'), 175 | }, 176 | person: { 177 | age: data[2].age, 178 | address: data[2].address, 179 | }, 180 | closeFriends: data[2].friends.slice(0, 2), 181 | }, 182 | { 183 | username: kebabCase(data[3].name), 184 | active: data[3].isActive, 185 | account: { 186 | balance: data[3].balance.substring(0, data[3].balance.length - 4).padEnd(data[3].balance.length, '*'), 187 | }, 188 | person: { 189 | age: data[3].age, 190 | address: data[3].address, 191 | }, 192 | closeFriends: data[3].friends.slice(0, 2), 193 | }, 194 | ]; 195 | 196 | expect(mapping([data[1], data[2], data[3]])).toStrictEqual(expectation); 197 | }); 198 | 199 | it('should perform mappings with multiple keys accordingly', () => { 200 | const mapping = mapper((map) => ({ 201 | coords: map(['latitude', 'longitude']).transform((latitude: number, longitude: number) => [ 202 | latitude, 203 | longitude, 204 | ]).value, 205 | contact: map(['phone', 'email']).transform((phone: number, email: number) => ({ 206 | phone, 207 | email, 208 | })).value, 209 | })); 210 | 211 | const expectation = [ 212 | { 213 | coords: [data[0].latitude, data[0].longitude], 214 | contact: { 215 | phone: data[0].phone, 216 | email: data[0].email, 217 | }, 218 | }, 219 | { 220 | coords: [data[1].latitude, data[1].longitude], 221 | contact: { 222 | phone: data[1].phone, 223 | email: data[1].email, 224 | }, 225 | }, 226 | { 227 | coords: [data[2].latitude, data[2].longitude], 228 | contact: { 229 | phone: data[2].phone, 230 | email: data[2].email, 231 | }, 232 | }, 233 | { 234 | coords: [data[3].latitude, data[3].longitude], 235 | contact: { 236 | phone: data[3].phone, 237 | email: data[3].email, 238 | }, 239 | }, 240 | ]; 241 | 242 | expect(mapping([data[0], data[1], data[2], data[3]])).toStrictEqual(expectation); 243 | }); 244 | }); 245 | }); 246 | 247 | describe('mapping', () => { 248 | describe('array', () => { 249 | it('should perform simple mappings accordingly', () => { 250 | const mapping = mapper((map) => [ 251 | { 252 | active: map('isActive').value, 253 | 'account.balance': map('balance').value, 254 | }, 255 | { 256 | 'person.age': map('age').value, 257 | 'person.address': map('address').value, 258 | friends: map('friends').value, 259 | }, 260 | ]); 261 | 262 | const expectation = [ 263 | { 264 | active: data[1].isActive, 265 | account: { 266 | balance: data[1].balance, 267 | }, 268 | }, 269 | { 270 | person: { 271 | age: data[1].age, 272 | address: data[1].address, 273 | }, 274 | friends: data[1].friends, 275 | }, 276 | ]; 277 | 278 | expect(mapping(data[1])).toStrictEqual(expectation); 279 | }); 280 | 281 | it('should perform mappings with transformations accordingly', () => { 282 | const mapping = mapper((map) => [ 283 | { 284 | username: map('name').transform((name: string) => kebabCase(name)).value, 285 | active: map('isActive').value, 286 | }, 287 | { 288 | 'account.balance': map('balance').transform((balance: string) => 289 | balance.substring(0, balance.length - 4).padEnd(balance.length, '*'), 290 | ).value, 291 | }, 292 | { 293 | 'person.age': map('age').value, 294 | 'person.address': map('address').value, 295 | closeFriends: map('friends').transform((friends: unknown[]) => friends.slice(0, 2)).value, 296 | }, 297 | ]); 298 | 299 | const expectation = [ 300 | { 301 | username: kebabCase(data[1].name), 302 | active: data[1].isActive, 303 | }, 304 | { 305 | account: { 306 | balance: data[1].balance.substring(0, data[1].balance.length - 4).padEnd(data[1].balance.length, '*'), 307 | }, 308 | }, 309 | { 310 | person: { 311 | age: data[1].age, 312 | address: data[1].address, 313 | }, 314 | closeFriends: data[1].friends.slice(0, 2), 315 | }, 316 | ]; 317 | 318 | expect(mapping(data[1])).toStrictEqual(expectation); 319 | }); 320 | 321 | it('should perform mappings with multiple keys accordingly', () => { 322 | const mapping = mapper((map) => [ 323 | { 324 | coords: map(['latitude', 'longitude']).transform((latitude: number, longitude: number) => [ 325 | latitude, 326 | longitude, 327 | ]).value, 328 | }, 329 | { 330 | contact: map(['phone', 'email']).transform((phone: number, email: number) => ({ 331 | phone, 332 | email, 333 | })).value, 334 | }, 335 | ]); 336 | 337 | const expectation = [ 338 | { 339 | coords: [data[3].latitude, data[3].longitude], 340 | }, 341 | 342 | { 343 | contact: { 344 | phone: data[3].phone, 345 | email: data[3].email, 346 | }, 347 | }, 348 | ]; 349 | 350 | expect(mapping(data[3])).toStrictEqual(expectation); 351 | }); 352 | }); 353 | }); 354 | 355 | describe('options', () => { 356 | describe('omitNullUndefined', () => { 357 | it('should omit "null" and "undefined" from a single entry', () => { 358 | const objectMapping = mapper((map) => ({ 359 | 'account.balance': map('non-available-key').value, 360 | })); 361 | 362 | const arrayMapping = mapper((map) => [ 363 | { 364 | 'account.balance': map('non-available-key').value, 365 | }, 366 | ]); 367 | 368 | expect(objectMapping(data[1], { omitNullUndefined: true })).toStrictEqual({}); 369 | 370 | expect(arrayMapping(data[1], { omitNullUndefined: true })).toStrictEqual([]); 371 | }); 372 | 373 | it('should omit "null" and "undefined" entries', () => { 374 | const mapping = mapper((map) => ({ 375 | active: map('isActive').value, 376 | 'account.balance': map('balance').value, 377 | 'person.age': map('age').value, 378 | 'person.address': map('address').value, 379 | commonFriends: map('commonFriends').value, 380 | })); 381 | 382 | const expectation = { 383 | active: data[1].isActive, 384 | account: { 385 | balance: data[1].balance, 386 | }, 387 | person: { 388 | age: data[1].age, 389 | address: data[1].address, 390 | }, 391 | }; 392 | 393 | expect(mapping(data[1], { omitNullUndefined: true })).toStrictEqual(expectation); 394 | }); 395 | }); 396 | 397 | describe('omitStrategy', () => { 398 | it('should omit entries through custom strategy', () => { 399 | const mapping = mapper((map) => ({ 400 | active: map('isActive').value, 401 | 'account.balance': map('balance').value, 402 | 'person.age': map('age').value, 403 | 'person.address': map('address').value, 404 | 'person.tags': map('tags').value, 405 | friends: map('friends').value, 406 | })); 407 | 408 | const expectation = { 409 | active: data[1].isActive, 410 | account: { 411 | balance: data[1].balance, 412 | }, 413 | person: { 414 | age: data[1].age, 415 | address: data[1].address, 416 | }, 417 | }; 418 | 419 | const omitStrategy = (value: unknown | unknown[]) => Array.isArray(value); 420 | 421 | expect(mapping(data[1], { omitStrategy })).toStrictEqual(expectation); 422 | }); 423 | }); 424 | }); 425 | }); 426 | /* eslint-enable @typescript-eslint/no-explicit-any, jest/no-mocks-import */ 427 | -------------------------------------------------------------------------------- /src/mapper.ts: -------------------------------------------------------------------------------- 1 | import { parse } from '@cookbook/dot-notation'; 2 | 3 | import map, { Map } from './map'; 4 | import type { Options } from './options'; 5 | import is from './utils/is'; 6 | import toArray from './utils/to-array'; 7 | import typeOf from './utils/type-of'; 8 | 9 | type Mapping = (map: Map) => Record | Record[]; 10 | 11 | type Mapper = ( 12 | mapping: Mapping, 13 | ) => ( 14 | source: Record | Record[], 15 | options?: Options, 16 | ) => T extends [] ? T[] : T; 17 | 18 | const mapper: Mapper = (mapping: Mapping) => { 19 | return ( 20 | source: Record | Record[], 21 | options: Options = {}, 22 | ): T extends [] ? T[] : T => { 23 | const result = toArray(source).map((src) => { 24 | if (!is.object(src)) { 25 | throw new TypeError(`Instance of "source" must be an object, but instead got "${typeOf(source)}"`); 26 | } 27 | 28 | const mapped = map.omitEntries(mapping(map(src, options))); 29 | 30 | if (is.array(mapped)) { 31 | const values: T[] = []; 32 | 33 | if (!mapped.length) { 34 | return values; 35 | } 36 | 37 | for (let i = 0; i < mapped.length; i += 1) { 38 | const value = mapped[i]; 39 | 40 | if (value && Object.keys(value).length) { 41 | values.push(parse(value as Record) as T); 42 | } 43 | } 44 | 45 | return values; 46 | } 47 | 48 | return (Object.keys(mapped).length ? parse(mapped) : {}) as T; 49 | }); 50 | 51 | return (is.array(source) ? result : result[0]) as T extends [] ? T[] : T; 52 | }; 53 | }; 54 | 55 | export type { Mapper, Mapping }; 56 | export default mapper; 57 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | omitNullUndefined?: boolean; 3 | omitStrategy?: (value: unknown) => boolean; 4 | } 5 | 6 | const defaultOptions: Options = { 7 | omitNullUndefined: undefined, 8 | omitStrategy: undefined, 9 | }; 10 | 11 | export type { Options }; 12 | export { defaultOptions }; 13 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/is.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utils/is should match snapshot 1`] = ` 4 | Object { 5 | "array": [Function], 6 | "nullOrUndefined": [Function], 7 | "object": [Function], 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/to-array.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utils/toArray() should match snapshot 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/utils/is.test.ts: -------------------------------------------------------------------------------- 1 | import is from './is'; 2 | 3 | describe('utils/is', () => { 4 | it('should match snapshot', () => { 5 | expect(is).toMatchSnapshot(); 6 | }); 7 | 8 | describe('is.nullOrUndefined()', () => { 9 | it('should flag data type correctly', () => { 10 | expect(is.nullOrUndefined(null)).toBe(true); 11 | expect(is.nullOrUndefined(undefined)).toBe(true); 12 | expect(is.nullOrUndefined('')).toBe(false); 13 | expect(is.nullOrUndefined('123')).toBe(false); 14 | }); 15 | }); 16 | 17 | describe('is.object()', () => { 18 | it(`should flag element data type correctly`, () => { 19 | expect(is.object({})).toBe(true); 20 | expect(is.object([])).toBe(false); 21 | expect(is.object(1)).toBe(false); 22 | expect(is.object(true)).toBe(false); 23 | expect(is.object(null)).toBe(false); 24 | expect(is.object('cd ..')).toBe(false); 25 | }); 26 | }); 27 | 28 | describe('is.array()', () => { 29 | it(`should flag element data type correctly`, () => { 30 | expect(is.array([])).toBe(true); 31 | expect(is.array({})).toBe(false); 32 | expect(is.array(1)).toBe(false); 33 | expect(is.array(true)).toBe(false); 34 | expect(is.array(null)).toBe(false); 35 | expect(is.array('cd ..')).toBe(false); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | nullOrUndefined: (value: unknown): boolean => value === null || value === undefined, 3 | object: >(value: unknown): value is T => 4 | value !== null && typeof value === 'object' && Object.prototype.toString.call(value) === '[object Object]', 5 | array: (value: T | Array): value is Array => Array.isArray(value), 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/to-array.test.ts: -------------------------------------------------------------------------------- 1 | import toArray from './to-array'; 2 | 3 | describe('utils/toArray()', () => { 4 | it('should match snapshot', () => { 5 | expect(toArray).toMatchSnapshot(); 6 | }); 7 | 8 | it('should parse type to array', () => { 9 | const source = 'lorem ipsum'; 10 | const expected = [source]; 11 | 12 | expect(toArray(source)).toStrictEqual(expected); 13 | }); 14 | 15 | it('should not parse if type is already an array', () => { 16 | const source = ['lorem ipsum']; 17 | const expected = source; 18 | 19 | expect(toArray(source)).toStrictEqual(expected); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/utils/to-array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Ensure that given value is array, if not, convert it. 3 | * @param value 4 | */ 5 | const toArray = (value: T | T[]): T extends Array ? T : Array => 6 | (Array.isArray(value) ? value : [value]) as T extends Array ? T : Array; 7 | 8 | export default toArray; 9 | -------------------------------------------------------------------------------- /src/utils/type-of.test.ts: -------------------------------------------------------------------------------- 1 | import typeOf from './type-of'; 2 | 3 | describe('utils/typeOf()', () => { 4 | const expectations: Record = { 5 | string: 'string', 6 | number: 1, 7 | boolean: true, 8 | null: null, 9 | object: {}, 10 | array: [], 11 | function: (): void => null, 12 | date: new Date(), 13 | }; 14 | 15 | Object.keys(expectations).forEach((type) => { 16 | it(`should flag "${expectations[type]}" as "${type}"`, () => { 17 | expect(typeOf(expectations[type])).toStrictEqual(type); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/utils/type-of.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a string with the data type from given value. 3 | * @example 4 | * typeOf('hello'); // output: string 5 | * typeOf(function() {}); // output: function 6 | * typeOf(new Date()); // output: date 7 | * @param value 8 | * @return {string} 9 | */ 10 | const typeOf = (value: T): string => 11 | ({}.toString 12 | .call(value) 13 | .match(/\s([A-Za-z]+)/)[1] 14 | .toLowerCase() as string); 15 | 16 | export default typeOf; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "rootDir": "./src", 5 | "outDir": "./lib", 6 | "sourceMap": true, 7 | "noImplicitAny": true, 8 | "moduleResolution": "node", 9 | "module": "esNext", 10 | "target": "es6", 11 | "lib": ["dom", "es2017"], 12 | "jsx": "preserve", 13 | "allowJs": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "**/*.test.ts", "**/__mocks__/**/*"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "esModuleInterop": true 6 | }, 7 | "include": ["src/**/*.ts", "src/**/*.test.ts"], 8 | "exclude": ["node_modules", "**/__mocks__/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./configurations/webpack'); 2 | --------------------------------------------------------------------------------