@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.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import jp from 'jsonpath';
|
|
2
2
|
import traverse from 'traverse';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import { Introspector, ModelManager, Factory, Serializer, ModelUtil } from '@accordproject/concerto-core';
|
|
4
5
|
import dayjs from 'dayjs';
|
|
5
6
|
import utc from 'dayjs/plugin/utc';
|
|
7
|
+
import { ToWords } from 'to-words';
|
|
6
8
|
import { TemplateMarkModel, ConcertoMetaModel, CommonMarkModel, CiceroMarkModel } from '@accordproject/markdown-common';
|
|
7
9
|
import { createDefaultMapFromNodeModules, createDefaultMapFromCDN } from '@typescript/vfs';
|
|
8
10
|
import { twoslasher } from '@typescript/twoslash';
|
|
@@ -11,6 +13,7 @@ import { InMemoryWriter } from '@accordproject/concerto-util';
|
|
|
11
13
|
import { templatemarkutil } from '@accordproject/markdown-template';
|
|
12
14
|
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
13
15
|
import * as lzstring from 'lz-string';
|
|
16
|
+
import child_process from 'child_process';
|
|
14
17
|
|
|
15
18
|
function booleanDrafter(value) {
|
|
16
19
|
if (value) {
|
|
@@ -34,21 +37,27 @@ function draftDoubleIEEE(value) {
|
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
function draftDoubleFormat(value, format) {
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
if (format === "text") {
|
|
41
|
+
const converter = new ToWords();
|
|
42
|
+
const res = converter.convert(value);
|
|
43
|
+
return res;
|
|
44
|
+
} else {
|
|
45
|
+
return format.replace(/0(.)0((.)(0+))?/gi, function(_a, sep1, _b, sep2, digits) {
|
|
46
|
+
const len = digits ? digits.length : 0;
|
|
47
|
+
const vs = value.toFixed(len);
|
|
48
|
+
let res = "";
|
|
49
|
+
if (sep2) {
|
|
50
|
+
const d = vs.substring(vs.length - len);
|
|
51
|
+
res += sep2 + d;
|
|
52
|
+
}
|
|
53
|
+
let i = vs.substring(0, vs.length - (len === 0 ? 0 : len + 1));
|
|
54
|
+
while (i.length > 3) {
|
|
55
|
+
res = sep1 + i.substring(i.length - 3) + res;
|
|
56
|
+
i = i.substring(0, i.length - 3);
|
|
57
|
+
}
|
|
58
|
+
return i + res;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
function doubleDrafter(value, format) {
|
|
@@ -63,16 +72,22 @@ function draftInteger(value) {
|
|
|
63
72
|
return "" + value;
|
|
64
73
|
}
|
|
65
74
|
function draftIntegerFormat(value, format) {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
if (format === "text") {
|
|
76
|
+
const converter = new ToWords();
|
|
77
|
+
const res = converter.convert(value);
|
|
78
|
+
return res;
|
|
79
|
+
} else {
|
|
80
|
+
return format.replace(/0(.)0/gi, function(_a, sep1) {
|
|
81
|
+
const vs = value.toFixed(0);
|
|
82
|
+
let res = "";
|
|
83
|
+
let i = vs.substring(0, vs.length);
|
|
84
|
+
while (i.length > 3) {
|
|
85
|
+
res = sep1 + i.substring(i.length - 3) + res;
|
|
86
|
+
i = i.substring(0, i.length - 3);
|
|
87
|
+
}
|
|
88
|
+
return i + res;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
76
91
|
}
|
|
77
92
|
|
|
78
93
|
function integerDrafter(value, format) {
|
|
@@ -431,12 +446,138 @@ function joinList(data, joinDef, options) {
|
|
|
431
446
|
}
|
|
432
447
|
}
|
|
433
448
|
|
|
449
|
+
async function sleep(msec) {
|
|
450
|
+
return new Promise((resolve) => setTimeout(resolve, msec));
|
|
451
|
+
}
|
|
452
|
+
class JavaScriptEvaluator {
|
|
453
|
+
options;
|
|
454
|
+
workers;
|
|
455
|
+
// child processes
|
|
456
|
+
queue;
|
|
457
|
+
// queue of work to do
|
|
458
|
+
constructor(options) {
|
|
459
|
+
this.options = options;
|
|
460
|
+
this.workers = [];
|
|
461
|
+
this.queue = [];
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Evaluates a JS function in process.
|
|
465
|
+
* @param {EvalRequest} request - the eval request
|
|
466
|
+
* @returns {Promise} a promise to the result
|
|
467
|
+
*/
|
|
468
|
+
async evalDangerously(request) {
|
|
469
|
+
return new Promise((resolve, reject) => {
|
|
470
|
+
try {
|
|
471
|
+
const start = (/* @__PURE__ */ new Date()).getTime();
|
|
472
|
+
const fun = new Function(...["dayjs", "jp", ...request.argumentNames], request.code);
|
|
473
|
+
const result = fun(...[dayjs, jp, ...request.arguments]);
|
|
474
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
475
|
+
resolve({ result, elapsed: end - start });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
reject({
|
|
478
|
+
message: err.message
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Evaluates a JS function using a node child process
|
|
485
|
+
* @param {EvalRequest} request the eval request
|
|
486
|
+
* @param {EvalOptions} options the options for the request
|
|
487
|
+
* @returns {Promise<EvalResponse>} the async result
|
|
488
|
+
*/
|
|
489
|
+
async evalChildProcess(request, options = { timeout: 1e4 }) {
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
492
|
+
const workItem = {
|
|
493
|
+
startTime: now,
|
|
494
|
+
expireTime: now + options.timeout,
|
|
495
|
+
request,
|
|
496
|
+
resolve,
|
|
497
|
+
reject
|
|
498
|
+
};
|
|
499
|
+
if (this.queue.length >= this.options.maxQueueDepth) {
|
|
500
|
+
reject({ maxQueueDepthExceeded: true, elapsed: 0 });
|
|
501
|
+
}
|
|
502
|
+
this.queue.push(workItem);
|
|
503
|
+
this.processQueue(options);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
processQueue(options) {
|
|
507
|
+
const now = (/* @__PURE__ */ new Date()).getTime();
|
|
508
|
+
const notExpired = this.queue.filter((w) => now < w.expireTime);
|
|
509
|
+
const expired = this.queue.filter((w) => now >= w.expireTime);
|
|
510
|
+
this.queue = notExpired;
|
|
511
|
+
expired.forEach((w) => {
|
|
512
|
+
w.reject({ timeout: true, starvation: true, elapsed: now - w.startTime });
|
|
513
|
+
});
|
|
514
|
+
if (this.workers.length < this.options.maxWorkers) {
|
|
515
|
+
const next = this.queue.shift();
|
|
516
|
+
if (next) {
|
|
517
|
+
this.doWork(next, options).then((result) => next.resolve(result)).catch((error) => next.reject(error));
|
|
518
|
+
}
|
|
519
|
+
} else {
|
|
520
|
+
sleep(this.options.waitInterval).then(() => {
|
|
521
|
+
this.processQueue(options);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
doWork(work, options) {
|
|
526
|
+
return new Promise((resolve, reject) => {
|
|
527
|
+
const start = (/* @__PURE__ */ new Date()).getTime();
|
|
528
|
+
if (!child_process.fork) {
|
|
529
|
+
reject({ message: "Cannot use evalChildProcess because child_process.fork is not defined." });
|
|
530
|
+
}
|
|
531
|
+
const worker = child_process.fork("./dist/worker.js", { timeout: options.timeout, env: {} });
|
|
532
|
+
if (!worker.pid) {
|
|
533
|
+
throw new Error("Failed to fork child process");
|
|
534
|
+
}
|
|
535
|
+
this.workers.push(worker);
|
|
536
|
+
work.pid = worker.pid;
|
|
537
|
+
let result;
|
|
538
|
+
worker.on("error", (err) => {
|
|
539
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
540
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
541
|
+
reject({ message: err.message, elapsed: end - start });
|
|
542
|
+
});
|
|
543
|
+
worker.on("message", (msg) => {
|
|
544
|
+
result = msg;
|
|
545
|
+
});
|
|
546
|
+
worker.on("exit", (code) => {
|
|
547
|
+
if (code === null) {
|
|
548
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
549
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
550
|
+
reject({ timeout: true, elapsed: end - start });
|
|
551
|
+
} else if (code === 0 && result) {
|
|
552
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
553
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
554
|
+
resolve({ ...result, elapsed: end - start });
|
|
555
|
+
} else {
|
|
556
|
+
this.workers = this.workers.filter((w) => w.pid !== worker.pid);
|
|
557
|
+
const end = (/* @__PURE__ */ new Date()).getTime();
|
|
558
|
+
reject({ code, result, elapsed: end - start });
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
worker.send(work.request);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
434
566
|
function checkCode(code) {
|
|
435
567
|
if (code.type !== CodeType.ES_2020) {
|
|
436
568
|
throw new Error(`Cannot run ${code.contents} as it is not ES_2020 JavaScript.`);
|
|
437
569
|
}
|
|
438
570
|
}
|
|
439
|
-
|
|
571
|
+
const availableProcessors = process.env.MAX_WORKERS ? Number.parseInt(process.env.MAX_WORKERS) : os.availableParallelism();
|
|
572
|
+
const javaScriptEvaluator = new JavaScriptEvaluator({
|
|
573
|
+
maxWorkers: availableProcessors,
|
|
574
|
+
// how many child processes
|
|
575
|
+
waitInterval: process.env.WAIT_INTERVAL ? Number.parseInt(process.env.WAIT_INTERVAL) : 50,
|
|
576
|
+
// how long to wait before rescheduling work
|
|
577
|
+
maxQueueDepth: process.env.MAX_QUEUE_DEPTH ? Number.parseInt(process.env.MAX_QUEUE_DEPTH) : 1e3
|
|
578
|
+
// max requests to queue
|
|
579
|
+
});
|
|
580
|
+
async function evaluateJavaScript(clauseLibrary, data, fn, options) {
|
|
440
581
|
if (options?.disableJavaScriptEvaluation) {
|
|
441
582
|
throw new Error("JavaScript evaluation is disabled.");
|
|
442
583
|
}
|
|
@@ -447,24 +588,26 @@ function evaluateJavaScript(clauseLibrary, data, fn, options) {
|
|
|
447
588
|
functionArgNames.push("data");
|
|
448
589
|
functionArgNames.push("library");
|
|
449
590
|
functionArgNames.push("options");
|
|
450
|
-
functionArgNames.push("dayjs");
|
|
451
|
-
functionArgNames.push("jp");
|
|
452
591
|
const functionArgValues = new Array();
|
|
453
592
|
functionArgValues.push(data);
|
|
454
593
|
functionArgValues.push(clauseLibrary);
|
|
455
594
|
functionArgValues.push(options);
|
|
456
|
-
functionArgValues.push(dayjs);
|
|
457
|
-
functionArgValues.push(jp);
|
|
458
595
|
const expression = fn.substring(fn.indexOf("{") + 1, fn.lastIndexOf("}"));
|
|
459
596
|
if (expression.trim().length === 0) {
|
|
460
597
|
throw new Error("Empty expression");
|
|
461
598
|
}
|
|
462
599
|
try {
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
600
|
+
const request = { code: expression, argumentNames: functionArgNames, arguments: functionArgValues };
|
|
601
|
+
if (options?.childProcessJavaScriptEvaluation) {
|
|
602
|
+
const evalOptions = options?.timeout ? { timeout: options.timeout } : void 0;
|
|
603
|
+
const r = await javaScriptEvaluator.evalChildProcess(request, evalOptions);
|
|
604
|
+
return typeof r.result === "object" ? JSON.stringify(r.result) : r.result.toString();
|
|
605
|
+
} else {
|
|
606
|
+
const r = await javaScriptEvaluator.evalDangerously(request);
|
|
607
|
+
return typeof r.result === "object" ? JSON.stringify(r.result) : r.result.toString();
|
|
608
|
+
}
|
|
466
609
|
} catch (err) {
|
|
467
|
-
throw new Error(`Caught error ${err} evaluating ${expression} with arguments ${functionArgValues}`);
|
|
610
|
+
throw new Error(`Caught error ${JSON.stringify(err)} evaluating ${expression} with arguments ${JSON.stringify(functionArgValues)}`);
|
|
468
611
|
}
|
|
469
612
|
}
|
|
470
613
|
function getJsonPath(rootData, currentNode, paths) {
|
|
@@ -495,8 +638,70 @@ function getJsonPath(rootData, currentNode, paths) {
|
|
|
495
638
|
}
|
|
496
639
|
return withPath.length > 0 ? `$${withPath.join("")}` : "$";
|
|
497
640
|
}
|
|
498
|
-
function
|
|
641
|
+
async function evaluateUserCode(clauseLibrary, templateMark, data, options) {
|
|
642
|
+
const result = {};
|
|
643
|
+
const paths = traverse(templateMark).paths();
|
|
644
|
+
for (let n = 0; n < paths.length; n++) {
|
|
645
|
+
const path = paths[n];
|
|
646
|
+
const context = traverse(templateMark).get(path);
|
|
647
|
+
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
648
|
+
const nodeClass = context.$class;
|
|
649
|
+
if (FORMULA_DEFINITION_RE.test(nodeClass)) {
|
|
650
|
+
if (context.code) {
|
|
651
|
+
checkCode(context.code);
|
|
652
|
+
result[path.join("/")] = await evaluateJavaScript(clauseLibrary, data, context.code.contents, options);
|
|
653
|
+
} else {
|
|
654
|
+
throw new Error("Formula node is missing code.");
|
|
655
|
+
}
|
|
656
|
+
} else if (CONDITIONAL_DEFINITION_RE.test(nodeClass) || CLAUSE_DEFINITION_RE.test(nodeClass)) {
|
|
657
|
+
if (context.condition) {
|
|
658
|
+
checkCode(context.condition);
|
|
659
|
+
result[path.join("/")] = await evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
async function generateListBlocks(modelManager, clauseLibrary, templateMark, data, options) {
|
|
667
|
+
const result = {};
|
|
668
|
+
const paths = traverse(templateMark).paths();
|
|
669
|
+
for (let n = 0; n < paths.length; n++) {
|
|
670
|
+
const thisPath = paths[n];
|
|
671
|
+
const context = traverse(templateMark).get(thisPath);
|
|
672
|
+
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
673
|
+
const nodeClass = context.$class;
|
|
674
|
+
if (LISTBLOCK_DEFINITION_RE.test(nodeClass)) {
|
|
675
|
+
const path = getJsonPath(templateMark, context, thisPath);
|
|
676
|
+
const variableValues = jp.query(data, path, 1);
|
|
677
|
+
if (variableValues.length === 0) {
|
|
678
|
+
throw new Error(`No values found for path '${path}' in data ${data}.`);
|
|
679
|
+
} else {
|
|
680
|
+
const arrayData = variableValues[0];
|
|
681
|
+
if (!Array.isArray(arrayData)) {
|
|
682
|
+
throw new Error(`Values found for path '${path}' in data ${data} is not an array: ${arrayData}.`);
|
|
683
|
+
} else {
|
|
684
|
+
const nodes = [];
|
|
685
|
+
for (let n2 = 0; n2 < arrayData.length; n2++) {
|
|
686
|
+
const arrayItem = arrayData[n2];
|
|
687
|
+
const subResult = await generateAgreement(modelManager, clauseLibrary, context.nodes[0].nodes[0], arrayItem, options);
|
|
688
|
+
nodes.push({
|
|
689
|
+
$class: `${CommonMarkModel.NAMESPACE}.Item`,
|
|
690
|
+
nodes: subResult.nodes ? subResult.nodes : []
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
result[thisPath.join("/")] = nodes;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
async function generateAgreement(modelManager, clauseLibrary, templateMark, data, options) {
|
|
499
702
|
const introspector = new Introspector(modelManager);
|
|
703
|
+
const userCodeResults = await evaluateUserCode(clauseLibrary, templateMark, data, options);
|
|
704
|
+
const listBlockResults = await generateListBlocks(modelManager, clauseLibrary, templateMark, data, options);
|
|
500
705
|
return traverse(templateMark).map(function(context) {
|
|
501
706
|
let stopHere = false;
|
|
502
707
|
if (typeof context === "object" && context.$class && typeof context.$class === "string") {
|
|
@@ -516,8 +721,7 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
516
721
|
delete context.elementType;
|
|
517
722
|
} else if (FORMULA_DEFINITION_RE.test(nodeClass)) {
|
|
518
723
|
if (context.code) {
|
|
519
|
-
|
|
520
|
-
const result = evaluateJavaScript(clauseLibrary, data, context.code.contents, options);
|
|
724
|
+
const result = userCodeResults[this.path.join("/")];
|
|
521
725
|
if (result === null) {
|
|
522
726
|
context.value = "<null>";
|
|
523
727
|
} else if (typeof result === "string") {
|
|
@@ -530,28 +734,11 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
530
734
|
throw new Error("Formula node is missing code.");
|
|
531
735
|
}
|
|
532
736
|
} else if (LISTBLOCK_DEFINITION_RE.test(nodeClass)) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const arrayData = variableValues[0];
|
|
539
|
-
if (!Array.isArray(arrayData)) {
|
|
540
|
-
throw new Error(`Values found for path '${path}' in data ${data} is not an array: ${arrayData}.`);
|
|
541
|
-
} else {
|
|
542
|
-
context.$class = `${CommonMarkModel.NAMESPACE}.List`;
|
|
543
|
-
delete context.elementType;
|
|
544
|
-
delete context.name;
|
|
545
|
-
context.nodes = arrayData.map((arrayItem) => {
|
|
546
|
-
const subResult = generateAgreement(modelManager, clauseLibrary, context.nodes[0].nodes[0], arrayItem);
|
|
547
|
-
return {
|
|
548
|
-
$class: `${CommonMarkModel.NAMESPACE}.Item`,
|
|
549
|
-
nodes: subResult.nodes ? subResult.nodes : []
|
|
550
|
-
};
|
|
551
|
-
});
|
|
552
|
-
stopHere = true;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
737
|
+
context.$class = `${CommonMarkModel.NAMESPACE}.List`;
|
|
738
|
+
delete context.elementType;
|
|
739
|
+
delete context.name;
|
|
740
|
+
context.nodes = listBlockResults[this.path.join("/")];
|
|
741
|
+
stopHere = true;
|
|
555
742
|
} else if (JOIN_DEFINITION_RE.test(nodeClass)) {
|
|
556
743
|
const path = getJsonPath(templateMark, context, this.path);
|
|
557
744
|
const variableValues = jp.query(data, path, 1);
|
|
@@ -597,8 +784,8 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
597
784
|
}
|
|
598
785
|
} else if (CONDITIONAL_DEFINITION_RE.test(nodeClass)) {
|
|
599
786
|
if (context.condition) {
|
|
600
|
-
|
|
601
|
-
context.isTrue = !!
|
|
787
|
+
const result = userCodeResults[this.path.join("/")];
|
|
788
|
+
context.isTrue = !!result;
|
|
602
789
|
} else {
|
|
603
790
|
const path = getJsonPath(templateMark, context, this.path);
|
|
604
791
|
const variableValues = jp.query(data, path, 1);
|
|
@@ -619,7 +806,7 @@ function generateAgreement(modelManager, clauseLibrary, templateMark, data, opti
|
|
|
619
806
|
} else if (CLAUSE_DEFINITION_RE.test(nodeClass)) {
|
|
620
807
|
if (context.condition) {
|
|
621
808
|
checkCode(context.condition);
|
|
622
|
-
const result = !!
|
|
809
|
+
const result = !!userCodeResults[this.path.join("/")];
|
|
623
810
|
if (!result) {
|
|
624
811
|
delete context.nodes;
|
|
625
812
|
stopHere = true;
|
|
@@ -718,7 +905,7 @@ class TemplateMarkInterpreter {
|
|
|
718
905
|
}
|
|
719
906
|
const typedTemplateMark = this.checkTypes(templateMark);
|
|
720
907
|
const jsTemplateMark = await this.compileTypeScriptToJavaScript(typedTemplateMark);
|
|
721
|
-
const ciceroMark = generateAgreement(this.modelManager, this.clauseLibrary, jsTemplateMark, data, options);
|
|
908
|
+
const ciceroMark = await generateAgreement(this.modelManager, this.clauseLibrary, jsTemplateMark, data, options);
|
|
722
909
|
return this.validateCiceroMark(ciceroMark);
|
|
723
910
|
}
|
|
724
911
|
}
|