Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 4 additions & 4 deletions src/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class ArgumentNullError extends Error {
this.name = "ArgumentNullError";
}

name: string;
override name: string;
}

export class GeneratorError extends Error {
Expand All @@ -15,7 +15,7 @@ export class GeneratorError extends Error {
this.name = "GeneratorError";
}

name: string;
override name: string;
}

export class ParserError extends Error {
Expand All @@ -24,7 +24,7 @@ export class ParserError extends Error {
this.name = "ParserError";
}

name: string;
override name: string;
}

export class QuerySyntaxError extends Error {
Expand All @@ -33,5 +33,5 @@ export class QuerySyntaxError extends Error {
this.name = "QuerySyntaxError";
}

name: string;
override name: string;
}
2 changes: 1 addition & 1 deletion src/X12FatInterchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class X12FatInterchange extends Array<X12Interchange> {
* @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;
Expand Down
4 changes: 2 additions & 2 deletions src/X12Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions src/X12QueryEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ export class X12QueryEngine {
}

const hlPathMatch = reference.match(/HL\+(\w\+?)+[+-]/g); // ex. HL+O+P+I
const segPathMatch = reference.match(/((?<!\+)[A-Z0-9]{2,3}-)+/g); // ex. PO1-N9-
const elmRefMatch = reference.match(/[A-Z0-9]{2,3}[0-9]{2}[^[]?/g); // ex. REF02; need to remove trailing ":" if exists
// A segment path always precedes the element reference and a qualifier always
// follows it, so only search ahead of the first ":". Searching the whole
// reference reads a hyphen inside a qualifier value (ex. REF02["SELF-FUNDED"])
// as a segment path.
const [segPathAndElmRef] = reference.split(":");
const segPathMatch = segPathAndElmRef.match(/((?<!\+)[A-Z0-9]{2,3}-)+/g); // ex. PO1-N9-
const elmRefMatch = reference.match(/[A-Z][A-Z0-9]{1,2}[0-9]{2}[^[]?/g); // ex. REF02; need to remove trailing ":" if exists
const qualMatch = reference.match(
/:[A-Z0-9]{2,3}[0-9]{2,}\[["'][^[\]"']+["']\]/g,
); // ex. :REF01["PO"]
Expand Down
30 changes: 24 additions & 6 deletions src/X12TransactionMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,22 @@ export class X12TransactionMap {

const clone = JSON.parse(JSON.stringify(map));
let clones: any = null;
// `clone` starts life holding the map's own query strings, and once a FOREACH
// key has produced `clones` it is `clones` that gets returned. So a key
// resolved after that point has to be written to every row too; writing it to
// `clone` alone leaves each row showing the raw query string it was cloned
// with. Object values are copied per row so rows never share a reference.
const assign = (key: string, value: any): void => {
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([
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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]));
}
}
});
Expand Down
10 changes: 5 additions & 5 deletions src/X12ValidationEngine/X12ValidationRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -222,7 +222,7 @@ export class X12GroupRule extends X12ValidationRule {
header: X12SegmentRule;
trailer: X12SegmentRule;

assert?(
override assert?(
group: X12FunctionalGroup,
controlNumber: number,
): true | ValidationReport {
Expand Down Expand Up @@ -336,7 +336,7 @@ export class X12TransactionRule extends X12ValidationRule {
header: X12SegmentRule;
trailer: X12SegmentRule;

assert?(
override assert?(
transaction: X12Transaction,
controlNumber: number,
): true | ValidationReport {
Expand Down Expand Up @@ -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[] = [];

Expand Down Expand Up @@ -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, "")] };
Expand Down
4 changes: 2 additions & 2 deletions test/GeneratorSuite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
Expand Down
67 changes: 67 additions & 0 deletions test/MappingSuite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
});
});
6 changes: 3 additions & 3 deletions test/ParserSuite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
);
Expand All @@ -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.",
);
Expand All @@ -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.",
);
Expand Down
53 changes: 53 additions & 0 deletions test/QuerySuite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion test/ValidationSuite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Loading