{"id":21584950,"url":"https://github.com/snatalenko/declarative-mapper","last_synced_at":"2026-03-01T09:32:18.994Z","repository":{"id":37456217,"uuid":"274411806","full_name":"snatalenko/declarative-mapper","owner":"snatalenko","description":"Declarative Mapper for NodeJS","archived":false,"fork":false,"pushed_at":"2025-07-25T18:35:59.000Z","size":712,"stargazers_count":4,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-02-21T01:44:50.920Z","etag":null,"topics":["declarative","mapping","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/snatalenko.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-06-23T13:23:10.000Z","updated_at":"2025-07-30T00:36:38.000Z","dependencies_parsed_at":"2025-04-10T20:05:32.752Z","dependency_job_id":"f55099a3-8ebc-4b3a-88e2-e1d676cfccbd","html_url":"https://github.com/snatalenko/declarative-mapper","commit_stats":{"total_commits":76,"total_committers":3,"mean_commits":"25.333333333333332","dds":0.02631578947368418,"last_synced_commit":"a488cd6c58006129897b99a2131bc474fa3e7545"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/snatalenko/declarative-mapper","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snatalenko%2Fdeclarative-mapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snatalenko%2Fdeclarative-mapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snatalenko%2Fdeclarative-mapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snatalenko%2Fdeclarative-mapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/snatalenko","download_url":"https://codeload.github.com/snatalenko/declarative-mapper/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snatalenko%2Fdeclarative-mapper/sbom","scorecard":{"id":834373,"data":{"date":"2025-08-11","repo":{"name":"github.com/snatalenko/declarative-mapper","commit":"260cc7ce72c917c305e8d89bd54e03b47fc4c27c"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.4,"checks":[{"name":"Code-Review","score":0,"reason":"Found 1/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":3,"reason":"4 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 3","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/npm_build.yml:1","Warn: no topLevel permission defined: .github/workflows/npm_test_coverage.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":5,"reason":"dependency not pinned by hash detected -- score normalized to 5","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm_build.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/snatalenko/declarative-mapper/npm_build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm_build.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/snatalenko/declarative-mapper/npm_build.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm_test_coverage.yml:14: update your workflow using https://app.stepsecurity.io/secureworkflow/snatalenko/declarative-mapper/npm_test_coverage.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/npm_test_coverage.yml:17: update your workflow using https://app.stepsecurity.io/secureworkflow/snatalenko/declarative-mapper/npm_test_coverage.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/npm_test_coverage.yml:30: update your workflow using https://app.stepsecurity.io/secureworkflow/snatalenko/declarative-mapper/npm_test_coverage.yml/master?enable=pin","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   2 out of   2 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/npm_build.yml:9"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 1 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-23T18:34:49.072Z","repository_id":37456217,"created_at":"2025-08-23T18:34:49.073Z","updated_at":"2025-08-23T18:34:49.073Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29965594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T06:55:38.174Z","status":"ssl_error","status_checked_at":"2026-03-01T06:53:04.810Z","response_time":124,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["declarative","mapping","typescript"],"created_at":"2024-11-24T15:08:32.399Z","updated_at":"2026-03-01T09:32:18.976Z","avatar_url":"https://github.com/snatalenko.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Declarative Mapper for NodeJS\n=============================\n\n[![NPM Version](https://img.shields.io/npm/v/declarative-mapper.svg)](https://www.npmjs.com/package/declarative-mapper)\n[![Build](https://github.com/snatalenko/declarative-mapper/workflows/build/badge.svg)](https://github.com/snatalenko/declarative-mapper/actions)\n[![Coverage Status](https://coveralls.io/repos/github/snatalenko/declarative-mapper/badge.svg?branch=master)](https://coveralls.io/github/snatalenko/declarative-mapper?branch=master)\n[![NPM Downloads](https://img.shields.io/npm/dm/declarative-mapper.svg)](https://www.npmjs.com/package/declarative-mapper)\n\n## Reasoning\n\nOn a couple of projects I needed a library that would allow users to convert one JSON format to another (say, Invoice from one system to another). It should have operated with a **declarative mapping** instructions to allow configuration by users from UI. Also, it should have been **flexible** enough to accommodate various tricky requirements, but **secure**, to prevent JS injections. And **fast**, to process incoming streams with millions of records.\n\nThat's where the \"declarative-mapping\" came in:\n\n- **Declarative** - declarative mapping instructions allows mapping configuration from UI. in simple scenarios no technical knowledge needed, but I still have plans to build a nicer mapping editor\n- **Flexible** - run JS underneath to allow any kind of instructions\n- **Secure** - restricts access to outside environment by executing mapping in a separate [V8 Virtual Machine](https://nodejs.org/api/vm.html) context\n- **Fast** - mapping instructions are compiled once on beginning, which allows to process ~200k obj/sec on Apple M1 Pro\n- **Typed** - written in TypeScript\n- **Lightweight** - no dependencies\n\n\nA simple example:\n\n```ts\nimport { createMapper } from 'declarative-mapper';\nimport { expect } from 'chai';\n\n\nconst sourceObject = {\n  foo: 'bar'\n};\n\nconst mappingInstructions = {\n  myObj: {\n    myField: 'foo',\n    myFieldLength: 'foo.length'\n  }\n};\n\n\n// Convert declarative instructions to a pre-compiled\n// function that can be executed any number of times\nconst mapper = createMapper(mappingInstructions);\n\nconst result = mapper(sourceObject);\n\nexpect(result).to.eql({\n  myObj: {\n    myField: 'bar',\n    myFieldLength: 3\n  }\n});\n```\n\n## Mapping Instructions\n\nIn mapping JSON, left side is a key in the resulting object, right side is either a string with a valid JS expression, or an object with mapping instructions.\n\n| Expression  | Description  |\n| --- | --- |\n| `\"key\": \"100\"`  | numeric value, produces `\"key\": 100`  |\n| `\"key\": \"true\"`  | boolean value, produces `\"key\": true`  |\n| `\"key\": \"\\\"test\\\"\"` or\u003cbr /\u003e `\"key\": \"'text'\"`  |  text value. as you can see, it has its own quotation marks. produces `\"key\": \"text\"`  |\n| `\"key\": \"foo\"`  | access to an input variable `foo`  |\n| `\"key\": \"Number(foo)\"`  | access to an input variable `foo` converted to a number, produces `\"key\": 100`  |\n| `\"key\": \"arr.filter(el =\u003e ...)\"`  | more complex JS expression that produces an array  |\n| `\"key\": { … }`  | object mapping, where “…” contains inner properties  |\n| `\"key\": { \"map\": { … } }`  | same as above, but more verbose  |\n| `\"key\": { \"forEach\": \"…\", \"map\": { … } }`  | array mapped from input  |\n| `\"key\": { \"0\": { … }, \"1\": { … } }`  | array with a predefined set of elements  |\n| `\"key\": { \"from\": \"…\", \"map\": { … } }`  | object mapping from a different context   |\n\n\n### Objects\n\nMapping of an object with inner properties:\n\n```json\n  \"key\": {\n    \"foo\": \"-1\"\n  }\n```\nor\n\n```json\n  \"key\": {\n    \"map\": {\n      \"foo\": \"-1\"\n    }\n  } \n```\n\nBoth above examples produce same result (despite that the second example is more verbose and made to have a consistent format with array mappings):\n\n```json\n  \"key\": {\n    \"foo\": -1\n  } \n```\n\n### Arrays\n\nLet's assume we have an input with an array of objects in it, and we need to produce an array of objects, one for each element in the input. In a such case the `\"forEach\": \"\", \"map\": {}` construction can be used:\n\n```json\n{\n  \"inputArray\": [{\n    \"arrayInnerProp\": \"value1\"\n  }, {\n    \"arrayInnerProp\": \"value2\"\n  }]\n}\n```\n\n```json\n  \"key\": {\n    \"forEach\": \"inputArray\",\n    \"map\": {\n      \"foo\": \"arrayInnerProperty\"\n    }\n  }\n```\n\nResult: \n\n```json\n  \"key\": [{\n    \"foo\": \"value1\"\n  }, {\n    \"foo\": \"value2\"\n  }]\n```\n\nNote that the execution context in a such mapping shifts into the input objects and inner properties can be referenced directly as `arrayInnerProperty` instead of `inputArray[index].arrayInnerProperty`. More on that in the [Context Switching](#context-switching)\n\n\n#### String[] from Object[]\n\nIn case array should contain plain values instead of objects, left side of the expression should contain `\"*\"` instead of the key name:\n\n```json\n  \"key\": {\n    \"forEach\": \"inputArray\",\n    \"map\": {\n      \"*\": \"arrayInnerProperty\"\n    }\n  }\n```\n\nProduces:\n\n```json\n  \"key\": [\n    \"value1\",\n    \"value2\"\n  ]\n```\n\n#### String[] from String[]\n\nArrays with simple values can be mapped in a same way, while current iterating element can accessed as `$record`:\n\n```json\n{\n  \"inputValues\": [1, 2, 3]\n}\n```\n\n```json\n{\n  \"forEach\": \"inputValues\",\n  \"map\": {\n    \"*\": \"$record * 2\"\n  }\n}\n```\n\nResult:\n\n```json\n[2, 4, 6]\n```\n\n### Array with predefined set of elements\n\nIn case array should have a predefined set of elements, each of the elements can be mapped by its index:\n\n```json\n  \"key\": {\n    \"0\": {\n      \"foo\": \"\\\"text1\\\"\"\n    },\n    \"2\": \"1000\"\n  }\n```\n\nResult:\n\n```json\n  \"key\": [\n    {\n      \"foo\": \"text1\"\n    },\n    null,\n    1000\n  ]\n```\n\n\n\n### Context Switching\n\nWhen arrays are mapped with the `\"forEach\": \"\", \"map\": {}` statement, the execution context switches automatically to the objects selected by `forEach` ([see above](#arrays)). Similar technique can be useful when a large number of properties need to be mapped from an object located outside of the current execution context. In a such case the `\"from\": \"\", \"map\": {}` statement can be used.\n\n\n\nDown in the source tree:\n\n```json\n  \"key\": {\n    \"from\": \"field.innerArray[0].innerObject\",\n    \"map\": {\n      \"foo\": \"nestedProperty\"\n    }\n  }\n```\n\nOr up in the source document:\n\n```json\n  \"key\": {\n    \"from\": \"$input.rootLevelProperty\",\n    \"map\": {\n      \"foo\": \"nestedProperty\"\n    }\n  }\n```\n\nHere are the special variables that can be handy for the context switching:\n\n- `$record` - current element of the array being iterated with `forEach`\n- `$index` - index of the current array element\n- `$collection` - entire collection of the elements being iterated\n- `$input` - entire document passed as mapping input\n\n\n## Complex Mapping Example\n\n```ts\n// Some kind of a document we expect on input\nconst input = {\n  LINE_ITEMS: [\n    { UPC: '123', QTY: 1, PRICE: 3.4 },\n    { UPC: '456', QTY: 2, PRICE: 5.7 }\n  ],\n  ALLOWANCES: [\n    { ITEM_UPC: '123', AMOUNT: 1.5 }\n  ]\n};\n\n// Additional information we want to pass to the mapping environment\nconst itemCatalog = [\n  { upc: '123', vendorCode: 'X-123' },\n  { upc: '456', vendorCode: 'X-456' }\n];\n\n// Some format we need\nconst desiredOutput = {\n  title: 'Invoice 1',\n  items: [\n    {\n      code: 'X-123',\n      qty: 1,\n      price: 3.4,\n      amount: 3.4,\n      allowances: [1.5]\n    },\n    {\n      code: 'X-456',\n      qty: 2,\n      price: 5.7,\n      amount: 11.4,\n      allowances: []\n    }\n  ],\n  total: 14.8\n};\n\n\n// Declarative instructions on how to convert the input format\n// to the desired format\nconst mapping = {\n  // mapping to a constant\n  title: '\"Invoice 1\"',\n\n  // array mapping from another array\n  items: {\n    forEach: 'LINE_ITEMS',\n    map: {\n      // data lookup from an additional source passed to `extensions`\n      code: 'itemCatalog.find(e =\u003e e.upc === UPC).vendorCode',\n      \n      // fields mapping in a context of `LINE_ITEMS` elements\n      qty: 'QTY',\n      price: 'PRICE',\n      amount: 'QTY * PRICE',\n\n      // data mapping from a source different from the current mapping context\n      // (ALLOWANCES are placed next to LINE_ITEMS in the input)\n      allowances: {\n        forEach: 'ALLOWANCES.filter(a =\u003e a.ITEM_UPC === UPC)',\n        map: {\n          '*': 'AMOUNT'\n        }\n      }\n    }\n  },\n\n  // property mapping from an array\n  total: 'LINE_ITEMS.reduce((sum, { QTY, PRICE }) =\u003e sum + (QTY * PRICE), 0)'\n};\n\n// Pre-compiled function that can be executed any number of times\nconst mapper = createMapper(mapping, {\n  extensions: {\n    itemCatalog\n  }\n});\n\nconst result = mapper(input);\n\nexpect(result).to.eql(desiredOutput);\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnatalenko%2Fdeclarative-mapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsnatalenko%2Fdeclarative-mapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnatalenko%2Fdeclarative-mapper/lists"}