├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── code_quality.yml │ └── release.yml ├── .gitignore ├── .sonarcloud.ts.json ├── .version ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── deno.jsonc ├── deps.ts ├── docs ├── API.md ├── JSEDINotation.md ├── QueryLanguage.md ├── TOC.md ├── Tests.md └── TransactionMapping.md ├── mod.ts ├── scripts ├── build_npm.ts └── create_labels.ts ├── sonar-project.properties ├── src ├── Errors.ts ├── JSEDINotation.ts ├── Positioning.ts ├── X12Diagnostic.ts ├── X12Element.ts ├── X12FatInterchange.ts ├── X12FunctionalGroup.ts ├── X12Generator.ts ├── X12Interchange.ts ├── X12Parser.ts ├── X12QueryEngine.ts ├── X12Segment.ts ├── X12SegmentHeader.ts ├── X12SerializationOptions.ts ├── X12Transaction.ts ├── X12TransactionMap.ts └── X12ValidationEngine │ ├── Interfaces.ts │ ├── X12ValidationEngine.ts │ ├── X12ValidationErrorCode.ts │ ├── X12ValidationRule.ts │ └── index.ts └── test ├── CoreSuite_test.ts ├── FormattingSuite_test.ts ├── GeneratorSuite_test.ts ├── MappingSuite_test.ts ├── ObjectModelSuite_test.ts ├── ParserSuite_test.ts ├── QuerySuite_test.ts ├── ValidationSuite_test.ts └── test-data ├── 271.edi ├── 850.edi ├── 850_2.edi ├── 850_3.edi ├── 850_fat.edi ├── 850_map.json ├── 850_map_result.json ├── 850_validation.rule.json ├── 850_validation_no_headers.rule.json ├── 850_validation_simple.rule.json ├── 855.edi ├── 856.edi ├── Transaction_data.json ├── Transaction_map.json └── Transaction_map_liquidjs.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Bug report about: Create a report to help us improve title: '[BUG]' 4 | labels: '' assignees: '' ---**Describe the bug** A clear and concise description 5 | of what the bug is. 6 | 7 | **To Reproduce** Steps to reproduce the behavior: 8 | 9 | 1. Go to '...' 10 | 2. Click on '....' 11 | 3. Scroll down to '....' 12 | 4. See error 13 | 14 | **Expected behavior** A clear and concise description of what you expected to 15 | happen. 16 | 17 | **Screenshots** If applicable, add screenshots to help explain your problem. 18 | 19 | **Environment (please complete the following information):** 20 | 21 | - OS: [e.g. iOS] 22 | - Version [e.g. 22] 23 | - Executor [e.g. server, cloud] 24 | 25 | **Additional context** Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/workflows/code_quality.yml: -------------------------------------------------------------------------------- 1 | name: Run Code Quality 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - 'src/**/*.ts' 8 | - 'mod.ts' 9 | - 'deps.ts' 10 | pull_request_target: 11 | types: 12 | - opened 13 | - ready_for_review 14 | branches: 15 | - main 16 | - prerelease 17 | - release 18 | jobs: 19 | cover: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout codebase 23 | uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | - name: Setup Deno 27 | uses: denoland/setup-deno@v1 28 | with: 29 | deno-version: v1.x 30 | - name: Setup Node (required for SonarCloud) 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: '17.x' 34 | registry-url: 'https://registry.npmjs.org' 35 | - name: Setup LCOV 36 | run: sudo apt install -y lcov 37 | - name: Run tests and coverage 38 | run: deno task cover 39 | - name: Fix LCOV output for SonarCloud 40 | run: sed -i 's@'$GITHUB_WORKSPACE'@/github/workspace/@g' coverage/report.lcov 41 | - name: SonarCloud Scan 42 | uses: SonarSource/sonarcloud-github-action@master 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Cut release 2 | on: 3 | pull_request_target: 4 | branches: 5 | - prerelease 6 | - release 7 | types: 8 | - closed 9 | env: 10 | VERSION_FLAG: '' 11 | VERSION_NUM: '' 12 | VERSION_TAG: latest 13 | jobs: 14 | release: 15 | if: github.event.pull_request.merged 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout codebase 19 | uses: actions/checkout@v3 20 | - name: Setup Deno 21 | uses: denoland/setup-deno@v1 22 | with: 23 | deno-version: v1.x 24 | - name: Setup Node (required for dnt) 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: '17.x' 28 | registry-url: 'https://registry.npmjs.org' 29 | - name: Check major version 30 | if: contains(github.event.pull_request.labels.*.name, 'version_major') 31 | run: | 32 | echo "VERSION_FLAG=major" >> $GITHUB_ENV 33 | - name: Check minor version 34 | if: contains(github.event.pull_request.labels.*.name, 'version_minor') 35 | run: | 36 | echo "VERSION_FLAG=minor" >> $GITHUB_ENV 37 | - name: Check patch version 38 | if: contains(github.event.pull_request.labels.*.name, 'version_patch') 39 | run: | 40 | echo "VERSION_FLAG=patch" >> $GITHUB_ENV 41 | - name: Check premajor version 42 | if: contains(github.event.pull_request.labels.*.name, 'version_premajor') 43 | run: | 44 | echo "VERSION_FLAG=premajor" >> $GITHUB_ENV 45 | - name: Check preminor version 46 | if: contains(github.event.pull_request.labels.*.name, 'version_preminor') 47 | run: | 48 | echo "VERSION_FLAG=preminor" >> $GITHUB_ENV 49 | - name: Check prepatch version 50 | if: contains(github.event.pull_request.labels.*.name, 'version_prepatch') 51 | run: | 52 | echo "VERSION_FLAG=prepatch" >> $GITHUB_ENV 53 | - name: Check prerelease version 54 | if: contains(github.event.pull_request.labels.*.name, 'version_prerelease') 55 | run: | 56 | echo "VERSION_FLAG=prerelease" >> $GITHUB_ENV 57 | - name: Build for NPM using dnt 58 | if: env.VERSION_FLAG != '' 59 | run: | 60 | deno task build -- --${{ env.VERSION_FLAG }} 61 | echo "VERSION_NUM=$(cat .version)" >> $GITHUB_ENV 62 | - name: Setup NPM tag 63 | if: env.VERSION_FLAG == 'premajor' || env.VERSION_FLAG == 'preminor' || env.VERSION_FLAG == 'prepatch' || env.VERSION_FLAG == 'prerelease' 64 | run: | 65 | echo "VERSION_TAG=prerelease" >> $GITHUB_ENV 66 | - name: Add NPMRC file 67 | if: env.VERSION_FLAG != '' && env.VERSION_NUM != '' 68 | run: | 69 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ./npm/.npmrc 70 | - name: Publish code to NPM 71 | if: env.VERSION_FLAG != '' && env.VERSION_NUM != '' 72 | run: | 73 | cd ./npm 74 | npm publish --tag=${{ env.VERSION_TAG }} 75 | env: 76 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 77 | - name: Commit new version number to ${{ github.event.pull_request.base.ref }} 78 | if: env.VERSION_FLAG != '' && env.VERSION_NUM != '' 79 | uses: stefanzweifel/git-auto-commit-action@v4 80 | with: 81 | branch: ${{ github.event.pull_request.base.ref }} 82 | tagging_message: ${{ env.VERSION_NUM }} 83 | - name: Merge branch ${{ github.event.pull_request.base.ref }} into main 84 | if: env.VERSION_FLAG != '' && env.VERSION_NUM != '' 85 | uses: devmasx/merge-branch@1.4.0 86 | with: 87 | type: now 88 | from_branch: ${{ github.event.pull_request.base.ref }} 89 | target_branch: main 90 | github_token: ${{ secrets.GITHUB_TOKEN }} 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm/ 2 | bower_components 3 | .scannerwork 4 | .nyc_output 5 | coverage 6 | node_modules 7 | dist 8 | esm 9 | es5 10 | 11 | cbor-js.zip 12 | cbor.min.js 13 | sauce_connect.log 14 | debug.log 15 | **/debug.log -------------------------------------------------------------------------------- /.sonarcloud.ts.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "mod.ts", 4 | "src/**/*.ts" 5 | ], 6 | "include": [ 7 | "mod.ts", 8 | "src/**" 9 | ], 10 | "exclude": [ 11 | "scripts/**", 12 | "test/**" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 1.8.0-1 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true, 6 | "**/*.js": { 7 | "when": "$(basename).ts" 8 | } 9 | }, 10 | "editor.tabSize": 2, 11 | "deno.enable": true, 12 | "deno.codeLens.test": true, 13 | "deno.codeLens.testArgs": [ 14 | "--allow-all" 15 | ], 16 | "deno.lint": true, 17 | "deno.suggest.autoImports": true, 18 | "deno.suggest.imports.autoDiscover": true, 19 | "deno.suggest.paths": true, 20 | "deno.unstable": true, 21 | "deno.suggest.imports.hosts": { 22 | "https://deno.land": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 TrueCommerce Copyright (c) 2019 Net Health Shops LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-X12 2 | 3 | An ASC X12 parser, generator, query engine, and mapper written for NodeJS and 4 | Deno. Parsing supports reading from Node streams to conserve resources in 5 | memory-intensive operations. 6 | 7 | [![Denoland/X Module](https://shield.deno.dev/x/x12)](https://deno.land/x/x12) 8 | [![npm version](https://badge.fury.io/js/node-x12.svg)](https://badge.fury.io/js/node-x12) 9 | [![GitHub last commit](https://img.shields.io/github/last-commit/aaronhuggins/node-x12)]() 10 | [![GitHub contributors](https://img.shields.io/github/contributors/aaronhuggins/node-x12)]()
11 | [![npm collaborators](https://img.shields.io/npm/collaborators/node-x12)]() 12 | [![GitHub top language](https://img.shields.io/github/languages/top/aaronhuggins/node-x12)]() 13 | [![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/aaronhuggins/node-x12)]() 14 | [![npm](https://img.shields.io/npm/dw/node-x12)]() 15 | [![NPM](https://img.shields.io/npm/l/node-x12)]()
16 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=aaronhuggins_node-x12&metric=alert_status)](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12) 17 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=aaronhuggins_node-x12&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12) 18 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=aaronhuggins_node-x12&metric=security_rating)](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12) 19 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=aaronhuggins_node-x12&metric=coverage)](https://sonarcloud.io/summary/new_code?id=aaronhuggins_node-x12) 20 | 21 | ## Installing 22 | 23 | Simply import into your runtime: [Deno](https://deno.land/x/x12), browser, 24 | [Node ESM, or Node CommonJS](https://www.npmjs.com/package/node-x12). Browser 25 | imports can be handled via your favorite bundler, or using Skypack CDN. 26 | 27 | For NPM: `npm i node-x12` 28 | 29 | ## Features 30 | 31 | Contributions by TrueCommerce up to April 2016: 32 | 33 | - Near-complete class object model of ASC X12 parts 34 | - Parser 35 | - Query Engine 36 | 37 | Enhancements original to this fork: 38 | 39 | - Simplified object notation class for EDI (allows for easy JSON support) 40 | - Streaming Parser (allows for parsing of large EDI files with reduced memory 41 | overhead) 42 | - Generator 43 | - Transaction set to object mapping 44 | - Object to transaction set mapping, now with support for 45 | [Liquid syntax](/docs/TransactionMapping.md#liquid-macro-language) 46 | - Support for fat EDI documents 47 | - Convenience methods for several generating/mapping scenarios 48 | - Intellisense support in VSCode with packaged type declarations 49 | 50 | See the [API](/docs/API.md) for more information. 51 | 52 | #### Future 53 | 54 | This library is in 55 | [maintenance mode as of 2021](https://github.com/aaronhuggins/node-x12/issues/24). 56 | Development of a next-generation ASC X12 parser is taking place in 57 | [js-edi](https://github.com/aaronhuggins/js-edi). This new library will support 58 | both ASC X12 and EDIFACT, as well as a more fleshed-out query language, by 59 | leveraging Antler4 grammars which closely follow publicly-provided details of 60 | their specs. 61 | 62 | ### Query Language 63 | 64 | The query language makes it possible to directly select values from the class 65 | object model. See [Query Language](/docs/QueryLanguage.md) for more information. 66 | 67 | **Example 1: Select `REF02` Elements**
`REF02` 68 | 69 | **Example 2: Select `REF02` Elements With a `PO` Qualifier in `REF01`**
70 | `REF02:REF01["PO"]` 71 | 72 | **Example 3: Select Line-Level PO Numbers (850)**
`PO1-REF02:REF01["PO"]` 73 | 74 | **Example 4: Select ASN Line Quantities**
`HL+S+O+P+I-LIN-SN102` 75 | 76 | **Example 5: Select values from a loop series**
77 | `FOREACH(LX)=>MAN02:MAN01["CP"]` 78 | 79 | ### Fat EDI Documents 80 | 81 | Some vendors will concatenate multiple valid EDI documents into a single request 82 | or file. This **DOES NOT CONFORM** to the ASC X12 spec, but it does happen. 83 | Implementing support for this scenario was trivial. When parsing an EDI 84 | document, it will be handled one of two ways: 85 | 86 | 1. When strict, the parser will return an `X12FatInterchange` object with 87 | property `interchanges`, an array of `X12Interchange` objects 88 | 2. When not strict, the parser will merge valid EDI documents into a single 89 | interchange 90 | 91 | In the latter of the two scenarios, the parser will set the header and trailer 92 | to the last available ISA and IEA segments. The element data of the discarded 93 | ISA and IEA segments will be lost if the original fat EDI document is not 94 | preserved. If all the header and trailer information is important to your 95 | organization, we recommend setting the parser to strict so that you get all the 96 | data into an object, or else go back to your implementer and request that they 97 | fix their EDI. 98 | 99 | ### Gotchas 100 | 101 | Implementers of ASC X12 are not guaranteed to conform completely to spec. There 102 | are scenarios that this library WILL NOT be able to handle and WILL NEVER be 103 | added. Despite the addition of functionality beyond the base parser from the 104 | original libray, the goal of this library is to remain a simple implementation 105 | of the spec. Some examples of scenarios this library won't handle: 106 | 107 | - Control characters in the content of an element 108 | - Mixed encrypted/non-encrypted documents 109 | - Missing elements in XYZ tag 110 | 111 | Such issues should be resolved between a user of this library and the 112 | implementer of ASC X12 documents they are working with. 113 | 114 | ## Documentation 115 | 116 | Additional documentation can be found [self-hosted](/docs/TOC.md) within the 117 | repository. 118 | 119 | ## Examples 120 | 121 | ```js 122 | const { X12Generator, X12Parser, X12TransactionMap } = require("node-x12"); 123 | 124 | // Parse valid ASC X12 EDI into an object. 125 | const parser = new X12Parser(true); 126 | let interchange = parser.parse("...raw X12 data..."); 127 | 128 | // Parse a stream of valid ASC X12 EDI 129 | const ediStream = fs.createReadStream("someFile.edi"); 130 | const segments = []; 131 | 132 | ediStream 133 | .pipe(parser) 134 | .on("data", (data) => { 135 | segments.push(data); 136 | }) 137 | .on("end", () => { 138 | interchange = parser.getInterchangeFromSegments(segments); 139 | }); 140 | 141 | // Generate valid ASC X12 EDI from an object. 142 | const jsen = { 143 | options: { 144 | elementDelimiter: "*", 145 | segmentTerminator: "\n", 146 | }, 147 | header: [ 148 | "00", 149 | "", 150 | "00", 151 | "", 152 | "ZZ", 153 | "10000000", 154 | "01", 155 | "100000000", 156 | "100000", 157 | "0425", 158 | "|", 159 | "00403", 160 | "100748195", 161 | "0", 162 | "P", 163 | ">", 164 | ], 165 | functionalGroups: [...etc], 166 | }; 167 | const generator = new X12Generator(jsen); 168 | 169 | // Query X12 like an object model 170 | const engine = new X12QueryEngine(); 171 | const results = engine.query(interchange, 'REF02:REF01["IA"]'); 172 | 173 | results.forEach((result) => { 174 | // Do something with each result. 175 | // result.interchange 176 | // result.functionalGroup 177 | // result.transaction 178 | // result.segment 179 | // result.element 180 | // result.value OR result.values 181 | }); 182 | 183 | // Map transaction sets to javascript objects 184 | const map = { 185 | status: "W0601", 186 | poNumber: "W0602", 187 | poDate: "W0603", 188 | shipto_name: 'N102:N101["ST"]', 189 | shipto_address: 'N1-N301:N101["ST"]', 190 | shipto_city: 'N1-N401:N101["ST"]', 191 | shipto_state: 'N1-N402:N101["ST"]', 192 | shipto_zip: 'N1-N403:N101["ST"]', 193 | }; 194 | 195 | interchange.functionalGroups.forEach((group) => { 196 | group.transactions.forEach((transaction) => { 197 | console.log(transaction.toObject(map)); 198 | }); 199 | }); 200 | ``` 201 | 202 | ## Credit 203 | 204 | Created originally for the 205 | [TC Toolbox](https://github.com/TrueCommerce/vscode-tctoolbox) project by 206 | TrueCommerce; the public repository for TC Toolbox has been taken offline. The 207 | original, parser-only library may be found at 208 | [TrueCommerce/node-x12](https://github.com/TrueCommerce/node-x12), which is 209 | still public at this time but no longer maintained. Without the good work done 210 | by TrueCommerce up until 2016, this library would not exist. 211 | 212 | Thanks to [@DotJoshJohnson](https://github.com/DotJoshJohnson). 213 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | // Allows shorthand test command with permissions baked in. 4 | "test": "deno test --unstable --allow-read --coverage=coverage", 5 | "lcov": "deno coverage coverage --lcov --output=coverage/report.lcov", 6 | "cover": "deno task clean && deno task test && deno task lcov && genhtml -o coverage/html coverage/report.lcov", 7 | // Command to build for npm. 8 | "build": "deno run -A scripts/build_npm.ts", 9 | // Command to publish to npm repository. 10 | "publish": "cd ./npm && npm publish", 11 | // Clean up the npm dir arbitrarily. 12 | "clean": "rm -rf ./npm ./coverage" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { Transform } from "https://deno.land/std@0.136.0/node/stream.ts"; 2 | export { StringDecoder } from "https://deno.land/std@0.136.0/node/string_decoder.ts"; 3 | export { Liquid } from "https://cdn.skypack.dev/pin/liquidjs@v9.37.0-dA2YkE2JlVe1VjIZ5g3G/mode=imports/optimized/liquidjs.js"; 4 | export * as crypto from "https://deno.land/std@0.136.0/node/crypto.ts"; 5 | -------------------------------------------------------------------------------- /docs/JSEDINotation.md: -------------------------------------------------------------------------------- 1 | ## JavaScript EDI Notation 2 | 3 | Node-X12 defines a simplified hierarchical object model for EDI objects called 4 | **JavaScript EDI Notation**. This object model is used for generating valid EDI 5 | in the `X12Generator` class, and for calling `JSON.stringify` on members of the 6 | X12 object model. The major difference is that the X12 object model is a 7 | **complete** description of an EDI file, while this notation only retains the 8 | data necessary to exchange EDI to and from JSON. 9 | 10 | A complete interface for this notation can be found at 11 | [src/JSEDINotation.ts](/src/JSEDINotation.ts). 12 | 13 | ### Format 14 | 15 | Each level in the hierarchy has two properties. A container object will have two 16 | array properties. At the bottom level of the hierarchy are the segments of a 17 | transaction set; these segments have a tag and a string array. 18 | 19 | The top level of the hierarchy also optionally allows to define the control 20 | characters. 21 | 22 | ### Hierarchy 23 | 24 | The hierarchy of JavaScript EDI Notation closely follows the X12 object model. 25 | 26 | - Root {object} 27 | 28 | - options {object} 29 | - header {Array<string>} 30 | - functionalGroups {Array<FunctionalGroup>} 31 | 32 | - FunctionalGroup {object} 33 | 34 | - header {Array<string>} 35 | - transactions {Array<Transaction>} 36 | 37 | - Transaction {object} 38 | 39 | - header {Array<string>} 40 | - segments {Array<Segment>} 41 | 42 | - Segment {object} 43 | - tag {string} 44 | - elements {Array<string>} 45 | 46 | ### ASC X12 Headers and Trailers 47 | 48 | The following ASC X12 headers are interpreted to and from headers in the 49 | hierarchy. Trailers are dynamically generated based on properties of the level. 50 | 51 | - ISA: Root level 52 | - GS: FunctionalGroup level 53 | - ST: Transaction level 54 | 55 | ### Sample JS EDI Notation 56 | 57 | A sample 856 generated from test [document](/test/test-data/856.edi). 58 | 59 | ```json 60 | { 61 | "header": [ 62 | "01", 63 | "0000000000", 64 | "01", 65 | "ABCCO ", 66 | "12", 67 | "4405197800 ", 68 | "01", 69 | "999999999 ", 70 | "111206", 71 | "1719", 72 | "-", 73 | "00406", 74 | "000000049", 75 | "0", 76 | "P", 77 | ">" 78 | ], 79 | "options": { 80 | "segmentTerminator": "~", 81 | "elementDelimiter": "*", 82 | "endOfLine": "\n", 83 | "format": false, 84 | "subElementDelimiter": ">" 85 | }, 86 | "functionalGroups": [ 87 | { 88 | "header": [ 89 | "SH", 90 | "4405197800", 91 | "999999999", 92 | "20111206", 93 | "1045", 94 | "49", 95 | "X", 96 | "004060" 97 | ], 98 | "transactions": [ 99 | { 100 | "header": ["856", "0008"], 101 | "segments": [ 102 | { 103 | "tag": "BSN", 104 | "elements": ["14", "829716", "20111206", "142428", "0002"] 105 | }, 106 | { 107 | "tag": "HL", 108 | "elements": ["1", "", "S"] 109 | }, 110 | { 111 | "tag": "TD1", 112 | "elements": ["PCS", "2", "", "", "", "A3", "60.310", "LB"] 113 | }, 114 | { 115 | "tag": "TD5", 116 | "elements": ["", "2", "XXXX", "", "XXXX"] 117 | }, 118 | { 119 | "tag": "REF", 120 | "elements": ["BM", "999999-001"] 121 | }, 122 | { 123 | "tag": "REF", 124 | "elements": ["CN", "5787970539"] 125 | }, 126 | { 127 | "tag": "DTM", 128 | "elements": ["011", "20111206"] 129 | }, 130 | { 131 | "tag": "N1", 132 | "elements": ["SH", "1 EDI SOURCE"] 133 | }, 134 | { 135 | "tag": "N3", 136 | "elements": ["31875 SOLON RD"] 137 | }, 138 | { 139 | "tag": "N4", 140 | "elements": ["SOLON", "OH", "44139"] 141 | }, 142 | { 143 | "tag": "N1", 144 | "elements": ["OB", "XYZ RETAIL"] 145 | }, 146 | { 147 | "tag": "N3", 148 | "elements": ["P O BOX 9999999"] 149 | }, 150 | { 151 | "tag": "N4", 152 | "elements": ["ATLANTA", "GA", "31139-0020", "", "SN", "9999"] 153 | }, 154 | { 155 | "tag": "N1", 156 | "elements": ["SF", "1 EDI SOURCE"] 157 | }, 158 | { 159 | "tag": "N3", 160 | "elements": ["31875 SOLON ROAD"] 161 | }, 162 | { 163 | "tag": "N4", 164 | "elements": ["SOLON", "OH", "44139"] 165 | }, 166 | { 167 | "tag": "HL", 168 | "elements": ["2", "1", "O"] 169 | }, 170 | { 171 | "tag": "PRF", 172 | "elements": ["99999817", "", "", "20111205"] 173 | }, 174 | { 175 | "tag": "HL", 176 | "elements": ["3", "2", "I"] 177 | }, 178 | { 179 | "tag": "LIN", 180 | "elements": ["1", "VP", "87787D", "UP", "999999310145"] 181 | }, 182 | { 183 | "tag": "SN1", 184 | "elements": ["1", "24", "EA"] 185 | }, 186 | { 187 | "tag": "PO4", 188 | "elements": ["1", "24", "EA"] 189 | }, 190 | { 191 | "tag": "PID", 192 | "elements": ["F", "", "", "", "BLUE WIDGET"] 193 | }, 194 | { 195 | "tag": "HL", 196 | "elements": ["4", "2", "I"] 197 | }, 198 | { 199 | "tag": "LIN", 200 | "elements": ["2", "VP", "99887D", "UP", "999999311746"] 201 | }, 202 | { 203 | "tag": "SN1", 204 | "elements": ["2", "6", "EA"] 205 | }, 206 | { 207 | "tag": "PO4", 208 | "elements": ["1", "6", "EA"] 209 | }, 210 | { 211 | "tag": "PID", 212 | "elements": ["F", "", "", "", "RED WIDGET"] 213 | }, 214 | { 215 | "tag": "CTT", 216 | "elements": ["4", "30"] 217 | } 218 | ] 219 | } 220 | ] 221 | } 222 | ] 223 | } 224 | ``` 225 | -------------------------------------------------------------------------------- /docs/QueryLanguage.md: -------------------------------------------------------------------------------- 1 | # Query Language 2 | 3 | The query language makes it possible to directly select values from the class 4 | object model. This also drives the transaction mapping functionality; in fact, 5 | the `FOREACH()` macro was added specifically to support this feature. 6 | 7 | ## Basics 8 | 9 | The basic query functionality is extremely simple: provide an EDI document as a 10 | string or as an `X12Interchange` and query the object model of that document. 11 | EDI is not really an object model format; however, the parser takes the rules of 12 | the EDI spec and populates JavaScript class objects. 13 | 14 | ### Object Model 15 | 16 | The model defined by node-X12 is the following hierarchy: 17 | 18 | ``` 19 | X12Interchange 20 | ┗╸ X12FunctionalGroup 21 | ┗╸ X12Transaction 22 | ┣╸ X12Segment 23 | ┣╸ X12Segment 24 | ┗╸ etc. 25 | ``` 26 | 27 | The query engine will take raw EDI and convert it to an interchange, so it is 28 | always using this model. 29 | 30 | ### Engine Behavior 31 | 32 | Due to the sequential nature of EDI, all look ups eventually are steps in an 33 | array. Although ASC X12 defines loops, the object model does not directly 34 | implement these loops. Therefore, the engine will actually step through each 35 | segment, checking the tag, and then attempting to return the value of the 36 | element at a specific psoition. 37 | 38 | When no element can be found for a particular query, no result will be returned. 39 | 40 | ### Reference Lookups 41 | 42 | References can be looked up using the tag name and position, using the 43 | traditional EDI reference name. In order to support parent-child segments, 44 | queries can refer to known values. When the known value of the reference is 45 | found, then each segment is stepped through to find the child segment. 46 | 47 | ## Macros 48 | 49 | The query language has been extended with a concept of macros; the idea is that 50 | sometimes it is necessary to operate against more than one value at a time. This 51 | was especially relevant to how the `X12TransactionMap` class is designed, as it 52 | was necessary to be able to query for EDI loops. 53 | 54 | Macros cannot be nested. If a nested macro appears to work then consider it to 55 | be unreliable, subject to misbehavior and breaking changes in future API 56 | updates/bugfixes. 57 | 58 | ### Supported Macros 59 | 60 | | Macro | Parameters | Example | Description | 61 | | ----------- | ------------------------------------------------- | -------------------------------- | --------------------------------------------------------- | 62 | | **FOREACH** | Tag: The segment tag to loop against | `FOREACH(LX)=>MAN02:MAN01['CP']` | Loop against a parent tag to retrieve an array of values. | 63 | | **CONCAT** | Query: A valid EDI query
Separator: A string | `CONCAT(REF02,-)=>REF01` | Lookup a value to concatenate to another value. | 64 | 65 | ## Examples 66 | 67 | | Section | Example | Description | 68 | | :---------------------: | :-------------: | --------------------------------------------------------------------------------------------------- | 69 | | **Macros** | `FOREACH(LX)=>` | Defines a multi-value operation on a query. | 70 | | **HL Path** | `HL+O+P+I` | Defines a path through a series of HL segments. | 71 | | **Parent Segment Path** | `PO1-REF` | Defines a path through a series of adjacent segments. | 72 | | **Element Reference** | `REF02` | Defines an element by position. | 73 | | **Value** | `"DP"` | Defines a value to be checked when evaluating qualifiers.
Single or double quotes may be used. | 74 | 75 | **Example 1: Select `REF02` Elements**
`REF02` 76 | 77 | **Example 2: Select `REF02` Elements With a `PO` Qualifier in `REF01`**
78 | `REF02:REF01["PO"]` 79 | 80 | **Example 3: Select Line-Level PO Numbers (850)**
`PO1-REF02:REF01["PO"]` 81 | 82 | **Example 4: Select ASN Line Quantities**
`HL+S+O+P+I-LIN-SN102` 83 | 84 | **Example 5: Select values from a loop series**
85 | `FOREACH(LX)=>MAN02:MAN01["CP"]` 86 | -------------------------------------------------------------------------------- /docs/TOC.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | | Document | Description | 4 | | :-------------------------------------------: | ------------------------------------------------- | 5 | | [API](./API.md) | Complete reference for the public API. | 6 | | [JSEDINotation](./JSEDINotation.md) | Detailed overview of the object notation for EDI. | 7 | | [QueryLanguage](./QueryLanguage.md) | Overview of the query language. | 8 | | [Tests](./Tests.md) | Complete reference for the latest test results. | 9 | | [TransactionMapping](./TransactionMapping.md) | Overview of transaction set mapping. | 10 | -------------------------------------------------------------------------------- /docs/TransactionMapping.md: -------------------------------------------------------------------------------- 1 | ## Transaction Mapping 2 | 3 | A factory for mapping transaction sets to Javascript objects has been built-in 4 | as an alternative to other mapping scenarios, such as XSL documents. The 5 | `X12TransactionMap` class uses the query engine to accomplish this 6 | functionality. It operates on only a single transaction at a time. 7 | 8 | The complete class for this factory can be found at 9 | [src/X12TransactionMap.ts](/src/X12TransactionMap.ts). 10 | 11 | ### To Object From Transaction Set 12 | 13 | #### Mapping Data 14 | 15 | The transaction mapper expects to be given an object with key/query pairs. 16 | Objects may be nested and will be resolved as they are encountered, descending 17 | further until the last object is handled. 18 | 19 | When loops in a transaction are encountered, the mapper will take the first 20 | value in the loop series unless the `FOREACH` query macro is used. With 21 | `FOREACH` queries, the containing object will be coerced into an array of 22 | objects with the corresponding values from the loop. 23 | 24 | Method `toObject` maps the transaction and returns the resulting object. 25 | 26 | For convenience, every instance of the `X12Transaction` class contains a 27 | `toObject` method, with a required parameter of `map`. 28 | 29 | #### Helper API 30 | 31 | The transaction mapper will take an optional helper function. This function will 32 | be executed for every resolved value; the output of the function will set the 33 | value of the key. One way that this is being used in production is to resolve 34 | SCAC codes dynamically to their long form. 35 | 36 | Supported parameters: 37 | 38 | - `key`: The current key being evaluated (required) 39 | - `value`: The current resolved value (required) 40 | - `query`: The current query that was resolved (optional) 41 | - `callback`: A callback to be executed within the helper function (optional) 42 | 43 | When a helper is provided to the mapper, it is set as a property of the class. 44 | It will not be executed until the `toObject()` method is called. This method 45 | takes two optional parameters, `map` and `callback`. This permits the mapper to 46 | override the current map instance or to pass the callback to the helper 47 | function. 48 | 49 | When calling `toObject()` from an instance of `X12Transaction`, a helper may be 50 | optionally passed. Callbacks are not supported in this scenario. 51 | 52 | #### Supported Maps 53 | 54 | At this time, only key/query maps are supported. These maps will resolve the 55 | query to a value (or values in the case of `FOREACH`) and return an object or 56 | objects conforming to the map. 57 | 58 | An initial effort has been put into mapping an array of queries, but there is 59 | insufficient use case at this time for this and it should be considered a very 60 | rough beta and unsupported at this time. 61 | 62 | ### To Transaction Set From Object 63 | 64 | #### Mapping Data 65 | 66 | Method `fromObject` maps the transaction and returns the resulting object. 67 | 68 | For convenience, every instance of the `X12Transaction` class contains a 69 | `fromObject` method, with required parameters of `input` and `map`. This will 70 | map the input to the current instance of `X12Transaction`. 71 | 72 | #### Liquid Macro Language 73 | 74 | The object map for mapping data to a transaction set differs significantly. It 75 | has more in common with [JS EDI Notation](./JSEDINotation.md). For example: 76 | 77 | 78 | 79 | ```json 80 | { 81 | "header": ["940", "{{ macro | random }}"], 82 | "segments": [ 83 | { "tag": "W05", "elements": ["N", "{{ input.internalOrderId }}", "{{ input.orderId }}"] }, 84 | { "tag": "N1", "elements": ["ST", "{{ input.shippingFirstName }} {{ input.shippingLastName }}"] }, 85 | { "tag": "N3", "elements": ["{{ input.shippingStreet1 }}", "{{ input.shippingStreet2 }}"] }, 86 | ...etc 87 | ] 88 | } 89 | ``` 90 | 91 | 92 | 93 | When loops are defined, it is done sequentially. Loops can either be manually 94 | written out, or some helper macros can be used to assist in generating them. If 95 | loops are to be generated dynamically, the first segment in the loop must have a 96 | `loopStart` and a `loopLength` property. The last segment in the loop must have 97 | a `loopEnd` property to signal that the loop has ended. For example: 98 | 99 | 100 | 101 | ```json 102 | { "tag": "LX", "elements": ["{{ 'LX' | sequence }}"], "loopStart": true, "loopLength": "{{ input.orderItems | json_parse | size }}" }, 103 | { "tag": "W01", "elements": 104 | [ 105 | "{{ input.orderItems | json_parse | map: 'quantity' | in_loop }}", 106 | "EA", 107 | "", 108 | "VN", 109 | "{{ input.orderItems | json_parse | map: 'sku' | in_loop }}" 110 | ] 111 | }, 112 | { "tag": "G69", "elements": ["{{ input.orderItems | json_parse | map: 'title' | truncate: 45 | in_loop }}"], "loopEnd": true }, 113 | ``` 114 | 115 | 116 | 117 | #### Liquid Macro API 118 | 119 | The syntax for mapping is pure [Liquid](https://liquidjs.com/) with some 120 | additional macros for convenience. The object to map from is always referred to 121 | as `input`. There are some macros which do not take an input; it is considered 122 | useful to access them with the word `macro`, but any value can be passed to them 123 | since the value will be discarded. 124 | 125 | When mapping from an object to a transaction set, an object may be passed as an 126 | argument which provides additional Liquid filters; the property name and the 127 | function value will be passed to 128 | [`Liquid.registerFilter`](https://liquidjs.com/api/classes/liquid_.liquid.html#registerFilter). 129 | 130 | To use Liquid, simply install `liquidjs` via npm in your project, and then set 131 | your options when mapping to use `'liquidjs'`. This library will attempt to 132 | `require` Liquid and use it as the engine for mapping from an object. 133 | 134 | The table of filters should not be considered exhaustive; see 135 | [Liquid's official site](https://shopify.github.io/liquid/) for details on 136 | filters, keeping in mind the custom filters that this library uses and provides 137 | in the table below. 138 | 139 | | Property | Parameters | Example | Description | 140 | | ------------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | 141 | | **sequence** | string | {{ 'LX' | sequence }} | Method for assigning sequence values in a loop. | 142 | | **in_loop** | string | {{ input.someValue | in_loop }} | Method for serializing values within a loop to preserve them for use by the loop; only use this as the last filter, see example for defining loops. | 143 | | **json_parse** | string | {{ '{\"example\": \"content\"\}' | json_parse }} | Method for returning an object from valid JSON. | 144 | | **json_stringify** | string | {{ inport.someObject | json_stringify }} | Method for converting a value to valid JSON. | 145 | | **size** | any[] | {{ input.someArray | size }} | Method for returning the length of an array or string. Default Liquid filter. | 146 | | **map** | any[], string | {{ input.someArray | map: 'someProperty' }} | Method for returning an array of a specific property in array of objects. | 147 | | **sum_array** | any[] | {{ input.someArray | sum_array }} | Method for returning the sum of an array of numbers. | 148 | | **truncate** | string \| string[], number | {{ input.someArray | truncate: 45 }} | Method for truncating a string or array of strings to the desired character length. Overrides default Liquid implementation. | 149 | | **random** | N/A | {{ macro | random }} | Method for returning a random 4 digit number. | 150 | | **edi_date** | N/A,string | {{ macro | edi_date: 'long' }} | The current date; takes argument of `'long'` for YYYYmmdd or `'short'` for YYmmdd. | 151 | | **edi_time** | N/A | {{ macro | edi_time }} | The current time in HHMM format. | 152 | 153 | #### Legacy Macro Language 154 | 155 | This option will be deprecated in version 2.x series to be replaced by Liquid. 156 | The internal, legacy macro language differs significantly from Liquid. For 157 | example: 158 | 159 | ```json 160 | { 161 | "header": ["940", "macro['random']()['val']"], 162 | "segments": [ 163 | { "tag": "W05", "elements": ["N", "input['sfOrderId']", "input['orderId']"] }, 164 | { "tag": "N1", "elements": ["ST", "`${input['firstName']} ${input['lastName']}`"] }, 165 | { "tag": "N3", "elements": ["input['addressStreet1']", "input['addressStreet2']"] } 166 | ...etc 167 | ] 168 | } 169 | ``` 170 | 171 | When loops are defined, it is done sequentially. Loops can either be manually 172 | written out, or some helper macros can be used to assist in generating them. If 173 | loops are to be generated dynamically, the first segment in the loop must have a 174 | `loopStart` and a `loopLength` property. The last segment in the loop must have 175 | a `loopEnd` property to signal that the loop has ended. For example: 176 | 177 | ```json 178 | ({ 179 | "tag": "LX", 180 | "elements": ["macro['sequence']('LX')['val']"], 181 | "loopStart": true, 182 | "loopLength": "macro['length'](macro['json'](input['orderItems'])['val'])['val']" 183 | }, 184 | { 185 | "tag": "W01", 186 | "elements": [ 187 | "macro['map'](macro['json'](input['orderItems'])['val'], 'quantity')['val']", 188 | "EA", 189 | "", 190 | "VN", 191 | "macro['map'](macro['json'](input['orderItems'])['val'], 'sku')['val']" 192 | ] 193 | }, 194 | { 195 | "tag": "G69", 196 | "elements": ["macro['map'](macro['json'](input['orderItems'])['val'], 'title')['val']"], 197 | "loopEnd": true 198 | }) 199 | ``` 200 | 201 | #### Legacy Macro API 202 | 203 | The syntax for legacy internal mapping is based on object properties. The object 204 | to map from is always referred to as `input`; to access the properties, use 205 | bracket notation. There is always a `macro` object; the properties and functions 206 | on this object should also be accessed by bracket notation. Macro functions 207 | should always return an object with a `val` property. When mapping from an 208 | object to a transaction set, an object may be passed as an argument which 209 | provides additional macro properties, or which may be used to override 210 | properties in the internal `macro` object. 211 | 212 | | Property | Parameters | Example | Description | 213 | | --------------- | -------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | 214 | | **currentDate** | N/A | `macro['currentDate']` | The current date in YYYYmmdd format. | 215 | | **sequence** | string | `macro['sequence']('LX')['val']` | Method for assigning sequence values in a loop. | 216 | | **json** | string | `macro['json']('{\"example\": \"content\"}')['val']` | Method for returning an object from valid JSON. | 217 | | **length** | any[] | `macro['length'](input['someArray'])['val']` | Method for returning the length of an array. | 218 | | **map** | any[], string | `macro['map'](input['someArrayOfObjects'], 'someProperty')['val']` | Method for returning an array of a specific property in array of objects. | 219 | | **sum** | any[], string, [number=0] | `macro['sum'](input['someArrayOfObjects'], 'someProperty')['val']` | Method for returning the sum of an array of numbers, with an optional decimal places parameter. | 220 | | **random** | N/A | `macro['random']()['val']` | Method for returning a random 4 digit number. | 221 | | **truncate** | string \| string[], number | `macro['truncate']("testing", 4)['val']` | Method for truncating a string or array of strings to the desired character length. | 222 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export * from "./src/JSEDINotation.ts"; 4 | export * from "./src/X12Element.ts"; 5 | export * from "./src/X12FatInterchange.ts"; 6 | export * from "./src/X12FunctionalGroup.ts"; 7 | export * from "./src/X12Generator.ts"; 8 | export * from "./src/X12Interchange.ts"; 9 | export * from "./src/X12Parser.ts"; 10 | export * from "./src/X12QueryEngine.ts"; 11 | export * from "./src/X12Segment.ts"; 12 | export * from "./src/X12SegmentHeader.ts"; 13 | export * from "./src/X12SerializationOptions.ts"; 14 | export * from "./src/X12Transaction.ts"; 15 | export * from "./src/X12TransactionMap.ts"; 16 | export * from "./src/X12ValidationEngine/index.ts"; 17 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parse, 3 | ParseOptions, 4 | } from "https://deno.land/std@0.136.0/flags/mod.ts"; 5 | import { copy } from "https://deno.land/std@0.136.0/fs/mod.ts"; 6 | import { inc as increment } from "https://deno.land/x/semver@v1.4.0/mod.ts"; 7 | import { build, emptyDir } from "https://deno.land/x/dnt@0.22.0/mod.ts"; 8 | 9 | const NPM_NAME = "node-x12"; 10 | 11 | await emptyDir("./npm"); 12 | await copy("test/test-data", "npm/esm/test/test-data", { overwrite: true }); 13 | await copy("test/test-data", "npm/script/test/test-data", { overwrite: true }); 14 | 15 | function versionHandler(): string { 16 | switch (true) { 17 | case args.major: 18 | return increment(version, "major") ?? version; 19 | case args.minor: 20 | return increment(version, "minor") ?? version; 21 | case args.patch: 22 | return increment(version, "patch") ?? version; 23 | case args.premajor: 24 | return increment(version, "premajor") ?? version; 25 | case args.preminor: 26 | return increment(version, "preminor") ?? version; 27 | case args.prepatch: 28 | return increment(version, "prepatch") ?? version; 29 | case args.prerelease: 30 | return increment(version, "prerelease") ?? version; 31 | } 32 | 33 | return version; 34 | } 35 | 36 | const versionFile = "./.version"; 37 | const version = await Deno.readTextFile(versionFile); 38 | const argsOpts: ParseOptions = { 39 | boolean: true, 40 | default: { 41 | major: false, 42 | minor: false, 43 | patch: false, 44 | premajor: false, 45 | preminor: false, 46 | prepatch: false, 47 | prerelease: false, 48 | }, 49 | }; 50 | const args = parse(Deno.args, argsOpts); 51 | const newVersion = versionHandler(); 52 | 53 | await build({ 54 | entryPoints: ["./mod.ts"], 55 | outDir: "./npm", 56 | shims: { 57 | deno: true, 58 | }, 59 | mappings: { 60 | "https://deno.land/std@0.136.0/node/stream.ts": "stream", 61 | "https://deno.land/std@0.136.0/node/string_decoder.ts": "string_decoder", 62 | "https://deno.land/std@0.136.0/node/crypto.ts": "crypto", 63 | "https://cdn.skypack.dev/pin/liquidjs@v9.37.0-dA2YkE2JlVe1VjIZ5g3G/mode=imports/optimized/liquidjs.js": 64 | { 65 | name: "liquidjs", 66 | version: "^9.37.0", 67 | }, 68 | }, 69 | package: { 70 | name: NPM_NAME, 71 | version: newVersion, 72 | description: 73 | "ASC X12 parser, generator, query engine, and mapper; now with support for streams.", 74 | keywords: [ 75 | "x12", 76 | "edi", 77 | "ansi", 78 | "asc", 79 | "ecommerce", 80 | ], 81 | homepage: `https://github.com/aaronhuggins/${NPM_NAME}#readme`, 82 | bugs: `https://github.com/aaronhuggins/${NPM_NAME}/issues`, 83 | license: "MIT", 84 | author: "Aaron Huggins ", 85 | repository: { 86 | type: "git", 87 | url: `https://github.com/aaronhuggins/${NPM_NAME}.git`, 88 | }, 89 | }, 90 | }); 91 | 92 | // post build steps 93 | class Appender { 94 | encoder = new TextEncoder(); 95 | file: Promise; 96 | constructor(file: string) { 97 | this.file = Deno.open(file, { append: true }); 98 | } 99 | async write(line: string) { 100 | (await this.file).write(this.encoder.encode(line + "\n")); 101 | } 102 | async close() { 103 | (await this.file).close(); 104 | } 105 | } 106 | 107 | const npmignore = new Appender("npm/.npmignore"); 108 | await npmignore.write("esm/test/test-data/**"); 109 | await npmignore.write("script/test/test-data/**"); 110 | await npmignore.close(); 111 | 112 | await Deno.copyFile("LICENSE.md", "npm/LICENSE.md"); 113 | await Deno.copyFile("README.md", "npm/README.md"); 114 | 115 | if (newVersion === version) { 116 | console.log( 117 | `[build_npm] Version did not change; nothing to deploy. ${NPM_NAME} v${version}`, 118 | ); 119 | } else { 120 | await Deno.writeTextFile(versionFile, newVersion); 121 | console.log(`[build_npm] ${NPM_NAME} v${newVersion} ready to deploy!`); 122 | } 123 | -------------------------------------------------------------------------------- /scripts/create_labels.ts: -------------------------------------------------------------------------------- 1 | const color = "17f384"; 2 | const names = [ 3 | "version_major", 4 | "version_minor", 5 | "version_patch", 6 | "version_premajor", 7 | "version_preminor", 8 | "version_prepatch", 9 | "version_prerelease", 10 | ]; 11 | const GITHUB_TOKEN = Deno.env.get("GITHUB_TOKEN"); 12 | const user = Deno.env.get("GITHUB_USER"); 13 | const repo = Deno.env.get("GITHUB_REPO"); 14 | 15 | console.log(`Creating labels for: ${user}/${repo}`); 16 | 17 | if (GITHUB_TOKEN) { 18 | for (const name of names) { 19 | const label = JSON.stringify({ name, color }); 20 | const response = await fetch( 21 | `https://api.github.com/repos/${user}/${repo}/labels`, 22 | { 23 | method: "POST", 24 | body: label, 25 | headers: { 26 | "content-type": "application/json", 27 | "Authorization": `token ${GITHUB_TOKEN}`, 28 | }, 29 | }, 30 | ); 31 | 32 | if (response.ok && response.status === 201) { 33 | console.log(`Label success: ${label}`); 34 | } else { 35 | console.log( 36 | `Label failed (status ${response.status} ${response.statusText}): ${label}`, 37 | ); 38 | } 39 | } 40 | } else { 41 | console.log("Labels all failed: Github token empty"); 42 | } 43 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=aaronhuggins_node-x12 2 | sonar.organization=aaronhuggins 3 | sonar.source=src/**,mod.ts 4 | sonar.exclusions=scripts/**,test/** 5 | sonar.typescript.tsconfigPath=.sonarcloud.ts.json 6 | sonar.javascript.lcov.reportPaths=coverage/report.lcov 7 | sonar.pullrequest.provider=GitHub 8 | sonar.pullrequest.github.repository=aaronhuggins/node-x12 -------------------------------------------------------------------------------- /src/Errors.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export class ArgumentNullError extends Error { 4 | constructor(argumentName: string) { 5 | super(`The argument, '${argumentName}', cannot be null.`); 6 | this.name = "ArgumentNullError"; 7 | } 8 | 9 | name: string; 10 | } 11 | 12 | export class GeneratorError extends Error { 13 | constructor(message?: string) { 14 | super(message); 15 | this.name = "GeneratorError"; 16 | } 17 | 18 | name: string; 19 | } 20 | 21 | export class ParserError extends Error { 22 | constructor(message?: string) { 23 | super(message); 24 | this.name = "ParserError"; 25 | } 26 | 27 | name: string; 28 | } 29 | 30 | export class QuerySyntaxError extends Error { 31 | constructor(message?: string) { 32 | super(message); 33 | this.name = "QuerySyntaxError"; 34 | } 35 | 36 | name: string; 37 | } 38 | -------------------------------------------------------------------------------- /src/JSEDINotation.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { X12SerializationOptions } from "./X12SerializationOptions.ts"; 4 | 5 | export class JSEDINotation { 6 | constructor(header?: string[], options?: X12SerializationOptions) { 7 | this.header = header === undefined ? new Array() : header; 8 | this.options = options === undefined ? {} : options; 9 | this.functionalGroups = new Array(); 10 | } 11 | 12 | options?: X12SerializationOptions; 13 | header: string[]; 14 | functionalGroups: JSEDIFunctionalGroup[]; 15 | 16 | addFunctionalGroup(header: string[]): JSEDIFunctionalGroup { 17 | const functionalGroup = new JSEDIFunctionalGroup(header); 18 | 19 | this.functionalGroups.push(functionalGroup); 20 | 21 | return functionalGroup; 22 | } 23 | } 24 | 25 | export class JSEDIFunctionalGroup { 26 | constructor(header?: string[]) { 27 | this.header = header === undefined ? new Array() : header; 28 | this.transactions = new Array(); 29 | } 30 | 31 | header: string[]; 32 | transactions: JSEDITransaction[]; 33 | 34 | addTransaction(header: string[]): JSEDITransaction { 35 | const transaction = new JSEDITransaction(header); 36 | 37 | this.transactions.push(transaction); 38 | 39 | return transaction; 40 | } 41 | } 42 | 43 | export class JSEDITransaction { 44 | constructor(header?: string[]) { 45 | this.header = header === undefined ? new Array() : header; 46 | this.segments = new Array(); 47 | } 48 | 49 | header: string[]; 50 | segments: JSEDISegment[]; 51 | 52 | addSegment(tag: string, elements: string[]): JSEDISegment { 53 | const segment = new JSEDISegment(tag, elements); 54 | 55 | this.segments.push(segment); 56 | 57 | return segment; 58 | } 59 | } 60 | 61 | export class JSEDISegment { 62 | constructor(tag: string, elements: string[]) { 63 | this.tag = tag; 64 | this.elements = elements; 65 | } 66 | 67 | tag: string; 68 | elements: string[]; 69 | } 70 | -------------------------------------------------------------------------------- /src/Positioning.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export class Position { 4 | constructor(line?: number, character?: number) { 5 | if (typeof line === "number" && typeof character === "number") { 6 | this.line = line; 7 | this.character = character; 8 | } 9 | } 10 | 11 | line!: number; 12 | character!: number; 13 | } 14 | 15 | export class Range { 16 | constructor( 17 | startLine?: number, 18 | startChar?: number, 19 | endLine?: number, 20 | endChar?: number, 21 | ) { 22 | if ( 23 | typeof startLine === "number" && 24 | typeof startChar === "number" && 25 | typeof endLine === "number" && 26 | typeof endChar === "number" 27 | ) { 28 | this.start = new Position(startLine, startChar); 29 | this.end = new Position(endLine, endChar); 30 | } 31 | } 32 | 33 | start!: Position; 34 | end!: Position; 35 | } 36 | -------------------------------------------------------------------------------- /src/X12Diagnostic.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Range } from "./Positioning.ts"; 4 | 5 | export enum X12DiagnosticLevel { 6 | Info = 0, 7 | Warning = 1, 8 | Error = 2, 9 | } 10 | 11 | export class X12Diagnostic { 12 | constructor(level?: X12DiagnosticLevel, message?: string, range?: Range) { 13 | this.level = level === undefined ? X12DiagnosticLevel.Error : level; 14 | this.message = message === undefined ? "" : message; 15 | this.range = range ?? new Range(); 16 | } 17 | 18 | level: X12DiagnosticLevel; 19 | message: string; 20 | range: Range; 21 | } 22 | -------------------------------------------------------------------------------- /src/X12Element.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { Range } from "./Positioning.ts"; 4 | 5 | export class X12Element { 6 | /** 7 | * @description Create an element. 8 | * @param {string} value - A value for this element. 9 | */ 10 | constructor(value: string = "") { 11 | this.range = new Range(); 12 | this.value = value; 13 | } 14 | 15 | range: Range; 16 | value: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/X12FatInterchange.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file ban-types 2 | "use strict"; 3 | 4 | import { JSEDINotation } from "./JSEDINotation.ts"; 5 | import { X12Interchange } from "./X12Interchange.ts"; 6 | import { 7 | defaultSerializationOptions, 8 | X12SerializationOptions, 9 | } from "./X12SerializationOptions.ts"; 10 | 11 | export class X12FatInterchange extends Array { 12 | /** 13 | * @description Create a fat interchange. 14 | * @param {X12Interchange[] | X12SerializationOptions} [items] - The items for this array or options for this interchange. 15 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 16 | */ 17 | constructor( 18 | items?: X12Interchange[] | X12SerializationOptions, 19 | options?: X12SerializationOptions, 20 | ) { 21 | super(); 22 | if (Array.isArray(items)) { 23 | super.push(...items); 24 | } else { 25 | options = items; 26 | } 27 | 28 | this.options = defaultSerializationOptions(options); 29 | 30 | this.interchanges = this; 31 | } 32 | 33 | interchanges: X12Interchange[]; 34 | options: X12SerializationOptions; 35 | 36 | /** 37 | * @description Serialize fat interchange to EDI string. 38 | * @param {X12SerializationOptions} [options] - Options to override serializing back to EDI. 39 | * @returns {string} This fat interchange converted to EDI string. 40 | */ 41 | toString(options?: X12SerializationOptions): string { 42 | options = options !== undefined 43 | ? defaultSerializationOptions(options) 44 | : this.options; 45 | 46 | let edi = ""; 47 | 48 | for (let i = 0; i < this.interchanges.length; i++) { 49 | edi += this.interchanges[i].toString(options); 50 | 51 | if (options.format) { 52 | edi += options.endOfLine; 53 | } 54 | } 55 | 56 | return edi; 57 | } 58 | 59 | /** 60 | * @description Serialize interchange to JS EDI Notation object. 61 | * @returns {JSEDINotation[]} This fat interchange converted to an array of JS EDI notation. 62 | */ 63 | toJSEDINotation(): JSEDINotation[] { 64 | const jsen = new Array(); 65 | 66 | this.interchanges.forEach((interchange) => { 67 | jsen.push(interchange.toJSEDINotation()); 68 | }); 69 | 70 | return jsen; 71 | } 72 | 73 | /** 74 | * @description Serialize interchange to JSON object. 75 | * @returns {object[]} This fat interchange converted to an array of objects. 76 | */ 77 | toJSON(): object[] { 78 | return this.toJSEDINotation(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/X12FunctionalGroup.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file ban-types 2 | "use strict"; 3 | 4 | import { JSEDIFunctionalGroup } from "./JSEDINotation.ts"; 5 | import { X12Segment } from "./X12Segment.ts"; 6 | import { GSSegmentHeader } from "./X12SegmentHeader.ts"; 7 | import { X12Transaction } from "./X12Transaction.ts"; 8 | import { 9 | defaultSerializationOptions, 10 | X12SerializationOptions, 11 | } from "./X12SerializationOptions.ts"; 12 | 13 | export class X12FunctionalGroup { 14 | /** 15 | * @description Create a functional group. 16 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 17 | */ 18 | constructor(options?: X12SerializationOptions) { 19 | this.transactions = new Array(); 20 | this.options = defaultSerializationOptions(options); 21 | } 22 | 23 | header!: X12Segment; 24 | trailer!: X12Segment; 25 | 26 | transactions: X12Transaction[]; 27 | 28 | options: X12SerializationOptions; 29 | 30 | /** 31 | * @description Set a GS header on this functional group. 32 | * @param {string[]} elements - An array of elements for a GS header. 33 | */ 34 | setHeader(elements: string[]): void { 35 | this.header = new X12Segment(GSSegmentHeader.tag, this.options); 36 | 37 | this.header.setElements(elements); 38 | 39 | this._setTrailer(); 40 | } 41 | 42 | /** 43 | * @description Add a transaction set to this functional group. 44 | * @returns {X12Transaction} The transaction which was added to this functional group. 45 | */ 46 | addTransaction(): X12Transaction { 47 | const transaction = new X12Transaction(this.options); 48 | 49 | this.transactions.push(transaction); 50 | 51 | this.trailer.replaceElement(`${this.transactions.length}`, 1); 52 | 53 | return transaction; 54 | } 55 | 56 | /** 57 | * @description Serialize functional group to EDI string. 58 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 59 | * @returns {string} This functional group converted to EDI string. 60 | */ 61 | toString(options?: X12SerializationOptions): string { 62 | options = options !== undefined 63 | ? defaultSerializationOptions(options) 64 | : this.options; 65 | 66 | let edi = this.header.toString(options); 67 | 68 | if (options.format) { 69 | edi += options.endOfLine; 70 | } 71 | 72 | for (let i = 0; i < this.transactions.length; i++) { 73 | edi += this.transactions[i].toString(options); 74 | 75 | if (options.format) { 76 | edi += options.endOfLine; 77 | } 78 | } 79 | 80 | edi += this.trailer.toString(options); 81 | 82 | return edi; 83 | } 84 | 85 | /** 86 | * @description Serialize functional group to JSON object. 87 | * @returns {object} This functional group converted to an object. 88 | */ 89 | toJSON(): object { 90 | const jsen = new JSEDIFunctionalGroup( 91 | this.header.elements.map((x) => x.value), 92 | ); 93 | 94 | this.transactions.forEach((transaction) => { 95 | const jsenTransaction = jsen.addTransaction( 96 | transaction.header.elements.map((x) => x.value), 97 | ); 98 | 99 | transaction.segments.forEach((segment) => { 100 | jsenTransaction.addSegment( 101 | segment.tag, 102 | segment.elements.map((x) => x.value), 103 | ); 104 | }); 105 | }); 106 | 107 | return jsen as object; 108 | } 109 | 110 | /** 111 | * @private 112 | * @description Set a GE trailer on this functional group. 113 | */ 114 | private _setTrailer(): void { 115 | this.trailer = new X12Segment(GSSegmentHeader.trailer, this.options); 116 | 117 | this.trailer.setElements([ 118 | `${this.transactions.length}`, 119 | this.header.valueOf(6) ?? "", 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/X12Generator.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { JSEDINotation } from "./JSEDINotation.ts"; 4 | import { X12Interchange } from "./X12Interchange.ts"; 5 | import { X12Parser } from "./X12Parser.ts"; 6 | import { 7 | defaultSerializationOptions, 8 | X12SerializationOptions, 9 | } from "./X12SerializationOptions.ts"; 10 | 11 | export class X12Generator { 12 | /** 13 | * @description Factory for generating EDI from JS EDI Notation. 14 | * @param {JSEDINotation} [jsen] - Javascript EDI Notation object to serialize. 15 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 16 | */ 17 | constructor(jsen?: JSEDINotation, options?: X12SerializationOptions) { 18 | this.jsen = jsen === undefined ? new JSEDINotation() : jsen; 19 | 20 | if ( 21 | typeof jsen === "object" && jsen.options !== undefined && 22 | options === undefined 23 | ) { 24 | this.options = defaultSerializationOptions(jsen.options); 25 | } else { 26 | this.options = defaultSerializationOptions(options); 27 | } 28 | 29 | this.interchange = new X12Interchange(this.options); 30 | } 31 | 32 | private jsen: JSEDINotation; 33 | private interchange: X12Interchange; 34 | private options: X12SerializationOptions; 35 | 36 | /** 37 | * @description Set the JS EDI Notation for this instance. 38 | * @param {JSEDINotation} [jsen] - Javascript EDI Notation object to serialize. 39 | */ 40 | setJSEDINotation(jsen: JSEDINotation): void { 41 | this.jsen = jsen; 42 | } 43 | 44 | /** 45 | * @description Get the JS EDI Notation for this instance. 46 | * @returns {JSEDINotation} The JS EDI Notation for this instance. 47 | */ 48 | getJSEDINotation(): JSEDINotation { 49 | return this.jsen; 50 | } 51 | 52 | /** 53 | * @description Set the serialization options for this instance. 54 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 55 | */ 56 | setOptions(options: X12SerializationOptions): void { 57 | this.options = defaultSerializationOptions(options); 58 | } 59 | 60 | /** 61 | * @description Get the serialization options for this instance. 62 | * @returns {X12SerializationOptions} The serialization options for this instance. 63 | */ 64 | getOptions(): X12SerializationOptions { 65 | return this.options; 66 | } 67 | 68 | /** 69 | * @description Validate the EDI in this instance. 70 | * @returns {X12Interchange} This instance converted to an interchange. 71 | */ 72 | validate(): X12Interchange { 73 | this._generate(); 74 | 75 | return new X12Parser(true).parse( 76 | this.interchange.toString(this.options), 77 | ) as X12Interchange; 78 | } 79 | 80 | /** 81 | * @description Serialize the EDI in this instance. 82 | * @returns {string} This instance converted to an EDI string. 83 | */ 84 | toString(): string { 85 | return this.validate().toString(this.options); 86 | } 87 | 88 | /** 89 | * @private 90 | * @description Generate an interchange from the JS EDI Notation in this instance. 91 | */ 92 | private _generate(): void { 93 | const genInterchange = new X12Interchange(this.options); 94 | 95 | genInterchange.setHeader(this.jsen.header); 96 | 97 | this.jsen.functionalGroups.forEach((functionalGroup) => { 98 | const genFunctionalGroup = genInterchange.addFunctionalGroup(); 99 | 100 | genFunctionalGroup.setHeader(functionalGroup.header); 101 | 102 | functionalGroup.transactions.forEach((transaction) => { 103 | const genTransaction = genFunctionalGroup.addTransaction(); 104 | 105 | genTransaction.setHeader(transaction.header); 106 | 107 | transaction.segments.forEach((segment) => { 108 | genTransaction.addSegment(segment.tag, segment.elements); 109 | }); 110 | }); 111 | }); 112 | 113 | this.interchange = genInterchange; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/X12Interchange.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file ban-types 2 | "use strict"; 3 | 4 | import { JSEDINotation } from "./JSEDINotation.ts"; 5 | import { X12FunctionalGroup } from "./X12FunctionalGroup.ts"; 6 | import { X12Segment } from "./X12Segment.ts"; 7 | import { ISASegmentHeader } from "./X12SegmentHeader.ts"; 8 | import { 9 | defaultSerializationOptions, 10 | X12SerializationOptions, 11 | } from "./X12SerializationOptions.ts"; 12 | 13 | export class X12Interchange { 14 | /** 15 | * @description Create an interchange. 16 | * @param {string|X12SerializationOptions} [segmentTerminator] - A character to terminate segments when serializing; or an instance of X12SerializationOptions. 17 | * @param {string} [elementDelimiter] - A character to separate elements when serializing; only required when segmentTerminator is a character. 18 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 19 | */ 20 | constructor( 21 | segmentTerminator?: string | X12SerializationOptions, 22 | elementDelimiter?: string, 23 | options?: X12SerializationOptions, 24 | ) { 25 | this.functionalGroups = new Array(); 26 | 27 | if (typeof segmentTerminator === "string") { 28 | this.segmentTerminator = segmentTerminator; 29 | if (typeof elementDelimiter === "string") { 30 | this.elementDelimiter = elementDelimiter; 31 | } else { 32 | throw new TypeError( 33 | 'Parameter "elementDelimiter" must be type of string.', 34 | ); 35 | } 36 | this.options = defaultSerializationOptions(options); 37 | } else if (typeof segmentTerminator === "object") { 38 | this.options = defaultSerializationOptions(segmentTerminator); 39 | this.segmentTerminator = this.options.segmentTerminator as string; 40 | this.elementDelimiter = this.options.elementDelimiter as string; 41 | } else { 42 | this.options = defaultSerializationOptions(options); 43 | this.segmentTerminator = this.options.segmentTerminator as string; 44 | this.elementDelimiter = this.options.elementDelimiter as string; 45 | } 46 | } 47 | 48 | header!: X12Segment; 49 | trailer!: X12Segment; 50 | 51 | functionalGroups: X12FunctionalGroup[]; 52 | 53 | segmentTerminator: string; 54 | elementDelimiter: string; 55 | options: X12SerializationOptions; 56 | 57 | /** 58 | * @description Set an ISA header on this interchange. 59 | * @param {string[]} elements - An array of elements for an ISA header. 60 | */ 61 | setHeader(elements: string[]): void { 62 | this.header = new X12Segment(ISASegmentHeader.tag, this.options); 63 | 64 | this.header.setElements(elements); 65 | 66 | this._setTrailer(); 67 | } 68 | 69 | /** 70 | * @description Add a functional group to this interchange. 71 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 72 | * @returns {X12FunctionalGroup} The functional group added to this interchange. 73 | */ 74 | addFunctionalGroup(options?: X12SerializationOptions): X12FunctionalGroup { 75 | options = options !== undefined 76 | ? defaultSerializationOptions(options) 77 | : this.options; 78 | 79 | const functionalGroup = new X12FunctionalGroup(options); 80 | 81 | this.functionalGroups.push(functionalGroup); 82 | 83 | this.trailer.replaceElement(`${this.functionalGroups.length}`, 1); 84 | 85 | return functionalGroup; 86 | } 87 | 88 | /** 89 | * @description Serialize interchange to EDI string. 90 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 91 | * @returns {string} This interchange converted to an EDI string. 92 | */ 93 | toString(options?: X12SerializationOptions): string { 94 | options = options !== undefined 95 | ? defaultSerializationOptions(options) 96 | : this.options; 97 | 98 | let edi = this.header.toString(options); 99 | 100 | if (options.format) { 101 | edi += options.endOfLine; 102 | } 103 | 104 | for (let i = 0; i < this.functionalGroups.length; i++) { 105 | edi += this.functionalGroups[i].toString(options); 106 | 107 | if (options.format) { 108 | edi += options.endOfLine; 109 | } 110 | } 111 | 112 | edi += this.trailer.toString(options); 113 | 114 | return edi; 115 | } 116 | 117 | /** 118 | * @description Serialize interchange to JS EDI Notation object. 119 | * @returns {JSEDINotation} This interchange converted to JS EDI Notation object. 120 | */ 121 | toJSEDINotation(): JSEDINotation { 122 | const jsen = new JSEDINotation( 123 | this.header.elements.map((x) => x.value.trim()), 124 | this.options, 125 | ); 126 | 127 | this.functionalGroups.forEach((functionalGroup) => { 128 | const jsenFunctionalGroup = jsen.addFunctionalGroup( 129 | functionalGroup.header.elements.map((x) => x.value), 130 | ); 131 | 132 | functionalGroup.transactions.forEach((transaction) => { 133 | const jsenTransaction = jsenFunctionalGroup.addTransaction( 134 | transaction.header.elements.map((x) => x.value), 135 | ); 136 | 137 | transaction.segments.forEach((segment) => { 138 | jsenTransaction.addSegment( 139 | segment.tag, 140 | segment.elements.map((x) => x.value), 141 | ); 142 | }); 143 | }); 144 | }); 145 | 146 | return jsen; 147 | } 148 | 149 | /** 150 | * @description Serialize interchange to JSON object. 151 | * @returns {object} This interchange converted to an object. 152 | */ 153 | toJSON(): object { 154 | return this.toJSEDINotation() as object; 155 | } 156 | 157 | /** 158 | * @private 159 | * @description Set an ISA trailer on this interchange. 160 | */ 161 | private _setTrailer(): void { 162 | this.trailer = new X12Segment(ISASegmentHeader.trailer, this.options); 163 | 164 | this.trailer.setElements([ 165 | `${this.functionalGroups.length}`, 166 | this.header.valueOf(13) ?? "", 167 | ]); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/X12QueryEngine.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | "use strict"; 3 | 4 | import { QuerySyntaxError } from "./Errors.ts"; 5 | import { X12Parser } from "./X12Parser.ts"; 6 | import { X12Interchange } from "./X12Interchange.ts"; 7 | import { X12FunctionalGroup } from "./X12FunctionalGroup.ts"; 8 | import { X12Transaction } from "./X12Transaction.ts"; 9 | import { X12Segment } from "./X12Segment.ts"; 10 | import { X12Element } from "./X12Element.ts"; 11 | 12 | export type X12QueryMode = "strict" | "loose"; 13 | 14 | export class X12QueryEngine { 15 | /** 16 | * @description Factory for querying EDI using the node-x12 object model. 17 | * @param {X12Parser|boolean} [parser] - Pass an external parser or set the strictness of the internal parser. 18 | * @param {'strict'|'loose'} [mode='strict'] - Sets the mode of the query engine, defaults to classic 'strict'; adds new behavior of 'loose', which will return an empty value for a missing element so long as the segment exists. 19 | */ 20 | constructor( 21 | parser: X12Parser | boolean = true, 22 | mode: X12QueryMode = "strict", 23 | ) { 24 | this._parser = typeof parser === "boolean" ? new X12Parser(parser) : parser; 25 | this._mode = mode; 26 | } 27 | 28 | private readonly _parser: X12Parser; 29 | private readonly _mode: X12QueryMode; 30 | private readonly _forEachPattern: RegExp = /FOREACH\([A-Z0-9]{2,3}\)=>.+/g; 31 | private readonly _concatPattern: RegExp = /CONCAT\(.+,.+\)=>.+/g; 32 | 33 | /** 34 | * @description Query all references in an EDI document. 35 | * @param {string|X12Interchange} rawEdi - An ASCII or UTF8 string of EDI to parse, or an interchange. 36 | * @param {string} reference - The query string to resolve. 37 | * @param {string} [defaultValue=null] - A default value to return if result not found. 38 | * @returns {X12QueryResult[]} An array of results from the EDI document. 39 | */ 40 | query( 41 | rawEdi: string | X12Interchange, 42 | reference: string, 43 | defaultValue: string | null = null, 44 | ): X12QueryResult[] { 45 | const interchange = typeof rawEdi === "string" 46 | ? (this._parser.parse(rawEdi) as X12Interchange) 47 | : rawEdi; 48 | 49 | const forEachMatch = reference.match(this._forEachPattern); // ex. FOREACH(LX)=>MAN02 50 | 51 | if (forEachMatch !== null) { 52 | reference = this._evaluateForEachQueryPart(forEachMatch[0]); 53 | } 54 | 55 | const concathMatch = reference.match(this._concatPattern); // ex. CONCAT(MAN01,-)=>MAN02 56 | let concat: any; 57 | 58 | if (concathMatch !== null) { 59 | concat = this._evaluateConcatQueryPart(interchange, concathMatch[0]); 60 | reference = concat.query; 61 | } 62 | 63 | const hlPathMatch = reference.match(/HL\+(\w\+?)+[+-]/g); // ex. HL+O+P+I 64 | const segPathMatch = reference.match(/((?(); 71 | 72 | for (const group of interchange.functionalGroups) { 73 | for (const txn of group.transactions) { 74 | let segments = txn.segments; 75 | 76 | if (hlPathMatch !== null) { 77 | segments = this._evaluateHLQueryPart(txn, hlPathMatch[0]); 78 | } 79 | 80 | if (segPathMatch !== null) { 81 | segments = this._evaluateSegmentPathQueryPart( 82 | segments, 83 | segPathMatch[0], 84 | ); 85 | } 86 | 87 | if (elmRefMatch === null) { 88 | throw new QuerySyntaxError( 89 | "Element reference queries must contain an element reference!", 90 | ); 91 | } 92 | 93 | const txnResults = this._evaluateElementReferenceQueryPart( 94 | interchange, 95 | group, 96 | txn, 97 | ([] as X12Segment[]).concat(segments, [ 98 | interchange.header, 99 | group.header, 100 | txn.header, 101 | txn.trailer, 102 | group.trailer, 103 | interchange.trailer, 104 | ]), 105 | elmRefMatch[0], 106 | qualMatch as string[], 107 | defaultValue, 108 | ); 109 | 110 | txnResults.forEach((res) => { 111 | if (concat !== undefined) { 112 | res.value = `${concat.value}${concat.separator}${res.value}`; 113 | } 114 | 115 | results.push(res); 116 | }); 117 | } 118 | } 119 | 120 | return results; 121 | } 122 | 123 | /** 124 | * @description Query all references in an EDI document and return the first result. 125 | * @param {string|X12Interchange} rawEdi - An ASCII or UTF8 string of EDI to parse, or an interchange. 126 | * @param {string} reference - The query string to resolve. 127 | * @param {string} [defaultValue=null] - A default value to return if result not found. 128 | * @returns {X12QueryResult} A result from the EDI document. 129 | */ 130 | querySingle( 131 | rawEdi: string | X12Interchange, 132 | reference: string, 133 | _defaultValue: string | null = null, 134 | ): X12QueryResult | null { 135 | const results = this.query(rawEdi, reference); 136 | 137 | if (reference.match(this._forEachPattern) !== null) { 138 | const values = results.map((result) => result.value); 139 | 140 | if (values.length !== 0) { 141 | results[0].value = null; 142 | results[0].values = values; 143 | } 144 | } 145 | 146 | return results.length === 0 ? null : results[0]; 147 | } 148 | 149 | private _getMacroParts(macroQuery: string): any { 150 | const macroPart = macroQuery.substr(0, macroQuery.indexOf("=>")); 151 | const queryPart = macroQuery.substr(macroQuery.indexOf("=>") + 2); 152 | const parameters = macroPart.substr( 153 | macroPart.indexOf("(") + 1, 154 | macroPart.length - macroPart.indexOf("(") - 2, 155 | ); 156 | 157 | return { 158 | macroPart, 159 | queryPart, 160 | parameters, 161 | }; 162 | } 163 | 164 | private _evaluateForEachQueryPart(forEachSegment: string): string { 165 | const { queryPart, parameters } = this._getMacroParts(forEachSegment); 166 | 167 | return `${parameters}-${queryPart}`; 168 | } 169 | 170 | private _evaluateConcatQueryPart( 171 | interchange: X12Interchange, 172 | concatSegment: string, 173 | ): any { 174 | const { queryPart, parameters } = this._getMacroParts(concatSegment); 175 | 176 | let value = ""; 177 | 178 | const expandedParams = parameters.split(","); 179 | 180 | if (expandedParams.length === 3) { 181 | expandedParams[1] = ","; 182 | } 183 | 184 | const result = this.querySingle(interchange, expandedParams[0]); 185 | 186 | if (result !== null) { 187 | if (result.value !== null && result.value !== undefined) { 188 | value = result.value; 189 | } else if (Array.isArray(result.values)) { 190 | value = result.values.join(expandedParams[1]); 191 | } 192 | } 193 | 194 | return { 195 | value, 196 | separator: expandedParams[1], 197 | query: queryPart, 198 | }; 199 | } 200 | 201 | private _evaluateHLQueryPart( 202 | transaction: X12Transaction, 203 | hlPath: string, 204 | ): X12Segment[] { 205 | let qualified = false; 206 | const pathParts = hlPath 207 | .replace("-", "") 208 | .split("+") 209 | .filter((value) => { 210 | return value !== "HL" && value !== "" && value !== null; 211 | }); 212 | const matches = new Array(); 213 | 214 | let lastParentIndex = -1; 215 | 216 | for (let i = 0, j = 0; i < transaction.segments.length; i++) { 217 | const segment = transaction.segments[i]; 218 | 219 | if (qualified && segment.tag === "HL") { 220 | const parentIndex = parseInt(segment.valueOf(2, "-1") ?? "-1"); 221 | 222 | if (parentIndex !== lastParentIndex) { 223 | j = 0; 224 | qualified = false; 225 | } 226 | } 227 | 228 | if ( 229 | !qualified && transaction.segments[i].tag === "HL" && 230 | transaction.segments[i].valueOf(3) === pathParts[j] 231 | ) { 232 | lastParentIndex = parseInt(segment.valueOf(2, "-1") ?? "-1"); 233 | j++; 234 | 235 | if (j === pathParts.length) { 236 | qualified = true; 237 | } 238 | } 239 | 240 | if (qualified) { 241 | matches.push(transaction.segments[i]); 242 | } 243 | } 244 | 245 | return matches; 246 | } 247 | 248 | private _evaluateSegmentPathQueryPart( 249 | segments: X12Segment[], 250 | segmentPath: string, 251 | ): X12Segment[] { 252 | let qualified = false; 253 | const pathParts = segmentPath.split("-").filter((value) => { 254 | // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 255 | return !!value; 256 | }); 257 | const matches = new Array(); 258 | 259 | for (let i = 0, j = 0; i < segments.length; i++) { 260 | if ( 261 | qualified && 262 | (segments[i].tag === "HL" || pathParts.indexOf(segments[i].tag) > -1) 263 | ) { 264 | j = 0; 265 | qualified = false; 266 | } 267 | 268 | if (!qualified && segments[i].tag === pathParts[j]) { 269 | j++; 270 | 271 | if (j === pathParts.length) { 272 | qualified = true; 273 | } 274 | } 275 | 276 | if (qualified) { 277 | matches.push(segments[i]); 278 | } 279 | } 280 | 281 | return matches; 282 | } 283 | 284 | private _evaluateElementReferenceQueryPart( 285 | interchange: X12Interchange, 286 | functionalGroup: X12FunctionalGroup, 287 | transaction: X12Transaction, 288 | segments: X12Segment[], 289 | elementReference: string, 290 | qualifiers: string[], 291 | defaultValue: string | null = null, 292 | ): X12QueryResult[] { 293 | const reference = elementReference.replace(":", ""); 294 | const tag = reference.substr(0, reference.length - 2); 295 | const pos = reference.substr(reference.length - 2, 2); 296 | const posint = parseInt(pos); 297 | 298 | const results = new Array(); 299 | 300 | for (const segment of segments) { 301 | if (segment === null || segment === undefined) { 302 | continue; 303 | } 304 | 305 | if (segment.tag !== tag) { 306 | continue; 307 | } 308 | 309 | const value = segment.valueOf(posint, defaultValue ?? undefined); 310 | 311 | if (this._testQualifiers(transaction, segment, qualifiers)) { 312 | if ((typeof value !== "undefined" && value !== null)) { 313 | results.push( 314 | new X12QueryResult( 315 | interchange, 316 | functionalGroup, 317 | transaction, 318 | segment, 319 | segment.elements[posint - 1], 320 | value, 321 | ), 322 | ); 323 | } else if (this._mode === "loose") { 324 | results.push( 325 | new X12QueryResult( 326 | interchange, 327 | functionalGroup, 328 | transaction, 329 | segment, 330 | new X12Element(), 331 | undefined, 332 | ), 333 | ); 334 | } 335 | } 336 | } 337 | 338 | return results; 339 | } 340 | 341 | private _testQualifiers( 342 | transaction: X12Transaction, 343 | segment: X12Segment, 344 | qualifiers: string[], 345 | ): boolean { 346 | if (qualifiers === undefined || qualifiers === null) { 347 | return true; 348 | } 349 | 350 | for (const qualifierValue of qualifiers) { 351 | const qualifier = qualifierValue.substr(1); 352 | const elementReference = qualifier.substring(0, qualifier.indexOf("[")); 353 | const elementValue = qualifier.substring( 354 | qualifier.indexOf("[") + 2, 355 | qualifier.lastIndexOf("]") - 1, 356 | ); 357 | const tag = elementReference.substr(0, elementReference.length - 2); 358 | const pos = elementReference.substr(elementReference.length - 2, 2); 359 | const posint = parseInt(pos); 360 | 361 | for (let j = transaction.segments.indexOf(segment); j > -1; j--) { 362 | const seg = transaction.segments[j]; 363 | const value = seg.valueOf(posint); 364 | 365 | if ( 366 | seg.tag === tag && seg.tag === segment.tag && value !== elementValue 367 | ) { 368 | return false; 369 | } else if (seg.tag === tag && value === elementValue) { 370 | break; 371 | } 372 | 373 | if (j === 0) { 374 | return false; 375 | } 376 | } 377 | } 378 | 379 | return true; 380 | } 381 | } 382 | 383 | /** 384 | * @description A result as resolved by the query engine. 385 | * @typedef {object} X12QueryResult 386 | * @property {X12Interchange} interchange 387 | * @property {X12FunctionalGroup} functionalGroup 388 | * @property {X12Transaction} transaction 389 | * @property {X12Segment} segment 390 | * @property {X12Element} element 391 | * @property {string} [value=null] 392 | * @property {Array} [values=[]] 393 | */ 394 | 395 | export class X12QueryResult { 396 | constructor( 397 | interchange?: X12Interchange, 398 | functionalGroup?: X12FunctionalGroup, 399 | transaction?: X12Transaction, 400 | segment?: X12Segment, 401 | element?: X12Element, 402 | value?: string, 403 | ) { 404 | this.interchange = interchange; 405 | this.functionalGroup = functionalGroup; 406 | this.transaction = transaction; 407 | this.segment = segment; 408 | this.element = element; 409 | this.value = value === null || value === undefined 410 | ? element?.value ?? null 411 | : value; 412 | this.values = new Array(); 413 | } 414 | 415 | interchange?: X12Interchange; 416 | functionalGroup?: X12FunctionalGroup; 417 | transaction?: X12Transaction; 418 | segment?: X12Segment; 419 | element?: X12Element; 420 | value: string | null; 421 | values: Array; 422 | } 423 | -------------------------------------------------------------------------------- /src/X12Segment.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file ban-types no-explicit-any 2 | "use strict"; 3 | 4 | import { JSEDISegment } from "./JSEDINotation.ts"; 5 | import { Range } from "./Positioning.ts"; 6 | import { X12Element } from "./X12Element.ts"; 7 | import { 8 | defaultSerializationOptions, 9 | X12SerializationOptions, 10 | } from "./X12SerializationOptions.ts"; 11 | import { ISASegmentHeader } from "./X12SegmentHeader.ts"; 12 | import { GeneratorError } from "./Errors.ts"; 13 | 14 | export class X12Segment { 15 | /** 16 | * @description Create a segment. 17 | * @param {string} tag - The tag for this segment. 18 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 19 | */ 20 | constructor(tag: string = "", options?: X12SerializationOptions) { 21 | this.tag = tag; 22 | this.elements = new Array(); 23 | this.range = new Range(); 24 | this.options = defaultSerializationOptions(options); 25 | } 26 | 27 | tag: string; 28 | elements: X12Element[]; 29 | range: Range; 30 | options: X12SerializationOptions; 31 | 32 | /** 33 | * @description Set the tag name for the segment if not provided when constructed. 34 | * @param {string} tag - The tag for this segment. 35 | */ 36 | setTag(tag: string): void { 37 | this.tag = tag; 38 | } 39 | 40 | /** 41 | * @description Set the elements of this segment. 42 | * @param {string[]} values - An array of element values. 43 | * @returns {X12Segment} The current instance of X12Segment. 44 | */ 45 | setElements(values: string[]): X12Segment { 46 | this._formatValues(values); 47 | this.elements = new Array(); 48 | values.forEach((value) => { 49 | this.elements.push(new X12Element(value)); 50 | }); 51 | 52 | return this; 53 | } 54 | 55 | /** 56 | * @description Add an element to this segment. 57 | * @param {string} value - A string value. 58 | * @returns {X12Element} The element that was added to this segment. 59 | */ 60 | addElement(value = ""): X12Element { 61 | const element = new X12Element(value); 62 | 63 | this.elements.push(element); 64 | 65 | return element; 66 | } 67 | 68 | /** 69 | * @description Replace an element at a position in the segment. 70 | * @param {string} value - A string value. 71 | * @param {number} segmentPosition - A 1-based number indicating the position in the segment. 72 | * @returns {X12Element} The new element if successful, or a null if failed. 73 | */ 74 | replaceElement(value: string, segmentPosition: number): X12Element | null { 75 | const index = segmentPosition - 1; 76 | 77 | if (this.elements.length <= index) { 78 | return null; 79 | } else { 80 | this.elements[index] = new X12Element(value); 81 | } 82 | 83 | return this.elements[index]; 84 | } 85 | 86 | /** 87 | * @description Insert an element at a position in the segment. 88 | * @param {string} value - A string value. 89 | * @param {number} segmentPosition - A 1-based number indicating the position in the segment. 90 | * @returns {boolean} True if successful, or false if failed. 91 | */ 92 | insertElement(value = "", segmentPosition = 1): boolean { 93 | const index = segmentPosition - 1; 94 | 95 | if (this.elements.length <= index) { 96 | return false; 97 | } 98 | 99 | return this.elements.splice(index, 0, new X12Element(value)).length === 1; 100 | } 101 | 102 | /** 103 | * @description Remove an element at a position in the segment. 104 | * @param {number} segmentPosition - A 1-based number indicating the position in the segment. 105 | * @returns {boolean} True if successful. 106 | */ 107 | removeElement(segmentPosition: number): boolean { 108 | const index = segmentPosition - 1; 109 | 110 | if (this.elements.length <= index) { 111 | return false; 112 | } 113 | 114 | return this.elements.splice(index, 1).length === 1; 115 | } 116 | 117 | /** 118 | * @description Get the value of an element in this segment. 119 | * @param {number} segmentPosition - A 1-based number indicating the position in the segment. 120 | * @param {string} [defaultValue] - A default value to return if there is no element found. 121 | * @returns {string} If no element is at this position, null or the default value will be returned. 122 | */ 123 | valueOf(segmentPosition: number, defaultValue?: string): string | null { 124 | const index = segmentPosition - 1; 125 | 126 | if (this.elements.length <= index) { 127 | return defaultValue === undefined ? null : defaultValue; 128 | } 129 | 130 | return this.elements[index].value === undefined 131 | ? defaultValue === undefined ? null : defaultValue 132 | : this.elements[index].value; 133 | } 134 | 135 | /** 136 | * @description Serialize segment to EDI string. 137 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 138 | * @returns {string} This segment converted to an EDI string. 139 | */ 140 | toString(options?: X12SerializationOptions): string { 141 | options = options !== undefined 142 | ? defaultSerializationOptions(options) 143 | : this.options; 144 | 145 | let edi = this.tag; 146 | 147 | for (let i = 0; i < this.elements.length; i++) { 148 | edi += options.elementDelimiter; 149 | if ((this.tag === "ISA" && i === 12) || (this.tag === "IEA" && i === 1)) { 150 | edi += String.prototype.padStart.call( 151 | this.elements[i].value, 152 | 9, 153 | "0", 154 | ) as string; 155 | } else { 156 | edi += this.elements[i].value; 157 | } 158 | } 159 | 160 | edi += options.segmentTerminator; 161 | 162 | return edi; 163 | } 164 | 165 | /** 166 | * @description Serialize transaction set to JSON object. 167 | * @returns {object} This segment converted to an object. 168 | */ 169 | toJSON(): object { 170 | return new JSEDISegment( 171 | this.tag, 172 | this.elements.map((x) => x.value), 173 | ) as object; 174 | } 175 | 176 | /** 177 | * @private 178 | * @description Check to see if segment is predefined. 179 | * @returns {boolean} True if segment is predefined. 180 | */ 181 | private _checkSupportedSegment(): boolean { 182 | return ( 183 | (this.options.segmentHeaders?.findIndex((sh) => { 184 | return sh.tag === this.tag; 185 | }) ?? -1) > -1 186 | ); 187 | } 188 | 189 | /** 190 | * @private 191 | * @description Get the definition of this segment. 192 | * @returns {object} The definition of this segment. 193 | */ 194 | private _getX12Enumerable(): any { 195 | const match = this.options.segmentHeaders?.find((sh) => { 196 | return sh.tag === this.tag; 197 | }); 198 | 199 | if (match !== undefined) { 200 | return match.layout; 201 | } else { 202 | throw Error( 203 | `Unable to find segment header for tag '${this.tag}' even though it should be supported.`, 204 | ); 205 | } 206 | } 207 | 208 | /** 209 | * @private 210 | * @description Format and validate the element values according the segment definition. 211 | * @param {string[]} values - An array of element values. 212 | */ 213 | private _formatValues(values: string[]): void { 214 | if (this._checkSupportedSegment()) { 215 | const enumerable = this._getX12Enumerable(); 216 | 217 | if ( 218 | this.tag === ISASegmentHeader.tag && 219 | (this.options as any).subElementDelimiter.length === 1 220 | ) { 221 | values[15] = (this.options as any).subElementDelimiter; 222 | } 223 | 224 | if ( 225 | values.length === enumerable.COUNT || 226 | values.length === enumerable.COUNT_MIN 227 | ) { 228 | for (let i = 0; i < values.length; i++) { 229 | const name = `${this.tag}${ 230 | String.prototype.padStart.call(i + 1, 2, "0") 231 | }`; 232 | const max = enumerable[name]; 233 | const min = enumerable[`${name}_MIN`] === undefined 234 | ? 0 235 | : enumerable[`${name}_MIN`]; 236 | 237 | values[i] = `${values[i]}`; 238 | 239 | if (values[i].length > max && values[i].length !== 0) { 240 | throw new GeneratorError( 241 | `Segment element "${name}" with value of "${ 242 | values[i] 243 | }" exceeds maximum of ${max} characters.`, 244 | ); 245 | } 246 | 247 | if (values[i].length < min && values[i].length !== 0) { 248 | throw new GeneratorError( 249 | `Segment element "${name}" with value of "${ 250 | values[i] 251 | }" does not meet minimum of ${min} characters.`, 252 | ); 253 | } 254 | 255 | if ( 256 | (enumerable.PADDING as boolean) && 257 | ((values[i].length < max && values[i].length > min) || 258 | values[i].length === 0) 259 | ) { 260 | if (name === "ISA13") { 261 | values[i] = String.prototype.padStart.call(values[i], max, "0"); 262 | } else { 263 | values[i] = String.prototype.padEnd.call(values[i], max, " "); 264 | } 265 | } 266 | } 267 | } else { 268 | throw new GeneratorError( 269 | typeof enumerable.COUNT_MIN === "number" 270 | ? `Segment "${this.tag}" with ${values.length} elements does not meet the required count of min ${enumerable.COUNT_MIN} or max ${enumerable.COUNT}.` 271 | : `Segment "${this.tag}" with ${values.length} elements does not meet the required count of ${enumerable.COUNT}.`, 272 | ); 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/X12SegmentHeader.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | "use strict"; 3 | 4 | export interface X12SegmentHeader { 5 | tag: string; 6 | trailer?: string; 7 | layout: any; 8 | } 9 | 10 | export const ISASegmentHeader: X12SegmentHeader = { 11 | tag: "ISA", 12 | trailer: "IEA", 13 | layout: { 14 | ISA01: 2, 15 | ISA02: 10, 16 | ISA03: 2, 17 | ISA04: 10, 18 | ISA05: 2, 19 | ISA06: 15, 20 | ISA07: 2, 21 | ISA08: 15, 22 | ISA09: 6, 23 | ISA10: 4, 24 | ISA11: 1, 25 | ISA12: 5, 26 | ISA13: 9, 27 | ISA14: 1, 28 | ISA15: 1, 29 | ISA16: 1, 30 | COUNT: 16, 31 | PADDING: true, 32 | }, 33 | }; 34 | 35 | export const GSSegmentHeader: X12SegmentHeader = { 36 | tag: "GS", 37 | trailer: "GE", 38 | layout: { 39 | GS01: 2, 40 | GS02: 15, 41 | GS02_MIN: 2, 42 | GS03: 15, 43 | GS03_MIN: 2, 44 | GS04: 8, 45 | GS05: 8, 46 | GS05_MIN: 4, 47 | GS06: 9, 48 | GS06_MIN: 1, 49 | GS07: 2, 50 | GS07_MIN: 1, 51 | GS08: 12, 52 | GS08_MIN: 1, 53 | COUNT: 8, 54 | PADDING: false, 55 | }, 56 | }; 57 | 58 | export const STSegmentHeader: X12SegmentHeader = { 59 | tag: "ST", 60 | trailer: "SE", 61 | layout: { 62 | ST01: 3, 63 | ST02: 9, 64 | ST02_MIN: 4, 65 | ST03: 35, 66 | ST03_MIN: 1, 67 | COUNT: 3, 68 | COUNT_MIN: 2, 69 | PADDING: false, 70 | }, 71 | }; 72 | -------------------------------------------------------------------------------- /src/X12SerializationOptions.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { 3 | GSSegmentHeader, 4 | ISASegmentHeader, 5 | STSegmentHeader, 6 | X12SegmentHeader, 7 | } from "./X12SegmentHeader.ts"; 8 | 9 | export type TxEngine = "liquidjs" | "internal"; 10 | 11 | /** 12 | * @description Options for serializing to and from EDI. 13 | * @typedef {object} X12SerializationOptions 14 | * @property {string} [elementDelimiter=*] The separator for elements within an EDI segment. 15 | * @property {string} [endOfLine=\n] The end of line charactor for formatting. 16 | * @property {boolean} [format=false] A flag to set formatting when serializing back to EDI. 17 | * @property {string} [segmentTerminator=~] The terminator for each EDI segment. 18 | * @property {string} [subElementDelimiter=>] A sub-element separator; typically found at element 16 of the ISA header segment. 19 | * @property {X12SegmentHeader[]} [segmentHeaders] Default array of known, pre-defined segment headers. 20 | * @property {'liquidjs'|'internal'} [txEngine='internal'] The engine to use for macros when mapping transaction sets from objects. 21 | */ 22 | 23 | /** 24 | * Class instance wrapper for serialization options. 25 | */ 26 | export class X12SerializationOptions { 27 | constructor(options: X12SerializationOptions = {}) { 28 | this.elementDelimiter = options.elementDelimiter === undefined 29 | ? "*" 30 | : options.elementDelimiter; 31 | this.endOfLine = options.endOfLine === undefined ? "\n" : options.endOfLine; 32 | this.format = options.format === undefined ? false : options.format; 33 | this.segmentTerminator = options.segmentTerminator === undefined 34 | ? "~" 35 | : options.segmentTerminator; 36 | this.subElementDelimiter = options.subElementDelimiter === undefined 37 | ? ">" 38 | : options.subElementDelimiter; 39 | this.repetitionDelimiter = options.repetitionDelimiter === undefined 40 | ? "^" 41 | : options.repetitionDelimiter; 42 | this.segmentHeaders = options.segmentHeaders === undefined 43 | ? [GSSegmentHeader, ISASegmentHeader, STSegmentHeader] 44 | : options.segmentHeaders; 45 | this.txEngine = options.txEngine === undefined 46 | ? "internal" 47 | : options.txEngine; 48 | 49 | if (this.segmentTerminator === "\n") { 50 | this.endOfLine = ""; 51 | } 52 | } 53 | 54 | elementDelimiter?: string; 55 | endOfLine?: string; 56 | format?: boolean; 57 | segmentTerminator?: string; 58 | subElementDelimiter?: string; 59 | repetitionDelimiter?: string; 60 | segmentHeaders?: X12SegmentHeader[]; 61 | txEngine?: TxEngine; 62 | } 63 | 64 | /** 65 | * @description Set default values for any missing X12SerializationOptions in an options object. 66 | * @param {X12SerializationOptions} [options] - Options for serializing to and from EDI. 67 | * @returns {X12SerializationOptions} Serialization options with defaults filled in. 68 | */ 69 | export function defaultSerializationOptions( 70 | options?: X12SerializationOptions, 71 | ): X12SerializationOptions { 72 | return new X12SerializationOptions(options); 73 | } 74 | -------------------------------------------------------------------------------- /src/X12Transaction.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any ban-types 2 | "use strict"; 3 | 4 | import { JSEDITransaction } from "./JSEDINotation.ts"; 5 | import { X12Segment } from "./X12Segment.ts"; 6 | import { STSegmentHeader } from "./X12SegmentHeader.ts"; 7 | import { X12TransactionMap } from "./X12TransactionMap.ts"; 8 | import { 9 | defaultSerializationOptions, 10 | X12SerializationOptions, 11 | } from "./X12SerializationOptions.ts"; 12 | import type { X12QueryMode } from "./X12QueryEngine.ts"; 13 | 14 | export class X12Transaction { 15 | /** 16 | * @description Create a transaction set. 17 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 18 | */ 19 | constructor(options?: X12SerializationOptions) { 20 | this.segments = new Array(); 21 | this.options = defaultSerializationOptions(options); 22 | } 23 | 24 | header!: X12Segment; 25 | trailer!: X12Segment; 26 | 27 | segments: X12Segment[]; 28 | 29 | options: X12SerializationOptions; 30 | 31 | /** 32 | * @description Set a ST header on this transaction set. 33 | * @param {string[]} elements - An array of elements for a ST header. 34 | */ 35 | setHeader(elements: string[]): void { 36 | this.header = new X12Segment(STSegmentHeader.tag, this.options); 37 | 38 | this.header.setElements(elements); 39 | 40 | this._setTrailer(); 41 | } 42 | 43 | /** 44 | * @description Add a segment to this transaction set. 45 | * @param {string} tag - The tag for this segment. 46 | * @param {string[]} elements - An array of elements for this segment. 47 | * @returns {X12Segment} The segment added to this transaction set. 48 | */ 49 | addSegment(tag: string, elements: string[]): X12Segment { 50 | const segment = new X12Segment(tag, this.options); 51 | 52 | segment.setElements(elements); 53 | 54 | this.segments.push(segment); 55 | 56 | this.trailer.replaceElement(`${this.segments.length + 2}`, 1); 57 | 58 | return segment; 59 | } 60 | 61 | /** 62 | * @description Map data from a javascript object to this transaction set. Will use the txEngine property for Liquid support from `this.options` if available. 63 | * @param {object} input - The input object to create the transaction from. 64 | * @param {object} map - The javascript object containing keys and querys to resolve. 65 | * @param {object} [macro] - A macro object to add or override methods for the macro directive; properties 'header' and 'segments' are reserved words. 66 | */ 67 | fromObject(input: any, map: any, macro?: any): void { 68 | const mapper = new X12TransactionMap(map, this, this.options.txEngine); 69 | 70 | mapper.fromObject(input, macro); 71 | } 72 | 73 | /** 74 | * @description Map data from a transaction set to a javascript object. 75 | * @param {object} map - The javascript object containing keys and querys to resolve. 76 | * @param {Function|'strict'|'loose'} [helper] - A helper function which will be executed on every resolved query value, or the mode for the query engine. 77 | * @param {'strict'|'loose'} [mode] - The mode for the query engine when performing the transform. 78 | * @returns {object} An object containing resolved values mapped to object keys. 79 | */ 80 | toObject( 81 | map: object, 82 | helper?: Function | X12QueryMode, 83 | mode?: X12QueryMode, 84 | ): object { 85 | const mapper = new X12TransactionMap(map, this, helper as Function, mode); 86 | 87 | return mapper.toObject(); 88 | } 89 | 90 | /** 91 | * @description Serialize transaction set to EDI string. 92 | * @param {X12SerializationOptions} [options] - Options for serializing back to EDI. 93 | * @returns {string} This transaction set converted to an EDI string. 94 | */ 95 | toString(options?: X12SerializationOptions): string { 96 | options = options !== undefined 97 | ? defaultSerializationOptions(options) 98 | : this.options; 99 | 100 | let edi = this.header.toString(options); 101 | 102 | if (options.format) { 103 | edi += options.endOfLine; 104 | } 105 | 106 | for (const segment of this.segments) { 107 | edi += segment.toString(options); 108 | 109 | if (options.format) { 110 | edi += options.endOfLine; 111 | } 112 | } 113 | 114 | edi += this.trailer.toString(options); 115 | 116 | return edi; 117 | } 118 | 119 | /** 120 | * @description Serialize transaction set to JSON object. 121 | * @returns {object} This transaction set converted to an object. 122 | */ 123 | toJSON(): object { 124 | const jsen = new JSEDITransaction(this.header.elements.map((x) => x.value)); 125 | 126 | this.segments.forEach((segment) => { 127 | jsen.addSegment( 128 | segment.tag, 129 | segment.elements.map((x) => x.value), 130 | ); 131 | }); 132 | 133 | return jsen as object; 134 | } 135 | 136 | /** 137 | * @private 138 | * @description Set a SE trailer on this transaction set. 139 | */ 140 | private _setTrailer(): void { 141 | this.trailer = new X12Segment(STSegmentHeader.trailer, this.options); 142 | 143 | this.trailer.setElements([ 144 | `${this.segments.length + 2}`, 145 | this.header?.valueOf(2) ?? "", 146 | ]); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/X12ValidationEngine/Interfaces.ts: -------------------------------------------------------------------------------- 1 | import { X12Segment } from "../X12Segment.ts"; 2 | import { X12SerializationOptions } from "../X12SerializationOptions.ts"; 3 | 4 | export type GroupResponseCode = "A" | "E" | "P" | "R" | "M" | "W" | "X"; 5 | 6 | export type ValidationType = 7 | | "element" 8 | | "segment" 9 | | "transaction" 10 | | "group" 11 | | "interchange"; 12 | 13 | export interface ValidationEngineOptions { 14 | acknowledgement?: { 15 | isa: X12Segment; 16 | gs: X12Segment; 17 | options?: X12SerializationOptions; 18 | handling?: "reject" | "note_errors" | "allow_partial"; 19 | }; 20 | throwError?: boolean; 21 | // deno-lint-ignore no-explicit-any 22 | ackMap?: any; 23 | } 24 | 25 | export interface ValidationReport { 26 | interchange?: { 27 | header?: ValidationReport; 28 | trailer?: ValidationReport; 29 | }; 30 | group?: { 31 | groupId: string; 32 | groupNumber: number; 33 | groupResponse?: GroupResponseCode; 34 | transactionCount: number; 35 | responseLevel?: "reject" | "note_errors" | "allow_partial"; 36 | errors: ValidationError[]; 37 | }; 38 | transaction?: { 39 | transactionId: string; 40 | transactionNumber: number; 41 | errors: ValidationError[]; 42 | }; 43 | segment?: { 44 | tag: string; 45 | position: number; 46 | errors: ValidationError[]; 47 | }; 48 | groups?: ValidationReport[]; 49 | transactions?: ValidationReport[]; 50 | segments?: ValidationReport[]; 51 | elements?: ValidationError[]; 52 | } 53 | 54 | export interface ValidationError { 55 | description: string; 56 | codeType: ValidationType; 57 | code: string; 58 | position?: number; 59 | dataSample?: string; 60 | } 61 | -------------------------------------------------------------------------------- /src/X12ValidationEngine/X12ValidationEngine.ts: -------------------------------------------------------------------------------- 1 | import { X12Segment } from "../X12Segment.ts"; 2 | import { X12Element } from "../X12Element.ts"; 3 | import { X12Transaction } from "../X12Transaction.ts"; 4 | import { X12FunctionalGroup } from "../X12FunctionalGroup.ts"; 5 | import { X12Interchange } from "../X12Interchange.ts"; 6 | import { X12SerializationOptions } from "../X12SerializationOptions.ts"; 7 | import { 8 | GroupResponseCode, 9 | ValidationEngineOptions, 10 | ValidationReport, 11 | } from "./Interfaces.ts"; 12 | import { 13 | X12ElementRule, 14 | X12GroupRule, 15 | X12InterchangeRule, 16 | X12SegmentRule, 17 | X12TransactionRule, 18 | X12ValidationRule, 19 | } from "./X12ValidationRule.ts"; 20 | 21 | const simpleAckMap = { 22 | header: ["997", "{{ macro | random }}"], 23 | segments: [ 24 | { 25 | tag: "AK1", 26 | elements: ["{{ input.group.groupId }}", "{{ input.group.groupNumber }}"], 27 | }, 28 | { 29 | tag: "AK2", 30 | elements: [ 31 | '{{ input.transactions | map: "transactionId" | in_loop }}', 32 | '{{ input.transactions | map: "transactionNumber" | in_loop }}', 33 | ], 34 | loopStart: true, 35 | loopLength: "{{ input.transactions | size }}", 36 | }, 37 | { 38 | tag: "AK5", 39 | elements: [ 40 | '{% assign len = input.transactions | size %}{% if len > 0 %}{% if input.group.responseLevel == "note_errors" %}E{% else %}R{% endif %}{% endif %}', 41 | '{{ input.transactions | map: "transaction.errors.0.code" }}', 42 | ], 43 | loopEnd: true, 44 | }, 45 | { 46 | tag: "AK9", 47 | elements: [ 48 | "{{ input.group.groupResponse }}", 49 | "{{ input.group.transactionCount }}", 50 | "{{ input.group.transactionCount }}", 51 | "{% assign errors = input.transactions | size %}{{ input.group.transactionCount | minus: errors }}", 52 | ], 53 | }, 54 | ], 55 | }; 56 | 57 | export class ValidationEngineError extends Error { 58 | constructor(message: string, report: ValidationReport) { 59 | super(message); 60 | 61 | Object.setPrototypeOf(this, ValidationEngineError.prototype); 62 | 63 | this.report = report; 64 | } 65 | 66 | report: ValidationReport; 67 | } 68 | 69 | export class X12ValidationEngine { 70 | constructor(options: ValidationEngineOptions = {}) { 71 | const { acknowledgement, throwError, ackMap } = options; 72 | this.pass = true; 73 | this.throwError = false; 74 | this.ackMap = typeof ackMap === "object" ? ackMap : simpleAckMap; 75 | 76 | if (typeof acknowledgement === "object") { 77 | const { isa, gs, options: x12options } = acknowledgement; 78 | 79 | this.setAcknowledgement(isa, gs, { ...x12options, txEngine: "liquidjs" }); 80 | } 81 | 82 | if (throwError) this.throwError = true; 83 | } 84 | 85 | pass: boolean; 86 | report?: ValidationReport; 87 | acknowledgement?: X12Interchange; 88 | hardErrors?: Error[]; 89 | throwError: boolean; 90 | // deno-lint-ignore no-explicit-any 91 | private readonly ackMap: any; 92 | 93 | private setAcknowledgement( 94 | isa?: X12Segment, 95 | gs?: X12Segment, 96 | options?: X12SerializationOptions, 97 | ): void { 98 | if (isa instanceof X12Segment && gs instanceof X12Segment) { 99 | this.acknowledgement = new X12Interchange(options); 100 | 101 | this.acknowledgement.setHeader( 102 | isa.elements.map((element) => element.value), 103 | ); 104 | this.acknowledgement.addFunctionalGroup().setHeader( 105 | gs.elements.map((element) => element.value), 106 | ); 107 | } 108 | } 109 | 110 | assert(actual: X12Element, expected: X12ElementRule): true | ValidationReport; 111 | assert(actual: X12Segment, expected: X12SegmentRule): true | ValidationReport; 112 | assert( 113 | actual: X12Transaction, 114 | expected: X12TransactionRule, 115 | ): true | ValidationReport; 116 | assert( 117 | actual: X12FunctionalGroup, 118 | expected: X12GroupRule, 119 | groupResponse?: GroupResponseCode, 120 | ): true | ValidationReport; 121 | assert( 122 | actual: X12Interchange, 123 | expected: X12InterchangeRule, 124 | groupResponse?: GroupResponseCode, 125 | ): true | ValidationReport; 126 | // deno-lint-ignore no-explicit-any 127 | assert( 128 | actual: any, 129 | expected: X12ValidationRule, 130 | _groupResponse?: GroupResponseCode, 131 | ): true | ValidationReport { 132 | const setReport = (results: true | ValidationReport): void => { 133 | if (results !== true) { 134 | this.pass = false; 135 | this.report = results; 136 | } 137 | }; 138 | const passingReport = function ( 139 | groupId: string, 140 | groupNumber: number, 141 | transactionCount: number, 142 | ): ValidationReport { 143 | return { 144 | group: { 145 | groupId, 146 | groupNumber, 147 | transactionCount, 148 | groupResponse: "A", 149 | errors: [], 150 | }, 151 | transactions: [], 152 | }; 153 | }; 154 | 155 | if ( 156 | actual instanceof X12Interchange && expected instanceof X12InterchangeRule 157 | ) { 158 | const groupId = actual.functionalGroups[0].header.valueOf(1) ?? ""; 159 | const groupNumber = parseFloat( 160 | actual.functionalGroups[0].header.valueOf(6, "0") ?? "0", 161 | ); 162 | const transactionCount = actual.functionalGroups[0].transactions.length; 163 | 164 | setReport(expected.assert?.(actual) ?? {}); 165 | 166 | if (this.pass) { 167 | this.report = { 168 | groups: [passingReport(groupId, groupNumber, transactionCount)], 169 | }; 170 | } 171 | } 172 | 173 | if ( 174 | actual instanceof X12FunctionalGroup && expected instanceof X12GroupRule 175 | ) { 176 | const groupId = actual.header.valueOf(1) ?? ""; 177 | const groupNumber = parseFloat(actual.header.valueOf(6, "0") ?? "0"); 178 | const transactionCount = actual.transactions.length; 179 | 180 | setReport(expected.assert?.(actual, groupNumber) ?? {}); 181 | 182 | if (this.pass) { 183 | this.report = passingReport(groupId, groupNumber, transactionCount); 184 | } 185 | } 186 | 187 | if ( 188 | actual instanceof X12Transaction && expected instanceof X12TransactionRule 189 | ) { 190 | const transactionNumber = parseFloat( 191 | actual.header.valueOf(2, "0") ?? "0", 192 | ); 193 | 194 | setReport(expected.assert?.(actual, transactionNumber) ?? {}); 195 | } 196 | 197 | if (actual instanceof X12Segment && expected instanceof X12SegmentRule) { 198 | setReport(expected.assert?.(actual) ?? {}); 199 | } 200 | 201 | if (actual instanceof X12Element && expected instanceof X12ElementRule) { 202 | setReport(expected.assert?.(actual) ?? {}); 203 | } 204 | 205 | if (this.throwError && !this.pass) { 206 | throw new ValidationEngineError( 207 | "The actual X12 document did not meet the expected validation.", 208 | this.report ?? {}, 209 | ); 210 | } 211 | 212 | return this.pass || (this.report ?? {}); 213 | } 214 | 215 | acknowledge( 216 | isa?: X12Segment, 217 | gs?: X12Segment, 218 | options?: X12SerializationOptions, 219 | ): X12Interchange | undefined { 220 | this.setAcknowledgement(isa, gs, options); 221 | 222 | if ( 223 | this.acknowledgement instanceof X12Interchange && 224 | typeof this.report === "object" && 225 | (Array.isArray(this.report.groups) || 226 | typeof this.report.group === "object") 227 | ) { 228 | this.acknowledgement.functionalGroups[0].addTransaction().fromObject( 229 | { 230 | group: typeof this.report.groups === "object" 231 | ? this.report.groups[0].group 232 | : this.report.group, 233 | }, 234 | this.ackMap, 235 | ); 236 | 237 | return this.acknowledgement; 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/X12ValidationEngine/X12ValidationErrorCode.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | import { ValidationError, ValidationType } from "./Interfaces.ts"; 3 | 4 | // Error codes taken from publicly available documentation 5 | // at https://docs.microsoft.com/en-us/biztalk/core/x12-997-acknowledgment-error-codes 6 | // and at https://support.edifabric.com/hc/en-us/articles/360000380131-X12-997-Acknowledgment-Error-Codes 7 | export const X12ValidationErrorCode: Record< 8 | string, 9 | (...args: any[]) => ValidationError 10 | > = { 11 | element( 12 | code: string, 13 | position?: number, 14 | dataSample?: string, 15 | ): ValidationError { 16 | const codeType = "element"; 17 | let description; 18 | 19 | switch (code) { 20 | case "1": 21 | description = "Mandatory data element missing"; 22 | break; 23 | case "2": 24 | description = "Conditional and required data element missing"; 25 | break; 26 | case "3": // Return this for any elements outside the validation range. 27 | description = "Too many data elements"; 28 | break; 29 | case "4": 30 | description = "The data element is too short"; 31 | break; 32 | case "5": 33 | description = "The data element is too long"; 34 | break; 35 | case "6": 36 | description = "Invalid character in data element"; 37 | break; 38 | case "8": 39 | description = "Invalid date"; 40 | break; 41 | case "9": 42 | description = "Invalid time"; 43 | break; 44 | case "10": 45 | description = "Exclusion condition violated"; 46 | break; 47 | case "11": 48 | description = "Too many repetitions"; 49 | break; 50 | case "12": 51 | description = "Too many components"; 52 | break; 53 | case "7": 54 | default: 55 | description = "Invalid code value"; 56 | code = "7"; 57 | break; 58 | } 59 | 60 | return { 61 | description, 62 | codeType, 63 | code, 64 | position, 65 | dataSample, 66 | }; 67 | }, 68 | 69 | segment(code: string, position?: number): ValidationError { 70 | const codeType = "segment"; 71 | let description; 72 | 73 | switch (code) { 74 | case "1": 75 | description = "Unrecognized segment ID"; 76 | break; 77 | case "2": 78 | description = "Unexpected segment"; 79 | break; 80 | case "3": 81 | description = "Mandatory segment missing"; 82 | break; 83 | case "4": 84 | description = "A loop occurs over maximum times"; 85 | break; 86 | case "5": 87 | description = "Segment exceeds maximum use"; 88 | break; 89 | case "6": 90 | description = "Segment not in a defined transaction set"; 91 | break; 92 | case "7": 93 | description = "Segment not in proper sequence"; 94 | break; 95 | case "8": 96 | default: 97 | description = "The segment has data element errors"; 98 | code = "8"; 99 | break; 100 | } 101 | 102 | return { 103 | description, 104 | codeType, 105 | code, 106 | position, 107 | }; 108 | }, 109 | 110 | transaction(code: string, position?: number): ValidationError { 111 | const codeType = "transaction"; 112 | let description; 113 | 114 | switch (code) { 115 | case "1": 116 | description = "The transaction set not supported"; 117 | break; 118 | case "2": 119 | description = "Transaction set trailer missing"; 120 | break; 121 | case "3": 122 | description = 123 | "The transaction set control number in header and trailer do not match"; 124 | break; 125 | case "4": 126 | description = "Number of included segments does not match actual count"; 127 | break; 128 | case "5": 129 | description = "One or more segments in error"; 130 | break; 131 | case "6": 132 | description = "Missing or invalid transaction set identifier"; 133 | break; 134 | case "7": 135 | default: 136 | description = 137 | "Missing or invalid transaction set control number (a duplicate transaction number may have occurred)"; 138 | code = "7"; 139 | break; 140 | } 141 | 142 | return { 143 | description, 144 | codeType, 145 | code, 146 | position, 147 | }; 148 | }, 149 | 150 | group(code: string, position?: number): ValidationError { 151 | const codeType = "group"; 152 | let description; 153 | 154 | switch (code) { 155 | case "1": 156 | description = "The functional group not supported"; 157 | break; 158 | case "2": 159 | description = "Functional group version not supported"; 160 | break; 161 | case "3": 162 | description = "Functional group trailer missing"; 163 | break; 164 | case "4": 165 | description = 166 | "Group control number in the functional group header and trailer do not agree"; 167 | break; 168 | case "5": 169 | description = 170 | "Number of included transaction sets does not match actual count"; 171 | break; 172 | case "6": 173 | default: 174 | description = 175 | "Group control number violates syntax (a duplicate group control number may have occurred)"; 176 | code = "6"; 177 | break; 178 | } 179 | 180 | return { 181 | description, 182 | codeType, 183 | code, 184 | position, 185 | }; 186 | }, 187 | 188 | acknowledgement( 189 | codeType: ValidationType, 190 | code: string, 191 | position?: number, 192 | ): ValidationError { 193 | let description; 194 | 195 | switch (code) { 196 | case "A": 197 | description = "Accepted"; 198 | break; 199 | case "M": 200 | description = "Rejected, message authentication code (MAC) failed"; 201 | break; 202 | case "P": 203 | description = 204 | "Partially accepted, at least one transaction set was rejected"; 205 | break; 206 | case "R": 207 | description = "Rejected"; 208 | break; 209 | case "W": 210 | description = "Rejected, assurance failed validity tests"; 211 | break; 212 | case "X": 213 | description = 214 | "Rejected, content after decryption could not be analyzed"; 215 | break; 216 | case "E": 217 | default: 218 | description = "Accepted but errors were noted"; 219 | code = "E"; 220 | break; 221 | } 222 | 223 | return { 224 | description, 225 | codeType, 226 | code, 227 | position, 228 | }; 229 | }, 230 | }; 231 | 232 | export function errorLookup( 233 | codeType?: "group", 234 | code?: string, 235 | position?: number, 236 | ): ValidationError; 237 | export function errorLookup( 238 | codeType?: "transaction", 239 | code?: string, 240 | position?: number, 241 | ): ValidationError; 242 | export function errorLookup( 243 | codeType?: "segment", 244 | code?: string, 245 | position?: number, 246 | ): ValidationError; 247 | export function errorLookup( 248 | codeType?: "element", 249 | code?: string, 250 | position?: number, 251 | dataSample?: string, 252 | ): ValidationError; 253 | /** 254 | * @description Look up a validation error by type and code. 255 | * @param {ValidationType} codeType - The type of validation being performed. 256 | * @param {string} code - The actual code to look up. 257 | * @param {number} [position] - The position at which the error occured. 258 | * @param {string} [dataSample] - A sample of data assciated with the error. 259 | * @returns {ValidationError} The validation error for the lookup. 260 | */ 261 | export function errorLookup( 262 | codeType?: ValidationType, 263 | code?: string, 264 | position?: number, 265 | dataSample?: string, 266 | ): ValidationError { 267 | return X12ValidationErrorCode[codeType ?? "segment"]( 268 | code, 269 | position, 270 | dataSample, 271 | ); 272 | } 273 | -------------------------------------------------------------------------------- /src/X12ValidationEngine/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Interfaces.ts"; 2 | export * from "./X12ValidationEngine.ts"; 3 | export * from "./X12ValidationErrorCode.ts"; 4 | export * from "./X12ValidationRule.ts"; 5 | -------------------------------------------------------------------------------- /test/CoreSuite_test.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 4 | import { 5 | ArgumentNullError, 6 | GeneratorError, 7 | ParserError, 8 | QuerySyntaxError, 9 | } from "../src/Errors.ts"; 10 | import { X12Diagnostic } from "../src/X12Diagnostic.ts"; 11 | import * as core from "../mod.ts"; 12 | 13 | describe("X12Core", () => { 14 | it("should export members", () => { 15 | if (!Object.keys(core).includes("X12Parser")) { 16 | throw new Error("X12 core is missing X12Parser."); 17 | } 18 | }); 19 | 20 | it("should create ArgumentNullError", () => { 21 | const error = new ArgumentNullError("test"); 22 | 23 | if (error.message !== "The argument, 'test', cannot be null.") { 24 | throw new Error("ArgumentNullError did not return the correct message."); 25 | } 26 | }); 27 | 28 | it("should create GeneratorError", () => { 29 | const error = new GeneratorError("test"); 30 | 31 | if (error.message !== "test") { 32 | throw new Error("GeneratorError did not return the correct message."); 33 | } 34 | }); 35 | 36 | it("should create ParserError", () => { 37 | const error = new ParserError("test"); 38 | 39 | if (error.message !== "test") { 40 | throw new Error("ParserError did not return the correct message."); 41 | } 42 | }); 43 | 44 | it("should create QuerySyntaxError", () => { 45 | const error = new QuerySyntaxError("test"); 46 | 47 | if (error.message !== "test") { 48 | throw new Error("QuerySyntaxError did not return the correct message."); 49 | } 50 | }); 51 | 52 | it("should create X12Diagnostic", () => { 53 | const diag = new X12Diagnostic(); 54 | 55 | if (!(diag instanceof X12Diagnostic)) { 56 | throw new Error("Could not create X12Diagnostic."); 57 | } 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/FormattingSuite_test.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 4 | import { X12FatInterchange } from "../src/X12FatInterchange.ts"; 5 | import { X12Interchange, X12Parser, X12SerializationOptions } from "../mod.ts"; 6 | 7 | describe("X12Formatting", () => { 8 | it("should replicate the source data unless changes are made", () => { 9 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 10 | const parser = new X12Parser(true); 11 | const interchange = parser.parse(edi) as X12Interchange; 12 | 13 | const edi2 = interchange.toString(); 14 | 15 | if (edi !== edi2) { 16 | throw new Error( 17 | `Formatted EDI does not match source. Found ${edi2}, expected ${edi}.`, 18 | ); 19 | } 20 | }); 21 | 22 | it("should replicate the source data for a fat interchange unless changes are made", () => { 23 | const edi = Deno.readTextFileSync("test/test-data/850_fat.edi"); 24 | const parser = new X12Parser(true); 25 | const interchange = parser.parse(edi) as X12FatInterchange; 26 | 27 | const options: X12SerializationOptions = { 28 | format: true, 29 | endOfLine: "\n", 30 | }; 31 | 32 | const edi2 = interchange.toString(options); 33 | 34 | if (edi !== edi2) { 35 | throw new Error( 36 | `Formatted EDI does not match source. Found ${edi2}, expected ${edi}.`, 37 | ); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/GeneratorSuite_test.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 4 | import { 5 | GSSegmentHeader, 6 | ISASegmentHeader, 7 | JSEDINotation, 8 | X12Generator, 9 | X12Interchange, 10 | X12Parser, 11 | } from "../mod.ts"; 12 | 13 | describe("X12Generator", () => { 14 | it("should create X12Generator", () => { 15 | const generator = new X12Generator(); 16 | const notation = generator.getJSEDINotation(); 17 | const options = generator.getOptions(); 18 | 19 | generator.setJSEDINotation(new JSEDINotation()); 20 | generator.setOptions({}); 21 | 22 | if (!(notation instanceof JSEDINotation) || typeof options !== "object") { 23 | throw new Error("Could not correctly create instance of X12Generator."); 24 | } 25 | }); 26 | 27 | it("should replicate the source data unless changes are made", () => { 28 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 29 | const parser = new X12Parser(true); 30 | const notation: JSEDINotation = parser.parse(edi) 31 | .toJSEDINotation() as JSEDINotation; 32 | 33 | const generator = new X12Generator(notation); 34 | 35 | const edi2 = generator.toString(); 36 | 37 | if (edi !== edi2) { 38 | throw new Error( 39 | `Formatted EDI does not match source. Found ${edi2}, expected ${edi}.`, 40 | ); 41 | } 42 | }); 43 | 44 | it("should replicate the source data to and from JSON unless changes are made", () => { 45 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 46 | const parser = new X12Parser(true); 47 | const interchange = parser.parse(edi); 48 | 49 | const json = JSON.stringify(interchange); 50 | 51 | const generator = new X12Generator(JSON.parse(json)); 52 | 53 | const edi2 = generator.toString(); 54 | 55 | if (edi !== edi2) { 56 | throw new Error( 57 | `Formatted EDI does not match source. Found ${edi2}, expected ${edi}.`, 58 | ); 59 | } 60 | }); 61 | 62 | it("should not generate 271 with 4 ST elements using default segment headers", () => { 63 | const fileEdi = Deno.readTextFileSync("test/test-data/271.edi").split("~"); 64 | 65 | const i = new X12Interchange(); 66 | 67 | i.setHeader(fileEdi[0].split("*").slice(1)); 68 | 69 | const fg = i.addFunctionalGroup(); 70 | 71 | fg.setHeader(fileEdi[1].split("*").slice(1)); 72 | 73 | const t = fg.addTransaction(); 74 | 75 | let error; 76 | try { 77 | t.setHeader([...fileEdi[2].split("*").slice(1), "N"]); 78 | } catch (err) { 79 | error = err.message; 80 | } 81 | 82 | if ( 83 | error !== 84 | 'Segment "ST" with 4 elements does not meet the required count of min 2 or max 3.' 85 | ) { 86 | throw new Error( 87 | "271 with 4 ST elements parsing succeed which should not happen", 88 | ); 89 | } 90 | }); 91 | 92 | it("should generate 271 with 3 ST elements using custom segment headers", () => { 93 | const fileEdi = Deno.readTextFileSync("test/test-data/271.edi").split("~"); 94 | 95 | const i = new X12Interchange({ 96 | segmentHeaders: [ 97 | ISASegmentHeader, 98 | GSSegmentHeader, 99 | { 100 | tag: "ST", 101 | layout: { 102 | ST01: 3, 103 | ST02: 9, 104 | ST02_MIN: 4, 105 | ST03: 35, 106 | ST03_MIN: 1, 107 | COUNT: 3, 108 | PADDING: false, 109 | }, 110 | }, 111 | ], 112 | }); 113 | 114 | i.setHeader(fileEdi[0].split("*").slice(1)); 115 | 116 | const fg = i.addFunctionalGroup(); 117 | 118 | fg.setHeader(fileEdi[1].split("*").slice(1)); 119 | 120 | const t = fg.addTransaction(); 121 | 122 | t.setHeader(fileEdi[2].split("*").slice(1)); 123 | }); 124 | 125 | it("should validate custom segment headers", () => { 126 | const edi = Deno.readTextFileSync("test/test-data/271.edi"); 127 | 128 | const options = { 129 | segmentHeaders: [ 130 | ISASegmentHeader, 131 | GSSegmentHeader, 132 | { 133 | tag: "ST", 134 | layout: { 135 | ST01: 3, 136 | ST02: 9, 137 | ST02_MIN: 4, 138 | ST03: 35, 139 | ST03_MIN: 1, 140 | COUNT: 3, 141 | PADDING: false, 142 | }, 143 | }, 144 | { 145 | tag: "HL", 146 | layout: { 147 | HL01: 3, 148 | HL02: 9, 149 | HL02_MIN: 4, 150 | HL03: 35, 151 | HL03_MIN: 1, 152 | COUNT: 3, 153 | PADDING: false, 154 | }, 155 | }, 156 | ], 157 | }; 158 | 159 | const parser = new X12Parser(true); 160 | const interchange = parser.parse(edi); 161 | 162 | const json = JSON.stringify(interchange); 163 | 164 | let error; 165 | try { 166 | const generator = new X12Generator(JSON.parse(json), options); 167 | generator.toString(); 168 | } catch (err) { 169 | error = err.message; 170 | } 171 | 172 | if ( 173 | error !== 174 | 'Segment "HL" with 4 elements does not meet the required count of 3.' 175 | ) { 176 | throw new Error( 177 | "271 with custom segment headers parsing succeed which should not happen", 178 | ); 179 | } 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /test/MappingSuite_test.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | "use strict"; 3 | 4 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 5 | import { 6 | X12Interchange, 7 | X12Parser, 8 | X12Transaction, 9 | X12TransactionMap, 10 | } from "../mod.ts"; 11 | import * as assert from "https://deno.land/std@0.133.0/node/assert.ts"; 12 | 13 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 14 | const edi855 = Deno.readTextFileSync("test/test-data/855.edi"); 15 | const mapJson = Deno.readTextFileSync("test/test-data/850_map.json"); 16 | const resultJson = Deno.readTextFileSync("test/test-data/850_map_result.json"); 17 | const transactionJson = Deno.readTextFileSync( 18 | "test/test-data/Transaction_map.json", 19 | ); 20 | const transactionJsonLiquid = Deno.readTextFileSync( 21 | "test/test-data/Transaction_map_liquidjs.json", 22 | ); 23 | const transactionData = Deno.readTextFileSync( 24 | "test/test-data/Transaction_data.json", 25 | ); 26 | 27 | describe("X12Mapping", () => { 28 | it("should map transaction to data", () => { 29 | const parser = new X12Parser(); 30 | const interchange = parser.parse(edi) as X12Interchange; 31 | const transaction = interchange.functionalGroups[0].transactions[0]; 32 | const mapper = new X12TransactionMap(JSON.parse(mapJson), transaction); 33 | 34 | assert.deepStrictEqual(mapper.toObject(), JSON.parse(resultJson)); 35 | }); 36 | 37 | it("should map data to transaction with custom macro", () => { 38 | const transaction = new X12Transaction(); 39 | const mapper = new X12TransactionMap( 40 | JSON.parse(transactionJson), 41 | transaction, 42 | ); 43 | const data = JSON.parse(transactionData); 44 | const result = mapper.fromObject(data, { 45 | toFixed: function toFixed(key: string, places: number) { 46 | return { 47 | val: parseFloat(key).toFixed(places), 48 | }; 49 | }, 50 | }); 51 | 52 | if (!(result instanceof X12Transaction)) { 53 | throw new Error( 54 | "An error occured when mapping an object to a transaction.", 55 | ); 56 | } 57 | }); 58 | 59 | it("should map data to transaction with LiquidJS", () => { 60 | const transaction = new X12Transaction(); 61 | const mapper = new X12TransactionMap( 62 | JSON.parse(transactionJsonLiquid), 63 | transaction, 64 | "liquidjs", 65 | ); 66 | const data = JSON.parse(transactionData); 67 | const result = mapper.fromObject(data, { 68 | to_fixed: (value: string, places: number) => 69 | parseFloat(value).toFixed(places), 70 | }); 71 | 72 | if (!(result instanceof X12Transaction)) { 73 | throw new Error( 74 | "An error occured when mapping an object to a transaction.", 75 | ); 76 | } 77 | }); 78 | 79 | it("should map empty data when element missing from qualified segment", () => { 80 | // Addresses issue https://github.com/ahuggins-nhs/node-x12/issues/23 81 | const mapObject = { author: 'FOREACH(PO1)=>PO109:PO103["UN"]' }; 82 | const parser = new X12Parser(); 83 | const interchange = parser.parse(edi855) as X12Interchange; 84 | const transaction = interchange.functionalGroups[0].transactions[0]; 85 | const mapperLoose = new X12TransactionMap(mapObject, transaction, "loose"); 86 | const mapperStrict = new X12TransactionMap( 87 | mapObject, 88 | transaction, 89 | "strict", 90 | ); 91 | 92 | const resultLoose: any[] = mapperLoose.toObject(); 93 | const resultStrict: any[] = mapperStrict.toObject(); 94 | 95 | assert.strictEqual(Array.isArray(resultLoose), true); 96 | assert.strictEqual(Array.isArray(resultStrict), true); 97 | assert.strictEqual(resultLoose.length, 4); 98 | assert.strictEqual(resultStrict.length, 3); 99 | assert.deepStrictEqual(resultLoose[2], { author: "" }); 100 | assert.deepStrictEqual(resultStrict[2], { author: "NOT APPLICABLE" }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/ObjectModelSuite_test.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 4 | import { 5 | JSEDINotation, 6 | X12FatInterchange, 7 | X12Interchange, 8 | X12Parser, 9 | X12Segment, 10 | } from "../mod.ts"; 11 | import { 12 | JSEDIFunctionalGroup, 13 | JSEDITransaction, 14 | } from "../src/JSEDINotation.ts"; 15 | 16 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 17 | 18 | describe("X12ObjectModel", () => { 19 | it("should create X12Interchange with string delimiters", () => { 20 | const interchange = new X12Interchange("~", "*"); 21 | 22 | if (interchange.elementDelimiter !== "*") { 23 | throw new Error("Instance of X12Interchange not successfully created."); 24 | } 25 | }); 26 | 27 | it("should create X12FatInterchange", () => { 28 | const parser = new X12Parser(); 29 | const interchange = parser.parse(edi) as X12Interchange; 30 | const fatInterchange = new X12FatInterchange([interchange]); 31 | const str = fatInterchange.toString(); 32 | const json = fatInterchange.toJSON(); 33 | 34 | if (!Array.isArray(json) || typeof str !== "string") { 35 | throw new Error( 36 | "Instance of X12FatInterchange not successfully created.", 37 | ); 38 | } 39 | }); 40 | 41 | it("should create X12Segment", () => { 42 | const segment = new X12Segment(); 43 | const noElement = segment.replaceElement("1", 1); 44 | const noInsert = segment.insertElement("1", 1); 45 | const noneToRemove = segment.removeElement(1); 46 | const defaultVal = segment.valueOf(1, "2"); 47 | 48 | segment.setTag("WX"); 49 | segment.addElement("1"); 50 | segment.insertElement("2", 1); 51 | segment.removeElement(2); 52 | 53 | if ( 54 | noElement !== null || 55 | noInsert !== false || 56 | typeof noneToRemove !== "boolean" || 57 | defaultVal !== "2" || 58 | segment.elements.length !== 1 || 59 | segment.elements[0].value !== "2" 60 | ) { 61 | throw new Error( 62 | "Instance of segment or methods did not execute as expected.", 63 | ); 64 | } 65 | }); 66 | 67 | it("should cast functional group to JSON", () => { 68 | const parser = new X12Parser(); 69 | const interchange = parser.parse(edi) as X12Interchange; 70 | const functionalGroup = interchange.functionalGroups[0]; 71 | 72 | if (typeof functionalGroup.toJSON() !== "object") { 73 | throw new Error("Instance of X12FunctionalGroup not cast to JSON."); 74 | } 75 | }); 76 | 77 | it("should cast transaction set to JSON", () => { 78 | const parser = new X12Parser(); 79 | const interchange = parser.parse(edi) as X12Interchange; 80 | const functionalGroup = interchange.functionalGroups[0]; 81 | const transaction = functionalGroup.transactions[0]; 82 | 83 | if (typeof transaction.toJSON() !== "object") { 84 | throw new Error("Instance of X12FunctionalGroup not cast to JSON."); 85 | } 86 | }); 87 | 88 | it("should cast segment to JSON", () => { 89 | const parser = new X12Parser(); 90 | const interchange = parser.parse(edi) as X12Interchange; 91 | const functionalGroup = interchange.functionalGroups[0]; 92 | const transaction = functionalGroup.transactions[0]; 93 | const segment = transaction.segments[0]; 94 | 95 | if (typeof segment.toJSON() !== "object") { 96 | throw new Error("Instance of X12FunctionalGroup not cast to JSON."); 97 | } 98 | }); 99 | 100 | it("should construct JSEDINotation objects", () => { 101 | const notation = new JSEDINotation(); 102 | const group = new JSEDIFunctionalGroup(); 103 | const transaction = new JSEDITransaction(); 104 | 105 | if ( 106 | !(notation instanceof JSEDINotation) || 107 | !(group instanceof JSEDIFunctionalGroup) || 108 | !(transaction instanceof JSEDITransaction) 109 | ) { 110 | throw new Error( 111 | "One or more JS EDI Notation objects could not be constructed.", 112 | ); 113 | } 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /test/ParserSuite_test.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | "use strict"; 3 | 4 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 5 | import { X12Interchange, X12Parser, X12Segment } from "../mod.ts"; 6 | import fs from "https://deno.land/std@0.136.0/node/fs.ts"; 7 | 8 | describe("X12Parser", () => { 9 | it("should parse a valid X12 document without throwing an error", () => { 10 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 11 | const parser = new X12Parser(); 12 | parser.parse(edi); 13 | }); 14 | 15 | it("should parse a fat X12 document without throwing an error", () => { 16 | const edi = Deno.readTextFileSync("test/test-data/850_fat.edi"); 17 | const parser = new X12Parser(true); 18 | parser.parse(edi); 19 | }); 20 | 21 | it("should parse and reconstruct a valid X12 stream without throwing an error", async () => { 22 | return await new Promise((resolve, reject) => { 23 | const ediStream = fs.createReadStream("test/test-data/850.edi"); // TODO: Replicate utf8 encoding mode 24 | const parser = new X12Parser(); 25 | const segments: X12Segment[] = []; 26 | 27 | ediStream.on("error", (error) => { 28 | reject(error); 29 | }); 30 | 31 | parser.on("error", (error) => { 32 | reject(error); 33 | }); 34 | 35 | ediStream 36 | .pipe(parser) 37 | .on("data", (data) => { 38 | segments.push(data); 39 | }) 40 | .on("end", () => { 41 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 42 | const interchange = parser.getInterchangeFromSegments(segments); 43 | 44 | if (interchange.toString() !== edi) { 45 | reject( 46 | new Error( 47 | "Expected parsed EDI stream to match raw EDI document.", 48 | ), 49 | ); 50 | } 51 | resolve(); 52 | }); 53 | }); 54 | }); 55 | 56 | it("should produce accurate line numbers for files with line breaks", () => { 57 | const edi = Deno.readTextFileSync("test/test-data/850_3.edi"); 58 | const parser = new X12Parser(); 59 | const interchange = parser.parse(edi) as X12Interchange; 60 | 61 | const segments = ([] as X12Segment[]).concat( 62 | [ 63 | interchange.header, 64 | interchange.functionalGroups[0].header, 65 | interchange.functionalGroups[0].transactions[0].header, 66 | ], 67 | interchange.functionalGroups[0].transactions[0].segments, 68 | [ 69 | interchange.functionalGroups[0].transactions[0].trailer, 70 | interchange.functionalGroups[0].trailer, 71 | interchange.trailer, 72 | ], 73 | ); 74 | 75 | for (let i = 0; i < segments.length; i++) { 76 | const segment: X12Segment = segments[i]; 77 | 78 | if (i !== segment.range.start.line) { 79 | throw new Error( 80 | `Segment line number incorrect. Expected ${i}, found ${segment.range.start.line}.`, 81 | ); 82 | } 83 | } 84 | }); 85 | 86 | it("should throw an ArgumentNullError", () => { 87 | const parser = new X12Parser(); 88 | let error; 89 | 90 | try { 91 | parser.parse(undefined as any); 92 | } catch (err) { 93 | error = err; 94 | } 95 | 96 | if (error.name !== "ArgumentNullError") { 97 | throw new Error( 98 | "ArgumentNullError expected when first argument to X12Parser.parse() is undefined.", 99 | ); 100 | } 101 | }); 102 | 103 | it("should throw an ParserError", () => { 104 | const parser = new X12Parser(true); 105 | let error; 106 | 107 | try { 108 | parser.parse(""); 109 | } catch (err) { 110 | error = err; 111 | } 112 | 113 | if (error.name !== "ParserError") { 114 | throw new Error( 115 | "ParserError expected when document length is too short and parser is strict.", 116 | ); 117 | } 118 | }); 119 | 120 | it("should find mismatched elementDelimiter", () => { 121 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 122 | const parser = new X12Parser(true); 123 | let error; 124 | 125 | try { 126 | parser.parse(edi, { elementDelimiter: "+" }); 127 | } catch (err) { 128 | error = err; 129 | } 130 | 131 | if (error.name !== "ParserError") { 132 | throw new Error( 133 | "ParserError expected when elementDelimiter in document does not match and parser is strict.", 134 | ); 135 | } 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/QuerySuite_test.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 4 | import { X12Parser, X12QueryEngine } from "../mod.ts"; 5 | 6 | describe("X12QueryEngine", () => { 7 | it("should handle basic element references", () => { 8 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 9 | const parser = new X12Parser(true); 10 | const engine = new X12QueryEngine(parser); 11 | const results = engine.query(edi, "REF02"); 12 | 13 | if (results.length !== 2) { 14 | throw new Error("Expected two matching elements for REF02."); 15 | } 16 | }); 17 | 18 | it("should handle qualified element references", () => { 19 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 20 | const parser = new X12Parser(true); 21 | const engine = new X12QueryEngine(parser); 22 | const results = engine.query(edi, 'REF02:REF01["DP"]'); 23 | 24 | if (results.length !== 1) { 25 | throw new Error('Expected one matching element for REF02:REF01["DP"].'); 26 | } else if (results[0].value !== "038") { 27 | throw new Error('Expected REF02 to be "038".'); 28 | } 29 | }); 30 | 31 | it("should handle segment path element references", () => { 32 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 33 | const parser = new X12Parser(true); 34 | const engine = new X12QueryEngine(parser); 35 | const results = engine.query(edi, 'PO1-PID05:PID01["F"]'); 36 | 37 | if (results.length !== 6) { 38 | throw new Error( 39 | `Expected six matching elements for PO1-PID05:PID01["F"]; received ${results.length}.`, 40 | ); 41 | } 42 | }); 43 | 44 | it("should handle HL path element references", () => { 45 | const edi = Deno.readTextFileSync("test/test-data/856.edi"); 46 | const parser = new X12Parser(true); 47 | const engine = new X12QueryEngine(parser); 48 | const results = engine.query(edi, "HL+S+O+I-LIN03"); 49 | 50 | if (results[0].value !== "87787D" || results[1].value !== "99887D") { 51 | throw new Error("Expected two matching elements for HL+S+O+I-LIN03."); 52 | } 53 | }); 54 | 55 | it("should handle HL paths where HL03 is a number", () => { 56 | const edi = Deno.readTextFileSync("test/test-data/271.edi"); 57 | const parser = new X12Parser(true); 58 | const engine = new X12QueryEngine(parser); 59 | const results = engine.query(edi, "HL+20+21+22-NM101"); 60 | 61 | if (results.length !== 2) { 62 | throw new Error("Expected two matching elements for HL+20+21+22-NM101."); 63 | } 64 | }); 65 | 66 | it("should handle FOREACH macro references", () => { 67 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 68 | const parser = new X12Parser(true); 69 | const engine = new X12QueryEngine(parser); 70 | const result = engine.querySingle(edi, 'FOREACH(PO1)=>PID05:PID01["F"]'); 71 | 72 | if (result?.values.length !== 6) { 73 | throw new Error( 74 | `Expected six matching elements for FOREACH(PO1)=>PID05:PID01["F"]; received ${ 75 | result?.values.length 76 | }.`, 77 | ); 78 | } 79 | }); 80 | 81 | it("should handle CONCAT macro references", () => { 82 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 83 | const parser = new X12Parser(true); 84 | const engine = new X12QueryEngine(parser); 85 | const result = engine.querySingle( 86 | edi, 87 | 'CONCAT(REF02:REF01["DP"], & )=>REF02:REF01["PS"]', 88 | ); 89 | 90 | if (result?.value !== "038 & R") { 91 | throw new Error(`Expected '038 & R'; received '${result?.value}'.`); 92 | } 93 | }); 94 | 95 | it("should return valid range information for segments and elements", () => { 96 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 97 | const parser = new X12Parser(true); 98 | const engine = new X12QueryEngine(parser); 99 | const result = engine.querySingle(edi, "BEG03"); 100 | 101 | if (result?.segment?.range.start.line !== 3) { 102 | throw new Error( 103 | `Start line for segment is incorrect; found ${ 104 | result?.segment?.range.start.line 105 | }, expected 3.`, 106 | ); 107 | } 108 | 109 | if (result.segment.range.start.character !== 0) { 110 | throw new Error( 111 | `Start char for segment is incorrect; found ${result.segment.range.start.character}, expected 0.`, 112 | ); 113 | } 114 | 115 | if (result?.element?.range.start.line !== 3) { 116 | throw new Error( 117 | `Start line for element is incorrect; found ${ 118 | result?.element?.range.start.line 119 | }, expected 3.`, 120 | ); 121 | } 122 | 123 | if (result.element.range.start.character !== 10) { 124 | throw new Error( 125 | `Start char for element is incorrect; found ${result.element.range.start.character}, expected 10.`, 126 | ); 127 | } 128 | 129 | if (result.segment.range.end.line !== 3) { 130 | throw new Error( 131 | `End line for segment is incorrect; found ${result.segment.range.end.line}, expected 3.`, 132 | ); 133 | } 134 | 135 | if (result.segment.range.end.character !== 41) { 136 | throw new Error( 137 | `End char for segment is incorrect; found ${result.segment.range.end.character}, expected 41.`, 138 | ); 139 | } 140 | 141 | if (result.element.range.end.line !== 3) { 142 | throw new Error( 143 | `End line for element is incorrect; found ${result.element.range.end.line}, expected 3.`, 144 | ); 145 | } 146 | 147 | if (result.element.range.end.character !== 20) { 148 | throw new Error( 149 | `End char for element is incorrect; found ${result.element.range.end.character}, expected 20.`, 150 | ); 151 | } 152 | }); 153 | 154 | it("should handle envelope queries", () => { 155 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 156 | const parser = new X12Parser(true); 157 | const engine = new X12QueryEngine(parser); 158 | const results = engine.query(edi, "ISA06"); 159 | 160 | if (results.length === 1) { 161 | if (results[0]?.value?.trim() !== "4405197800") { 162 | throw new Error(`Expected 4405197800, found ${results[0].value}.`); 163 | } 164 | } else { 165 | throw new Error(`Expected exactly one result. Found ${results.length}.`); 166 | } 167 | }); 168 | 169 | it("should handle queries for files with line feed segment terminators", () => { 170 | const edi = Deno.readTextFileSync("test/test-data/850_2.edi"); 171 | const parser = new X12Parser(true); 172 | const engine = new X12QueryEngine(parser); 173 | const result = engine.querySingle(edi, 'REF02:REF01["DP"]'); 174 | 175 | if (result?.value?.trim() !== "038") { 176 | throw new Error(`Expected 038, found ${result?.value}.`); 177 | } 178 | }); 179 | 180 | it("should handle chained qualifiers", () => { 181 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 182 | const parser = new X12Parser(true); 183 | const engine = new X12QueryEngine(parser); 184 | const results = engine.query(edi, 'REF02:REF01["DP"]:BEG02["SA"]'); 185 | 186 | if (results.length === 1) { 187 | if (results[0]?.value?.trim() !== "038") { 188 | throw new Error(`Expected 038, found ${results[0].value}.`); 189 | } 190 | } else { 191 | throw new Error(`Expected exactly one result. Found ${results.length}.`); 192 | } 193 | }); 194 | }); 195 | -------------------------------------------------------------------------------- /test/ValidationSuite_test.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | "use strict"; 3 | 4 | import { describe, it } from "https://deno.land/x/deno_mocha@0.3.0/mod.ts" 5 | import * as assert from "https://deno.land/std@0.133.0/node/assert.ts"; 6 | import { 7 | errorLookup, 8 | X12Interchange, 9 | X12Parser, 10 | X12Segment, 11 | X12ValidationEngine, 12 | X12ValidationErrorCode, 13 | } from "../mod.ts"; 14 | import { 15 | X12ElementRule, 16 | X12GroupRule, 17 | X12InterchangeRule, 18 | X12SegmentRule, 19 | X12TransactionRule, 20 | X12ValidationRule, 21 | } from "../src/X12ValidationEngine/index.ts"; 22 | 23 | const edi = Deno.readTextFileSync("test/test-data/850.edi"); 24 | const edi2 = Deno.readTextFileSync("test/test-data/856.edi"); 25 | const validationRule850 = Deno.readTextFileSync( 26 | "test/test-data/850_validation.rule.json", 27 | ); 28 | const validationRuleSimple850 = Deno.readTextFileSync( 29 | "test/test-data/850_validation_simple.rule.json", 30 | ); 31 | const validationRuleNoHeader850 = Deno.readTextFileSync( 32 | "test/test-data/850_validation_no_headers.rule.json", 33 | ); 34 | 35 | describe("X12ValidationEngine", () => { 36 | it("should create validation rule", () => { 37 | const rule = new X12ValidationRule({ engine: /ab+c/gu }); 38 | 39 | assert.deepStrictEqual(rule instanceof X12ValidationRule, true); 40 | }); 41 | 42 | it("should create validation rule from JSON", () => { 43 | const ruleJson = JSON.parse(validationRule850); 44 | const rule = new X12InterchangeRule(ruleJson); 45 | const stringJson = JSON.stringify(rule); 46 | 47 | assert.deepStrictEqual(JSON.parse(stringJson), ruleJson); 48 | // fs.writeFileSync('test/test-data/850_validation.rule.json', JSON.stringify(rule, null, 2)) 49 | }); 50 | 51 | it("should create validation rule regardless of header or trailer", () => { 52 | const ruleJson = JSON.parse(validationRuleNoHeader850); 53 | const rule = new X12InterchangeRule(ruleJson); 54 | 55 | assert.deepStrictEqual(rule instanceof X12InterchangeRule, true); 56 | // fs.writeFileSync('test/test-data/850_validation.rule.json', JSON.stringify(rule, null, 2)) 57 | }); 58 | 59 | it("should validate X12 document", () => { 60 | const ruleJson = JSON.parse(validationRuleSimple850); 61 | const parser = new X12Parser(); 62 | const interchange = parser.parse(edi) as X12Interchange; 63 | const validator = new X12ValidationEngine(); 64 | let rule: any = new X12InterchangeRule(ruleJson); 65 | let report = validator.assert(interchange, rule); 66 | 67 | assert.strictEqual(report, true); 68 | 69 | rule = new X12GroupRule(ruleJson.group); 70 | report = validator.assert(interchange.functionalGroups[0], rule); 71 | 72 | assert.strictEqual(report, true); 73 | 74 | rule = new X12TransactionRule(ruleJson.group.transaction); 75 | report = validator.assert( 76 | interchange.functionalGroups[0].transactions[0], 77 | rule, 78 | ); 79 | 80 | assert.strictEqual(report, true); 81 | 82 | rule = new X12SegmentRule(ruleJson.group.transaction.segments[0]); 83 | report = validator.assert( 84 | interchange.functionalGroups[0].transactions[0].segments[0], 85 | rule, 86 | ); 87 | 88 | assert.strictEqual(report, true); 89 | 90 | rule = new X12ElementRule( 91 | ruleJson.group.transaction.segments[0].elements[0], 92 | ); 93 | report = validator.assert( 94 | interchange.functionalGroups[0].transactions[0].segments[0].elements[0], 95 | rule, 96 | ); 97 | 98 | assert.strictEqual(report, true); 99 | }); 100 | 101 | it("should invalidate X12 document", () => { 102 | const ruleJson = JSON.parse(validationRuleSimple850); 103 | const parser = new X12Parser(); 104 | const interchange = parser.parse(edi2) as X12Interchange; 105 | const rule = new X12InterchangeRule(ruleJson); 106 | const validator = new X12ValidationEngine({ 107 | throwError: true, 108 | acknowledgement: { 109 | isa: new X12Segment("ISA").setElements([ 110 | "00", 111 | "", 112 | "00", 113 | "", 114 | "ZZ", 115 | "TEST1", 116 | "ZZ", 117 | "TEST2", 118 | "200731", 119 | "0430", 120 | "U", 121 | "00401", 122 | "1", 123 | "1", 124 | "P", 125 | ">", 126 | ]), 127 | gs: new X12Segment("GS").setElements([ 128 | "FA", 129 | "TEST1", 130 | "TEST2", 131 | "20200731", 132 | "0430", 133 | "1", 134 | "X", 135 | "004010", 136 | ]), 137 | }, 138 | }); 139 | 140 | try { 141 | validator.assert(interchange, rule); 142 | } catch (error) { 143 | const { report } = error; 144 | 145 | assert.strictEqual(typeof report, "object"); 146 | } 147 | 148 | const acknowledgement = validator.acknowledge(); 149 | 150 | assert.strictEqual(acknowledgement instanceof X12Interchange, true); 151 | }); 152 | 153 | it("should resolve error codes", () => { 154 | const errorTypes = ["element", "segment", "transaction", "group"]; 155 | const ackCodes = "AMPRWXE"; 156 | 157 | for (const errorType of errorTypes) { 158 | for (let i = 1, j = 1; i <= j; i += 1) { 159 | const result = errorLookup(errorType as any, j.toString()); 160 | 161 | assert.strictEqual(typeof result, "object"); 162 | 163 | if (parseFloat(result.code) > i - 1) { 164 | j += 1; 165 | } 166 | } 167 | } 168 | 169 | for (const char of ackCodes) { 170 | const result = X12ValidationErrorCode.acknowledgement("group", char); 171 | 172 | assert.strictEqual(typeof result, "object"); 173 | } 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/test-data/271.edi: -------------------------------------------------------------------------------- 1 | ISA*00*Authorizat*00*Security I*ZZ*InterchangeSen *ZZ*Interchange Rec*141001*1037*^*00501*000031033*0*T*:~ 2 | GS*HS*Sample Sen*Sample Rec*20141001*1037*123456*X*005010X279A1~ 3 | ST*271*000000001*005010X279A1~ 4 | BHT*0022*11*7237581e2e400890ee96a51bc62ed0*20200308*1901~ 5 | HL*1**20*1~ 6 | NM1*PR*2*CMS*****PI*CMS~ 7 | HL*2*1*21*1~ 8 | NM1*1P*2*SINAI HADASSAH MEDICAL ASSOCIATES*****XX*1508165317~ 9 | HL*3*2*22*0~ 10 | TRN*1*2223255592*99Trizetto~ 11 | NM1*IL*1*BELMAR*ALBERT*R***MI*9UY4R33KV83~ 12 | N3*1029 SALEM AVE~ 13 | N4*WOODBURY*NJ*080966062~ 14 | DMG*D8*19500722*M~ 15 | DTP*307*RD8*20191230-20191230~ 16 | EB*1**88~ 17 | EB*1**30^42^45^48^49^69^76^83^A5^A7^AG^BT^BU^BV*MA~ 18 | DTP*291*D8*20150701~ 19 | EB*1**30^2^23^24^25^26^27^28^3^33^36^37^38^39^40^42^50^51^52^53^67^69^73^76^83^86^98^A4^A6^A8^AI^AJ^AK^AL^BT^BU^BV^DM^UC*MB~ 20 | DTP*291*D8*20150701~ 21 | EB*A**30*QM*Medicare Part B*27**0~ 22 | DTP*291*RD8*20190101-20191231~ 23 | EB*C**30*QM*Medicare Part A*26*0~ 24 | DTP*291*RD8*20190101-20191231~ 25 | EB*C**30*QM*Medicare Part B*23*0~ 26 | DTP*291*RD8*20190101-20191231~ 27 | EB*I**41^54~ 28 | EB*R***QM*NJ QMB Plan~ 29 | DTP*290*D8*20170201~ 30 | EB*R**88*OT~ 31 | REF*18*S5601~ 32 | REF*N6*008*SilverScript Choice~ 33 | DTP*292*D8*20170101~ 34 | LS*2120~ 35 | NM1*PR*2*SILVERSCRIPT INSURANCE COMPANY~ 36 | N3*445 Great Circle Road~ 37 | N4*Nashville*TN*37228~ 38 | PER*IC**TE*8664430934*UR*www.silverscript.com~ 39 | LE*2120~ 40 | SE*38*000000001~ -------------------------------------------------------------------------------- /test/test-data/850.edi: -------------------------------------------------------------------------------- 1 | ISA*01*0000000000*01*ABCCO *12*4405197800 *01*999999999 *101127*1719*U*00400*000003438*0*P*>~ 2 | GS*PO*4405197800*999999999*20101127*1719*1421*X*004010VICS~ 3 | ST*850*000000010~ 4 | BEG*00*SA*08292233294**20101127*610385385~ 5 | REF*DP*038~ 6 | REF*PS*R~ 7 | ITD*14*3*2**45**46~ 8 | DTM*002*20101214~ 9 | PKG*F*68***PALLETIZE SHIPMENT~ 10 | PKG*F*66***REGULAR~ 11 | TD5*A*92*P3**SEE XYZ RETAIL ROUTING GUIDE~ 12 | N1*ST*XYZ RETAIL*9*0003947268292~ 13 | N3*31875 SOLON RD~ 14 | N4*SOLON*OH*44139~ 15 | PO1*1*120*EA*9.25*TE*CB*065322-117*PR*RO*VN*AB3542~ 16 | PID*F****SMALL WIDGET~ 17 | PO4*4*4*EA*PLT94**3*LR*15*CT~ 18 | PO1*2*220*EA*13.79*TE*CB*066850-116*PR*RO*VN*RD5322~ 19 | PID*F****MEDIUM WIDGET~ 20 | PO4*2*2*EA~ 21 | PO1*3*126*EA*10.99*TE*CB*060733-110*PR*RO*VN*XY5266~ 22 | PID*F****LARGE WIDGET~ 23 | PO4*6*1*EA*PLT94**3*LR*12*CT~ 24 | PO1*4*76*EA*4.35*TE*CB*065308-116*PR*RO*VN*VX2332~ 25 | PID*F****NANO WIDGET~ 26 | PO4*4*4*EA*PLT94**6*LR*19*CT~ 27 | PO1*5*72*EA*7.5*TE*CB*065374-118*PR*RO*VN*RV0524~ 28 | PID*F****BLUE WIDGET~ 29 | PO4*4*4*EA~ 30 | PO1*6*696*EA*9.55*TE*CB*067504-118*PR*RO*VN*DX1875~ 31 | PID*F****ORANGE WIDGET~ 32 | PO4*6*6*EA*PLT94**3*LR*10*CT~ 33 | CTT*6~ 34 | AMT*1*13045.94~ 35 | SE*33*000000010~ 36 | GE*1*1421~ 37 | IEA*1*000003438~ -------------------------------------------------------------------------------- /test/test-data/850_2.edi: -------------------------------------------------------------------------------- 1 | ISA*01*0000000000*01*ABCCO *12*4405197800 *01*999999999 *101127*1719*U*00400*000003438*0*P*> 2 | GS*PO*4405197800*999999999*20101127*1719*1421*X*004010VICS 3 | ST*850*000000010 4 | BEG*00*SA*08292233294**20101127*610385385 5 | REF*DP*038 6 | REF*PS*R 7 | ITD*14*3*2**45**46 8 | DTM*002*20101214 9 | PKG*F*68***PALLETIZE SHIPMENT 10 | PKG*F*66***REGULAR 11 | TD5*A*92*P3**SEE XYZ RETAIL ROUTING GUIDE 12 | N1*ST*XYZ RETAIL*9*0003947268292 13 | N3*31875 SOLON RD 14 | N4*SOLON*OH*44139 15 | PO1*1*120*EA*9.25*TE*CB*065322-117*PR*RO*VN*AB3542 16 | PID*F****SMALL WIDGET 17 | PO4*4*4*EA*PLT94**3*LR*15*CT 18 | PO1*2*220*EA*13.79*TE*CB*066850-116*PR*RO*VN*RD5322 19 | PID*F****MEDIUM WIDGET 20 | PO4*2*2*EA 21 | PO1*3*126*EA*10.99*TE*CB*060733-110*PR*RO*VN*XY5266 22 | PID*F****LARGE WIDGET 23 | PO4*6*1*EA*PLT94**3*LR*12*CT 24 | PO1*4*76*EA*4.35*TE*CB*065308-116*PR*RO*VN*VX2332 25 | PID*F****NANO WIDGET 26 | PO4*4*4*EA*PLT94**6*LR*19*CT 27 | PO1*5*72*EA*7.5*TE*CB*065374-118*PR*RO*VN*RV0524 28 | PID*F****BLUE WIDGET 29 | PO4*4*4*EA 30 | PO1*6*696*EA*9.55*TE*CB*067504-118*PR*RO*VN*DX1875 31 | PID*F****ORANGE WIDGET 32 | PO4*6*6*EA*PLT94**3*LR*10*CT 33 | CTT*6 34 | AMT*1*13045.94 35 | SE*33*000000010 36 | GE*1*1421 37 | IEA*1*000003438 38 | -------------------------------------------------------------------------------- /test/test-data/850_3.edi: -------------------------------------------------------------------------------- 1 | ISA*00* *00* *12*0000000000 *12*0000000000 *160426*1301*U*00401*010001398*0*P*> 2 | GS*PO*0000000000*0000000000*20160426*1301*10000774*X*004010 3 | ST*850*8830 4 | BEG*00*NE*----------**20160426 5 | DTM*106*20160502 6 | N9*ZZ*0 7 | MSG*000000000001010700 BUYER --------- ------ SHIP TO: 000 -------- ----- --. SAN ANTONIO, TX 78245 ATTN. --------- ------ 8 | N1*VN*----- ------ --------- ---*ZZ*----- 9 | N3*----- ------ -- 10 | N4*HUNTINGTON BEACH*CA*92647 11 | N1*ST*PETCO - CORPORATE*92*100 12 | N3*9125 REHCO RD 13 | N4*SAN DIEGO*CA*92121 14 | PO1**1*KI*225**VN*UNKNOWN*PD*SU - SPARK - AQ FREEZER PUSHER*SK*000000000001010700 15 | CTT*000001**0000000.00*01 16 | SE*14*8830 17 | GE*1*10000774 18 | IEA*1*010001398 19 | -------------------------------------------------------------------------------- /test/test-data/850_fat.edi: -------------------------------------------------------------------------------- 1 | ISA*01*0000000000*01*ABCCO *12*4405197800 *01*999999999 *101127*1719*U*00400*000003438*0*P*>~ 2 | GS*PO*4405197800*999999999*20101127*1719*1421*X*004010VICS~ 3 | ST*850*000000010~ 4 | BEG*00*SA*08292233294**20101127*610385385~ 5 | REF*DP*038~ 6 | REF*PS*R~ 7 | ITD*14*3*2**45**46~ 8 | DTM*002*20101214~ 9 | PKG*F*68***PALLETIZE SHIPMENT~ 10 | PKG*F*66***REGULAR~ 11 | TD5*A*92*P3**SEE XYZ RETAIL ROUTING GUIDE~ 12 | N1*ST*XYZ RETAIL*9*0003947268292~ 13 | N3*31875 SOLON RD~ 14 | N4*SOLON*OH*44139~ 15 | PO1*1*120*EA*9.25*TE*CB*065322-117*PR*RO*VN*AB3542~ 16 | PID*F****SMALL WIDGET~ 17 | PO4*4*4*EA*PLT94**3*LR*15*CT~ 18 | PO1*2*220*EA*13.79*TE*CB*066850-116*PR*RO*VN*RD5322~ 19 | PID*F****MEDIUM WIDGET~ 20 | PO4*2*2*EA~ 21 | PO1*3*126*EA*10.99*TE*CB*060733-110*PR*RO*VN*XY5266~ 22 | PID*F****LARGE WIDGET~ 23 | PO4*6*1*EA*PLT94**3*LR*12*CT~ 24 | PO1*4*76*EA*4.35*TE*CB*065308-116*PR*RO*VN*VX2332~ 25 | PID*F****NANO WIDGET~ 26 | PO4*4*4*EA*PLT94**6*LR*19*CT~ 27 | PO1*5*72*EA*7.5*TE*CB*065374-118*PR*RO*VN*RV0524~ 28 | PID*F****BLUE WIDGET~ 29 | PO4*4*4*EA~ 30 | PO1*6*696*EA*9.55*TE*CB*067504-118*PR*RO*VN*DX1875~ 31 | PID*F****ORANGE WIDGET~ 32 | PO4*6*6*EA*PLT94**3*LR*10*CT~ 33 | CTT*6~ 34 | AMT*1*13045.94~ 35 | SE*33*000000010~ 36 | GE*1*1421~ 37 | IEA*1*000003438~ 38 | ISA*01*0000000000*01*ABCCO *12*4405197800 *01*999999999 *101127*1719*U*00400*000003438*0*P*>~ 39 | GS*PO*4405197800*999999999*20101127*1719*1421*X*004010VICS~ 40 | ST*850*000000010~ 41 | BEG*00*SA*08292233294**20101127*610385385~ 42 | REF*DP*038~ 43 | REF*PS*R~ 44 | ITD*14*3*2**45**46~ 45 | DTM*002*20101214~ 46 | PKG*F*68***PALLETIZE SHIPMENT~ 47 | PKG*F*66***REGULAR~ 48 | TD5*A*92*P3**SEE XYZ RETAIL ROUTING GUIDE~ 49 | N1*ST*XYZ RETAIL*9*0003947268292~ 50 | N3*31875 SOLON RD~ 51 | N4*SOLON*OH*44139~ 52 | PO1*1*120*EA*9.25*TE*CB*065322-117*PR*RO*VN*AB3542~ 53 | PID*F****SMALL WIDGET~ 54 | PO4*4*4*EA*PLT94**3*LR*15*CT~ 55 | PO1*2*220*EA*13.79*TE*CB*066850-116*PR*RO*VN*RD5322~ 56 | PID*F****MEDIUM WIDGET~ 57 | PO4*2*2*EA~ 58 | PO1*3*126*EA*10.99*TE*CB*060733-110*PR*RO*VN*XY5266~ 59 | PID*F****LARGE WIDGET~ 60 | PO4*6*1*EA*PLT94**3*LR*12*CT~ 61 | PO1*4*76*EA*4.35*TE*CB*065308-116*PR*RO*VN*VX2332~ 62 | PID*F****NANO WIDGET~ 63 | PO4*4*4*EA*PLT94**6*LR*19*CT~ 64 | PO1*5*72*EA*7.5*TE*CB*065374-118*PR*RO*VN*RV0524~ 65 | PID*F****BLUE WIDGET~ 66 | PO4*4*4*EA~ 67 | PO1*6*696*EA*9.55*TE*CB*067504-118*PR*RO*VN*DX1875~ 68 | PID*F****ORANGE WIDGET~ 69 | PO4*6*6*EA*PLT94**3*LR*10*CT~ 70 | CTT*6~ 71 | AMT*1*13045.94~ 72 | SE*33*000000010~ 73 | GE*1*1421~ 74 | IEA*1*000003438~ 75 | -------------------------------------------------------------------------------- /test/test-data/850_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "SetID": "ST01", 3 | "ControlNumber": "ST02", 4 | "OrderNumber": "BEG03", 5 | "Date": "BEG04", 6 | "ShipTo": { 7 | "Name": "N102:N101['ST']", 8 | "Destination": "N104:N101['ST']", 9 | "Address": "N1-N301:N101['ST']", 10 | "City": "N1-N401:N101['ST']", 11 | "State": "N1-N402:N101['ST']", 12 | "Zip": "N1-N403:N101['ST']" 13 | }, 14 | "OrderLines": { 15 | "Quantity": "FOREACH(PO1)=>PO102" 16 | }, 17 | "Trailer": ["SE01", "SE02"] 18 | } 19 | -------------------------------------------------------------------------------- /test/test-data/850_map_result.json: -------------------------------------------------------------------------------- 1 | { 2 | "SetID": "850", 3 | "ControlNumber": "000000010", 4 | "OrderNumber": "08292233294", 5 | "Date": "", 6 | "ShipTo": { 7 | "Name": "XYZ RETAIL", 8 | "Destination": "0003947268292", 9 | "Address": "31875 SOLON RD", 10 | "City": "SOLON", 11 | "State": "OH", 12 | "Zip": "44139" 13 | }, 14 | "OrderLines": [ 15 | { "Quantity": "120" }, 16 | { "Quantity": "220" }, 17 | { "Quantity": "126" }, 18 | { "Quantity": "76" }, 19 | { "Quantity": "72" }, 20 | { "Quantity": "696" } 21 | ], 22 | "Trailer": ["33", "000000010"] 23 | } 24 | -------------------------------------------------------------------------------- /test/test-data/850_validation.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleType": "interchange", 3 | "engine": "rule", 4 | "header": { 5 | "ruleType": "segment", 6 | "engine": "rule", 7 | "tag": "ISA", 8 | "elements": [ 9 | { 10 | "ruleType": "element", 11 | "engine": "rule", 12 | "minMax": [2, 2], 13 | "mandatory": true, 14 | "checkType": "id" 15 | }, 16 | { 17 | "ruleType": "element", 18 | "engine": "rule", 19 | "allowBlank": true, 20 | "minMax": [10, 10], 21 | "mandatory": true, 22 | "checkType": "alphanumeric" 23 | }, 24 | { 25 | "ruleType": "element", 26 | "engine": "rule", 27 | "minMax": [2, 2], 28 | "mandatory": true, 29 | "checkType": "id" 30 | }, 31 | { 32 | "ruleType": "element", 33 | "engine": "rule", 34 | "allowBlank": true, 35 | "minMax": [10, 10], 36 | "mandatory": true, 37 | "checkType": "alphanumeric" 38 | }, 39 | { 40 | "ruleType": "element", 41 | "engine": "rule", 42 | "minMax": [2, 2], 43 | "mandatory": true, 44 | "checkType": "id" 45 | }, 46 | { 47 | "ruleType": "element", 48 | "engine": "rule", 49 | "minMax": [15, 15], 50 | "mandatory": true, 51 | "checkType": "alphanumeric" 52 | }, 53 | { 54 | "ruleType": "element", 55 | "engine": "rule", 56 | "minMax": [2, 2], 57 | "mandatory": true, 58 | "checkType": "id" 59 | }, 60 | { 61 | "ruleType": "element", 62 | "engine": "rule", 63 | "minMax": [15, 15], 64 | "mandatory": true, 65 | "checkType": "alphanumeric" 66 | }, 67 | { 68 | "ruleType": "element", 69 | "engine": "rule", 70 | "mandatory": true, 71 | "checkType": "dateshort" 72 | }, 73 | { 74 | "ruleType": "element", 75 | "engine": "rule", 76 | "mandatory": true, 77 | "checkType": "timeshort" 78 | }, 79 | { 80 | "ruleType": "element", 81 | "engine": "rule", 82 | "minMax": [1, 1], 83 | "mandatory": true, 84 | "checkType": "id" 85 | }, 86 | { 87 | "ruleType": "element", 88 | "engine": "rule", 89 | "minMax": [5, 5], 90 | "mandatory": true, 91 | "checkType": "id" 92 | }, 93 | { 94 | "ruleType": "element", 95 | "engine": "rule", 96 | "minMax": [9, 9], 97 | "padLength": true, 98 | "mandatory": true, 99 | "checkType": "number" 100 | }, 101 | { 102 | "ruleType": "element", 103 | "engine": "rule", 104 | "minMax": [1, 1], 105 | "mandatory": true, 106 | "checkType": "id" 107 | }, 108 | { 109 | "ruleType": "element", 110 | "engine": "rule", 111 | "minMax": [1, 1], 112 | "mandatory": true, 113 | "checkType": "id" 114 | }, 115 | { 116 | "ruleType": "element", 117 | "engine": "rule", 118 | "minMax": [1, 1], 119 | "mandatory": true, 120 | "checkType": "alphanumeric" 121 | } 122 | ], 123 | "mandatory": true 124 | }, 125 | "group": { 126 | "ruleType": "group", 127 | "engine": "rule", 128 | "header": { 129 | "ruleType": "segment", 130 | "engine": "rule", 131 | "tag": "GS", 132 | "elements": [ 133 | { 134 | "ruleType": "element", 135 | "engine": "rule", 136 | "checkType": "gs01" 137 | }, 138 | { 139 | "ruleType": "element", 140 | "engine": "rule", 141 | "minMax": [2, 15], 142 | "checkType": "alphanumeric" 143 | }, 144 | { 145 | "ruleType": "element", 146 | "engine": "rule", 147 | "minMax": [2, 15], 148 | "checkType": "alphanumeric" 149 | }, 150 | { 151 | "ruleType": "element", 152 | "engine": "rule", 153 | "checkType": "date" 154 | }, 155 | { 156 | "ruleType": "element", 157 | "engine": "rule", 158 | "checkType": "time" 159 | }, 160 | { 161 | "ruleType": "element", 162 | "engine": "rule", 163 | "minMax": [1, 9], 164 | "checkType": "number" 165 | }, 166 | { 167 | "ruleType": "element", 168 | "engine": "rule", 169 | "minMax": [1, 2], 170 | "checkType": "alphanumeric" 171 | }, 172 | { 173 | "ruleType": "element", 174 | "engine": "rule", 175 | "minMax": [1, 12], 176 | "checkType": "alphanumeric" 177 | } 178 | ], 179 | "mandatory": true 180 | }, 181 | "transaction": { 182 | "ruleType": "transaction", 183 | "engine": "rule", 184 | "header": { 185 | "ruleType": "segment", 186 | "engine": "rule", 187 | "tag": "ST", 188 | "elements": [ 189 | { 190 | "ruleType": "element", 191 | "engine": "rule", 192 | "checkType": "st01" 193 | }, 194 | { 195 | "ruleType": "element", 196 | "engine": "rule", 197 | "minMax": [4, 9], 198 | "checkType": "number" 199 | } 200 | ], 201 | "mandatory": true 202 | }, 203 | "segments": [ 204 | { 205 | "ruleType": "segment", 206 | "engine": "rule", 207 | "tag": "BEG", 208 | "elements": [ 209 | { 210 | "ruleType": "element", 211 | "engine": "rule", 212 | "maxLength": 2, 213 | "checkType": "number" 214 | }, 215 | { 216 | "ruleType": "element", 217 | "engine": "rule", 218 | "maxLength": 2, 219 | "checkType": "id" 220 | }, 221 | { 222 | "ruleType": "element", 223 | "engine": "rule", 224 | "minLength": 1, 225 | "checkType": "alphanumeric" 226 | }, 227 | { 228 | "ruleType": "element", 229 | "engine": "rule", 230 | "skip": true 231 | }, 232 | { 233 | "ruleType": "element", 234 | "engine": "rule", 235 | "checkType": "date" 236 | }, 237 | { 238 | "ruleType": "element", 239 | "engine": "rule", 240 | "minLength": 1, 241 | "checkType": "alphanumeric" 242 | } 243 | ], 244 | "mandatory": true 245 | }, 246 | { 247 | "ruleType": "segment", 248 | "engine": "rule", 249 | "tag": "REF", 250 | "elements": [ 251 | { 252 | "ruleType": "element", 253 | "engine": "rule", 254 | "maxLength": 2, 255 | "checkType": "id" 256 | }, 257 | { 258 | "ruleType": "element", 259 | "engine": "rule", 260 | "minLength": 1, 261 | "checkType": "alphanumeric" 262 | } 263 | ], 264 | "mandatory": true 265 | }, 266 | { 267 | "ruleType": "segment", 268 | "engine": "rule", 269 | "tag": "REF", 270 | "elements": [ 271 | { 272 | "ruleType": "element", 273 | "engine": "rule", 274 | "maxLength": 2, 275 | "checkType": "id" 276 | }, 277 | { 278 | "ruleType": "element", 279 | "engine": "rule", 280 | "minLength": 1, 281 | "checkType": "alphanumeric" 282 | } 283 | ], 284 | "mandatory": true 285 | }, 286 | { 287 | "ruleType": "segment", 288 | "engine": "rule", 289 | "tag": "ITD", 290 | "elements": [ 291 | { 292 | "ruleType": "element", 293 | "engine": "rule", 294 | "maxLength": 2, 295 | "checkType": "id" 296 | }, 297 | { 298 | "ruleType": "element", 299 | "engine": "rule", 300 | "minLength": 1, 301 | "checkType": "alphanumeric" 302 | }, 303 | { 304 | "ruleType": "element", 305 | "engine": "rule", 306 | "minLength": 1, 307 | "checkType": "alphanumeric" 308 | }, 309 | { 310 | "ruleType": "element", 311 | "engine": "rule", 312 | "skip": true 313 | }, 314 | { 315 | "ruleType": "element", 316 | "engine": "rule", 317 | "maxLength": 2, 318 | "checkType": "id" 319 | }, 320 | { 321 | "ruleType": "element", 322 | "engine": "rule", 323 | "skip": true 324 | }, 325 | { 326 | "ruleType": "element", 327 | "engine": "rule", 328 | "maxLength": 2, 329 | "checkType": "id" 330 | } 331 | ], 332 | "mandatory": true 333 | }, 334 | { 335 | "ruleType": "segment", 336 | "engine": "rule", 337 | "tag": "DTM", 338 | "elements": [ 339 | { 340 | "ruleType": "element", 341 | "engine": "rule", 342 | "maxLength": 3, 343 | "checkType": "id" 344 | }, 345 | { 346 | "ruleType": "element", 347 | "engine": "rule", 348 | "checkType": "datelong" 349 | } 350 | ], 351 | "mandatory": true 352 | }, 353 | { 354 | "ruleType": "segment", 355 | "engine": "rule", 356 | "tag": "PKG", 357 | "elements": [ 358 | { 359 | "ruleType": "element", 360 | "engine": "rule", 361 | "maxLength": 1, 362 | "checkType": "id" 363 | }, 364 | { 365 | "ruleType": "element", 366 | "engine": "rule", 367 | "maxLength": 2, 368 | "checkType": "id" 369 | }, 370 | { 371 | "ruleType": "element", 372 | "engine": "rule", 373 | "skip": true 374 | }, 375 | { 376 | "ruleType": "element", 377 | "engine": "rule", 378 | "skip": true 379 | }, 380 | { 381 | "ruleType": "element", 382 | "engine": "rule", 383 | "checkType": "alphanumeric" 384 | } 385 | ], 386 | "mandatory": true 387 | }, 388 | { 389 | "ruleType": "segment", 390 | "engine": "rule", 391 | "tag": "PKG", 392 | "elements": [ 393 | { 394 | "ruleType": "element", 395 | "engine": "rule", 396 | "maxLength": 1, 397 | "checkType": "id" 398 | }, 399 | { 400 | "ruleType": "element", 401 | "engine": "rule", 402 | "maxLength": 2, 403 | "checkType": "id" 404 | }, 405 | { 406 | "ruleType": "element", 407 | "engine": "rule", 408 | "skip": true 409 | }, 410 | { 411 | "ruleType": "element", 412 | "engine": "rule", 413 | "skip": true 414 | }, 415 | { 416 | "ruleType": "element", 417 | "engine": "rule", 418 | "checkType": "alphanumeric" 419 | } 420 | ], 421 | "mandatory": true 422 | }, 423 | { 424 | "ruleType": "segment", 425 | "engine": "rule", 426 | "tag": "TD5", 427 | "elements": [ 428 | { 429 | "ruleType": "element", 430 | "engine": "rule", 431 | "maxLength": 1, 432 | "checkType": "id" 433 | }, 434 | { 435 | "ruleType": "element", 436 | "engine": "rule", 437 | "maxLength": 2, 438 | "checkType": "id" 439 | }, 440 | { 441 | "ruleType": "element", 442 | "engine": "rule", 443 | "maxLength": 2, 444 | "checkType": "id" 445 | }, 446 | { 447 | "ruleType": "element", 448 | "engine": "rule", 449 | "skip": true 450 | }, 451 | { 452 | "ruleType": "element", 453 | "engine": "rule", 454 | "checkType": "alphanumeric" 455 | } 456 | ], 457 | "mandatory": true 458 | }, 459 | { 460 | "ruleType": "segment", 461 | "engine": "rule", 462 | "tag": "N1", 463 | "elements": "skip", 464 | "mandatory": true 465 | }, 466 | { 467 | "ruleType": "segment", 468 | "engine": "rule", 469 | "tag": "N3", 470 | "elements": "skip", 471 | "mandatory": true 472 | }, 473 | { 474 | "ruleType": "segment", 475 | "engine": "rule", 476 | "tag": "N4", 477 | "elements": "skip", 478 | "mandatory": true 479 | }, 480 | { 481 | "ruleType": "segment", 482 | "engine": "rule", 483 | "tag": "PO1", 484 | "elements": [ 485 | { 486 | "ruleType": "element", 487 | "engine": "rule", 488 | "minMax": [1, 20], 489 | "checkType": "alphanumeric" 490 | }, 491 | { 492 | "ruleType": "element", 493 | "engine": "rule", 494 | "minMax": [1, 15], 495 | "checkType": "decimal" 496 | }, 497 | { 498 | "ruleType": "element", 499 | "engine": "rule", 500 | "expect": "EA" 501 | }, 502 | { 503 | "ruleType": "element", 504 | "engine": "rule", 505 | "minMax": [1, 17], 506 | "checkType": "decimal" 507 | }, 508 | { 509 | "ruleType": "element", 510 | "engine": "rule", 511 | "expect": "TE" 512 | }, 513 | { 514 | "ruleType": "element", 515 | "engine": "rule", 516 | "expect": "CB" 517 | }, 518 | { 519 | "ruleType": "element", 520 | "engine": "rule", 521 | "minMax": [10, 10], 522 | "checkType": "alphanumeric" 523 | }, 524 | { 525 | "ruleType": "element", 526 | "engine": "rule", 527 | "expect": "PR" 528 | }, 529 | { 530 | "ruleType": "element", 531 | "engine": "rule", 532 | "expect": "RO" 533 | }, 534 | { 535 | "ruleType": "element", 536 | "engine": "rule", 537 | "expect": "VN" 538 | }, 539 | { 540 | "ruleType": "element", 541 | "engine": "rule", 542 | "checkType": "alphanumeric" 543 | } 544 | ], 545 | "loopStart": true, 546 | "mandatory": true 547 | }, 548 | { 549 | "ruleType": "segment", 550 | "engine": "rule", 551 | "tag": "PID", 552 | "elements": "skip", 553 | "mandatory": true 554 | }, 555 | { 556 | "ruleType": "segment", 557 | "engine": "rule", 558 | "tag": "PO4", 559 | "elements": "skip", 560 | "loopEnd": true, 561 | "mandatory": true 562 | }, 563 | { 564 | "ruleType": "segment", 565 | "engine": "rule", 566 | "tag": "CTT", 567 | "elements": [ 568 | { 569 | "ruleType": "element", 570 | "engine": "rule", 571 | "minMax": [1, 6], 572 | "checkType": "number" 573 | } 574 | ], 575 | "mandatory": true 576 | }, 577 | { 578 | "ruleType": "segment", 579 | "engine": "rule", 580 | "tag": "AMT", 581 | "elements": [ 582 | { 583 | "ruleType": "element", 584 | "engine": "rule", 585 | "maxLength": 1, 586 | "checkType": "id" 587 | }, 588 | { 589 | "ruleType": "element", 590 | "engine": "rule", 591 | "checkType": "decimal" 592 | } 593 | ], 594 | "mandatory": true 595 | } 596 | ], 597 | "trailer": { 598 | "ruleType": "segment", 599 | "engine": "rule", 600 | "tag": "SE", 601 | "elements": [ 602 | { 603 | "ruleType": "element", 604 | "engine": "rule", 605 | "maxLength": 10, 606 | "mandatory": true, 607 | "checkType": "number" 608 | }, 609 | { 610 | "ruleType": "element", 611 | "engine": "rule", 612 | "minMax": [4, 9], 613 | "checkType": "number" 614 | } 615 | ], 616 | "mandatory": true 617 | } 618 | }, 619 | "trailer": { 620 | "ruleType": "segment", 621 | "engine": "rule", 622 | "tag": "GE", 623 | "elements": [ 624 | { 625 | "ruleType": "element", 626 | "engine": "rule", 627 | "minMax": [1, 6], 628 | "checkType": "number" 629 | }, 630 | { 631 | "ruleType": "element", 632 | "engine": "rule", 633 | "minMax": [1, 9], 634 | "checkType": "number" 635 | } 636 | ], 637 | "mandatory": true 638 | } 639 | }, 640 | "trailer": { 641 | "ruleType": "segment", 642 | "engine": "rule", 643 | "tag": "IEA", 644 | "elements": [ 645 | { 646 | "ruleType": "element", 647 | "engine": "rule", 648 | "maxLength": 5, 649 | "mandatory": true, 650 | "checkType": "number" 651 | }, 652 | { 653 | "ruleType": "element", 654 | "engine": "rule", 655 | "minMax": [9, 9], 656 | "padLength": true, 657 | "mandatory": true, 658 | "checkType": "number" 659 | } 660 | ] 661 | } 662 | } 663 | -------------------------------------------------------------------------------- /test/test-data/850_validation_no_headers.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleType": "interchange", 3 | "engine": "rule", 4 | "group": { 5 | "ruleType": "group", 6 | "engine": "rule", 7 | "transaction": { 8 | "ruleType": "transaction", 9 | "engine": "rule", 10 | "segments": [ 11 | { 12 | "ruleType": "segment", 13 | "engine": "rule", 14 | "tag": "BEG", 15 | "elements": [ 16 | { 17 | "ruleType": "element", 18 | "engine": "rule", 19 | "maxLength": 2, 20 | "checkType": "number" 21 | }, 22 | { 23 | "ruleType": "element", 24 | "engine": "rule", 25 | "maxLength": 2, 26 | "checkType": "id" 27 | }, 28 | { 29 | "ruleType": "element", 30 | "engine": "rule", 31 | "minLength": 1, 32 | "checkType": "alphanumeric" 33 | }, 34 | { 35 | "ruleType": "element", 36 | "engine": "rule", 37 | "skip": true 38 | }, 39 | { 40 | "ruleType": "element", 41 | "engine": "rule", 42 | "checkType": "date" 43 | }, 44 | { 45 | "ruleType": "element", 46 | "engine": "rule", 47 | "minLength": 1, 48 | "checkType": "alphanumeric" 49 | } 50 | ], 51 | "mandatory": true 52 | }, 53 | { 54 | "ruleType": "segment", 55 | "engine": "rule", 56 | "tag": "REF", 57 | "elements": [ 58 | { 59 | "ruleType": "element", 60 | "engine": "rule", 61 | "maxLength": 2, 62 | "checkType": "id" 63 | }, 64 | { 65 | "ruleType": "element", 66 | "engine": "rule", 67 | "minLength": 1, 68 | "checkType": "alphanumeric" 69 | } 70 | ], 71 | "mandatory": true 72 | }, 73 | { 74 | "ruleType": "segment", 75 | "engine": "rule", 76 | "tag": "REF", 77 | "elements": [ 78 | { 79 | "ruleType": "element", 80 | "engine": "rule", 81 | "maxLength": 2, 82 | "checkType": "id" 83 | }, 84 | { 85 | "ruleType": "element", 86 | "engine": "rule", 87 | "minLength": 1, 88 | "checkType": "alphanumeric" 89 | } 90 | ], 91 | "mandatory": true 92 | }, 93 | { 94 | "ruleType": "segment", 95 | "engine": "rule", 96 | "tag": "ITD", 97 | "elements": [ 98 | { 99 | "ruleType": "element", 100 | "engine": "rule", 101 | "maxLength": 2, 102 | "checkType": "id" 103 | }, 104 | { 105 | "ruleType": "element", 106 | "engine": "rule", 107 | "minLength": 1, 108 | "checkType": "alphanumeric" 109 | }, 110 | { 111 | "ruleType": "element", 112 | "engine": "rule", 113 | "minLength": 1, 114 | "checkType": "alphanumeric" 115 | }, 116 | { 117 | "ruleType": "element", 118 | "engine": "rule", 119 | "skip": true 120 | }, 121 | { 122 | "ruleType": "element", 123 | "engine": "rule", 124 | "maxLength": 2, 125 | "checkType": "id" 126 | }, 127 | { 128 | "ruleType": "element", 129 | "engine": "rule", 130 | "skip": true 131 | }, 132 | { 133 | "ruleType": "element", 134 | "engine": "rule", 135 | "maxLength": 2, 136 | "checkType": "id" 137 | } 138 | ], 139 | "mandatory": true 140 | }, 141 | { 142 | "ruleType": "segment", 143 | "engine": "rule", 144 | "tag": "DTM", 145 | "elements": [ 146 | { 147 | "ruleType": "element", 148 | "engine": "rule", 149 | "maxLength": 3, 150 | "checkType": "id" 151 | }, 152 | { 153 | "ruleType": "element", 154 | "engine": "rule", 155 | "checkType": "datelong" 156 | } 157 | ], 158 | "mandatory": true 159 | }, 160 | { 161 | "ruleType": "segment", 162 | "engine": "rule", 163 | "tag": "PKG", 164 | "elements": [ 165 | { 166 | "ruleType": "element", 167 | "engine": "rule", 168 | "maxLength": 1, 169 | "checkType": "id" 170 | }, 171 | { 172 | "ruleType": "element", 173 | "engine": "rule", 174 | "maxLength": 2, 175 | "checkType": "id" 176 | }, 177 | { 178 | "ruleType": "element", 179 | "engine": "rule", 180 | "skip": true 181 | }, 182 | { 183 | "ruleType": "element", 184 | "engine": "rule", 185 | "skip": true 186 | }, 187 | { 188 | "ruleType": "element", 189 | "engine": "rule", 190 | "checkType": "alphanumeric" 191 | } 192 | ], 193 | "mandatory": true 194 | }, 195 | { 196 | "ruleType": "segment", 197 | "engine": "rule", 198 | "tag": "PKG", 199 | "elements": [ 200 | { 201 | "ruleType": "element", 202 | "engine": "rule", 203 | "maxLength": 1, 204 | "checkType": "id" 205 | }, 206 | { 207 | "ruleType": "element", 208 | "engine": "rule", 209 | "maxLength": 2, 210 | "checkType": "id" 211 | }, 212 | { 213 | "ruleType": "element", 214 | "engine": "rule", 215 | "skip": true 216 | }, 217 | { 218 | "ruleType": "element", 219 | "engine": "rule", 220 | "skip": true 221 | }, 222 | { 223 | "ruleType": "element", 224 | "engine": "rule", 225 | "checkType": "alphanumeric" 226 | } 227 | ], 228 | "mandatory": true 229 | }, 230 | { 231 | "ruleType": "segment", 232 | "engine": "rule", 233 | "tag": "TD5", 234 | "elements": [ 235 | { 236 | "ruleType": "element", 237 | "engine": "rule", 238 | "maxLength": 1, 239 | "checkType": "id" 240 | }, 241 | { 242 | "ruleType": "element", 243 | "engine": "rule", 244 | "maxLength": 2, 245 | "checkType": "id" 246 | }, 247 | { 248 | "ruleType": "element", 249 | "engine": "rule", 250 | "maxLength": 2, 251 | "checkType": "id" 252 | }, 253 | { 254 | "ruleType": "element", 255 | "engine": "rule", 256 | "skip": true 257 | }, 258 | { 259 | "ruleType": "element", 260 | "engine": "rule", 261 | "checkType": "alphanumeric" 262 | } 263 | ], 264 | "mandatory": true 265 | }, 266 | { 267 | "ruleType": "segment", 268 | "engine": "rule", 269 | "tag": "N1", 270 | "elements": "skip", 271 | "mandatory": true 272 | }, 273 | { 274 | "ruleType": "segment", 275 | "engine": "rule", 276 | "tag": "N3", 277 | "elements": "skip", 278 | "mandatory": true 279 | }, 280 | { 281 | "ruleType": "segment", 282 | "engine": "rule", 283 | "tag": "N4", 284 | "elements": "skip", 285 | "mandatory": true 286 | }, 287 | { 288 | "ruleType": "segment", 289 | "engine": "rule", 290 | "tag": "PO1", 291 | "elements": [ 292 | { 293 | "ruleType": "element", 294 | "engine": "rule", 295 | "minMax": [1, 20], 296 | "checkType": "alphanumeric" 297 | }, 298 | { 299 | "ruleType": "element", 300 | "engine": "rule", 301 | "minMax": [1, 15], 302 | "checkType": "decimal" 303 | }, 304 | { 305 | "ruleType": "element", 306 | "engine": "rule", 307 | "expect": "EA" 308 | }, 309 | { 310 | "ruleType": "element", 311 | "engine": "rule", 312 | "minMax": [1, 17], 313 | "checkType": "decimal" 314 | }, 315 | { 316 | "ruleType": "element", 317 | "engine": "rule", 318 | "expect": "TE" 319 | }, 320 | { 321 | "ruleType": "element", 322 | "engine": "rule", 323 | "expect": "CB" 324 | }, 325 | { 326 | "ruleType": "element", 327 | "engine": "rule", 328 | "minMax": [10, 10], 329 | "checkType": "alphanumeric" 330 | }, 331 | { 332 | "ruleType": "element", 333 | "engine": "rule", 334 | "expect": "PR" 335 | }, 336 | { 337 | "ruleType": "element", 338 | "engine": "rule", 339 | "expect": "RO" 340 | }, 341 | { 342 | "ruleType": "element", 343 | "engine": "rule", 344 | "expect": "VN" 345 | }, 346 | { 347 | "ruleType": "element", 348 | "engine": "rule", 349 | "checkType": "alphanumeric" 350 | } 351 | ], 352 | "loopStart": true, 353 | "mandatory": true 354 | }, 355 | { 356 | "ruleType": "segment", 357 | "engine": "rule", 358 | "tag": "PID", 359 | "elements": "skip", 360 | "mandatory": true 361 | }, 362 | { 363 | "ruleType": "segment", 364 | "engine": "rule", 365 | "tag": "PO4", 366 | "elements": "skip", 367 | "loopEnd": true, 368 | "mandatory": true 369 | }, 370 | { 371 | "ruleType": "segment", 372 | "engine": "rule", 373 | "tag": "CTT", 374 | "elements": [ 375 | { 376 | "ruleType": "element", 377 | "engine": "rule", 378 | "minMax": [1, 6], 379 | "checkType": "number" 380 | } 381 | ], 382 | "mandatory": true 383 | }, 384 | { 385 | "ruleType": "segment", 386 | "engine": "rule", 387 | "tag": "AMT", 388 | "elements": [ 389 | { 390 | "ruleType": "element", 391 | "engine": "rule", 392 | "maxLength": 1, 393 | "checkType": "id" 394 | }, 395 | { 396 | "ruleType": "element", 397 | "engine": "rule", 398 | "checkType": "decimal" 399 | } 400 | ], 401 | "mandatory": true 402 | } 403 | ] 404 | } 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /test/test-data/850_validation_simple.rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": { 3 | "transaction": { 4 | "segments": [ 5 | { 6 | "tag": "BEG", 7 | "elements": [ 8 | { 9 | "maxLength": 2, 10 | "checkType": "number" 11 | }, 12 | { 13 | "maxLength": 2, 14 | "checkType": "id" 15 | }, 16 | { 17 | "minLength": 1, 18 | "checkType": "alphanumeric" 19 | }, 20 | { 21 | "skip": true 22 | }, 23 | { 24 | "checkType": "date" 25 | }, 26 | { 27 | "engine": "^[0-9]+$" 28 | } 29 | ], 30 | "mandatory": true 31 | }, 32 | { 33 | "tag": "REF", 34 | "elements": [ 35 | { 36 | "maxLength": 2, 37 | "checkType": "id" 38 | }, 39 | { 40 | "minLength": 1, 41 | "checkType": "alphanumeric" 42 | } 43 | ], 44 | "mandatory": true 45 | }, 46 | { 47 | "tag": "REF", 48 | "elements": [ 49 | { 50 | "maxLength": 2, 51 | "checkType": "id" 52 | }, 53 | { 54 | "minLength": 1, 55 | "checkType": "alphanumeric" 56 | } 57 | ], 58 | "mandatory": true 59 | }, 60 | { 61 | "tag": "ITD", 62 | "elements": [ 63 | { 64 | "maxLength": 2, 65 | "checkType": "id" 66 | }, 67 | { 68 | "minLength": 1, 69 | "checkType": "alphanumeric" 70 | }, 71 | { 72 | "minLength": 1, 73 | "checkType": "alphanumeric" 74 | }, 75 | { 76 | "skip": true 77 | }, 78 | { 79 | "maxLength": 2, 80 | "checkType": "id" 81 | }, 82 | { 83 | "skip": true 84 | }, 85 | { 86 | "maxLength": 2, 87 | "checkType": "id" 88 | } 89 | ], 90 | "mandatory": true 91 | }, 92 | { 93 | "tag": "DTM", 94 | "elements": [ 95 | { 96 | "maxLength": 3, 97 | "checkType": "id" 98 | }, 99 | { 100 | "checkType": "datelong" 101 | } 102 | ], 103 | "mandatory": true 104 | }, 105 | { 106 | "tag": "PKG", 107 | "elements": [ 108 | { 109 | "maxLength": 1, 110 | "checkType": "id" 111 | }, 112 | { 113 | "maxLength": 2, 114 | "checkType": "id" 115 | }, 116 | { 117 | "skip": true 118 | }, 119 | { 120 | "skip": true 121 | }, 122 | { 123 | "checkType": "alphanumeric" 124 | } 125 | ], 126 | "mandatory": true 127 | }, 128 | { 129 | "tag": "PKG", 130 | "elements": [ 131 | { 132 | "maxLength": 1, 133 | "checkType": "id" 134 | }, 135 | { 136 | "maxLength": 2, 137 | "checkType": "id" 138 | }, 139 | { 140 | "skip": true 141 | }, 142 | { 143 | "skip": true 144 | }, 145 | { 146 | "checkType": "alphanumeric" 147 | } 148 | ], 149 | "mandatory": true 150 | }, 151 | { 152 | "tag": "TD5", 153 | "elements": [ 154 | { 155 | "maxLength": 1, 156 | "checkType": "id" 157 | }, 158 | { 159 | "maxLength": 2, 160 | "checkType": "id" 161 | }, 162 | { 163 | "maxLength": 2, 164 | "checkType": "id" 165 | }, 166 | { 167 | "skip": true 168 | }, 169 | { 170 | "checkType": "alphanumeric" 171 | } 172 | ], 173 | "mandatory": true 174 | }, 175 | { 176 | "tag": "N1", 177 | "elements": "skip", 178 | "mandatory": true 179 | }, 180 | { 181 | "tag": "N3", 182 | "elements": "skip", 183 | "mandatory": true 184 | }, 185 | { 186 | "tag": "N4", 187 | "elements": "skip", 188 | "mandatory": true 189 | }, 190 | { 191 | "tag": "PO1", 192 | "elements": [ 193 | { 194 | "minMax": [1, 20], 195 | "checkType": "alphanumeric" 196 | }, 197 | { 198 | "minMax": [1, 15], 199 | "checkType": "decimal" 200 | }, 201 | { 202 | "expect": "EA" 203 | }, 204 | { 205 | "minMax": [1, 17], 206 | "checkType": "decimal" 207 | }, 208 | { 209 | "expect": "TE" 210 | }, 211 | { 212 | "expect": "CB" 213 | }, 214 | { 215 | "minMax": [10, 10], 216 | "checkType": "alphanumeric" 217 | }, 218 | { 219 | "expect": "PR" 220 | }, 221 | { 222 | "expect": "RO" 223 | }, 224 | { 225 | "expect": "VN" 226 | }, 227 | { 228 | "checkType": "alphanumeric" 229 | } 230 | ], 231 | "loopStart": true, 232 | "mandatory": true 233 | }, 234 | { 235 | "tag": "PID", 236 | "elements": "skip", 237 | "mandatory": true 238 | }, 239 | { 240 | "tag": "PO4", 241 | "elements": "skip", 242 | "loopEnd": true, 243 | "mandatory": true 244 | }, 245 | { 246 | "tag": "CTT", 247 | "elements": [ 248 | { 249 | "minMax": [1, 6], 250 | "checkType": "number" 251 | } 252 | ], 253 | "mandatory": true 254 | }, 255 | { 256 | "tag": "AMT", 257 | "elements": [ 258 | { 259 | "maxLength": 1, 260 | "checkType": "id" 261 | }, 262 | { 263 | "checkType": "decimal" 264 | } 265 | ], 266 | "mandatory": true 267 | } 268 | ] 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /test/test-data/855.edi: -------------------------------------------------------------------------------- 1 | ISA*00* *00* *ZZ*1556150 *ZZ*123MILL *201015*1534*U*00401*000000005*0*P*>~ 2 | GS*PR*1556150*123MILL*20201015*1534*5*X*004010~ 3 | ST*855*0005~ 4 | BAK*00*AD*POTEST1112*20201015**60169126***20201015~ 5 | CUR*BT*USD~ 6 | REF*ZZZ*ORDER PROCESSED~ 7 | CSH****123MILL 0001~ 8 | N1*ST*EDI TEST ACCOUNT-SUFFIX*15*123MILL 0001~ 9 | N3*REGULAR EDI ACCOUNT*ATTN ACQ SERVS~ 10 | N3*7615 DISALLE BOULEVARD~ 11 | N4*FORT WAYNE*IN*46825~ 12 | PO1*1*3*UN*4.5*PE*EN*9780446360265*B6*NEUFELDT, VICTORIA*B3*00*B4*P~ 13 | PID*F****DIC WEBSTERS NEW WORLD~ 14 | ACK*IA*3*UN************************BI*ACK*AC~ 15 | SCH*3*UN*WH*MO*080*20041231*****000~ 16 | PO1*2*2*UN*15.99*PE*EN*9780767928427*B6*CLAPTON, ERIC*B3*00~ 17 | PID*F****CLAPTON THE AUTOBIOGRAPHY~ 18 | ACK*IR*0*UN************************BI*ACK*CW~ 19 | SCH*0*UN*WH*MO*080*20071009*****AD~ 20 | PO1*3*1*UN*0*PE*EN*9780099599531~ 21 | ACK*IR*0*UN************************BI*ACK*KK~ 22 | SCH*0*UN*WH*CO*080*20041231*****NFC~ 23 | PO1*4*2*UN*4.5*PE*EN*9780446360272*B6*NOT APPLICABLE*B3*00*B4*P~ 24 | PID*F****WEBSTERS NEW WORLD THESAURUS~ 25 | ACK*IA*2*UN************************BI*ACK*AC~ 26 | SCH*2*UN*WH*MO*080*20041231*****000~ 27 | CTT*4*5~ 28 | SE*26*0005~ 29 | GE*1*5~ 30 | IEA*1*000000005~ 31 | -------------------------------------------------------------------------------- /test/test-data/856.edi: -------------------------------------------------------------------------------- 1 | ISA*01*0000000000*01*ABCCO *12*4405197800 *01*999999999 *111206*1719*-*00406*000000049*0*P*>~ 2 | GS*SH*4405197800*999999999*20111206*1045*49*X*004060~ 3 | ST*856*0008~ 4 | BSN*14*829716*20111206*142428*0002~ 5 | HL*1**S~ 6 | TD1*PCS*2****A3*60.310*LB~ 7 | TD5**2*XXXX**XXXX~ 8 | REF*BM*999999-001~ 9 | REF*CN*5787970539~ 10 | DTM*011*20111206~ 11 | N1*SH*1 EDI SOURCE~ 12 | N3*31875 SOLON RD~ 13 | N4*SOLON*OH*44139~ 14 | N1*OB*XYZ RETAIL~ 15 | N3*P O BOX 9999999~ 16 | N4*ATLANTA*GA*31139-0020**SN*9999~ 17 | N1*SF*1 EDI SOURCE~ 18 | N3*31875 SOLON ROAD~ 19 | N4*SOLON*OH*44139~ 20 | HL*2*1*O~ 21 | PRF*99999817***20111205~ 22 | HL*3*2*I~ 23 | LIN*1*VP*87787D*UP*999999310145~ 24 | SN1*1*24*EA~ 25 | PO4*1*24*EA~ 26 | PID*F****BLUE WIDGET~ 27 | HL*4*2*I~ 28 | LIN*2*VP*99887D*UP*999999311746~ 29 | SN1*2*6*EA~ 30 | PO4*1*6*EA~ 31 | PID*F****RED WIDGET~ 32 | CTT*4*30~ 33 | SE*31*0008~ 34 | GE*1*49~ 35 | IEA*1*000000049~ -------------------------------------------------------------------------------- /test/test-data/Transaction_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "internalOrderId": "ORDER-1234", 3 | "orderId": "OD-879", 4 | "shippingFirstName": "Somebody", 5 | "shippingLastName": "Once", 6 | "shippingStreet1": "456 Told Me Ave.", 7 | "shippingStreet2": "", 8 | "shippingCity": "Dubuque", 9 | "shippingStateCode": "IA", 10 | "shippingPostalCode": "87654", 11 | "shippingCountryCode": "US", 12 | "orderItems": "[{\"quantity\":3,\"sku\":\"SKU-123\",\"title\":\"Some Title\",\"weight\":12.134,\"volume\":2.8},{\"quantity\":1,\"sku\":\"SKU-456\",\"title\":\"Some Title 2\",\"weight\":10.2,\"volume\":4.18}]" 13 | } 14 | -------------------------------------------------------------------------------- /test/test-data/Transaction_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": ["940", "macro['random']()['val']"], 3 | "segments": [ 4 | { 5 | "tag": "W05", 6 | "elements": ["N", "input['internalOrderId']", "input['orderId']"] 7 | }, 8 | { 9 | "tag": "N1", 10 | "elements": [ 11 | "ST", 12 | "`${input['shippingFirstName']} ${input['shippingLastName']}`" 13 | ] 14 | }, 15 | { 16 | "tag": "N3", 17 | "elements": ["input['shippingStreet1']", "input['shippingStreet2']"] 18 | }, 19 | { 20 | "tag": "N4", 21 | "elements": [ 22 | "input['shippingCity']", 23 | "input['shippingStateCode']", 24 | "input['shippingPostalCode']", 25 | "input['shippingCountryCode']" 26 | ] 27 | }, 28 | { "tag": "N1", "elements": ["BT", "My Company LLC"] }, 29 | { "tag": "N3", "elements": ["1234 Company Dr"] }, 30 | { "tag": "N4", "elements": ["Madison", "WI", "12345", "US"] }, 31 | { "tag": "N9", "elements": ["VR", "54321"] }, 32 | { "tag": "N9", "elements": ["14", "567"] }, 33 | { "tag": "N9", "elements": ["11", "8765"] }, 34 | { "tag": "N9", "elements": ["12", "987654321"] }, 35 | { "tag": "N9", "elements": ["23", "12345"] }, 36 | { "tag": "G62", "elements": ["37", "macro['currentDate']"] }, 37 | { 38 | "tag": "W66", 39 | "elements": ["PP", "M", "", "", "GR", "", "", "", "", "UPSN"] 40 | }, 41 | { 42 | "tag": "LX", 43 | "elements": ["macro['sequence']('LX')['val']"], 44 | "loopStart": true, 45 | "loopLength": "macro['length'](macro['json'](input['orderItems'])['val'])['val']" 46 | }, 47 | { 48 | "tag": "W01", 49 | "elements": [ 50 | "macro['map'](macro['json'](input['orderItems'])['val'], 'quantity')['val']", 51 | "EA", 52 | "", 53 | "VN", 54 | "macro['map'](macro['json'](input['orderItems'])['val'], 'sku')['val']" 55 | ] 56 | }, 57 | { 58 | "tag": "G69", 59 | "elements": [ 60 | "macro['truncate'](macro['map'](macro['json'](input['orderItems'])['val'], 'title')['val'], 45)['val']" 61 | ], 62 | "loopEnd": true 63 | }, 64 | { 65 | "tag": "W76", 66 | "elements": [ 67 | "macro['sum'](macro['json'](input['orderItems'])['val'], 'quantity')['val']", 68 | "macro['toFixed'](macro['sum'](macro['json'](input['orderItems'])['val'], 'weight', 2)['val'], 2)['val']", 69 | "LB", 70 | "macro['toFixed'](macro['sum'](macro['json'](input['orderItems'])['val'], 'volume', 2)['val'], 2)['val']", 71 | "CF" 72 | ] 73 | } 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /test/test-data/Transaction_map_liquidjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": ["940", "{{ macro | random }}"], 3 | "segments": [ 4 | { 5 | "tag": "W05", 6 | "elements": ["N", "{{ input.internalOrderId }}", "{{ input.orderId }}"] 7 | }, 8 | { 9 | "tag": "N1", 10 | "elements": [ 11 | "ST", 12 | "{{ input.shippingFirstName }} {{ input.shippingLastName }}" 13 | ] 14 | }, 15 | { 16 | "tag": "N3", 17 | "elements": ["{{ input.shippingStreet1 }}", "{{ input.shippingStreet2 }}"] 18 | }, 19 | { 20 | "tag": "N4", 21 | "elements": [ 22 | "{{ input.shippingCity }}", 23 | "{{ input.shippingStateCode }}", 24 | "{{ input.shippingPostalCode }}", 25 | "{{ input.shippingCountryCode }}" 26 | ] 27 | }, 28 | { "tag": "N1", "elements": ["BT", "My Company LLC"] }, 29 | { "tag": "N3", "elements": ["1234 Company Dr"] }, 30 | { "tag": "N4", "elements": ["Madison", "WI", "12345", "US"] }, 31 | { "tag": "N9", "elements": ["VR", "54321"] }, 32 | { "tag": "N9", "elements": ["14", "567"] }, 33 | { "tag": "N9", "elements": ["11", "8765"] }, 34 | { "tag": "N9", "elements": ["12", "987654321"] }, 35 | { "tag": "N9", "elements": ["23", "12345"] }, 36 | { "tag": "G62", "elements": ["37", "{{ macro | edi_date }}"] }, 37 | { 38 | "tag": "W66", 39 | "elements": ["PP", "M", "", "", "GR", "", "", "", "", "UPSN"] 40 | }, 41 | { 42 | "tag": "LX", 43 | "elements": ["{{ 'LX' | sequence }}"], 44 | "loopStart": true, 45 | "loopLength": "{{ input.orderItems | json_parse | size }}" 46 | }, 47 | { 48 | "tag": "W01", 49 | "elements": [ 50 | "{{ input.orderItems | json_parse | map: 'quantity' | in_loop }}", 51 | "EA", 52 | "", 53 | "VN", 54 | "{{ input.orderItems | json_parse | map: 'sku' | in_loop }}" 55 | ] 56 | }, 57 | { 58 | "tag": "G69", 59 | "elements": [ 60 | "{{ input.orderItems | json_parse | map: 'title' | truncate: 45 | in_loop }}" 61 | ], 62 | "loopEnd": true 63 | }, 64 | { 65 | "tag": "W76", 66 | "elements": [ 67 | "{{ input.orderItems | json_parse | map: 'quantity' | sum_array }}", 68 | "{{ input.orderItems | json_parse | map: 'weight' | sum_array | to_fixed: 2 }}", 69 | "LB", 70 | "{{ input.orderItems | json_parse | map: 'volume' | sum_array | to_fixed: 2 }}", 71 | "CF" 72 | ] 73 | } 74 | ] 75 | } 76 | --------------------------------------------------------------------------------