@accordproject/template-engine 2.4.1 → 2.5.4

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 CHANGED
@@ -10,6 +10,8 @@ type GenerationOptions = {
10
10
  now?: string;
11
11
  locale?: string;
12
12
  disableJavaScriptEvaluation?: boolean;
13
+ childProcessJavaScriptEvaluation?: boolean;
14
+ timeout?: number;
13
15
  };
14
16
 
15
17
  /**
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
- return format.replace(/0(.)0((.)(0+))?/gi, function(_a, sep1, _b, sep2, digits) {
59
- const len = digits ? digits.length : 0;
60
- const vs = value.toFixed(len);
61
- let res = "";
62
- if (sep2) {
63
- const d = vs.substring(vs.length - len);
64
- res += sep2 + d;
65
- }
66
- let i = vs.substring(0, vs.length - (len === 0 ? 0 : len + 1));
67
- while (i.length > 3) {
68
- res = sep1 + i.substring(i.length - 3) + res;
69
- i = i.substring(0, i.length - 3);
70
- }
71
- return i + res;
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
- return format.replace(/0(.)0/gi, function(_a, sep1) {
88
- const vs = value.toFixed(0);
89
- let res = "";
90
- let i = vs.substring(0, vs.length);
91
- while (i.length > 3) {
92
- res = sep1 + i.substring(i.length - 3) + res;
93
- i = i.substring(0, i.length - 3);
94
- }
95
- return i + res;
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
- function evaluateJavaScript(clauseLibrary, data, fn, options) {
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 fun = new Function(...functionArgNames, expression);
485
- const result = fun(...functionArgValues);
486
- return result;
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 generateAgreement(modelManager, clauseLibrary, templateMark, data, options) {
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
- checkCode(context.code);
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
- const path = getJsonPath(templateMark, context, this.path);
555
- const variableValues = jp.query(data, path, 1);
556
- if (variableValues.length === 0) {
557
- throw new Error(`No values found for path '${path}' in data ${data}.`);
558
- } else {
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
- checkCode(context.condition);
622
- context.isTrue = !!evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
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 = !!evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
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
  }