diff --git a/deno.jsonc b/deno.jsonc index 4e3b036..1c58e55 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,7 +1,12 @@ { + // Build output. Without this, `deno test` walks npm/ and type-checks dnt's + // copied tests, which cannot resolve their shims outside an npm install. + "exclude": ["npm", "coverage"], "tasks": { // Allows shorthand test command with permissions baked in. - "test": "deno test --unstable --allow-read --coverage=coverage", + // --allow-import is needed for the deno.land test imports; --unstable was + // removed in Deno 2 and nothing here needs it. + "test": "deno test --allow-read --allow-import --coverage=coverage", "lcov": "deno coverage coverage --lcov --output=coverage/report.lcov", "cover": "deno task clean && deno task test && deno task lcov && genhtml -o coverage/html coverage/report.lcov", // Command to build for npm. diff --git a/src/Errors.ts b/src/Errors.ts index 19a20d0..48a9831 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -6,7 +6,7 @@ export class ArgumentNullError extends Error { this.name = "ArgumentNullError"; } - name: string; + override name: string; } export class GeneratorError extends Error { @@ -15,7 +15,7 @@ export class GeneratorError extends Error { this.name = "GeneratorError"; } - name: string; + override name: string; } export class ParserError extends Error { @@ -24,7 +24,7 @@ export class ParserError extends Error { this.name = "ParserError"; } - name: string; + override name: string; } export class QuerySyntaxError extends Error { @@ -33,5 +33,5 @@ export class QuerySyntaxError extends Error { this.name = "QuerySyntaxError"; } - name: string; + override name: string; } diff --git a/src/X12FatInterchange.ts b/src/X12FatInterchange.ts index 9d48fee..055f6cc 100644 --- a/src/X12FatInterchange.ts +++ b/src/X12FatInterchange.ts @@ -38,7 +38,7 @@ export class X12FatInterchange extends Array { * @param {X12SerializationOptions} [options] - Options to override serializing back to EDI. * @returns {string} This fat interchange converted to EDI string. */ - toString(options?: X12SerializationOptions): string { + override toString(options?: X12SerializationOptions): string { options = options !== undefined ? defaultSerializationOptions(options) : this.options; diff --git a/src/X12Parser.ts b/src/X12Parser.ts index 40b500c..98333fe 100644 --- a/src/X12Parser.ts +++ b/src/X12Parser.ts @@ -641,7 +641,7 @@ export class X12Parser extends Transform { * @description Flush method for Node API Transform stream. * @param {Function} callback - Callback to execute when finished. */ - public _flush(callback: Function): void { + public override _flush(callback: Function): void { this._flushing = true; this._consumeChunk(this._dataCache); this._flushing = false; @@ -657,7 +657,7 @@ export class X12Parser extends Transform { * @param {string} encoding - Chunk enoding. * @param {Function} callback - Callback signalling chunk is processed and instance is ready for next chunk. */ - public _transform(chunk: any, _encoding: string, callback: Function): void { + public override _transform(chunk: any, _encoding: string, callback: Function): void { this._consumeChunk(this._decoder.write(chunk)); callback(); diff --git a/src/X12QueryEngine.ts b/src/X12QueryEngine.ts index 40f678e..663bdd1 100644 --- a/src/X12QueryEngine.ts +++ b/src/X12QueryEngine.ts @@ -61,8 +61,13 @@ export class X12QueryEngine { } const hlPathMatch = reference.match(/HL\+(\w\+?)+[+-]/g); // ex. HL+O+P+I - const segPathMatch = reference.match(/((? { + clone[key] = value; + + if (Array.isArray(clones)) { + clones.forEach((cloned: any) => { + cloned[key] = value !== null && typeof value === "object" + ? JSON.parse(JSON.stringify(value)) + : value; + }); + } + }; const engine = new X12QueryEngine(false, this._mode); const interchange = new X12Interchange(); interchange.setHeader([ @@ -214,19 +230,20 @@ export class X12TransactionMap { newArray.push(this.helper(key, result.value, query, callback)); } } catch (err) { + const message = err instanceof Error ? err.message : String(err); throw new QuerySyntaxError( - `${err.message}; bad query in ${map[key]}`, + `${message}; bad query in ${map[key]}`, ); } }); - clone[key] = newArray; + assign(key, newArray); } else if (typeof map[key] === "string") { try { const result = engine.querySingle(interchange, map[key], ""); if (result === null) { - clone[key] = null; + assign(key, null); } else if (result.value === null || Array.isArray(clones)) { if (result.value !== null) { clones.forEach((cloned: any) => { @@ -256,15 +273,16 @@ export class X12TransactionMap { }); } } else { - clone[key] = this.helper(key, result.value, map[key], callback); + assign(key, this.helper(key, result.value, map[key], callback)); } } catch (err) { + const message = err instanceof Error ? err.message : String(err); throw new QuerySyntaxError( - `${err.message}; bad query in ${map[key]}`, + `${message}; bad query in ${map[key]}`, ); } } else { - clone[key] = this.toObject(map[key]); + assign(key, this.toObject(map[key])); } } }); diff --git a/src/X12ValidationEngine/X12ValidationRule.ts b/src/X12ValidationEngine/X12ValidationRule.ts index 21668af..bdd3599 100644 --- a/src/X12ValidationEngine/X12ValidationRule.ts +++ b/src/X12ValidationEngine/X12ValidationRule.ts @@ -136,7 +136,7 @@ export class X12InterchangeRule extends X12ValidationRule { header: X12SegmentRule; trailer: X12SegmentRule; - assert?(interchange: X12Interchange): true | ValidationReport { + override assert?(interchange: X12Interchange): true | ValidationReport { const report: ValidationReport = {}; const headerResult = this.header.assert?.(interchange.header); let pass = true; @@ -222,7 +222,7 @@ export class X12GroupRule extends X12ValidationRule { header: X12SegmentRule; trailer: X12SegmentRule; - assert?( + override assert?( group: X12FunctionalGroup, controlNumber: number, ): true | ValidationReport { @@ -336,7 +336,7 @@ export class X12TransactionRule extends X12ValidationRule { header: X12SegmentRule; trailer: X12SegmentRule; - assert?( + override assert?( transaction: X12Transaction, controlNumber: number, ): true | ValidationReport { @@ -504,7 +504,7 @@ export class X12SegmentRule extends X12ValidationRule { loopEnd?: boolean; mandatory?: boolean; - assert?(segment: X12Segment, position = 1): true | ValidationReport { + override assert?(segment: X12Segment, position = 1): true | ValidationReport { const errors: ValidationError[] = []; const elements: ValidationError[] = []; @@ -587,7 +587,7 @@ export class X12ElementRule extends X12ValidationRule { | "gs01" | "st01"; - assert?(element: X12Element, position?: number): true | ValidationReport { + override assert?(element: X12Element, position?: number): true | ValidationReport { if (this.skip) return true; if (typeof element === "undefined") { return { elements: [errorLookup(this.ruleType, "2", position, "")] }; diff --git a/test/GeneratorSuite_test.ts b/test/GeneratorSuite_test.ts index e0ff645..489e8d2 100644 --- a/test/GeneratorSuite_test.ts +++ b/test/GeneratorSuite_test.ts @@ -76,7 +76,7 @@ describe("X12Generator", () => { try { t.setHeader([...fileEdi[2].split("*").slice(1), "N"]); } catch (err) { - error = err.message; + error = (err as Error).message; } if ( @@ -166,7 +166,7 @@ describe("X12Generator", () => { const generator = new X12Generator(JSON.parse(json), options); generator.toString(); } catch (err) { - error = err.message; + error = (err as Error).message; } if ( diff --git a/test/MappingSuite_test.ts b/test/MappingSuite_test.ts index eb12467..d9d2ddd 100644 --- a/test/MappingSuite_test.ts +++ b/test/MappingSuite_test.ts @@ -99,4 +99,71 @@ describe("X12Mapping", () => { assert.deepStrictEqual(resultLoose[2], { author: "" }); assert.deepStrictEqual(resultStrict[2], { author: "NOT APPLICABLE" }); }); + + it('should return null for nonexistent values when using FOREACH', () => { + const parser = new X12Parser() + const interchange = parser.parse(edi) as X12Interchange + const transaction = interchange.functionalGroups[0].transactions[0] + + const mapper1 = new X12TransactionMap({ TestKey: 'PR03' }, transaction) + const wOutFOREACH = mapper1.toObject() + + const mapper2 = new X12TransactionMap({ TestFOREACH: 'FOREACH(PO1)=>PO102', TestKey: 'PR03' }, transaction) + const wFOREACHFirst = mapper2.toObject() + + const mapper3 = new X12TransactionMap({ TestKey: 'PR03', TestFOREACH: 'FOREACH(PO1)=>PO102' }, transaction) + const wFOREACHAfter = mapper3.toObject() + + // A map containing a FOREACH resolves to one row per value, so the comparison + // has to reach into a row; reading TestKey off the array itself is undefined + // no matter what the mapper does. + if (wOutFOREACH.TestKey !== wFOREACHFirst[0].TestKey || wFOREACHFirst[0].TestKey !== wFOREACHAfter[0].TestKey) { + throw new Error('Got different result based on location of FOREACH. Expected the same result no matter what.') + } + }); + + it('should resolve array and nested map values after a FOREACH', () => { + const parser = new X12Parser() + const interchange = parser.parse(edi) as X12Interchange + const transaction = interchange.functionalGroups[0].transactions[0] + const foreach = 'FOREACH(PO1)=>PO102' + + // REF02 resolves to "038" and PR03 matches nothing. Every one of these keys + // is resolved after the FOREACH key, so each row has to carry the resolved + // value rather than the query string the row was cloned with. + const map = { + TestFOREACH: foreach, + TestString: 'REF02', + TestArray: ['REF02'], + TestNested: { Inner: 'REF02' }, + TestMissString: 'PR03', + TestMissArray: ['PR03'], + TestMissNested: { Inner: 'PR03' }, + } + const rows = new X12TransactionMap(map, transaction).toObject() + + assert.strictEqual(Array.isArray(rows), true) + + for (const row of rows) { + assert.strictEqual(row.TestString, '038') + assert.deepStrictEqual(row.TestArray, ['038']) + assert.deepStrictEqual(row.TestNested, { Inner: '038' }) + assert.strictEqual(row.TestMissString, null) + assert.deepStrictEqual(row.TestMissArray, [null]) + assert.deepStrictEqual(row.TestMissNested, { Inner: null }) + } + }); + + it('should not share object values between FOREACH rows', () => { + const parser = new X12Parser() + const interchange = parser.parse(edi) as X12Interchange + const transaction = interchange.functionalGroups[0].transactions[0] + + const map = { TestFOREACH: 'FOREACH(PO1)=>PO102', TestNested: { Inner: 'REF02' } } + const rows = new X12TransactionMap(map, transaction).toObject() + + rows[0].TestNested.Inner = 'MUTATED' + + assert.strictEqual(rows[1].TestNested.Inner, '038') + }); }); diff --git a/test/ParserSuite_test.ts b/test/ParserSuite_test.ts index 875bdd8..197b750 100644 --- a/test/ParserSuite_test.ts +++ b/test/ParserSuite_test.ts @@ -93,7 +93,7 @@ describe("X12Parser", () => { error = err; } - if (error.name !== "ArgumentNullError") { + if ((error as Error).name !== "ArgumentNullError") { throw new Error( "ArgumentNullError expected when first argument to X12Parser.parse() is undefined.", ); @@ -110,7 +110,7 @@ describe("X12Parser", () => { error = err; } - if (error.name !== "ParserError") { + if ((error as Error).name !== "ParserError") { throw new Error( "ParserError expected when document length is too short and parser is strict.", ); @@ -128,7 +128,7 @@ describe("X12Parser", () => { error = err; } - if (error.name !== "ParserError") { + if ((error as Error).name !== "ParserError") { throw new Error( "ParserError expected when elementDelimiter in document does not match and parser is strict.", ); diff --git a/test/QuerySuite_test.ts b/test/QuerySuite_test.ts index 16f61bb..b6d65ab 100644 --- a/test/QuerySuite_test.ts +++ b/test/QuerySuite_test.ts @@ -41,6 +41,59 @@ describe("X12QueryEngine", () => { } }); + it("should scope a segment path to the innermost loop", () => { + // 835.edi carries an OA adjustment on the claim (CAS*OA*109*-494.94) and one + // on each service line (CAS*OA*100 and CAS*OA*45). All three qualify on + // CAS01 === "OA", so only the CLP-SVC- path prefix can separate them. + // + // Note the result is the first service line alone: a multi-part path stops + // qualifying at the second SVC and never sees a second CLP to restart from. + // A single-part path such as PO1- re-qualifies on every loop. + const edi = Deno.readTextFileSync("test/test-data/835.edi"); + const parser = new X12Parser(true); + const engine = new X12QueryEngine(parser); + const results = engine.query(edi, 'CLP-SVC-CAS03:CAS01["OA"]'); + const values = results.map((result) => result.value); + + if (values.includes("-494.94")) { + throw new Error( + `Expected CLP-SVC-CAS03:CAS01["OA"] to exclude the claim-level adjustment; received ${values.join(", ")}.`, + ); + } else if (values.length !== 1 || values[0] !== "88.80") { + throw new Error( + `Expected the service line adjustment 88.80; received ${values.join(", ")}.`, + ); + } + }); + + it("should return every matching segment when there is no segment path", () => { + // The same query without the path prefix reaches the claim-level adjustment + // too; this is what makes the prefix in the test above load-bearing. + const edi = Deno.readTextFileSync("test/test-data/835.edi"); + const parser = new X12Parser(true); + const engine = new X12QueryEngine(parser); + const results = engine.query(edi, 'CAS03:CAS01["OA"]'); + + if (results.length !== 3) { + throw new Error( + `Expected three matching elements for CAS03:CAS01["OA"]; received ${results.length}.`, + ); + } + }); + + it("should handle hyphenated qualifiers values", () => { + const edi = Deno.readTextFileSync("test/test-data/850.edi"); + const parser = new X12Parser(true); + const engine = new X12QueryEngine(parser); + const results = engine.query(edi, 'PO102:PO107["065374-118"]'); + + console.log(results.length); + + if (results.length !== 1) { + throw new Error('Expected one matching elements for PO102:PO107["065374-118"].'); + } + }); + it("should handle HL path element references", () => { const edi = Deno.readTextFileSync("test/test-data/856.edi"); const parser = new X12Parser(true); diff --git a/test/ValidationSuite_test.ts b/test/ValidationSuite_test.ts index 8bff999..a2a2db8 100644 --- a/test/ValidationSuite_test.ts +++ b/test/ValidationSuite_test.ts @@ -140,7 +140,7 @@ describe("X12ValidationEngine", () => { try { validator.assert(interchange, rule); } catch (error) { - const { report } = error; + const { report } = error as Error & { report: any }; assert.strictEqual(typeof report, "object"); } diff --git a/test/test-data/835.edi b/test/test-data/835.edi new file mode 100644 index 0000000..3c209b5 --- /dev/null +++ b/test/test-data/835.edi @@ -0,0 +1,20 @@ +ISA*00* *00* *ZZ*SENDERID *ZZ*RECEIVERID *260730*2007*^*00501*000000001*0*P*>~ +GS*HP*SENDERID*RECEIVERID*20260730*2007*1*X*005010X221A1~ +ST*835*000000001~ +BPR*I*594.18*C*ACH*CCP*01*111111111*DA*1234567*1512345678**01*222222222*DA*7654321*20260729~ +TRN*1*0000000001*1999999999~ +N1*PR*ACME HEALTH PLAN~ +N1*PE*ACME CLINICAL LABORATORY*XX*1234567893~ +LX*1~ +CLP*CLAIM0000001*1*594.18*494.94*99.24*13*9000000000001*81*1~ +CAS*OA*109*-494.94~ +NM1*QC*1*DOE*JANE****MI*MEMBER000000001~ +SVC*HC>82306*88.80*0**1~ +DTM*472*20260624~ +CAS*OA*100*88.80~ +SVC*HC>82172*63.27*0**1~ +DTM*472*20260624~ +CAS*OA*45*63.27~ +SE*16*000000001~ +GE*1*1~ +IEA*1*000000001~