@accordproject/template-engine 2.8.0 → 3.0.1
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/README.md +26 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +95 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +95 -22
- package/dist/index.mjs.map +1 -1
- package/dist/worker.js +6 -4
- package/dist/worker.js.map +1 -1
- package/package.json +18 -10
package/README.md
CHANGED
|
@@ -20,6 +20,31 @@ At a high-level the template engine converts a TemplateMark DOM to an AgreementM
|
|
|
20
20
|
|
|
21
21
|

|
|
22
22
|
|
|
23
|
+
## Execution Pipeline Overview
|
|
24
|
+
|
|
25
|
+
Internally, the Template Engine evaluates templates through a structured execution pipeline:
|
|
26
|
+
|
|
27
|
+
1. **Type Validation**
|
|
28
|
+
The TemplateMark JSON document and the incoming agreement data are validated against their respective Concerto models to ensure structural and type correctness.
|
|
29
|
+
|
|
30
|
+
2. **TypeScript Compilation**
|
|
31
|
+
Embedded TypeScript expressions (such as conditionals, clauses, and formulae) are compiled into JavaScript using the `TemplateMarkToJavaScriptCompiler`.
|
|
32
|
+
|
|
33
|
+
3. **User Code Evaluation**
|
|
34
|
+
During agreement generation, compiled JavaScript expressions are evaluated using the `JavaScriptEvaluator`.
|
|
35
|
+
The evaluator supports two execution strategies:
|
|
36
|
+
- `evalDangerously()` — Executes JavaScript directly within the current process.
|
|
37
|
+
This should only be used with trusted code or in a sandboxed environment (e.g., browser).
|
|
38
|
+
- `evalChildProcess()` — Executes JavaScript in an isolated Node.js child process for improved safety and isolation and is recommended for untrusted template content on the server.
|
|
39
|
+
|
|
40
|
+
4. **AgreementMark Generation**
|
|
41
|
+
The TemplateMark document is traversed, evaluated values are inserted into the document structure, and an AgreementMark JSON document is produced.
|
|
42
|
+
|
|
43
|
+
5. **Output Validation**
|
|
44
|
+
The generated AgreementMark document is validated before being returned.
|
|
45
|
+
|
|
46
|
+
This layered architecture ensures type-safety, deterministic execution of template logic, and isolation during runtime evaluation.
|
|
47
|
+
|
|
23
48
|
## Hello World Template
|
|
24
49
|
|
|
25
50
|
Let's create the simplest template imaginable, the infamous "hello world"!
|
|
@@ -102,7 +127,7 @@ This AgreementMark JSON document can then be passed to the `@accordproject/markd
|
|
|
102
127
|
|
|
103
128
|
The Hello World example just scratches the surface of what can be accomplished! TemplateMark can define optional sections, conditional sections, TypeScript formulae/calculations and even reference external data.
|
|
104
129
|
|
|
105
|
-
Refer to the [full](https://github.com/accordproject/template-engine/tree/main/test/templates/full) example for details.
|
|
130
|
+
Refer to the [full](https://github.com/accordproject/template-engine/tree/main/test/templates/good/full) example for details.
|
|
106
131
|
|
|
107
132
|
> More detailed syntax documentation is to come!
|
|
108
133
|
Read the existing documentation at: https://docs.accordproject.org/docs/markup-templatemark.html
|
package/dist/index.d.ts
CHANGED
|
@@ -152,15 +152,15 @@ declare class TemplateArchiveProcessor {
|
|
|
152
152
|
* @param {object} request - the request to send to the template logic
|
|
153
153
|
* @param {object} state - the current state of the template
|
|
154
154
|
* @param {[string]} currentTime - the current time, defaults to now
|
|
155
|
-
* @param {[number]} utcOffset - the UTC
|
|
155
|
+
* @param {[number]} utcOffset - the UTC offset, defaults to zero
|
|
156
156
|
* @returns {Promise} the response and any events
|
|
157
157
|
*/
|
|
158
158
|
trigger(data: any, request: any, state?: any, currentTime?: string, utcOffset?: number): Promise<TriggerResponse>;
|
|
159
159
|
/**
|
|
160
160
|
* Init the logic of a template
|
|
161
161
|
* @param {[string]} currentTime - the current time, defaults to now
|
|
162
|
-
* @param {[number]} utcOffset - the UTC
|
|
163
|
-
* @returns {Promise} the
|
|
162
|
+
* @param {[number]} utcOffset - the UTC offset, defaults to zero
|
|
163
|
+
* @returns {Promise<InitResponse>} the new state
|
|
164
164
|
*/
|
|
165
165
|
init(data: any, currentTime?: string, utcOffset?: number): Promise<InitResponse>;
|
|
166
166
|
}
|
package/dist/index.js
CHANGED
|
@@ -122,6 +122,16 @@ function integerDrafter(value, format) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function isDuration(value) {
|
|
126
|
+
return value != null && typeof value === "object" && "amount" in value && "unit" in value && typeof value.amount === "number" && typeof value.unit === "string";
|
|
127
|
+
}
|
|
128
|
+
function durationDrafter(value) {
|
|
129
|
+
if (!isDuration(value)) {
|
|
130
|
+
return "0 unknown";
|
|
131
|
+
}
|
|
132
|
+
return `${value.amount} ${value.unit}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
125
135
|
function longDrafter(value, format) {
|
|
126
136
|
if (format) {
|
|
127
137
|
return draftIntegerFormat(value, format);
|
|
@@ -210,6 +220,10 @@ function getDrafter(typeName) {
|
|
|
210
220
|
return longDrafter;
|
|
211
221
|
case "org.accordproject.money@0.3.0.MonetaryAmount":
|
|
212
222
|
return monetaryAmountDrafter;
|
|
223
|
+
case "org.accordproject.time@0.3.0.Duration":
|
|
224
|
+
return durationDrafter;
|
|
225
|
+
case "org.accordproject.time@0.3.0.Period":
|
|
226
|
+
return durationDrafter;
|
|
213
227
|
case "String":
|
|
214
228
|
return stringDrafter;
|
|
215
229
|
default:
|
|
@@ -700,7 +714,7 @@ function getJsonPath(rootData, currentNode, paths) {
|
|
|
700
714
|
}
|
|
701
715
|
}
|
|
702
716
|
}
|
|
703
|
-
if (currentNode.name !== "this") {
|
|
717
|
+
if (currentNode.name !== "this" && currentNode.name !== "top") {
|
|
704
718
|
withPath.push(`['${currentNode.name}']`);
|
|
705
719
|
}
|
|
706
720
|
return withPath.length > 0 ? `$${withPath.join("")}` : "$";
|
|
@@ -890,8 +904,17 @@ async function generateAgreement(modelManager, clauseLibrary, templateMark, data
|
|
|
890
904
|
}
|
|
891
905
|
} else if (CONDITIONAL_DEFINITION_RE.test(nodeClass)) {
|
|
892
906
|
if (context.condition) {
|
|
893
|
-
const
|
|
894
|
-
|
|
907
|
+
const key = this.path.join("/");
|
|
908
|
+
const resultStr = userCodeResults[key];
|
|
909
|
+
if (resultStr === void 0 || resultStr === null) {
|
|
910
|
+
context.isTrue = false;
|
|
911
|
+
} else {
|
|
912
|
+
try {
|
|
913
|
+
context.isTrue = !!JSON.parse(resultStr);
|
|
914
|
+
} catch (err) {
|
|
915
|
+
throw new Error(`Invalid JSON boolean result for condition '${key}': ${String(err)}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
895
918
|
} else {
|
|
896
919
|
const path = getJsonPath(templateMark, context, this.path);
|
|
897
920
|
const variableValues = jp.query(data, path, 1);
|
|
@@ -912,16 +935,27 @@ async function generateAgreement(modelManager, clauseLibrary, templateMark, data
|
|
|
912
935
|
} else if (CLAUSE_DEFINITION_RE.test(nodeClass)) {
|
|
913
936
|
const path = getJsonPath(templateMark, context, this.path);
|
|
914
937
|
const variableValues = jp.query(data, path, 1);
|
|
915
|
-
if (context.
|
|
916
|
-
delete context.nodes;
|
|
917
|
-
stopHere = true;
|
|
918
|
-
} else if (context.condition) {
|
|
938
|
+
if (context.condition) {
|
|
919
939
|
checkCode(context.condition);
|
|
920
|
-
const
|
|
940
|
+
const key = this.path.join("/");
|
|
941
|
+
const resultStr = userCodeResults[key];
|
|
942
|
+
let result = false;
|
|
943
|
+
if (resultStr === void 0 || resultStr === null) {
|
|
944
|
+
result = false;
|
|
945
|
+
} else {
|
|
946
|
+
try {
|
|
947
|
+
result = !!JSON.parse(resultStr);
|
|
948
|
+
} catch (err) {
|
|
949
|
+
throw new Error(`Invalid JSON boolean result for condition '${key}': ${String(err)}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
921
952
|
if (!result) {
|
|
922
953
|
delete context.nodes;
|
|
923
954
|
stopHere = true;
|
|
924
955
|
}
|
|
956
|
+
} else if (context.name !== "top" && (variableValues.length === 0 || variableValues[0] === void 0 || variableValues[0] === null)) {
|
|
957
|
+
delete context.nodes;
|
|
958
|
+
stopHere = true;
|
|
925
959
|
}
|
|
926
960
|
delete context.condition;
|
|
927
961
|
delete context.functionName;
|
|
@@ -970,18 +1004,53 @@ class TemplateMarkInterpreter {
|
|
|
970
1004
|
* @throws {Error} if the templateMark document is invalid
|
|
971
1005
|
*/
|
|
972
1006
|
checkTypes(templateMark) {
|
|
973
|
-
const modelManager = new concertoCore.ModelManager(
|
|
1007
|
+
const modelManager = new concertoCore.ModelManager();
|
|
974
1008
|
modelManager.addCTOModel(markdownCommon.ConcertoMetaModel.MODEL, "concertometamodel.cto");
|
|
975
1009
|
modelManager.addCTOModel(markdownCommon.CommonMarkModel.MODEL, "commonmark.cto");
|
|
976
1010
|
modelManager.addCTOModel(markdownCommon.TemplateMarkModel.MODEL, "templatemark.cto");
|
|
977
1011
|
const factory = new concertoCore.Factory(modelManager);
|
|
978
|
-
const serializer = new concertoCore.Serializer(factory, modelManager);
|
|
1012
|
+
const serializer = new concertoCore.Serializer(factory, modelManager, {});
|
|
979
1013
|
try {
|
|
980
|
-
serializer.fromJSON(templateMark);
|
|
981
|
-
return templateMark;
|
|
1014
|
+
serializer.fromJSON(templateMark, {});
|
|
982
1015
|
} catch (err) {
|
|
983
1016
|
throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(templateMark, null, 2)}`);
|
|
984
1017
|
}
|
|
1018
|
+
const errors = [];
|
|
1019
|
+
const templateClass = this.templateClass;
|
|
1020
|
+
const guardBlockPaths = /* @__PURE__ */ new Map();
|
|
1021
|
+
traverse(templateMark).forEach(function(node) {
|
|
1022
|
+
if (!node || typeof node !== "object" || !node.$class) return;
|
|
1023
|
+
const currentPath = this.path.join("/");
|
|
1024
|
+
if (OPTIONAL_DEFINITION_RE.test(node.$class) || CONDITIONAL_DEFINITION_RE.test(node.$class) || WITH_DEFINITION_RE.test(node.$class)) {
|
|
1025
|
+
guardBlockPaths.set(node.name, currentPath);
|
|
1026
|
+
}
|
|
1027
|
+
if (VARIABLE_DEFINITION_RE.test(node.$class) || ENUM_VARIABLE_DEFINITION_RE.test(node.$class) || FORMATTED_VARIABLE_DEFINITION_RE.test(node.$class)) {
|
|
1028
|
+
const propName = node.name;
|
|
1029
|
+
if (propName && propName !== "this") {
|
|
1030
|
+
try {
|
|
1031
|
+
const property = templateClass.getProperty(propName);
|
|
1032
|
+
if (property && property.isOptional()) {
|
|
1033
|
+
const guardPath = guardBlockPaths.get(propName);
|
|
1034
|
+
const isGuarded = guardPath !== void 0 && (currentPath === guardPath || currentPath.startsWith(guardPath + "/"));
|
|
1035
|
+
if (!isGuarded) {
|
|
1036
|
+
errors.push({
|
|
1037
|
+
propertyName: propName,
|
|
1038
|
+
message: `Optional property '${propName}' is used without a guard. Wrap it in {{#optional ${propName}}}...{{/optional}} or {{#if ${propName}}}...{{/if}}.`
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
if (errors.length > 0) {
|
|
1048
|
+
const errorMessage = `Optional properties used without guards: ${errors.map((e) => e.propertyName).join(", ")}`;
|
|
1049
|
+
const error = new Error(errorMessage);
|
|
1050
|
+
error.errors = errors;
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
return templateMark;
|
|
985
1054
|
}
|
|
986
1055
|
/**
|
|
987
1056
|
* Compiles the code nodes containing TS to code nodes containing JS.
|
|
@@ -1014,22 +1083,22 @@ class TemplateMarkInterpreter {
|
|
|
1014
1083
|
return compiler.compile(templateMark);
|
|
1015
1084
|
}
|
|
1016
1085
|
validateCiceroMark(ciceroMark) {
|
|
1017
|
-
const modelManager = new concertoCore.ModelManager(
|
|
1086
|
+
const modelManager = new concertoCore.ModelManager();
|
|
1018
1087
|
modelManager.addCTOModel(markdownCommon.ConcertoMetaModel.MODEL, "concertometamodel.cto");
|
|
1019
1088
|
modelManager.addCTOModel(markdownCommon.CommonMarkModel.MODEL, "commonmark.cto");
|
|
1020
1089
|
modelManager.addCTOModel(markdownCommon.CiceroMarkModel.MODEL, "ciceromark.cto");
|
|
1021
1090
|
const factory = new concertoCore.Factory(modelManager);
|
|
1022
|
-
const serializer = new concertoCore.Serializer(factory, modelManager);
|
|
1091
|
+
const serializer = new concertoCore.Serializer(factory, modelManager, {});
|
|
1023
1092
|
try {
|
|
1024
|
-
return serializer.fromJSON(ciceroMark);
|
|
1093
|
+
return serializer.fromJSON(ciceroMark, {});
|
|
1025
1094
|
} catch (err) {
|
|
1026
1095
|
throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(ciceroMark, null, 2)}`);
|
|
1027
1096
|
}
|
|
1028
1097
|
}
|
|
1029
1098
|
async generate(templateMark, data, options) {
|
|
1030
1099
|
const factory = new concertoCore.Factory(this.modelManager);
|
|
1031
|
-
const serializer = new concertoCore.Serializer(factory, this.modelManager);
|
|
1032
|
-
const templateData = serializer.fromJSON(data);
|
|
1100
|
+
const serializer = new concertoCore.Serializer(factory, this.modelManager, {});
|
|
1101
|
+
const templateData = serializer.fromJSON(data, {});
|
|
1033
1102
|
if (templateData.getFullyQualifiedType() !== this.templateClass.getFullyQualifiedName()) {
|
|
1034
1103
|
throw new Error(`Template data must be of type '${this.templateClass.getFullyQualifiedName()}'.`);
|
|
1035
1104
|
}
|
|
@@ -1078,7 +1147,7 @@ class TemplateArchiveProcessor {
|
|
|
1078
1147
|
* @param {object} request - the request to send to the template logic
|
|
1079
1148
|
* @param {object} state - the current state of the template
|
|
1080
1149
|
* @param {[string]} currentTime - the current time, defaults to now
|
|
1081
|
-
* @param {[number]} utcOffset - the UTC
|
|
1150
|
+
* @param {[number]} utcOffset - the UTC offset, defaults to zero
|
|
1082
1151
|
* @returns {Promise} the response and any events
|
|
1083
1152
|
*/
|
|
1084
1153
|
async trigger(data, request, state, currentTime, utcOffset) {
|
|
@@ -1098,6 +1167,8 @@ class TemplateArchiveProcessor {
|
|
|
1098
1167
|
const result = compiler.compile(code);
|
|
1099
1168
|
compiledCode[tsFile.getIdentifier()] = result;
|
|
1100
1169
|
}
|
|
1170
|
+
const resolvedTime = currentTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1171
|
+
const resolvedOffset = utcOffset ?? 0;
|
|
1101
1172
|
const evaluator = new JavaScriptEvaluator();
|
|
1102
1173
|
const evalResponse = await evaluator.evalDangerously({
|
|
1103
1174
|
templateLogic: true,
|
|
@@ -1106,7 +1177,7 @@ class TemplateArchiveProcessor {
|
|
|
1106
1177
|
code: compiledCode["logic/logic.ts"].code,
|
|
1107
1178
|
// TODO DCS - how to find the code to run?
|
|
1108
1179
|
argumentNames: ["data", "request", "state"],
|
|
1109
|
-
arguments: [data, request, state,
|
|
1180
|
+
arguments: [data, request, state, resolvedTime, resolvedOffset]
|
|
1110
1181
|
});
|
|
1111
1182
|
if (evalResponse.result) {
|
|
1112
1183
|
return evalResponse.result;
|
|
@@ -1120,8 +1191,8 @@ class TemplateArchiveProcessor {
|
|
|
1120
1191
|
/**
|
|
1121
1192
|
* Init the logic of a template
|
|
1122
1193
|
* @param {[string]} currentTime - the current time, defaults to now
|
|
1123
|
-
* @param {[number]} utcOffset - the UTC
|
|
1124
|
-
* @returns {Promise} the
|
|
1194
|
+
* @param {[number]} utcOffset - the UTC offset, defaults to zero
|
|
1195
|
+
* @returns {Promise<InitResponse>} the new state
|
|
1125
1196
|
*/
|
|
1126
1197
|
async init(data, currentTime, utcOffset) {
|
|
1127
1198
|
const logicManager = this.template.getLogicManager();
|
|
@@ -1140,6 +1211,8 @@ class TemplateArchiveProcessor {
|
|
|
1140
1211
|
const result = compiler.compile(code);
|
|
1141
1212
|
compiledCode[tsFile.getIdentifier()] = result;
|
|
1142
1213
|
}
|
|
1214
|
+
const resolvedTime = currentTime ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1215
|
+
const resolvedOffset = utcOffset ?? 0;
|
|
1143
1216
|
const evaluator = new JavaScriptEvaluator();
|
|
1144
1217
|
const evalResponse = await evaluator.evalDangerously({
|
|
1145
1218
|
templateLogic: true,
|
|
@@ -1148,7 +1221,7 @@ class TemplateArchiveProcessor {
|
|
|
1148
1221
|
code: compiledCode["logic/logic.ts"].code,
|
|
1149
1222
|
// TODO DCS - how to find the code to run?
|
|
1150
1223
|
argumentNames: ["data"],
|
|
1151
|
-
arguments: [data,
|
|
1224
|
+
arguments: [data, resolvedTime, resolvedOffset]
|
|
1152
1225
|
});
|
|
1153
1226
|
if (evalResponse.result) {
|
|
1154
1227
|
return evalResponse.result;
|