@accordproject/template-engine 2.4.1 → 2.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +250 -63
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +250 -63
- package/dist/index.mjs.map +1 -1
- package/dist/worker.js +48 -0
- package/dist/worker.js.map +1 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
var jp = require('jsonpath');
|
|
4
4
|
var traverse = require('traverse');
|
|
5
|
+
var os = require('os');
|
|
5
6
|
var concertoCore = require('@accordproject/concerto-core');
|
|
6
7
|
var dayjs = require('dayjs');
|
|
7
8
|
var utc = require('dayjs/plugin/utc');
|
|
9
|
+
var toWords = require('to-words');
|
|
8
10
|
var markdownCommon = require('@accordproject/markdown-common');
|
|
9
11
|
var vfs = require('@typescript/vfs');
|
|
10
12
|
var twoslash = require('@typescript/twoslash');
|
|
@@ -13,6 +15,7 @@ var concertoUtil = require('@accordproject/concerto-util');
|
|
|
13
15
|
var markdownTemplate = require('@accordproject/markdown-template');
|
|
14
16
|
var fs = require('fs');
|
|
15
17
|
var lzstring = require('lz-string');
|
|
18
|
+
var child_process = require('child_process');
|
|
16
19
|
|
|
17
20
|
function _interopNamespaceDefault(e) {
|
|
18
21
|
var n = Object.create(null);
|
|
@@ -55,21 +58,27 @@ function draftDoubleIEEE(value) {
|
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
function draftDoubleFormat(value, format) {
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
if (format === "text") {
|
|
62
|
+
const converter = new toWords.ToWords();
|
|
63
|
+
const res = converter.convert(value);
|
|
64
|
+
return res;
|
|
65
|
+
} else {
|
|
66
|
+
return format.replace(/0(.)0((.)(0+))?/gi, function(_a, sep1, _b, sep2, digits) {
|
|
67
|
+
const len = digits ? digits.length : 0;
|
|
68
|
+
const vs = value.toFixed(len);
|
|
69
|
+
let res = "";
|
|
70
|
+
if (sep2) {
|
|
71
|
+
const d = vs.substring(vs.length - len);
|
|
72
|
+
res += sep2 + d;
|
|
73
|
+
}
|
|
74
|
+
let i = vs.substring(0, vs.length - (len === 0 ? 0 : len + 1));
|
|
75
|
+
while (i.length > 3) {
|
|
76
|
+
res = sep1 + i.substring(i.length - 3) + res;
|
|
77
|
+
i = i.substring(0, i.length - 3);
|
|
78
|
+
}
|
|
79
|
+
return i + res;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
function doubleDrafter(value, format) {
|
|
@@ -84,16 +93,22 @@ function draftInteger(value) {
|
|
|
84
93
|
return "" + value;
|
|
85
94
|
}
|
|
86
95
|
function draftIntegerFormat(value, format) {
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
if (format === "text") {
|
|
97
|
+
const converter = new toWords.ToWords();
|
|
98
|
+
const res = converter.convert(value);
|
|
99
|
+
return res;
|
|
100
|
+
} else {
|
|
101
|
+
return format.replace(/0(.)0/gi, function(_a, sep1) {
|
|
102
|
+
const vs = value.toFixed(0);
|
|
103
|
+
let res = "";
|
|
104
|
+
let i = vs.substring(0, vs.length);
|
|
105
|
+
while (i.length > 3) {
|
|
106
|
+
res = sep1 + i.substring(i.length - 3) + res;
|
|
107
|
+
i = i.substring(0, i.length - 3);
|
|
108
|
+
}
|
|
109
|
+
return i + res;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
function integerDrafter(value, format) {
|
|
@@ -452,12 +467,138 @@ function joinList(data, joinDef, options) {
|
|
|
452
467
|
}
|
|
453
468
|
}
|
|
454
469
|
|
|
470
|
+
async function sleep(msec) {
|
|
471
|
+
return new Promise((resolve) => setTimeout(resolve, msec));
|
|
472
|
+
}
|
|
473
|
+
class JavaScriptEvaluator {
|
|
474
|
+
options;
|
|
475
|
+
workers;
|
|
476
|
+
// child processes
|
|
477
|
+
queue;
|
|
478
|
+
// queue of work to do
|
|
479
|
+
constructor(options) {
|
|
480
|
+
this.options = options;
|
|
481
|
+
this.workers = [];
|
|
482
|
+
this.queue = [];
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Evaluates a JS function in process.
|
|
486
|
+
* @param {EvalRequest} request - the eval request
|
|
487
|
+
* @returns {Promise} a promise to the result
|
|
488
|
+
*/
|
|
489
|
+
async evalDangerously(request) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
try {
|
|
492
|
+
const start = (/* @__PURE__ */ new Date()).getTime();
|
|
493
|
+
const fun = new Function(...["dayjs", "jp", ...request.argumentNames], request.code);
|
|
494
|
+
const result = fun(...[dayjs, jp, ...request.arguments]);
|
|
495
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
496
|
+
resolve({ result, elapsed: end - start });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
reject({
|
|
499
|
+
message: err.message
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Evaluates a JS function using a node child process
|
|
506
|
+
* @param {EvalRequest} request the eval request
|
|
507
|
+
* @param {EvalOptions} options the options for the request
|
|
508
|
+
* @returns {Promise<EvalResponse>} the async result
|
|
509
|
+
*/
|
|
510
|
+
async evalChildProcess(request, options = { timeout: 1e4 }) {
|
|
511
|
+
return new Promise((resolve, reject) => {
|
|
512
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
513
|
+
const workItem = {
|
|
514
|
+
startTime: now,
|
|
515
|
+
expireTime: now + options.timeout,
|
|
516
|
+
request,
|
|
517
|
+
resolve,
|
|
518
|
+
reject
|
|
519
|
+
};
|
|
520
|
+
if (this.queue.length >= this.options.maxQueueDepth) {
|
|
521
|
+
reject({ maxQueueDepthExceeded: true, elapsed: 0 });
|
|
522
|
+
}
|
|
523
|
+
this.queue.push(workItem);
|
|
524
|
+
this.processQueue(options);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
processQueue(options) {
|
|
528
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
529
|
+
const notExpired = this.queue.filter((w) => now < w.expireTime);
|
|
530
|
+
const expired = this.queue.filter((w) => now >= w.expireTime);
|
|
531
|
+
this.queue = notExpired;
|
|
532
|
+
expired.forEach((w) => {
|
|
533
|
+
w.reject({ timeout: true, starvation: true, elapsed: now - w.startTime });
|
|
534
|
+
});
|
|
535
|
+
if (this.workers.length < this.options.maxWorkers) {
|
|
536
|
+
const next = this.queue.shift();
|
|
537
|
+
if (next) {
|
|
538
|
+
this.doWork(next, options).then((result) => next.resolve(result)).catch((error) => next.reject(error));
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
sleep(this.options.waitInterval).then(() => {
|
|
542
|
+
this.processQueue(options);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
doWork(work, options) {
|
|
547
|
+
return new Promise((resolve, reject) => {
|
|
548
|
+
const start = (/* @__PURE__ */ new Date()).getTime();
|
|
549
|
+
if (!child_process.fork) {
|
|
550
|
+
reject({ message: "Cannot use evalChildProcess because child_process.fork is not defined." });
|
|
551
|
+
}
|
|
552
|
+
const worker = child_process.fork("./dist/worker.js", { timeout: options.timeout, env: {} });
|
|
553
|
+
if (!worker.pid) {
|
|
554
|
+
throw new Error("Failed to fork child process");
|
|
555
|
+
}
|
|
556
|
+
this.workers.push(worker);
|
|
557
|
+
work.pid = worker.pid;
|
|
558
|
+
let result;
|
|
559
|
+
worker.on("error", (err) => {
|
|
560
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
561
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
562
|
+
reject({ message: err.message, elapsed: end - start });
|
|
563
|
+
});
|
|
564
|
+
worker.on("message", (msg) => {
|
|
565
|
+
result = msg;
|
|
566
|
+
});
|
|
567
|
+
worker.on("exit", (code) => {
|
|
568
|
+
if (code === null) {
|
|
569
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
570
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
571
|
+
reject({ timeout: true, elapsed: end - start });
|
|
572
|
+
} else if (code === 0 && result) {
|
|
573
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
574
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
575
|
+
resolve({ ...result, elapsed: end - start });
|
|
576
|
+
} else {
|
|
577
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
578
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
579
|
+
reject({ code, result, elapsed: end - start });
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
worker.send(work.request);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
455
587
|
function checkCode(code) {
|
|
456
588
|
if (code.type !== CodeType.ES_2020) {
|
|
457
589
|
throw new Error(`Cannot run ${code.contents} as it is not ES_2020 JavaScript.`);
|
|
458
590
|
}
|
|
459
591
|
}
|
|
460
|
-
|
|
592
|
+
const availableProcessors = process.env.MAX_WORKERS ? Number.parseInt(process.env.MAX_WORKERS) : os.availableParallelism();
|
|
593
|
+
const javaScriptEvaluator = new JavaScriptEvaluator({
|
|
594
|
+
maxWorkers: availableProcessors,
|
|
595
|
+
// how many child processes
|
|
596
|
+
waitInterval: process.env.WAIT_INTERVAL ? Number.parseInt(process.env.WAIT_INTERVAL) : 50,
|
|
597
|
+
// how long to wait before rescheduling work
|
|
598
|
+
maxQueueDepth: process.env.MAX_QUEUE_DEPTH ? Number.parseInt(process.env.MAX_QUEUE_DEPTH) : 1e3
|
|
599
|
+
// max requests to queue
|
|
600
|
+
});
|
|
601
|
+
async function evaluateJavaScript(clauseLibrary, data, fn, options) {
|
|
461
602
|
if (options?.disableJavaScriptEvaluation) {
|
|
462
603
|
throw new Error("JavaScript evaluation is disabled.");
|
|
463
604
|
}
|
|
@@ -468,24 +609,26 @@ function evaluateJavaScript(clauseLibrary, data, fn, options) {
|
|
|
468
609
|
functionArgNames.push("data");
|
|
469
610
|
functionArgNames.push("library");
|
|
470
611
|
functionArgNames.push("options");
|
|
471
|
-
functionArgNames.push("dayjs");
|
|
472
|
-
functionArgNames.push("jp");
|
|
473
612
|
const functionArgValues = new Array();
|
|
474
613
|
functionArgValues.push(data);
|
|
475
614
|
functionArgValues.push(clauseLibrary);
|
|
476
615
|
functionArgValues.push(options);
|
|
477
|
-
functionArgValues.push(dayjs);
|
|
478
|
-
functionArgValues.push(jp);
|
|
479
616
|
const expression = fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}"));
|
|
480
617
|
if (expression.trim().length === 0) {
|
|
481
618
|
throw new Error("Empty expression");
|
|
482
619
|
}
|
|
483
620
|
try {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
621
|
+
const request = { code: expression, argumentNames: functionArgNames, arguments: functionArgValues };
|
|
622
|
+
if (options?.childProcessJavaScriptEvaluation) {
|
|
623
|
+
const evalOptions = options?.timeout ? { timeout: options.timeout } : void 0;
|
|
624
|
+
const r = await javaScriptEvaluator.evalChildProcess(request, evalOptions);
|
|
625
|
+
return typeof r.result === "object" ? JSON.stringify(r.result) : r.result.toString();
|
|
626
|
+
} else {
|
|
627
|
+
const r = await javaScriptEvaluator.evalDangerously(request);
|
|
628
|
+
return typeof r.result === "object" ? JSON.stringify(r.result) : r.result.toString();
|
|
629
|
+
}
|
|
487
630
|
} catch (err) {
|
|
488
|
-
throw new Error(`Caught error ${err} evaluating ${expression} with arguments ${functionArgValues}`);
|
|
631
|
+
throw new Error(`Caught error ${JSON.stringify(err)} evaluating ${expression} with arguments ${JSON.stringify(functionArgValues)}`);
|
|
489
632
|
}
|
|
490
633
|
}
|
|
491
634
|
function getJsonPath(rootData, currentNode, paths) {
|
|
@@ -516,8 +659,70 @@ function getJsonPath(rootData, currentNode, paths) {
|
|
|
516
659
|
}
|
|
517
660
|
return withPath.length > 0 ? `$${withPath.join("")}` : "$";
|
|
518
661
|
}
|
|
519
|
-
function
|
|
662
|
+
async function evaluateUserCode(clauseLibrary, templateMark, data, options) {
|
|
663
|
+
const result = {};
|
|
664
|
+
const paths = traverse(templateMark).paths();
|
|
665
|
+
for (let n = 0; n < paths.length; n++) {
|
|
666
|
+
const path = paths[n];
|
|
667
|
+
const context = traverse(templateMark).get(path);
|
|
668
|
+
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
669
|
+
const nodeClass = context.$class;
|
|
670
|
+
if (FORMULA_DEFINITION_RE.test(nodeClass)) {
|
|
671
|
+
if (context.code) {
|
|
672
|
+
checkCode(context.code);
|
|
673
|
+
result[path.join("/")] = await evaluateJavaScript(clauseLibrary, data, context.code.contents, options);
|
|
674
|
+
} else {
|
|
675
|
+
throw new Error("Formula node is missing code.");
|
|
676
|
+
}
|
|
677
|
+
} else if (CONDITIONAL_DEFINITION_RE.test(nodeClass) || CLAUSE_DEFINITION_RE.test(nodeClass)) {
|
|
678
|
+
if (context.condition) {
|
|
679
|
+
checkCode(context.condition);
|
|
680
|
+
result[path.join("/")] = await evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return result;
|
|
686
|
+
}
|
|
687
|
+
async function generateListBlocks(modelManager, clauseLibrary, templateMark, data, options) {
|
|
688
|
+
const result = {};
|
|
689
|
+
const paths = traverse(templateMark).paths();
|
|
690
|
+
for (let n = 0; n < paths.length; n++) {
|
|
691
|
+
const thisPath = paths[n];
|
|
692
|
+
const context = traverse(templateMark).get(thisPath);
|
|
693
|
+
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
694
|
+
const nodeClass = context.$class;
|
|
695
|
+
if (LISTBLOCK_DEFINITION_RE.test(nodeClass)) {
|
|
696
|
+
const path = getJsonPath(templateMark, context, thisPath);
|
|
697
|
+
const variableValues = jp.query(data, path, 1);
|
|
698
|
+
if (variableValues.length === 0) {
|
|
699
|
+
throw new Error(`No values found for path '${path}' in data ${data}.`);
|
|
700
|
+
} else {
|
|
701
|
+
const arrayData = variableValues[0];
|
|
702
|
+
if (!Array.isArray(arrayData)) {
|
|
703
|
+
throw new Error(`Values found for path '${path}' in data ${data} is not an array: ${arrayData}.`);
|
|
704
|
+
} else {
|
|
705
|
+
const nodes = [];
|
|
706
|
+
for (let n2 = 0; n2 < arrayData.length; n2++) {
|
|
707
|
+
const arrayItem = arrayData[n2];
|
|
708
|
+
const subResult = await generateAgreement(modelManager, clauseLibrary, context.nodes[0].nodes[0], arrayItem, options);
|
|
709
|
+
nodes.push({
|
|
710
|
+
$class: `${markdownCommon.CommonMarkModel.NAMESPACE}.Item`,
|
|
711
|
+
nodes: subResult.nodes ? subResult.nodes : []
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
result[thisPath.join("/")] = nodes;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
async function generateAgreement(modelManager, clauseLibrary, templateMark, data, options) {
|
|
520
723
|
const introspector = new concertoCore.Introspector(modelManager);
|
|
724
|
+
const userCodeResults = await evaluateUserCode(clauseLibrary, templateMark, data, options);
|
|
725
|
+
const listBlockResults = await generateListBlocks(modelManager, clauseLibrary, templateMark, data, options);
|
|
521
726
|
return traverse(templateMark).map(function(context) {
|
|
522
727
|
let stopHere = false;
|
|
523
728
|
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
@@ -537,8 +742,7 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
537
742
|
delete context.elementType;
|
|
538
743
|
} else if (FORMULA_DEFINITION_RE.test(nodeClass)) {
|
|
539
744
|
if (context.code) {
|
|
540
|
-
|
|
541
|
-
const result = evaluateJavaScript(clauseLibrary, data, context.code.contents, options);
|
|
745
|
+
const result = userCodeResults[this.path.join("/")];
|
|
542
746
|
if (result === null) {
|
|
543
747
|
context.value = "<null>";
|
|
544
748
|
} else if (typeof result === "string") {
|
|
@@ -551,28 +755,11 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
551
755
|
throw new Error("Formula node is missing code.");
|
|
552
756
|
}
|
|
553
757
|
} else if (LISTBLOCK_DEFINITION_RE.test(nodeClass)) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const arrayData = variableValues[0];
|
|
560
|
-
if (!Array.isArray(arrayData)) {
|
|
561
|
-
throw new Error(`Values found for path '${path}' in data ${data} is not an array: ${arrayData}.`);
|
|
562
|
-
} else {
|
|
563
|
-
context.$class = `${markdownCommon.CommonMarkModel.NAMESPACE}.List`;
|
|
564
|
-
delete context.elementType;
|
|
565
|
-
delete context.name;
|
|
566
|
-
context.nodes = arrayData.map((arrayItem) => {
|
|
567
|
-
const subResult = generateAgreement(modelManager, clauseLibrary, context.nodes[0].nodes[0], arrayItem);
|
|
568
|
-
return {
|
|
569
|
-
$class: `${markdownCommon.CommonMarkModel.NAMESPACE}.Item`,
|
|
570
|
-
nodes: subResult.nodes ? subResult.nodes : []
|
|
571
|
-
};
|
|
572
|
-
});
|
|
573
|
-
stopHere = true;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
758
|
+
context.$class = `${markdownCommon.CommonMarkModel.NAMESPACE}.List`;
|
|
759
|
+
delete context.elementType;
|
|
760
|
+
delete context.name;
|
|
761
|
+
context.nodes = listBlockResults[this.path.join("/")];
|
|
762
|
+
stopHere = true;
|
|
576
763
|
} else if (JOIN_DEFINITION_RE.test(nodeClass)) {
|
|
577
764
|
const path = getJsonPath(templateMark, context, this.path);
|
|
578
765
|
const variableValues = jp.query(data, path, 1);
|
|
@@ -618,8 +805,8 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
618
805
|
}
|
|
619
806
|
} else if (CONDITIONAL_DEFINITION_RE.test(nodeClass)) {
|
|
620
807
|
if (context.condition) {
|
|
621
|
-
|
|
622
|
-
context.isTrue = !!
|
|
808
|
+
const result = userCodeResults[this.path.join("/")];
|
|
809
|
+
context.isTrue = !!result;
|
|
623
810
|
} else {
|
|
624
811
|
const path = getJsonPath(templateMark, context, this.path);
|
|
625
812
|
const variableValues = jp.query(data, path, 1);
|
|
@@ -640,7 +827,7 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
640
827
|
} else if (CLAUSE_DEFINITION_RE.test(nodeClass)) {
|
|
641
828
|
if (context.condition) {
|
|
642
829
|
checkCode(context.condition);
|
|
643
|
-
const result = !!
|
|
830
|
+
const result = !!userCodeResults[this.path.join("/")];
|
|
644
831
|
if (!result) {
|
|
645
832
|
delete context.nodes;
|
|
646
833
|
stopHere = true;
|
|
@@ -739,7 +926,7 @@ class TemplateMarkInterpreter {
|
|
|
739
926
|
}
|
|
740
927
|
const typedTemplateMark = this.checkTypes(templateMark);
|
|
741
928
|
const jsTemplateMark = await this.compileTypeScriptToJavaScript(typedTemplateMark);
|
|
742
|
-
const ciceroMark = generateAgreement(this.modelManager, this.clauseLibrary, jsTemplateMark, data, options);
|
|
929
|
+
const ciceroMark = await generateAgreement(this.modelManager, this.clauseLibrary, jsTemplateMark, data, options);
|
|
743
930
|
return this.validateCiceroMark(ciceroMark);
|
|
744
931
|
}
|
|
745
932
|
}
|