@formio/uag 1.3.0
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/LICENSE.txt +19 -0
- package/README.md +336 -0
- package/lib/UAGFormInterface.d.ts +113 -0
- package/lib/UAGFormInterface.js +371 -0
- package/lib/UAGProjectInterface.d.ts +26 -0
- package/lib/UAGProjectInterface.js +96 -0
- package/lib/config.d.ts +54 -0
- package/lib/config.js +2 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +43 -0
- package/lib/router.d.ts +3 -0
- package/lib/router.js +74 -0
- package/lib/template.d.ts +54 -0
- package/lib/template.js +120 -0
- package/lib/templates/allFieldsCollected.md +17 -0
- package/lib/templates/collectedData.md +5 -0
- package/lib/templates/confirmFormSubmission.md +18 -0
- package/lib/templates/fieldCollectedNext.md +14 -0
- package/lib/templates/fieldList.md +5 -0
- package/lib/templates/fieldRules.md +4 -0
- package/lib/templates/fieldValidationErrors.md +7 -0
- package/lib/templates/fields.md +19 -0
- package/lib/templates/formNotFound.md +1 -0
- package/lib/templates/formSubmitted.md +7 -0
- package/lib/templates/getAvailableForms.md +18 -0
- package/lib/templates/getFormFields.md +16 -0
- package/lib/templates/getFormFieldsEmpty.md +4 -0
- package/lib/templates/getFormFieldsError.md +7 -0
- package/lib/templates/getFormFieldsInfo.md +6 -0
- package/lib/templates/getOptionalFields.md +19 -0
- package/lib/templates/noFormsAvailable.md +3 -0
- package/lib/templates/noSubmissionsFound.md +11 -0
- package/lib/templates/submissionNotFound.md +6 -0
- package/lib/templates/submissionPartialIdAmbiguous.md +12 -0
- package/lib/templates/submissionPartialIdNotFound.md +12 -0
- package/lib/templates/submissionSearchError.md +8 -0
- package/lib/templates/submissionUpdateError.md +6 -0
- package/lib/templates/submissionUpdated.md +15 -0
- package/lib/templates/submissionsFound.md +25 -0
- package/lib/templates/submitValidationError.md +7 -0
- package/lib/templates/submittedData.md +4 -0
- package/lib/tools/SchemaBuilder.d.ts +136 -0
- package/lib/tools/SchemaBuilder.js +192 -0
- package/lib/tools/collectData.d.ts +3 -0
- package/lib/tools/collectData.js +72 -0
- package/lib/tools/confirmSubmission.d.ts +3 -0
- package/lib/tools/confirmSubmission.js +56 -0
- package/lib/tools/findSubmission.d.ts +3 -0
- package/lib/tools/findSubmission.js +165 -0
- package/lib/tools/getFieldInfo.d.ts +3 -0
- package/lib/tools/getFieldInfo.js +41 -0
- package/lib/tools/getFormFields.d.ts +3 -0
- package/lib/tools/getFormFields.js +99 -0
- package/lib/tools/getForms.d.ts +3 -0
- package/lib/tools/getForms.js +38 -0
- package/lib/tools/index.d.ts +13 -0
- package/lib/tools/index.js +47 -0
- package/lib/tools/submissionUpdate.d.ts +3 -0
- package/lib/tools/submissionUpdate.js +79 -0
- package/lib/tools/submitForm.d.ts +3 -0
- package/lib/tools/submitForm.js +62 -0
- package/lib/tools/utils.d.ts +28 -0
- package/lib/tools/utils.js +27 -0
- package/package.json +57 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UAGFormInterface = void 0;
|
|
4
|
+
const appserver_1 = require("@formio/appserver");
|
|
5
|
+
const core_1 = require("@formio/core");
|
|
6
|
+
const lodash_1 = require("lodash");
|
|
7
|
+
class UAGFormInterface extends appserver_1.FormInterface {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.uag = null;
|
|
11
|
+
}
|
|
12
|
+
getComponentFormat(component) {
|
|
13
|
+
switch (component.type) {
|
|
14
|
+
case 'phoneNumber':
|
|
15
|
+
return component.inputMask || '(999) 999-9999';
|
|
16
|
+
case 'datetime':
|
|
17
|
+
return component.widget?.format || 'yyyy-MM-dd hh:mm a';
|
|
18
|
+
case 'day':
|
|
19
|
+
return component.dayFirst ? 'dd/MM/yyyy' : 'MM/dd/yyyy';
|
|
20
|
+
case 'time':
|
|
21
|
+
return component.format || 'HH:mm';
|
|
22
|
+
default:
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
getComponentInfo(component, path) {
|
|
27
|
+
const fieldInfo = {
|
|
28
|
+
path,
|
|
29
|
+
label: component.label || component.key,
|
|
30
|
+
type: component.type,
|
|
31
|
+
format: this.getComponentFormat(component),
|
|
32
|
+
description: component.description || component.tooltip || '',
|
|
33
|
+
validation: component.validate || {},
|
|
34
|
+
};
|
|
35
|
+
if (component.placeholder) {
|
|
36
|
+
fieldInfo.prompt = component.placeholder;
|
|
37
|
+
}
|
|
38
|
+
if (component.type === 'select' || component.type === 'selectboxes') {
|
|
39
|
+
if (component.dataSrc === 'url') {
|
|
40
|
+
fieldInfo.options = [{ label: '** ANY VALUE IS ALLOWED **', value: 'Options are loaded from a URL.' }];
|
|
41
|
+
}
|
|
42
|
+
else if (component.dataSrc === 'resource') {
|
|
43
|
+
fieldInfo.options = [{ label: '** ANY VALUE IS ALLOWED **', value: `Options are loaded from the Form.io resource (${component.data.resource}).` }];
|
|
44
|
+
}
|
|
45
|
+
else if (component.dataSrc === 'json') {
|
|
46
|
+
fieldInfo.options = [{ label: '** ANY VALUE IS ALLOWED **', value: 'Options are not dynamically defined.' }];
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const values = component.data?.values || component.values;
|
|
50
|
+
if (!values || !Array.isArray(values)) {
|
|
51
|
+
fieldInfo.options = [{ label: '', value: 'No options available' }];
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
fieldInfo.options = values.reduce((acc, v) => {
|
|
55
|
+
acc.push({ label: v.label, value: v.value });
|
|
56
|
+
return acc;
|
|
57
|
+
}, []);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
fieldInfo.nested = this.isNestedComponent(component);
|
|
62
|
+
return fieldInfo;
|
|
63
|
+
}
|
|
64
|
+
isMultiple(component) {
|
|
65
|
+
if (!component)
|
|
66
|
+
return false;
|
|
67
|
+
return !!component.multiple || component.type === 'selectboxes' || component.type === 'tags';
|
|
68
|
+
}
|
|
69
|
+
getParentToolDescription(parent) {
|
|
70
|
+
if (!parent)
|
|
71
|
+
return '';
|
|
72
|
+
return `To collect data for this component, separately use the \`collect_field_data\` tool with the following set for the \`parent\` parameter: \`parent=${JSON.stringify(parent)}\`. Use the \`get_form_fields\` tool with the same \`parent\` parameter to retrieve all the fields within this parent field. `;
|
|
73
|
+
}
|
|
74
|
+
getComponentValueRule(component) {
|
|
75
|
+
let rule = '';
|
|
76
|
+
const parent = {
|
|
77
|
+
type: component.type,
|
|
78
|
+
label: component.label || component.key,
|
|
79
|
+
data_path: '<data_path>'
|
|
80
|
+
};
|
|
81
|
+
if (component.tree ||
|
|
82
|
+
component.type === 'datagrid' ||
|
|
83
|
+
component.type === 'editgrid') {
|
|
84
|
+
parent.isTable = true;
|
|
85
|
+
rule += 'The value is a table of rows (array of objects). ' + this.getParentToolDescription(parent) + 'All `data_path`(s) for the components within this table should contain the current row index (e.g. `dataGrid[0].a`, `dataGrid[0].b`, `dataGrid[1].b`, etc.).';
|
|
86
|
+
return rule;
|
|
87
|
+
}
|
|
88
|
+
if (component.type === 'form') {
|
|
89
|
+
parent.isForm = true;
|
|
90
|
+
rule += 'The value is a nested form submission in the format `{data: {...}}` where `{...}` is the values for the child components. ' + this.getParentToolDescription(parent) + 'All `data_path`(s) for the components within this nested form should be prefixed with `data` (e.g. `nestedForm.data.exampleField`).';
|
|
91
|
+
return rule;
|
|
92
|
+
}
|
|
93
|
+
if (component.type === 'container') {
|
|
94
|
+
parent.isContainer = true;
|
|
95
|
+
rule += 'The value is an object/map of nested component values in the format `{...}`. ' + this.getParentToolDescription(parent) + 'All `data_path`(s) for the components within this container should be prefixed with the container\'s `data_path` (e.g. `container.exampleField`).';
|
|
96
|
+
return rule;
|
|
97
|
+
}
|
|
98
|
+
switch (component.type) {
|
|
99
|
+
case 'tags':
|
|
100
|
+
rule += 'The value can be any alphanumeric string, containing letters, numbers, but no white space characters or symbols. Multiple tags should be comma-separated.';
|
|
101
|
+
break;
|
|
102
|
+
case 'signature':
|
|
103
|
+
rule += 'Have the user draw their signature. The value will be a base64-encoded PNG image string of that signature.';
|
|
104
|
+
break;
|
|
105
|
+
case 'textfield':
|
|
106
|
+
case 'textarea':
|
|
107
|
+
case 'hidden':
|
|
108
|
+
rule += 'The value can be any alphanumeric string, containing letters, numbers, white space characters, and common symbols.';
|
|
109
|
+
break;
|
|
110
|
+
case 'checkbox':
|
|
111
|
+
rule += 'The value must be either boolean true (checked) or false (unchecked). A checkbox is checked if the user confirms the value. (e.g. "I agree to the terms and conditions" -> true)';
|
|
112
|
+
break;
|
|
113
|
+
case 'number':
|
|
114
|
+
rule += 'The value must be a valid number.';
|
|
115
|
+
break;
|
|
116
|
+
case 'currency':
|
|
117
|
+
rule += 'The value must be a valid currency amount (a number with up to two decimal places).';
|
|
118
|
+
break;
|
|
119
|
+
case 'password':
|
|
120
|
+
rule += 'Do not allow the user to submit passwords.';
|
|
121
|
+
break;
|
|
122
|
+
case 'phoneNumber':
|
|
123
|
+
rule += 'The value must be a valid phone number, containing only numbers, spaces, parentheses, dashes, and must follow the format defined in the "**Format**" section of that component.';
|
|
124
|
+
break;
|
|
125
|
+
case 'selectboxes':
|
|
126
|
+
case 'select':
|
|
127
|
+
case 'radio':
|
|
128
|
+
rule += `The value must be ${this.isMultiple(component) ? 'one or more (as comma separated values)' : 'one'} of the following options provided in the "**Options**" section of that component, formatted as " - Label (value)":`;
|
|
129
|
+
break;
|
|
130
|
+
case 'datetime':
|
|
131
|
+
rule += 'The value must be a valid date and time string in the format provided by the **Format** section of that component.';
|
|
132
|
+
break;
|
|
133
|
+
case 'day':
|
|
134
|
+
rule += 'The value must be a valid day string in the format provided by the **Format** section of that component.';
|
|
135
|
+
break;
|
|
136
|
+
case 'time':
|
|
137
|
+
rule += 'The value must be a valid time string in the format provided by the **Format** section of that component.';
|
|
138
|
+
break;
|
|
139
|
+
case 'url':
|
|
140
|
+
rule += 'The value must be a valid URL, starting with http:// or https://';
|
|
141
|
+
break;
|
|
142
|
+
case 'email':
|
|
143
|
+
rule += 'The value must be a valid email address.';
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
rule += '';
|
|
147
|
+
}
|
|
148
|
+
return rule;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Determine if the component is a nested data components. For these components, the UAG treats them as
|
|
152
|
+
* separate data collection units where the agent will explicitely call out to collect data for these components.
|
|
153
|
+
*
|
|
154
|
+
* @import { Component } from "@formio/core";
|
|
155
|
+
* @param component { Component } - The component to check.
|
|
156
|
+
* @returns { boolean } - True if the component is a nested data component, false otherwise.
|
|
157
|
+
*/
|
|
158
|
+
isNestedComponent(component) {
|
|
159
|
+
if (component.tree ||
|
|
160
|
+
component.type === 'datagrid' ||
|
|
161
|
+
component.type === 'editgrid' ||
|
|
162
|
+
component.type === 'form') {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Determine if the component is a non-input component that should be skipped when collecting fields.
|
|
169
|
+
*
|
|
170
|
+
* @import { Component } from "@formio/core";
|
|
171
|
+
* @param component { Component } - The component to check.
|
|
172
|
+
* @returns { boolean } - True if the component is an input component, false otherwise.
|
|
173
|
+
*/
|
|
174
|
+
inputComponent(component) {
|
|
175
|
+
const modelType = core_1.Utils.getModelType(component);
|
|
176
|
+
if (component.input === false || component.type === 'button') {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if (!component.type || modelType === 'none' || modelType === 'content') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get the relevant fields from the current form. This will return any non-nested input components whose
|
|
186
|
+
* values have not already been set within the data model. This allows the agent to know what fields still need to be
|
|
187
|
+
* collected from the user, as well as provides a mechanism to break up large forms into smaller chunks of data collection
|
|
188
|
+
* using the 'nested' components (datagrid, editgrid, nested form, etc.).
|
|
189
|
+
*
|
|
190
|
+
* @import { Submission } from "@formio/core";
|
|
191
|
+
* @import { AuthRequest } from "@formio/appserver";
|
|
192
|
+
* @param submission { Submission } - The current submission data model.
|
|
193
|
+
* @param authInfo { AuthRequest } - The current authentication information.
|
|
194
|
+
* @param within { string | string[] } - Optional data path or array of data paths to limit the field extraction within.
|
|
195
|
+
* - If within is an array, then it works like "includes".
|
|
196
|
+
* - If within is a string, then it is a single path to limit components within (non-inclusive).
|
|
197
|
+
* @returns { Promise<FormFieldInfo> } - The extracted form field information.
|
|
198
|
+
*/
|
|
199
|
+
async getFields(submission, authInfo, within = '') {
|
|
200
|
+
const fieldInfo = {
|
|
201
|
+
rowIndex: -1,
|
|
202
|
+
total: 0,
|
|
203
|
+
totalRequired: 0,
|
|
204
|
+
totalRequiredCollected: 0,
|
|
205
|
+
errors: [],
|
|
206
|
+
required: { components: [], rules: {} },
|
|
207
|
+
optional: { components: [], rules: {} }
|
|
208
|
+
};
|
|
209
|
+
const nestedPaths = [];
|
|
210
|
+
const context = await this.process(submission, authInfo, null, null, null, [
|
|
211
|
+
...core_1.Processors,
|
|
212
|
+
{
|
|
213
|
+
name: 'getFields',
|
|
214
|
+
shouldProcess: () => true,
|
|
215
|
+
process: async (context) => {
|
|
216
|
+
const { component, path, value } = context;
|
|
217
|
+
if (this.isNestedComponent(component)) {
|
|
218
|
+
nestedPaths.push(path);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
postProcess: async (context) => {
|
|
222
|
+
const { component, path, value } = context;
|
|
223
|
+
// Get the current nested path (if any).
|
|
224
|
+
const nestedPath = nestedPaths?.length ? nestedPaths[nestedPaths.length - 1] : null;
|
|
225
|
+
// If the nested path IS the nested component, then pop it off the stack and skip it.
|
|
226
|
+
if (nestedPath === path) {
|
|
227
|
+
nestedPaths.pop();
|
|
228
|
+
}
|
|
229
|
+
if (within) {
|
|
230
|
+
// If within is an array, then this works like "includes".
|
|
231
|
+
if (Array.isArray(within) && !within.includes(path)) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (typeof within === 'string') {
|
|
235
|
+
if (path.startsWith(within) && (path !== within)) {
|
|
236
|
+
// Make sure to set the largest index for the nested component.
|
|
237
|
+
const indexMatch = path.match(/\[(\d+)\]/);
|
|
238
|
+
fieldInfo.rowIndex = indexMatch?.length ? parseInt(indexMatch[indexMatch.length - 1], 10) : 0;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// If within is a string, then it is a single path to limit components within (non-inclusive).
|
|
245
|
+
if ((typeof within === 'string') && (!path.startsWith(within) || (path === within))) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// If this is a component within the nested path, then skip it...
|
|
250
|
+
else if (nestedPath && path.startsWith(nestedPath) && (path !== nestedPath)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// Skip non-input components and conditionally hidden components.
|
|
254
|
+
if (!this.inputComponent(component) || component.scope?.conditionallyHidden) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Increment the total field count regardless of whether it has a value or not.
|
|
258
|
+
fieldInfo.total++;
|
|
259
|
+
if (component.validate?.required) {
|
|
260
|
+
fieldInfo.totalRequired++;
|
|
261
|
+
}
|
|
262
|
+
// If the component hass a value, then skip it.
|
|
263
|
+
if ((0, core_1.componentHasValue)(component, value)) {
|
|
264
|
+
if (component.validate?.required) {
|
|
265
|
+
fieldInfo.totalRequiredCollected++;
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Add the component info to the appropriate list.
|
|
270
|
+
const criteria = component.validate?.required ? 'required' : 'optional';
|
|
271
|
+
fieldInfo[criteria].rules[component.type] = this.getComponentValueRule(component);
|
|
272
|
+
fieldInfo[criteria].components.push(this.getComponentInfo(component, path));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
]);
|
|
276
|
+
if (context.scope?.errors?.length) {
|
|
277
|
+
// Filter the "required" validation since those are added to the fieldInfo separately.
|
|
278
|
+
context.scope.errors = context.scope.errors.filter((error) => (error.ruleName !== 'required'));
|
|
279
|
+
if (context.scope.errors?.length) {
|
|
280
|
+
fieldInfo.errors = this.convertToFormFieldErrors((0, core_1.interpolateErrors)(context.scope.errors));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return fieldInfo;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Return the component at the specified data path.
|
|
287
|
+
* @param path { string } - The data path of the component to retrieve.
|
|
288
|
+
* @returns { Component | undefined } - The component at the specified path, or undefined if not found.
|
|
289
|
+
*/
|
|
290
|
+
getComponent(path) {
|
|
291
|
+
return core_1.Utils.getComponent(this.form.components, path);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Convert a list of InterpolatedErrors into FormFieldErrors.
|
|
295
|
+
* @param errors
|
|
296
|
+
* @returns
|
|
297
|
+
*/
|
|
298
|
+
convertToFormFieldErrors(errors) {
|
|
299
|
+
return errors.map((error) => {
|
|
300
|
+
return {
|
|
301
|
+
label: error.context?.label || 'Field',
|
|
302
|
+
path: error.context?.path,
|
|
303
|
+
error: error.message || 'Unknown validation error'
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Perform a validation process on the collected form data.
|
|
309
|
+
* @param data
|
|
310
|
+
* @param auth
|
|
311
|
+
* @returns
|
|
312
|
+
*/
|
|
313
|
+
async validateData(submission, auth) {
|
|
314
|
+
return this.convertToFormFieldErrors(await this.validate(submission, auth));
|
|
315
|
+
}
|
|
316
|
+
convertToSubmission(data) {
|
|
317
|
+
const submission = { data: {} };
|
|
318
|
+
for (let [path, value] of Object.entries(data || {})) {
|
|
319
|
+
const comp = this.getComponent(path);
|
|
320
|
+
if (value && comp?.type === 'selectboxes' && typeof value === 'string') {
|
|
321
|
+
value = value.split(',').reduce((obj, v) => {
|
|
322
|
+
obj[v.trim()] = true;
|
|
323
|
+
return obj;
|
|
324
|
+
}, {});
|
|
325
|
+
}
|
|
326
|
+
else if (value && this.isMultiple(comp) && typeof value === 'string') {
|
|
327
|
+
value = value.split(',').map((v) => v.trim());
|
|
328
|
+
}
|
|
329
|
+
(0, lodash_1.set)(submission.data, path, value);
|
|
330
|
+
}
|
|
331
|
+
return submission;
|
|
332
|
+
}
|
|
333
|
+
formatData(data = {}) {
|
|
334
|
+
const uagData = [];
|
|
335
|
+
let prefix = '';
|
|
336
|
+
core_1.Utils.eachComponentData(this.form?.components || [], data, (component, data, row, path) => {
|
|
337
|
+
let value = (0, lodash_1.get)(data, path);
|
|
338
|
+
if (this.isNestedComponent(component)) {
|
|
339
|
+
uagData.push({
|
|
340
|
+
prefix,
|
|
341
|
+
path,
|
|
342
|
+
label: component.label || component.key || path,
|
|
343
|
+
value: ''
|
|
344
|
+
});
|
|
345
|
+
prefix += ' ';
|
|
346
|
+
}
|
|
347
|
+
else if (this.inputComponent(component) && (0, core_1.componentHasValue)(component, value)) {
|
|
348
|
+
uagData.push({
|
|
349
|
+
prefix,
|
|
350
|
+
path,
|
|
351
|
+
label: component.label || component.key || path,
|
|
352
|
+
value: (0, lodash_1.isObjectLike)(value) ? JSON.stringify(value) : value
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}, false, false, undefined, undefined, undefined, (component, data) => {
|
|
356
|
+
if (this.isNestedComponent(component)) {
|
|
357
|
+
prefix = prefix.slice(0, -3);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return uagData;
|
|
361
|
+
}
|
|
362
|
+
formatSubmission(submission) {
|
|
363
|
+
return {
|
|
364
|
+
_id: submission._id,
|
|
365
|
+
data: this.formatData(submission.data),
|
|
366
|
+
created: submission.created,
|
|
367
|
+
modified: submission.modified
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
exports.UAGFormInterface = UAGFormInterface;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ProjectInterface, SubmissionRequest } from "@formio/appserver";
|
|
2
|
+
import { UAGConfig } from "./config";
|
|
3
|
+
import { Form } from "@formio/core";
|
|
4
|
+
import { NextFunction, Response } from "express";
|
|
5
|
+
import { UAGFormInterface } from "./UAGFormInterface";
|
|
6
|
+
import { ResponseTemplate, UAGTemplate } from "./template";
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
export declare class UAGProjectInterface extends ProjectInterface {
|
|
9
|
+
user: any;
|
|
10
|
+
formNames: string[];
|
|
11
|
+
uagTemplate: UAGTemplate | null;
|
|
12
|
+
mcpServer: McpServer;
|
|
13
|
+
get config(): UAGConfig;
|
|
14
|
+
constructor(endpoint?: string);
|
|
15
|
+
initialize(): Promise<void>;
|
|
16
|
+
addForm(form: Form, key: string): UAGFormInterface | undefined;
|
|
17
|
+
router(): Promise<any>;
|
|
18
|
+
authorizeRequest(req: SubmissionRequest, res: Response, next: NextFunction): Promise<Response<any, Record<string, any>> | undefined>;
|
|
19
|
+
mcpResponse(templateName: ResponseTemplate, data?: object, isError?: boolean): {
|
|
20
|
+
isError?: boolean | undefined;
|
|
21
|
+
content: {
|
|
22
|
+
type: "text";
|
|
23
|
+
text: string;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UAGProjectInterface = void 0;
|
|
4
|
+
const appserver_1 = require("@formio/appserver");
|
|
5
|
+
const tools_1 = require("./tools");
|
|
6
|
+
const router_1 = require("./router");
|
|
7
|
+
const template_1 = require("./template");
|
|
8
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
const debug = require('debug')('formio:uag:UAGProjectInterface');
|
|
10
|
+
class UAGProjectInterface extends appserver_1.ProjectInterface {
|
|
11
|
+
get config() { return appserver_1.ProjectInterface.module?.config || {}; }
|
|
12
|
+
constructor(endpoint) {
|
|
13
|
+
super(endpoint);
|
|
14
|
+
this.user = null;
|
|
15
|
+
this.formNames = [];
|
|
16
|
+
this.uagTemplate = null;
|
|
17
|
+
this.mcpServer = new mcp_js_1.McpServer({ name: 'formio-uag', version: '1.0.0' });
|
|
18
|
+
}
|
|
19
|
+
async initialize() {
|
|
20
|
+
await super.initialize();
|
|
21
|
+
this.uagTemplate = new template_1.UAGTemplate(this.config?.responseTemplates || {});
|
|
22
|
+
// Get the standard UAG tools.
|
|
23
|
+
const tools = await (0, tools_1.getTools)(this);
|
|
24
|
+
// Add the custom tools from config.
|
|
25
|
+
if (this.config?.tools?.length) {
|
|
26
|
+
tools.push(...this.config.tools);
|
|
27
|
+
}
|
|
28
|
+
// Iterate and register all the tools.
|
|
29
|
+
for (const tool of tools) {
|
|
30
|
+
if (tool?.name) {
|
|
31
|
+
this.mcpServer.registerTool(tool.name, {
|
|
32
|
+
title: tool.title,
|
|
33
|
+
description: tool.description,
|
|
34
|
+
inputSchema: tool.inputSchema
|
|
35
|
+
}, tool.execute);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
addForm(form, key) {
|
|
40
|
+
const formInterface = super.addForm(form, key);
|
|
41
|
+
if (!formInterface)
|
|
42
|
+
return;
|
|
43
|
+
key = key || formInterface?.form?.key;
|
|
44
|
+
if (form.tags?.includes('uag')) {
|
|
45
|
+
debug(`Registering UAG form: ${key}`);
|
|
46
|
+
formInterface.uag = {
|
|
47
|
+
machineName: key,
|
|
48
|
+
name: form.name || form.path || key,
|
|
49
|
+
title: form.title || form.name || key,
|
|
50
|
+
description: form.properties?.description || `A form to submit new ${form.title} records.`
|
|
51
|
+
};
|
|
52
|
+
this.formNames.push(formInterface.uag.name);
|
|
53
|
+
}
|
|
54
|
+
return formInterface;
|
|
55
|
+
}
|
|
56
|
+
async router() {
|
|
57
|
+
const router = await super.router();
|
|
58
|
+
const uagRouter = (0, router_1.UAGRouter)(this);
|
|
59
|
+
router.use('/uag', (req, res, next) => this.authorizeRequest(req, res, next), uagRouter);
|
|
60
|
+
router.use('/mcp', (req, res, next) => this.authorizeRequest(req, res, next), uagRouter);
|
|
61
|
+
return router;
|
|
62
|
+
}
|
|
63
|
+
async authorizeRequest(req, res, next) {
|
|
64
|
+
try {
|
|
65
|
+
const pkce = this.provider('auth.pkce');
|
|
66
|
+
if (!pkce) {
|
|
67
|
+
throw new Error('No PKCE provider configured');
|
|
68
|
+
}
|
|
69
|
+
const auth = await pkce?.authorize(req.get('authorization'));
|
|
70
|
+
if (!auth || !auth.user) {
|
|
71
|
+
throw new Error('Unauthorized');
|
|
72
|
+
}
|
|
73
|
+
req.auth = auth;
|
|
74
|
+
next();
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return res.status(401).set({
|
|
78
|
+
'WWW-Authenticate': `Bearer realm="uag-server", resource_metadata="${this.config?.baseUrl}/.well-known/oauth-protected-resource"`
|
|
79
|
+
}).json({
|
|
80
|
+
detail: err.message || err
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
mcpResponse(templateName, data = {}, isError = false) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: this.uagTemplate?.renderTemplate(templateName, data) || '',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
...(isError && { isError: true }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.UAGProjectInterface = UAGProjectInterface;
|
package/lib/config.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ServerConfig } from '@formio/appserver';
|
|
2
|
+
import { ToolInfo } from './tools/utils';
|
|
3
|
+
export type UAGForm = {
|
|
4
|
+
name: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
machineName?: string;
|
|
8
|
+
};
|
|
9
|
+
export type UAGTemplateConfig = {
|
|
10
|
+
collectedData?: string;
|
|
11
|
+
submittedData?: string;
|
|
12
|
+
fieldRules?: string;
|
|
13
|
+
allFieldsCollected?: string;
|
|
14
|
+
fieldCollectedNext?: string;
|
|
15
|
+
submitValidationError?: string;
|
|
16
|
+
formSubmitted?: string;
|
|
17
|
+
listAvailableForms?: string;
|
|
18
|
+
confirmFormSubmission?: string;
|
|
19
|
+
getOptionalFields?: string;
|
|
20
|
+
formNotFound?: string;
|
|
21
|
+
noSubmissionsFound?: string;
|
|
22
|
+
submissionsFound?: string;
|
|
23
|
+
submissionSearchError?: string;
|
|
24
|
+
submissionNotFound?: string;
|
|
25
|
+
updateNotConfirmed?: string;
|
|
26
|
+
submissionUpdateError?: string;
|
|
27
|
+
submissionUpdated?: string;
|
|
28
|
+
submissionPartialIdAmbiguous?: string;
|
|
29
|
+
submissionPartialIdNotFound?: string;
|
|
30
|
+
getFormFields?: string;
|
|
31
|
+
getFormFieldsError?: string;
|
|
32
|
+
fieldValidationErrors?: string;
|
|
33
|
+
fields?: string;
|
|
34
|
+
getAvailableForms?: string;
|
|
35
|
+
noFormsAvailable?: string;
|
|
36
|
+
[key: string]: string | undefined;
|
|
37
|
+
};
|
|
38
|
+
export type UAGToolOverride = {
|
|
39
|
+
get_forms?: ToolInfo;
|
|
40
|
+
get_form_fields?: ToolInfo;
|
|
41
|
+
get_field_info?: ToolInfo;
|
|
42
|
+
collect_field_data?: ToolInfo;
|
|
43
|
+
confirm_form_submission?: ToolInfo;
|
|
44
|
+
submit_completed_form?: ToolInfo;
|
|
45
|
+
submission_update?: ToolInfo;
|
|
46
|
+
find_submissions?: ToolInfo;
|
|
47
|
+
};
|
|
48
|
+
export interface UAGConfig extends ServerConfig {
|
|
49
|
+
baseUrl?: string;
|
|
50
|
+
loginForm?: string;
|
|
51
|
+
responseTemplates?: UAGTemplateConfig;
|
|
52
|
+
toolOverrides?: UAGToolOverride;
|
|
53
|
+
tools?: ToolInfo[];
|
|
54
|
+
}
|
package/lib/config.js
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Server, ServerModule } from '@formio/appserver';
|
|
2
|
+
import { UAGProjectInterface } from './UAGProjectInterface';
|
|
3
|
+
import { UAGFormInterface } from './UAGFormInterface';
|
|
4
|
+
import { UAGConfig } from './config';
|
|
5
|
+
export type UAGModule = ServerModule & {
|
|
6
|
+
config?: UAGConfig;
|
|
7
|
+
};
|
|
8
|
+
export declare class UAGServer extends Server {
|
|
9
|
+
constructor(config?: UAGConfig);
|
|
10
|
+
}
|
|
11
|
+
export * from '@formio/appserver';
|
|
12
|
+
export * from './config';
|
|
13
|
+
export * from './tools';
|
|
14
|
+
export * from './router';
|
|
15
|
+
export * from './template';
|
|
16
|
+
export { UAGProjectInterface, UAGFormInterface };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.UAGFormInterface = exports.UAGProjectInterface = exports.UAGServer = void 0;
|
|
18
|
+
const appserver_1 = require("@formio/appserver");
|
|
19
|
+
const UAGProjectInterface_1 = require("./UAGProjectInterface");
|
|
20
|
+
Object.defineProperty(exports, "UAGProjectInterface", { enumerable: true, get: function () { return UAGProjectInterface_1.UAGProjectInterface; } });
|
|
21
|
+
const UAGFormInterface_1 = require("./UAGFormInterface");
|
|
22
|
+
Object.defineProperty(exports, "UAGFormInterface", { enumerable: true, get: function () { return UAGFormInterface_1.UAGFormInterface; } });
|
|
23
|
+
const lodash_1 = require("lodash");
|
|
24
|
+
class UAGServer extends appserver_1.Server {
|
|
25
|
+
constructor(config) {
|
|
26
|
+
super((0, lodash_1.defaultsDeep)(config || {}, {
|
|
27
|
+
baseUrl: (0, lodash_1.get)(process.env, 'BASE_URL', '').toString(),
|
|
28
|
+
license: (0, lodash_1.get)(process.env, 'UAG_LICENSE', '').toString(),
|
|
29
|
+
submissionProxy: true,
|
|
30
|
+
auth: { pkce: true }
|
|
31
|
+
}));
|
|
32
|
+
this.use({
|
|
33
|
+
ProjectInterface: UAGProjectInterface_1.UAGProjectInterface,
|
|
34
|
+
FormInterface: UAGFormInterface_1.UAGFormInterface
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.UAGServer = UAGServer;
|
|
39
|
+
__exportStar(require("@formio/appserver"), exports);
|
|
40
|
+
__exportStar(require("./config"), exports);
|
|
41
|
+
__exportStar(require("./tools"), exports);
|
|
42
|
+
__exportStar(require("./router"), exports);
|
|
43
|
+
__exportStar(require("./template"), exports);
|
package/lib/router.d.ts
ADDED