├── .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 | [](https://deno.land/x/x12)
8 | [](https://badge.fury.io/js/node-x12)
9 | []()
10 | []()
11 | []()
12 | []()
13 | []()
14 | []()
15 | []()
16 | [](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12)
17 | [](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12)
18 | [](https://sonarcloud.io/dashboard?id=aaronhuggins_node-x12)
19 | [](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 |
--------------------------------------------------------------------------------