@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.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
- return format.replace(/0(.)0((.)(0+))?/gi, function(_a, sep1, _b, sep2, digits) {
38
- const len = digits ? digits.length : 0;
39
- const vs = value.toFixed(len);
40
- let res = "";
41
- if (sep2) {
42
- const d = vs.substring(vs.length - len);
43
- res += sep2 + d;
44
- }
45
- let i = vs.substring(0, vs.length - (len === 0 ? 0 : len + 1));
46
- while (i.length > 3) {
47
- res = sep1 + i.substring(i.length - 3) + res;
48
- i = i.substring(0, i.length - 3);
49
- }
50
- return i + res;
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
- return format.replace(/0(.)0/gi, function(_a, sep1) {
67
- const vs = value.toFixed(0);
68
- let res = "";
69
- let i = vs.substring(0, vs.length);
70
- while (i.length > 3) {
71
- res = sep1 + i.substring(i.length - 3) + res;
72
- i = i.substring(0, i.length - 3);
73
- }
74
- return i + res;
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
- function evaluateJavaScript(clauseLibrary, data, fn, options) {
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 fun = new Function(...functionArgNames, expression);
464
- const result = fun(...functionArgValues);
465
- return result;
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 generateAgreement(modelManager, clauseLibrary, templateMark, data, options) {
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
- checkCode(context.code);
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
- const path = getJsonPath(templateMark, context, this.path);
534
- const variableValues = jp.query(data, path, 1);
535
- if (variableValues.length === 0) {
536
- throw new Error(`No values found for path '${path}' in data ${data}.`);
537
- } else {
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
- checkCode(context.condition);
601
- context.isTrue = !!evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
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 = !!evaluateJavaScript(clauseLibrary, data, context.condition.contents, options);
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
  }