@oneblink/sdk-core 10.1.0-beta.7 → 10.1.0-beta.9
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.
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import morphExpressions from 'morph-expressions';
|
|
2
|
+
// morph-expressions is CommonJS and assigns `exports.default`, which Node's ESM
|
|
3
|
+
// interop exposes as the whole `module.exports` rather than the constructor.
|
|
4
|
+
const ExpressionParser = morphExpressions.default ?? morphExpressions;
|
|
2
5
|
import { findFormElement, flattenFormElements, matchElementsTagRegex, } from './formElementsService.js';
|
|
3
6
|
import { DATE_ONLY_PATTERN, } from './conditionalLogicService/types.js';
|
|
4
7
|
const isUnenteredValue = (value) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calculationService.js","sourceRoot":"","sources":["../src/calculationService.ts"],"names":[],"mappings":"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","sourcesContent":["import ExpressionParser from 'morph-expressions'\nimport { FormTypes, SubmissionTypes } from '@oneblink/types'\nimport {\n findFormElement,\n flattenFormElements,\n matchElementsTagRegex,\n} from './formElementsService.js'\nimport {\n DATE_ONLY_PATTERN,\n type ParseDayOnlyDate,\n} from './conditionalLogicService/types.js'\n\nexport type { ParseDayOnlyDate } from './conditionalLogicService/types.js'\n\nexport type EvaluateExpressionResult =\n | { type: 'RESULT'; value: number }\n | { type: 'MISSING_VALUES' }\n | { type: 'INVALID_EXPRESSION'; error: Error }\n\nconst isUnenteredValue = (value: unknown | undefined) => {\n return !value && value !== 0\n}\n\nconst isObjectWithValue = (obj: unknown): obj is { value: unknown } => {\n return typeof obj === 'object' && obj !== null && 'value' in obj\n}\n\n/**\n * Escape special RegExp characters in a string so it can be used as a literal\n * pattern.\n */\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Workaround for `.toFixed()` not rounding floating point numbers correctly.\n */\nfunction roundToFixed(number: number, decimals: number) {\n const multiplier = Math.pow(10, decimals)\n const roundedNumber = Math.round(number * multiplier) / multiplier\n return roundedNumber.toFixed(decimals)\n}\n\nfunction findFormElementByNamePath(\n formElements: FormTypes.FormElement[],\n elementName: string,\n): FormTypes.FormElement | undefined {\n const parts = elementName.split('|')\n let searchElements = formElements\n let found: FormTypes.FormElement | undefined\n\n for (let i = 0; i < parts.length; i++) {\n const name = parts[i]\n // Match flattenFormElements: page/section are transparent for naming, but\n // form/repeatableSet/infoPage require an explicit parent path segment.\n found = flattenFormElements(searchElements).find(\n (element) => 'name' in element && element.name === name,\n )\n if (!found) {\n return\n }\n\n if (i < parts.length - 1) {\n if (!('elements' in found) || !found.elements) {\n return\n }\n searchElements = found.elements\n }\n }\n\n return found\n}\n\n/**\n * Find `{ELEMENT:...}` references in an expression whose name paths do not\n * exist in the form definition.\n *\n * Intended for server-side payment validation. Skip this on the client during\n * interactive calculation evaluation — it walks the form definition for every\n * referenced element and is unnecessary for display.\n *\n * @returns Missing element name paths (e.g. `Amount` or `Items|Amount`), or an\n * empty array when every referenced element exists.\n */\nexport function findMissingFormElementsInExpression({\n expression,\n formElements,\n}: {\n expression: string\n formElements: FormTypes.FormElement[]\n}): string[] {\n if (!expression) {\n return []\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const missingFormElementNames: string[] = []\n const seen = new Set<string>()\n for (const elementName of elementNames) {\n if (seen.has(elementName)) {\n continue\n }\n seen.add(elementName)\n if (!findFormElementByNamePath(formElements, elementName)) {\n missingFormElementNames.push(elementName)\n }\n }\n return missingFormElementNames\n}\n\nfunction resolveElementValue({\n submission,\n nestedElementNames,\n formElements,\n parseDayOnlyDate,\n}: {\n submission: SubmissionTypes.S3SubmissionData['submission']\n nestedElementNames: string[]\n formElements: FormTypes.FormElement[]\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n const defaultAccumulator = submission[nestedElementNames[0]]\n return nestedElementNames.reduce(\n (elementValue: unknown | undefined, elementName: string, index: number) => {\n // Numbers can just be returned as is\n if (typeof elementValue === 'number') {\n return elementValue\n }\n\n // Attempt to get a number from the element value as a string.\n // NaN is accounted for in the calculation so we can return that from here.\n if (typeof elementValue === 'string') {\n // Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so\n // callers control timezone. Other date strings use `new Date(value)`.\n // Numeric strings (e.g. `\"10\"`) fall through to parseFloat — `Date`\n // can otherwise treat them as valid dates.\n const isDayOnly = DATE_ONLY_PATTERN.test(elementValue)\n if (isDayOnly) {\n const parsedDate = parseDayOnlyDate(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n } else if (Number.isNaN(Number(elementValue))) {\n const parsedDate = new Date(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n }\n\n return parseFloat(elementValue)\n }\n\n if (Array.isArray(elementValue)) {\n // If there are no entries, return NaN to prevent the calculation from\n // running.\n if (!elementValue.length) {\n return NaN\n }\n\n // An array could be an element that allows multiple values e.g.\n // checkboxes. If that is the case, add them all together and move on.\n const elementValues = elementValue.map((entry) => parseFloat(entry))\n if (elementValues.every((entry) => !Number.isNaN(entry))) {\n return elementValues.reduce((number, entry) => number + entry, 0)\n }\n\n // Otherwise attempt to process it as a repeatable set. If we found\n // another repeatable set to process, pass it to the next element name\n // to iterate over the entries. If we are processing the entries in a\n // repeatable set, we can sum the number elements in the entries.\n const nextElementName = nestedElementNames[index + 1]\n\n let isNestedRepeatableSet = false\n const nestedElementValues = elementValue.reduce(\n (nestedElementValues, entry) => {\n if (entry) {\n const nextElementValue = entry[nextElementName]\n if (Array.isArray(nextElementValue)) {\n if (nextElementValue.length) {\n nestedElementValues.push(...nextElementValue)\n isNestedRepeatableSet = true\n }\n } else {\n nestedElementValues.push(nextElementValue)\n }\n }\n return nestedElementValues\n },\n [] as unknown[],\n )\n\n // If the nested element values are all arrays, pass them on to the\n // next iteration.\n if (isNestedRepeatableSet) {\n return nestedElementValues\n }\n\n return nestedElementValues.reduce(\n (total: number, nestedElementValue: unknown | undefined) => {\n if (Number.isNaN(total)) {\n return NaN\n }\n const value = parseFloat(nestedElementValue as string)\n if (Number.isNaN(value)) {\n return NaN\n }\n return total + value\n },\n 0,\n )\n }\n\n // If the value is an object, take the element name and check to see if\n // this element is a nested form element. If so, take the next nested\n // element name, find its value in the object and return it for the next\n // iteration to handle.\n if (typeof elementValue === 'object') {\n const formFormElement = findFormElement(\n formElements,\n (e) => e.type === 'form' && e.name === nestedElementNames[index],\n )\n const nextElementName = nestedElementNames[index + 1]\n if (formFormElement && nextElementName) {\n return (elementValue as Record<string, unknown>)[nextElementName]\n }\n }\n\n // \"compliance\" form element has an object value with a \"value\" property.\n if (\n isObjectWithValue(elementValue) &&\n typeof elementValue.value === 'string'\n ) {\n return parseFloat(elementValue.value)\n }\n\n // We did not find a number value from the known elements, assume we are\n // at the end of the line.\n return NaN\n },\n defaultAccumulator,\n )\n}\n\n/**\n * Evaluate a OneBlink calculation expression against form submission data.\n *\n * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so\n * callers control timezone-aware behaviour (client vs server), matching the\n * pattern used by `conditionalLogicService`. Other date strings use\n * `new Date(value)`.\n *\n * #### Expression syntax\n *\n * Expressions are mathematical / logical formulas. Form element values are\n * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via\n * `|`, e.g. `{ELEMENT:Children|Age}`).\n *\n * ##### Literals\n *\n * - **Numbers** — e.g. `0`, `5`, `5.4`\n * - **Strings** — single or double quoted, e.g. `\"abc\"`, `'abc'`\n * - **Booleans** — `true`, `false`\n *\n * ##### Arithmetic operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------- |\n * | `+` | Addition (also unary plus) | `1 + 2` → `3` |\n * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |\n * | `*` | Multiplication | `4 * 2` → `8` |\n * | `/` | Division | `4 / 2` → `2` |\n * | `%` | Remainder | `8 % 3` → `2` |\n * | `++` | Unary increment | `++2` → `3` |\n * | `--` | Unary decrement | `--2` → `1` |\n *\n * Standard operator precedence applies. Use parentheses to override it, e.g.\n * `3 * (2 + 1)` → `9`.\n *\n * ##### Comparison operators\n *\n * | Operator | Description | Example |\n * | -------- | --------------------- | ------------------- |\n * | `==` | Equal | `2 == 2` → `true` |\n * | `!=` | Not equal | `2 != 3` → `true` |\n * | `>` | Greater than | `2 > 1` → `true` |\n * | `>=` | Greater than or equal | `2 >= 2` → `true` |\n * | `<` | Less than | `2 < 3` → `true` |\n * | `<=` | Less than or equal | `2 <= 2` → `true` |\n *\n * ##### Logical operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------------------- |\n * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |\n * | `\\|\\|` | Logical OR (returns first truthy) | `false \\|\\| true` → `true` |\n * | `!` | Logical NOT | `!false` → `true` |\n *\n * ##### Element references\n *\n * - `{ELEMENT:Number}` — value of the root element named `Number`\n * - `{ELEMENT:Set\\|Child}` — value of `Child` within repeatable set / nested\n * form element `Set`\n * - Nested paths may include multiple segments:\n * `{ELEMENT:Parents\\|Children\\|Age}`\n *\n * How values are coerced for evaluation:\n *\n * - Numbers are used as-is\n * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other\n * non-numeric date strings use `new Date(value)`. Valid dates use\n * `Date#getTime()`, otherwise the string is parsed as a float via\n * `parseFloat`\n * - Arrays of numeric strings (e.g. checkboxes) are summed\n * - Repeatable set entries sum the referenced nested numeric values\n * - Empty arrays yield `NaN` (result type `MISSING_VALUES`)\n * - Compliance element objects use their `value` property\n *\n * ##### Built-in functions\n *\n * | Function | Description | Example |\n * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |\n * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |\n * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |\n * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |\n * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`\"\"`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |\n *\n * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or\n * not finite.\n *\n * ##### Examples\n *\n * ```js\n * const result = calculationService.evaluateExpression({\n * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',\n * submission: { Quantity: 3, Price: 12.5 },\n * formElements: form.elements,\n * // Client: local timezone. Server: organisation timezone.\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // result === { type: 'RESULT', value: 37.5 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',\n * submission: { Amount: 100 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 110 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',\n * submission: { Required: 5 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 5 }\n * ```\n *\n * @param options\n * @returns A discriminated result describing the outcome of evaluation. Empty\n * expressions and parse failures return\n * `{ type: 'INVALID_EXPRESSION', error }`. Unexpected runtime errors are\n * rethrown.\n */\nexport function evaluateExpression({\n expression,\n submission,\n formElements,\n parseDayOnlyDate,\n}: {\n /** The calculation expression string to evaluate */\n expression: string\n /** Form submission data used to resolve `{ELEMENT:...}` references */\n submission: SubmissionTypes.S3SubmissionData['submission']\n /**\n * Form elements definition. Used to resolve nested `form` element values\n * referenced in the expression and to validate `{ELEMENT:...}` references.\n */\n formElements: FormTypes.FormElement[]\n /**\n * Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core\n * only calls this for day-only values; other date strings use\n * `new Date(value)`.\n */\n parseDayOnlyDate: ParseDayOnlyDate\n}): EvaluateExpressionResult {\n if (!expression) {\n return {\n type: 'INVALID_EXPRESSION',\n error: new Error('Expression is required.'),\n }\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const exprParser = new ExpressionParser<\n SubmissionTypes.S3SubmissionData['submission']\n >()\n exprParser.registerFunction('ROUND', (value: number, precision: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return parseFloat(roundToFixed(value, precision))\n }\n return null\n })\n exprParser.registerFunction('ROUND_DOWN', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.floor(value)\n }\n return null\n })\n exprParser.registerFunction('ROUND_UP', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.ceil(value)\n }\n return null\n })\n exprParser.registerFunction(\n 'ISNULL',\n (value: unknown | undefined, defaultValue: number) => {\n if (isUnenteredValue(value)) {\n return defaultValue || 0\n }\n return value\n },\n )\n\n const code = elementNames.reduce((code, elementName, index) => {\n const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g')\n const replacement = `a${index}`\n exprParser.registerProperty(\n replacement,\n (submissionScope: SubmissionTypes.S3SubmissionData['submission']) =>\n resolveElementValue({\n submission: submissionScope,\n nestedElementNames: elementName.split('|'),\n formElements,\n parseDayOnlyDate,\n }),\n )\n return code.replace(regex, replacement)\n }, expression)\n\n let parsed\n try {\n parsed = exprParser.parse(code.trim())\n } catch (error) {\n return {\n type: 'INVALID_EXPRESSION',\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n\n const result = parsed.eval(submission)\n // Match historical behaviour: any non-NaN number (including Infinity) is a\n // usable calculation result for display.\n if (typeof result === 'number' && !Number.isNaN(result)) {\n return { type: 'RESULT', value: result }\n }\n\n // Successfully parsed expressions that do not yield a number (e.g. `true`,\n // non-numeric element values) previously returned `undefined` with no error\n // in the form UI. Treat them the same as missing submission values.\n return { type: 'MISSING_VALUES' }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"calculationService.js","sourceRoot":"","sources":["../src/calculationService.ts"],"names":[],"mappings":"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","sourcesContent":["import morphExpressions from 'morph-expressions'\n// morph-expressions is CommonJS and assigns `exports.default`, which Node's ESM\n// interop exposes as the whole `module.exports` rather than the constructor.\nconst ExpressionParser = morphExpressions.default ?? morphExpressions\nimport { FormTypes, SubmissionTypes } from '@oneblink/types'\nimport {\n findFormElement,\n flattenFormElements,\n matchElementsTagRegex,\n} from './formElementsService.js'\nimport {\n DATE_ONLY_PATTERN,\n type ParseDayOnlyDate,\n} from './conditionalLogicService/types.js'\n\nexport type { ParseDayOnlyDate } from './conditionalLogicService/types.js'\n\nexport type EvaluateExpressionResult =\n | { type: 'RESULT'; value: number }\n | { type: 'MISSING_VALUES' }\n | { type: 'INVALID_EXPRESSION'; error: Error }\n\nconst isUnenteredValue = (value: unknown | undefined) => {\n return !value && value !== 0\n}\n\nconst isObjectWithValue = (obj: unknown): obj is { value: unknown } => {\n return typeof obj === 'object' && obj !== null && 'value' in obj\n}\n\n/**\n * Escape special RegExp characters in a string so it can be used as a literal\n * pattern.\n */\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * Workaround for `.toFixed()` not rounding floating point numbers correctly.\n */\nfunction roundToFixed(number: number, decimals: number) {\n const multiplier = Math.pow(10, decimals)\n const roundedNumber = Math.round(number * multiplier) / multiplier\n return roundedNumber.toFixed(decimals)\n}\n\nfunction findFormElementByNamePath(\n formElements: FormTypes.FormElement[],\n elementName: string,\n): FormTypes.FormElement | undefined {\n const parts = elementName.split('|')\n let searchElements = formElements\n let found: FormTypes.FormElement | undefined\n\n for (let i = 0; i < parts.length; i++) {\n const name = parts[i]\n // Match flattenFormElements: page/section are transparent for naming, but\n // form/repeatableSet/infoPage require an explicit parent path segment.\n found = flattenFormElements(searchElements).find(\n (element) => 'name' in element && element.name === name,\n )\n if (!found) {\n return\n }\n\n if (i < parts.length - 1) {\n if (!('elements' in found) || !found.elements) {\n return\n }\n searchElements = found.elements\n }\n }\n\n return found\n}\n\n/**\n * Find `{ELEMENT:...}` references in an expression whose name paths do not\n * exist in the form definition.\n *\n * Intended for server-side payment validation. Skip this on the client during\n * interactive calculation evaluation — it walks the form definition for every\n * referenced element and is unnecessary for display.\n *\n * @returns Missing element name paths (e.g. `Amount` or `Items|Amount`), or an\n * empty array when every referenced element exists.\n */\nexport function findMissingFormElementsInExpression({\n expression,\n formElements,\n}: {\n expression: string\n formElements: FormTypes.FormElement[]\n}): string[] {\n if (!expression) {\n return []\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const missingFormElementNames: string[] = []\n const seen = new Set<string>()\n for (const elementName of elementNames) {\n if (seen.has(elementName)) {\n continue\n }\n seen.add(elementName)\n if (!findFormElementByNamePath(formElements, elementName)) {\n missingFormElementNames.push(elementName)\n }\n }\n return missingFormElementNames\n}\n\nfunction resolveElementValue({\n submission,\n nestedElementNames,\n formElements,\n parseDayOnlyDate,\n}: {\n submission: SubmissionTypes.S3SubmissionData['submission']\n nestedElementNames: string[]\n formElements: FormTypes.FormElement[]\n parseDayOnlyDate: ParseDayOnlyDate\n}): unknown {\n const defaultAccumulator = submission[nestedElementNames[0]]\n return nestedElementNames.reduce(\n (elementValue: unknown | undefined, elementName: string, index: number) => {\n // Numbers can just be returned as is\n if (typeof elementValue === 'number') {\n return elementValue\n }\n\n // Attempt to get a number from the element value as a string.\n // NaN is accounted for in the calculation so we can return that from here.\n if (typeof elementValue === 'string') {\n // Day-only (`YYYY-MM-DD`) strings are parsed via parseDayOnlyDate so\n // callers control timezone. Other date strings use `new Date(value)`.\n // Numeric strings (e.g. `\"10\"`) fall through to parseFloat — `Date`\n // can otherwise treat them as valid dates.\n const isDayOnly = DATE_ONLY_PATTERN.test(elementValue)\n if (isDayOnly) {\n const parsedDate = parseDayOnlyDate(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n } else if (Number.isNaN(Number(elementValue))) {\n const parsedDate = new Date(elementValue)\n if (!Number.isNaN(parsedDate.getTime())) {\n return parsedDate.getTime()\n }\n }\n\n return parseFloat(elementValue)\n }\n\n if (Array.isArray(elementValue)) {\n // If there are no entries, return NaN to prevent the calculation from\n // running.\n if (!elementValue.length) {\n return NaN\n }\n\n // An array could be an element that allows multiple values e.g.\n // checkboxes. If that is the case, add them all together and move on.\n const elementValues = elementValue.map((entry) => parseFloat(entry))\n if (elementValues.every((entry) => !Number.isNaN(entry))) {\n return elementValues.reduce((number, entry) => number + entry, 0)\n }\n\n // Otherwise attempt to process it as a repeatable set. If we found\n // another repeatable set to process, pass it to the next element name\n // to iterate over the entries. If we are processing the entries in a\n // repeatable set, we can sum the number elements in the entries.\n const nextElementName = nestedElementNames[index + 1]\n\n let isNestedRepeatableSet = false\n const nestedElementValues = elementValue.reduce(\n (nestedElementValues, entry) => {\n if (entry) {\n const nextElementValue = entry[nextElementName]\n if (Array.isArray(nextElementValue)) {\n if (nextElementValue.length) {\n nestedElementValues.push(...nextElementValue)\n isNestedRepeatableSet = true\n }\n } else {\n nestedElementValues.push(nextElementValue)\n }\n }\n return nestedElementValues\n },\n [] as unknown[],\n )\n\n // If the nested element values are all arrays, pass them on to the\n // next iteration.\n if (isNestedRepeatableSet) {\n return nestedElementValues\n }\n\n return nestedElementValues.reduce(\n (total: number, nestedElementValue: unknown | undefined) => {\n if (Number.isNaN(total)) {\n return NaN\n }\n const value = parseFloat(nestedElementValue as string)\n if (Number.isNaN(value)) {\n return NaN\n }\n return total + value\n },\n 0,\n )\n }\n\n // If the value is an object, take the element name and check to see if\n // this element is a nested form element. If so, take the next nested\n // element name, find its value in the object and return it for the next\n // iteration to handle.\n if (typeof elementValue === 'object') {\n const formFormElement = findFormElement(\n formElements,\n (e) => e.type === 'form' && e.name === nestedElementNames[index],\n )\n const nextElementName = nestedElementNames[index + 1]\n if (formFormElement && nextElementName) {\n return (elementValue as Record<string, unknown>)[nextElementName]\n }\n }\n\n // \"compliance\" form element has an object value with a \"value\" property.\n if (\n isObjectWithValue(elementValue) &&\n typeof elementValue.value === 'string'\n ) {\n return parseFloat(elementValue.value)\n }\n\n // We did not find a number value from the known elements, assume we are\n // at the end of the line.\n return NaN\n },\n defaultAccumulator,\n )\n}\n\n/**\n * Evaluate a OneBlink calculation expression against form submission data.\n *\n * Day-only (`YYYY-MM-DD`) date parsing is injected via `parseDayOnlyDate` so\n * callers control timezone-aware behaviour (client vs server), matching the\n * pattern used by `conditionalLogicService`. Other date strings use\n * `new Date(value)`.\n *\n * #### Expression syntax\n *\n * Expressions are mathematical / logical formulas. Form element values are\n * referenced with `{ELEMENT:<elementName>}` tags (including nested paths via\n * `|`, e.g. `{ELEMENT:Children|Age}`).\n *\n * ##### Literals\n *\n * - **Numbers** — e.g. `0`, `5`, `5.4`\n * - **Strings** — single or double quoted, e.g. `\"abc\"`, `'abc'`\n * - **Booleans** — `true`, `false`\n *\n * ##### Arithmetic operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------- |\n * | `+` | Addition (also unary plus) | `1 + 2` → `3` |\n * | `-` | Subtraction (also unary minus) | `1 - 2` → `-1` |\n * | `*` | Multiplication | `4 * 2` → `8` |\n * | `/` | Division | `4 / 2` → `2` |\n * | `%` | Remainder | `8 % 3` → `2` |\n * | `++` | Unary increment | `++2` → `3` |\n * | `--` | Unary decrement | `--2` → `1` |\n *\n * Standard operator precedence applies. Use parentheses to override it, e.g.\n * `3 * (2 + 1)` → `9`.\n *\n * ##### Comparison operators\n *\n * | Operator | Description | Example |\n * | -------- | --------------------- | ------------------- |\n * | `==` | Equal | `2 == 2` → `true` |\n * | `!=` | Not equal | `2 != 3` → `true` |\n * | `>` | Greater than | `2 > 1` → `true` |\n * | `>=` | Greater than or equal | `2 >= 2` → `true` |\n * | `<` | Less than | `2 < 3` → `true` |\n * | `<=` | Less than or equal | `2 <= 2` → `true` |\n *\n * ##### Logical operators\n *\n * | Operator | Description | Example |\n * | -------- | ------------------------------------ | -------------------------- |\n * | `&&` | Logical AND (returns last operand) | `true && false` → `false` |\n * | `\\|\\|` | Logical OR (returns first truthy) | `false \\|\\| true` → `true` |\n * | `!` | Logical NOT | `!false` → `true` |\n *\n * ##### Element references\n *\n * - `{ELEMENT:Number}` — value of the root element named `Number`\n * - `{ELEMENT:Set\\|Child}` — value of `Child` within repeatable set / nested\n * form element `Set`\n * - Nested paths may include multiple segments:\n * `{ELEMENT:Parents\\|Children\\|Age}`\n *\n * How values are coerced for evaluation:\n *\n * - Numbers are used as-is\n * - Day-only (`YYYY-MM-DD`) strings are parsed via `parseDayOnlyDate`; other\n * non-numeric date strings use `new Date(value)`. Valid dates use\n * `Date#getTime()`, otherwise the string is parsed as a float via\n * `parseFloat`\n * - Arrays of numeric strings (e.g. checkboxes) are summed\n * - Repeatable set entries sum the referenced nested numeric values\n * - Empty arrays yield `NaN` (result type `MISSING_VALUES`)\n * - Compliance element objects use their `value` property\n *\n * ##### Built-in functions\n *\n * | Function | Description | Example |\n * | -------------------------------- | --------------------------------------------------------------------------- | ------------------------------- |\n * | `ROUND(value, precision)` | Round `value` to `precision` decimal places (correct floating-point rounds) | `ROUND(1.255, 2)` → `1.26` |\n * | `ROUND_DOWN(value)` | Round down to the nearest integer (`Math.floor`) | `ROUND_DOWN(1.9)` → `1` |\n * | `ROUND_UP(value)` | Round up to the nearest integer (`Math.ceil`) | `ROUND_UP(1.1)` → `2` |\n * | `ISNULL(value, defaultValue)` | If `value` is unentered (`null`/`undefined`/`\"\"`), return `defaultValue` or `0`; otherwise return `value` | `ISNULL({ELEMENT:A}, 10)` |\n *\n * `ROUND`, `ROUND_DOWN`, and `ROUND_UP` return `null` when `value` is `NaN` or\n * not finite.\n *\n * ##### Examples\n *\n * ```js\n * const result = calculationService.evaluateExpression({\n * expression: '{ELEMENT:Quantity} * {ELEMENT:Price}',\n * submission: { Quantity: 3, Price: 12.5 },\n * formElements: form.elements,\n * // Client: local timezone. Server: organisation timezone.\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // result === { type: 'RESULT', value: 37.5 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ROUND({ELEMENT:Amount} * 1.1, 2)',\n * submission: { Amount: 100 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 110 }\n * ```\n *\n * ```js\n * calculationService.evaluateExpression({\n * expression: 'ISNULL({ELEMENT:Optional}, 0) + {ELEMENT:Required}',\n * submission: { Required: 5 },\n * formElements: form.elements,\n * parseDayOnlyDate: (value) => new Date(`${value}T00:00:00.000Z`),\n * })\n * // { type: 'RESULT', value: 5 }\n * ```\n *\n * @param options\n * @returns A discriminated result describing the outcome of evaluation. Empty\n * expressions and parse failures return\n * `{ type: 'INVALID_EXPRESSION', error }`. Unexpected runtime errors are\n * rethrown.\n */\nexport function evaluateExpression({\n expression,\n submission,\n formElements,\n parseDayOnlyDate,\n}: {\n /** The calculation expression string to evaluate */\n expression: string\n /** Form submission data used to resolve `{ELEMENT:...}` references */\n submission: SubmissionTypes.S3SubmissionData['submission']\n /**\n * Form elements definition. Used to resolve nested `form` element values\n * referenced in the expression and to validate `{ELEMENT:...}` references.\n */\n formElements: FormTypes.FormElement[]\n /**\n * Parse `YYYY-MM-DD` strings when resolving date element values. sdk-core\n * only calls this for day-only values; other date strings use\n * `new Date(value)`.\n */\n parseDayOnlyDate: ParseDayOnlyDate\n}): EvaluateExpressionResult {\n if (!expression) {\n return {\n type: 'INVALID_EXPRESSION',\n error: new Error('Expression is required.'),\n }\n }\n\n const elementNames: string[] = []\n matchElementsTagRegex(expression, ({ elementName }) => {\n elementNames.push(elementName)\n })\n\n const exprParser = new ExpressionParser<\n SubmissionTypes.S3SubmissionData['submission']\n >()\n exprParser.registerFunction('ROUND', (value: number, precision: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return parseFloat(roundToFixed(value, precision))\n }\n return null\n })\n exprParser.registerFunction('ROUND_DOWN', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.floor(value)\n }\n return null\n })\n exprParser.registerFunction('ROUND_UP', (value: number) => {\n if (!Number.isNaN(value) && Number.isFinite(value)) {\n return Math.ceil(value)\n }\n return null\n })\n exprParser.registerFunction(\n 'ISNULL',\n (value: unknown | undefined, defaultValue: number) => {\n if (isUnenteredValue(value)) {\n return defaultValue || 0\n }\n return value\n },\n )\n\n const code = elementNames.reduce((code, elementName, index) => {\n const regex = new RegExp(escapeRegExp(`{ELEMENT:${elementName}}`), 'g')\n const replacement = `a${index}`\n exprParser.registerProperty(\n replacement,\n (submissionScope: SubmissionTypes.S3SubmissionData['submission']) =>\n resolveElementValue({\n submission: submissionScope,\n nestedElementNames: elementName.split('|'),\n formElements,\n parseDayOnlyDate,\n }),\n )\n return code.replace(regex, replacement)\n }, expression)\n\n let parsed\n try {\n parsed = exprParser.parse(code.trim())\n } catch (error) {\n return {\n type: 'INVALID_EXPRESSION',\n error: error instanceof Error ? error : new Error(String(error)),\n }\n }\n\n const result = parsed.eval(submission)\n // Match historical behaviour: any non-NaN number (including Infinity) is a\n // usable calculation result for display.\n if (typeof result === 'number' && !Number.isNaN(result)) {\n return { type: 'RESULT', value: result }\n }\n\n // Successfully parsed expressions that do not yield a number (e.g. `true`,\n // non-numeric element values) previously returned `undefined` with no error\n // in the form UI. Treat them the same as missing submission values.\n return { type: 'MISSING_VALUES' }\n}\n"]}
|
|
@@ -115,4 +115,21 @@ declare const fixElementName: (elementName: string) => string;
|
|
|
115
115
|
* @returns
|
|
116
116
|
*/
|
|
117
117
|
declare function injectFormElementsIntoForm(form: FormTypes.Form, forms: FormTypes.Form[], injectAuthenticatedForms?: boolean): FormTypes.FormElement[];
|
|
118
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Async counterpart to {@link injectFormElementsIntoForm}. Injects embedded
|
|
120
|
+
* `form` / `infoPage` elements for each form in `forms`. Uses `getForm` to load
|
|
121
|
+
* embedded forms and caches results by id for the duration of the call so each
|
|
122
|
+
* form is retrieved at most once across the whole batch.
|
|
123
|
+
*
|
|
124
|
+
* Mutates each form object in `forms` in place (the array entries are updated
|
|
125
|
+
* by reference); nothing is returned. Forms returned from `getForm` are not
|
|
126
|
+
* mutated — their element trees are cloned before embedding.
|
|
127
|
+
*
|
|
128
|
+
* @param forms The forms to inject elements into (mutated in place)
|
|
129
|
+
* @param getForm Retrieves a single form by id when it is referenced by a
|
|
130
|
+
* `form` / `infoPage` element
|
|
131
|
+
* @param injectAuthenticatedForms Indicates whether forms requiring
|
|
132
|
+
* authentication should be injected, defaults to true
|
|
133
|
+
*/
|
|
134
|
+
declare function injectFormElementsIntoForms(forms: FormTypes.Form[], getForm: (formId: number) => Promise<FormTypes.Form | undefined>, injectAuthenticatedForms?: boolean): Promise<void>;
|
|
135
|
+
export { forEachFormElement, forEachFormElementWithOptions, findFormElement, parseDynamicFormElementOptions, flattenFormElements, determineIsInfoPage, fixElementName, injectFormElementsIntoForm, injectFormElementsIntoForms, };
|
|
@@ -252,37 +252,24 @@ function injectFormElements(elements, forms, parentIds, injectAuthenticatedForms
|
|
|
252
252
|
element.elements = childElements;
|
|
253
253
|
}
|
|
254
254
|
if (element.type === 'form' || element.type === 'infoPage') {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
255
|
+
const resolved = resolveEmbeddedFormElement(element, forms.find((form) => element.formId === form.id), parentIds, injectAuthenticatedForms);
|
|
256
|
+
switch (resolved.type) {
|
|
257
|
+
case 'replace': {
|
|
258
|
+
newElements.push(resolved.element);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case 'skip': {
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case 'continue': {
|
|
265
|
+
// Clone so inject mutations (e.g. containers rewritten when cycle
|
|
266
|
+
// edges are omitted) do not alter the source form, which may be
|
|
267
|
+
// shared via the forms list.
|
|
268
|
+
element.elements = injectFormElements(structuredClone(resolved.form.elements), forms, [...parentIds, resolved.form.id], injectAuthenticatedForms);
|
|
269
|
+
newElements.push(element);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
266
272
|
}
|
|
267
|
-
if (formToInject.isAuthenticated && !injectAuthenticatedForms) {
|
|
268
|
-
console.log(`No form elements injected for element id: ${element.id}, as request was unauthenticated and target form (form id: ${formToInject.id}) requires authentication.`);
|
|
269
|
-
const newElement = {
|
|
270
|
-
...element,
|
|
271
|
-
type: 'html',
|
|
272
|
-
name: 'Form_requires_authenticated',
|
|
273
|
-
label: 'Form Requires Authentication.',
|
|
274
|
-
defaultValue: 'Unable to display the embedded form for this element, as the form requires authentication. Please login and refresh to view this embedded form.',
|
|
275
|
-
};
|
|
276
|
-
newElements.push(newElement);
|
|
277
|
-
return newElements;
|
|
278
|
-
}
|
|
279
|
-
const injectingParentForm = parentIds && parentIds.find((id) => element.formId === id);
|
|
280
|
-
if (injectingParentForm) {
|
|
281
|
-
console.log(`Infinite loop was detected while attempting to inject form id: ${injectingParentForm}. Ignoring elements...`);
|
|
282
|
-
return newElements;
|
|
283
|
-
}
|
|
284
|
-
element.elements = injectFormElements(formToInject.elements, forms, [...parentIds, formToInject.id], injectAuthenticatedForms);
|
|
285
|
-
newElements.push(element);
|
|
286
273
|
}
|
|
287
274
|
else {
|
|
288
275
|
newElements.push(element);
|
|
@@ -290,5 +277,104 @@ function injectFormElements(elements, forms, parentIds, injectAuthenticatedForms
|
|
|
290
277
|
return newElements;
|
|
291
278
|
}, []);
|
|
292
279
|
}
|
|
293
|
-
|
|
280
|
+
function resolveEmbeddedFormElement(element, formToInject, parentIds, injectAuthenticatedForms) {
|
|
281
|
+
if (!formToInject) {
|
|
282
|
+
return {
|
|
283
|
+
type: 'replace',
|
|
284
|
+
element: {
|
|
285
|
+
...element,
|
|
286
|
+
type: 'html',
|
|
287
|
+
name: 'Form_not_found',
|
|
288
|
+
label: 'Form not found.',
|
|
289
|
+
defaultValue: 'Unable to display the embedded form for this element, as the form was not found. Please contact your Administrator.',
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (formToInject.isAuthenticated && !injectAuthenticatedForms) {
|
|
294
|
+
console.log(`No form elements injected for element id: ${element.id}, as request was unauthenticated and target form (form id: ${formToInject.id}) requires authentication.`);
|
|
295
|
+
return {
|
|
296
|
+
type: 'replace',
|
|
297
|
+
element: {
|
|
298
|
+
...element,
|
|
299
|
+
type: 'html',
|
|
300
|
+
name: 'Form_requires_authenticated',
|
|
301
|
+
label: 'Form Requires Authentication.',
|
|
302
|
+
defaultValue: 'Unable to display the embedded form for this element, as the form requires authentication. Please login and refresh to view this embedded form.',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const injectingParentForm = parentIds.find((id) => element.formId === id);
|
|
307
|
+
if (injectingParentForm) {
|
|
308
|
+
console.log(`Infinite loop was detected while attempting to inject form id: ${injectingParentForm}. Ignoring elements...`);
|
|
309
|
+
return { type: 'skip' };
|
|
310
|
+
}
|
|
311
|
+
return { type: 'continue', form: formToInject };
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Async counterpart to {@link injectFormElementsIntoForm}. Injects embedded
|
|
315
|
+
* `form` / `infoPage` elements for each form in `forms`. Uses `getForm` to load
|
|
316
|
+
* embedded forms and caches results by id for the duration of the call so each
|
|
317
|
+
* form is retrieved at most once across the whole batch.
|
|
318
|
+
*
|
|
319
|
+
* Mutates each form object in `forms` in place (the array entries are updated
|
|
320
|
+
* by reference); nothing is returned. Forms returned from `getForm` are not
|
|
321
|
+
* mutated — their element trees are cloned before embedding.
|
|
322
|
+
*
|
|
323
|
+
* @param forms The forms to inject elements into (mutated in place)
|
|
324
|
+
* @param getForm Retrieves a single form by id when it is referenced by a
|
|
325
|
+
* `form` / `infoPage` element
|
|
326
|
+
* @param injectAuthenticatedForms Indicates whether forms requiring
|
|
327
|
+
* authentication should be injected, defaults to true
|
|
328
|
+
*/
|
|
329
|
+
async function injectFormElementsIntoForms(forms, getForm, injectAuthenticatedForms = true) {
|
|
330
|
+
const formCache = new Map();
|
|
331
|
+
const getFormCached = async (formId) => {
|
|
332
|
+
if (formCache.has(formId)) {
|
|
333
|
+
return formCache.get(formId);
|
|
334
|
+
}
|
|
335
|
+
const retrieved = (await getForm(formId)) ?? undefined;
|
|
336
|
+
formCache.set(formId, retrieved);
|
|
337
|
+
return retrieved;
|
|
338
|
+
};
|
|
339
|
+
for (const form of forms) {
|
|
340
|
+
form.elements = await injectFormElementsAsync(form.elements, getFormCached, [form.id], injectAuthenticatedForms);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function injectFormElementsAsync(elements, getForm, parentIds, injectAuthenticatedForms) {
|
|
344
|
+
const newElements = [];
|
|
345
|
+
for (const element of elements) {
|
|
346
|
+
if ((element.type === 'page' ||
|
|
347
|
+
element.type === 'repeatableSet' ||
|
|
348
|
+
element.type === 'section') &&
|
|
349
|
+
Array.isArray(element.elements)) {
|
|
350
|
+
element.elements = await injectFormElementsAsync(element.elements, getForm, parentIds, injectAuthenticatedForms);
|
|
351
|
+
}
|
|
352
|
+
if (element.type === 'form' || element.type === 'infoPage') {
|
|
353
|
+
const form = await getForm(element.formId);
|
|
354
|
+
const resolved = resolveEmbeddedFormElement(element, form, parentIds, injectAuthenticatedForms);
|
|
355
|
+
switch (resolved.type) {
|
|
356
|
+
case 'replace': {
|
|
357
|
+
newElements.push(resolved.element);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
case 'skip': {
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case 'continue': {
|
|
364
|
+
// Clone so inject mutations (e.g. containers rewritten when cycle
|
|
365
|
+
// edges are omitted) do not alter the source form, which may be
|
|
366
|
+
// cached or a later batch root.
|
|
367
|
+
element.elements = await injectFormElementsAsync(structuredClone(resolved.form.elements), getForm, [...parentIds, resolved.form.id], injectAuthenticatedForms);
|
|
368
|
+
newElements.push(element);
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
newElements.push(element);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return newElements;
|
|
378
|
+
}
|
|
379
|
+
export { forEachFormElement, forEachFormElementWithOptions, findFormElement, parseDynamicFormElementOptions, flattenFormElements, determineIsInfoPage, fixElementName, injectFormElementsIntoForm, injectFormElementsIntoForms, };
|
|
294
380
|
//# sourceMappingURL=formElementsService.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formElementsService.js","sourceRoot":"","sources":["../src/formElementsService.ts"],"names":[],"mappings":"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","sourcesContent":["import { FormTypes } from '@oneblink/types'\nimport { typeCastService } from './index.js'\nexport { matchElementsTagRegex } from './form-elements-regex.js'\n\n/**\n * Iterate over all form elements, also iterating over nested form element (e.g.\n * page elements).\n *\n * #### Example\n *\n * ```js\n * formElementsService.forEachFormElement(form.elements, (formElement) => {\n * // do something with formElement\n * })\n * ```\n *\n * @param elements The form elements to iterate over\n * @param forEach Function to execute on each form element\n */\nfunction forEachFormElement(\n elements: FormTypes.FormElement[],\n forEach: (\n element: FormTypes.FormElement,\n elements: FormTypes.FormElement[],\n ) => void,\n): void {\n findFormElement(elements, (formElement, parentElements) => {\n forEach(formElement, parentElements)\n return false\n })\n}\n\n/**\n * Iterate over all form elements that have options (e.g. `'select'` type\n * elements), also iterating over nested form element (e.g. page elements).\n *\n * #### Example\n *\n * ```js\n * formElementsService.forEachFormElementWithOptions(\n * form.elements,\n * (formElementWithOptions) => {\n * // do something with formElementWithOptions\n * },\n * )\n * ```\n *\n * @param elements The form elements to iterate over\n * @param forEach Function to execute on each form element with options\n */\nfunction forEachFormElementWithOptions(\n elements: FormTypes.FormElement[],\n forEach: (\n elementWithOptions: FormTypes.FormElementWithOptions,\n elements: FormTypes.FormElement[],\n ) => void,\n): void {\n findFormElement(elements, (formElement, parentElements) => {\n const optionsFormElement =\n typeCastService.formElements.toOptionsElement(formElement)\n if (optionsFormElement) {\n forEach(optionsFormElement, parentElements)\n }\n return false\n })\n}\n\n/**\n * Iterate over all form elements and return an element that matches a\n * predicate, also iterating over nested form element (e.g. page elements). Will\n * return `undefined` if no matching element is found.\n *\n * #### Example\n *\n * ```js\n * const formElement = formElementsService.findFormElement(\n * form.elements,\n * (formElement) => {\n * return formElement.id === '123-abc'\n * },\n * )\n * ```\n *\n * @param elements The form elements to iterate over\n * @param predicate Predicate function to execute on each form element\n * @param parentElements\n * @returns\n */\nfunction findFormElement(\n elements: FormTypes.FormElement[],\n predicate: (\n element: FormTypes.FormElement,\n elements: FormTypes.FormElement[],\n ) => boolean,\n parentElements: FormTypes.FormElement[] = [],\n): FormTypes.FormElement | undefined {\n for (const element of elements) {\n if (predicate(element, parentElements)) {\n return element\n }\n\n if (\n (element.type === 'repeatableSet' ||\n element.type === 'page' ||\n element.type === 'form' ||\n element.type === 'infoPage' ||\n element.type === 'section') &&\n Array.isArray(element.elements)\n ) {\n const nestedElement = findFormElement(element.elements, predicate, [\n ...parentElements,\n element,\n ])\n\n if (nestedElement) {\n return nestedElement\n }\n }\n }\n}\n\n/**\n * Parse unknown data as valid dynamic options for a forms element. This will\n * always return an Array of valid dynamic options.\n *\n * #### Example\n *\n * ```js\n * const options = formElementsService.parseDynamicFormElementOptions(data)\n * // \"options\" are valid for a form element\n * ```\n *\n * @param data\n * @returns\n */\nfunction parseDynamicFormElementOptions(\n data: unknown,\n): FormTypes.DynamicChoiceElementOption[] {\n if (!Array.isArray(data)) {\n return []\n }\n return data.reduce<FormTypes.DynamicChoiceElementOption[]>(\n (options, record, index) => {\n if (typeof record === 'string') {\n options.push({\n value: record,\n label: record,\n })\n } else if (typeof record === 'object') {\n const option = record as Record<string, unknown>\n const value =\n typeof option.value === 'string' && option.value\n ? option.value\n : index.toString()\n const label =\n typeof option.label === 'string' && option.label\n ? option.label\n : value\n const colour =\n typeof option.colour === 'string' && option.colour\n ? option.colour\n : undefined\n const displayAlways =\n typeof option.displayAlways === 'boolean'\n ? option.displayAlways\n : undefined\n const imageUrl =\n typeof option.imageUrl === 'string' && option.imageUrl\n ? option.imageUrl\n : undefined\n const attributes = Array.isArray(option.attributes)\n ? option.attributes.reduce<\n FormTypes.DynamicChoiceElementOptionAttribute[]\n >((memo, attribute: unknown) => {\n if (\n typeof attribute === 'object' &&\n attribute &&\n 'value' in attribute &&\n typeof attribute.value === 'string' &&\n 'label' in attribute &&\n typeof attribute.label === 'string'\n ) {\n memo.push({\n value: attribute.value,\n label: attribute.label,\n })\n }\n return memo\n }, [])\n : undefined\n options.push({\n value,\n label,\n colour,\n attributes,\n displayAlways,\n imageUrl,\n options: Array.isArray(option.options)\n ? parseDynamicFormElementOptions(option.options)\n : undefined,\n })\n }\n return options\n },\n [],\n )\n}\n\n/**\n * Takes the nested definition structure and returns all form elements as 1d\n * array.\n *\n * #### Example\n *\n * ```js\n * const flattenedElements = formElementsService.flattenFormElements(\n * form.elements,\n * )\n * ```\n *\n * @param elements\n * @returns\n */\nfunction flattenFormElements(\n elements: FormTypes.FormElement[],\n): FormTypes.FormElement[] {\n return elements.reduce<FormTypes.FormElement[]>(\n (flattenedElements, element) => {\n flattenedElements.push(element)\n switch (element.type) {\n case 'section':\n case 'page': {\n flattenedElements.push(...flattenFormElements(element.elements))\n }\n }\n return flattenedElements\n },\n [],\n )\n}\n\nconst infoPageElements: FormTypes.FormElementType[] = [\n 'heading',\n 'html',\n 'image',\n 'section',\n 'page',\n 'infoPage',\n 'form',\n]\n/**\n * Determine a form is considered an info page. This means the form does not\n * allow any user input.\n *\n * @param form\n * @returns\n */\nfunction determineIsInfoPage(form: FormTypes.Form): boolean {\n const foundInputElement = findFormElement(form.elements, (e) => {\n return !infoPageElements.includes(e.type)\n })\n return !foundInputElement\n}\n\n/**\n * Remove invalid characters from a form element name.\n *\n * @param elementName\n * @returns\n */\nconst fixElementName = (elementName: string) => {\n // removes characters that aren't letters, numbers, underscores or dashes\n // replaces empty spaces with _\n return elementName\n .replace(/[^-\\w\\s]/g, '')\n .replace(/\\s/g, '_')\n .trim()\n}\n\n/**\n * Injects the elements of any elements with type `FORM` or `INFOPAGE` into the\n * form\n *\n * @param form The form to inject elements into\n * @param forms The forms that will be used to inject elements from if\n * referenced\n * @param injectAuthenticatedForms Indicates whether forms requiring\n * authentication should be injected, defaults to true\n * @returns\n */\nfunction injectFormElementsIntoForm(\n form: FormTypes.Form,\n forms: FormTypes.Form[],\n injectAuthenticatedForms = true,\n): FormTypes.FormElement[] {\n const elementsWithInjectedForms = injectFormElements(\n form.elements,\n forms,\n [form.id],\n injectAuthenticatedForms,\n )\n form.elements = elementsWithInjectedForms\n return form.elements\n}\n\nfunction injectFormElements(\n elements: FormTypes.FormElement[],\n forms: FormTypes.Form[],\n parentIds: number[],\n injectAuthenticatedForms: boolean,\n): FormTypes.FormElement[] {\n return elements.reduce<FormTypes.FormElement[]>((newElements, element) => {\n if (\n (element.type === 'page' ||\n element.type === 'repeatableSet' ||\n element.type === 'section') &&\n Array.isArray(element.elements)\n ) {\n const childElements = injectFormElements(\n element.elements,\n forms,\n parentIds,\n injectAuthenticatedForms,\n )\n element.elements = childElements\n }\n\n if (element.type === 'form' || element.type === 'infoPage') {\n const formToInject = forms.find((form) => element.formId === form.id)\n\n if (!formToInject) {\n const newElement: FormTypes.HtmlElement = {\n ...element,\n type: 'html',\n name: 'Form_not_found',\n label: 'Form not found.',\n defaultValue:\n 'Unable to display the embedded form for this element, as the form was not found. Please contact your Administrator.',\n }\n newElements.push(newElement)\n return newElements\n }\n\n if (formToInject.isAuthenticated && !injectAuthenticatedForms) {\n console.log(\n `No form elements injected for element id: ${element.id}, as request was unauthenticated and target form (form id: ${formToInject.id}) requires authentication.`,\n )\n\n const newElement: FormTypes.HtmlElement = {\n ...element,\n type: 'html',\n name: 'Form_requires_authenticated',\n label: 'Form Requires Authentication.',\n defaultValue:\n 'Unable to display the embedded form for this element, as the form requires authentication. Please login and refresh to view this embedded form.',\n }\n newElements.push(newElement)\n return newElements\n }\n\n const injectingParentForm =\n parentIds && parentIds.find((id) => element.formId === id)\n\n if (injectingParentForm) {\n console.log(\n `Infinite loop was detected while attempting to inject form id: ${injectingParentForm}. Ignoring elements...`,\n )\n return newElements\n }\n\n element.elements = injectFormElements(\n formToInject.elements,\n forms,\n [...parentIds, formToInject.id],\n injectAuthenticatedForms,\n )\n newElements.push(element)\n } else {\n newElements.push(element)\n }\n\n return newElements\n }, [])\n}\n\nexport {\n forEachFormElement,\n forEachFormElementWithOptions,\n findFormElement,\n parseDynamicFormElementOptions,\n flattenFormElements,\n determineIsInfoPage,\n fixElementName,\n injectFormElementsIntoForm,\n}\n"]}
|
|
1
|
+
{"version":3,"file":"formElementsService.js","sourceRoot":"","sources":["../src/formElementsService.ts"],"names":[],"mappings":"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","sourcesContent":["import { FormTypes } from '@oneblink/types'\nimport { typeCastService } from './index.js'\nexport { matchElementsTagRegex } from './form-elements-regex.js'\n\n/**\n * Iterate over all form elements, also iterating over nested form element (e.g.\n * page elements).\n *\n * #### Example\n *\n * ```js\n * formElementsService.forEachFormElement(form.elements, (formElement) => {\n * // do something with formElement\n * })\n * ```\n *\n * @param elements The form elements to iterate over\n * @param forEach Function to execute on each form element\n */\nfunction forEachFormElement(\n elements: FormTypes.FormElement[],\n forEach: (\n element: FormTypes.FormElement,\n elements: FormTypes.FormElement[],\n ) => void,\n): void {\n findFormElement(elements, (formElement, parentElements) => {\n forEach(formElement, parentElements)\n return false\n })\n}\n\n/**\n * Iterate over all form elements that have options (e.g. `'select'` type\n * elements), also iterating over nested form element (e.g. page elements).\n *\n * #### Example\n *\n * ```js\n * formElementsService.forEachFormElementWithOptions(\n * form.elements,\n * (formElementWithOptions) => {\n * // do something with formElementWithOptions\n * },\n * )\n * ```\n *\n * @param elements The form elements to iterate over\n * @param forEach Function to execute on each form element with options\n */\nfunction forEachFormElementWithOptions(\n elements: FormTypes.FormElement[],\n forEach: (\n elementWithOptions: FormTypes.FormElementWithOptions,\n elements: FormTypes.FormElement[],\n ) => void,\n): void {\n findFormElement(elements, (formElement, parentElements) => {\n const optionsFormElement =\n typeCastService.formElements.toOptionsElement(formElement)\n if (optionsFormElement) {\n forEach(optionsFormElement, parentElements)\n }\n return false\n })\n}\n\n/**\n * Iterate over all form elements and return an element that matches a\n * predicate, also iterating over nested form element (e.g. page elements). Will\n * return `undefined` if no matching element is found.\n *\n * #### Example\n *\n * ```js\n * const formElement = formElementsService.findFormElement(\n * form.elements,\n * (formElement) => {\n * return formElement.id === '123-abc'\n * },\n * )\n * ```\n *\n * @param elements The form elements to iterate over\n * @param predicate Predicate function to execute on each form element\n * @param parentElements\n * @returns\n */\nfunction findFormElement(\n elements: FormTypes.FormElement[],\n predicate: (\n element: FormTypes.FormElement,\n elements: FormTypes.FormElement[],\n ) => boolean,\n parentElements: FormTypes.FormElement[] = [],\n): FormTypes.FormElement | undefined {\n for (const element of elements) {\n if (predicate(element, parentElements)) {\n return element\n }\n\n if (\n (element.type === 'repeatableSet' ||\n element.type === 'page' ||\n element.type === 'form' ||\n element.type === 'infoPage' ||\n element.type === 'section') &&\n Array.isArray(element.elements)\n ) {\n const nestedElement = findFormElement(element.elements, predicate, [\n ...parentElements,\n element,\n ])\n\n if (nestedElement) {\n return nestedElement\n }\n }\n }\n}\n\n/**\n * Parse unknown data as valid dynamic options for a forms element. This will\n * always return an Array of valid dynamic options.\n *\n * #### Example\n *\n * ```js\n * const options = formElementsService.parseDynamicFormElementOptions(data)\n * // \"options\" are valid for a form element\n * ```\n *\n * @param data\n * @returns\n */\nfunction parseDynamicFormElementOptions(\n data: unknown,\n): FormTypes.DynamicChoiceElementOption[] {\n if (!Array.isArray(data)) {\n return []\n }\n return data.reduce<FormTypes.DynamicChoiceElementOption[]>(\n (options, record, index) => {\n if (typeof record === 'string') {\n options.push({\n value: record,\n label: record,\n })\n } else if (typeof record === 'object') {\n const option = record as Record<string, unknown>\n const value =\n typeof option.value === 'string' && option.value\n ? option.value\n : index.toString()\n const label =\n typeof option.label === 'string' && option.label\n ? option.label\n : value\n const colour =\n typeof option.colour === 'string' && option.colour\n ? option.colour\n : undefined\n const displayAlways =\n typeof option.displayAlways === 'boolean'\n ? option.displayAlways\n : undefined\n const imageUrl =\n typeof option.imageUrl === 'string' && option.imageUrl\n ? option.imageUrl\n : undefined\n const attributes = Array.isArray(option.attributes)\n ? option.attributes.reduce<\n FormTypes.DynamicChoiceElementOptionAttribute[]\n >((memo, attribute: unknown) => {\n if (\n typeof attribute === 'object' &&\n attribute &&\n 'value' in attribute &&\n typeof attribute.value === 'string' &&\n 'label' in attribute &&\n typeof attribute.label === 'string'\n ) {\n memo.push({\n value: attribute.value,\n label: attribute.label,\n })\n }\n return memo\n }, [])\n : undefined\n options.push({\n value,\n label,\n colour,\n attributes,\n displayAlways,\n imageUrl,\n options: Array.isArray(option.options)\n ? parseDynamicFormElementOptions(option.options)\n : undefined,\n })\n }\n return options\n },\n [],\n )\n}\n\n/**\n * Takes the nested definition structure and returns all form elements as 1d\n * array.\n *\n * #### Example\n *\n * ```js\n * const flattenedElements = formElementsService.flattenFormElements(\n * form.elements,\n * )\n * ```\n *\n * @param elements\n * @returns\n */\nfunction flattenFormElements(\n elements: FormTypes.FormElement[],\n): FormTypes.FormElement[] {\n return elements.reduce<FormTypes.FormElement[]>(\n (flattenedElements, element) => {\n flattenedElements.push(element)\n switch (element.type) {\n case 'section':\n case 'page': {\n flattenedElements.push(...flattenFormElements(element.elements))\n }\n }\n return flattenedElements\n },\n [],\n )\n}\n\nconst infoPageElements: FormTypes.FormElementType[] = [\n 'heading',\n 'html',\n 'image',\n 'section',\n 'page',\n 'infoPage',\n 'form',\n]\n/**\n * Determine a form is considered an info page. This means the form does not\n * allow any user input.\n *\n * @param form\n * @returns\n */\nfunction determineIsInfoPage(form: FormTypes.Form): boolean {\n const foundInputElement = findFormElement(form.elements, (e) => {\n return !infoPageElements.includes(e.type)\n })\n return !foundInputElement\n}\n\n/**\n * Remove invalid characters from a form element name.\n *\n * @param elementName\n * @returns\n */\nconst fixElementName = (elementName: string) => {\n // removes characters that aren't letters, numbers, underscores or dashes\n // replaces empty spaces with _\n return elementName\n .replace(/[^-\\w\\s]/g, '')\n .replace(/\\s/g, '_')\n .trim()\n}\n\n/**\n * Injects the elements of any elements with type `FORM` or `INFOPAGE` into the\n * form\n *\n * @param form The form to inject elements into\n * @param forms The forms that will be used to inject elements from if\n * referenced\n * @param injectAuthenticatedForms Indicates whether forms requiring\n * authentication should be injected, defaults to true\n * @returns\n */\nfunction injectFormElementsIntoForm(\n form: FormTypes.Form,\n forms: FormTypes.Form[],\n injectAuthenticatedForms = true,\n): FormTypes.FormElement[] {\n const elementsWithInjectedForms = injectFormElements(\n form.elements,\n forms,\n [form.id],\n injectAuthenticatedForms,\n )\n form.elements = elementsWithInjectedForms\n return form.elements\n}\n\nfunction injectFormElements(\n elements: FormTypes.FormElement[],\n forms: FormTypes.Form[],\n parentIds: number[],\n injectAuthenticatedForms: boolean,\n): FormTypes.FormElement[] {\n return elements.reduce<FormTypes.FormElement[]>((newElements, element) => {\n if (\n (element.type === 'page' ||\n element.type === 'repeatableSet' ||\n element.type === 'section') &&\n Array.isArray(element.elements)\n ) {\n const childElements = injectFormElements(\n element.elements,\n forms,\n parentIds,\n injectAuthenticatedForms,\n )\n element.elements = childElements\n }\n\n if (element.type === 'form' || element.type === 'infoPage') {\n const resolved = resolveEmbeddedFormElement(\n element,\n forms.find((form) => element.formId === form.id),\n parentIds,\n injectAuthenticatedForms,\n )\n\n switch (resolved.type) {\n case 'replace': {\n newElements.push(resolved.element)\n break\n }\n case 'skip': {\n break\n }\n case 'continue': {\n // Clone so inject mutations (e.g. containers rewritten when cycle\n // edges are omitted) do not alter the source form, which may be\n // shared via the forms list.\n element.elements = injectFormElements(\n structuredClone(resolved.form.elements),\n forms,\n [...parentIds, resolved.form.id],\n injectAuthenticatedForms,\n )\n newElements.push(element)\n break\n }\n }\n } else {\n newElements.push(element)\n }\n\n return newElements\n }, [])\n}\n\ntype ResolveEmbeddedFormResult =\n | { type: 'replace'; element: FormTypes.HtmlElement }\n | { type: 'skip' }\n | { type: 'continue'; form: FormTypes.Form }\n\nfunction resolveEmbeddedFormElement(\n element: FormTypes.FormFormElement,\n formToInject: FormTypes.Form | undefined,\n parentIds: number[],\n injectAuthenticatedForms: boolean,\n): ResolveEmbeddedFormResult {\n if (!formToInject) {\n return {\n type: 'replace',\n element: {\n ...element,\n type: 'html',\n name: 'Form_not_found',\n label: 'Form not found.',\n defaultValue:\n 'Unable to display the embedded form for this element, as the form was not found. Please contact your Administrator.',\n },\n }\n }\n\n if (formToInject.isAuthenticated && !injectAuthenticatedForms) {\n console.log(\n `No form elements injected for element id: ${element.id}, as request was unauthenticated and target form (form id: ${formToInject.id}) requires authentication.`,\n )\n\n return {\n type: 'replace',\n element: {\n ...element,\n type: 'html',\n name: 'Form_requires_authenticated',\n label: 'Form Requires Authentication.',\n defaultValue:\n 'Unable to display the embedded form for this element, as the form requires authentication. Please login and refresh to view this embedded form.',\n },\n }\n }\n\n const injectingParentForm = parentIds.find((id) => element.formId === id)\n\n if (injectingParentForm) {\n console.log(\n `Infinite loop was detected while attempting to inject form id: ${injectingParentForm}. Ignoring elements...`,\n )\n return { type: 'skip' }\n }\n\n return { type: 'continue', form: formToInject }\n}\n\n/**\n * Async counterpart to {@link injectFormElementsIntoForm}. Injects embedded\n * `form` / `infoPage` elements for each form in `forms`. Uses `getForm` to load\n * embedded forms and caches results by id for the duration of the call so each\n * form is retrieved at most once across the whole batch.\n *\n * Mutates each form object in `forms` in place (the array entries are updated\n * by reference); nothing is returned. Forms returned from `getForm` are not\n * mutated — their element trees are cloned before embedding.\n *\n * @param forms The forms to inject elements into (mutated in place)\n * @param getForm Retrieves a single form by id when it is referenced by a\n * `form` / `infoPage` element\n * @param injectAuthenticatedForms Indicates whether forms requiring\n * authentication should be injected, defaults to true\n */\nasync function injectFormElementsIntoForms(\n forms: FormTypes.Form[],\n getForm: (formId: number) => Promise<FormTypes.Form | undefined>,\n injectAuthenticatedForms = true,\n): Promise<void> {\n const formCache = new Map<number, FormTypes.Form | undefined>()\n\n const getFormCached = async (formId: number) => {\n if (formCache.has(formId)) {\n return formCache.get(formId)\n }\n const retrieved = (await getForm(formId)) ?? undefined\n formCache.set(formId, retrieved)\n return retrieved\n }\n\n for (const form of forms) {\n form.elements = await injectFormElementsAsync(\n form.elements,\n getFormCached,\n [form.id],\n injectAuthenticatedForms,\n )\n }\n}\n\nasync function injectFormElementsAsync(\n elements: FormTypes.FormElement[],\n getForm: (formId: number) => Promise<FormTypes.Form | undefined>,\n parentIds: number[],\n injectAuthenticatedForms: boolean,\n): Promise<FormTypes.FormElement[]> {\n const newElements: FormTypes.FormElement[] = []\n\n for (const element of elements) {\n if (\n (element.type === 'page' ||\n element.type === 'repeatableSet' ||\n element.type === 'section') &&\n Array.isArray(element.elements)\n ) {\n element.elements = await injectFormElementsAsync(\n element.elements,\n getForm,\n parentIds,\n injectAuthenticatedForms,\n )\n }\n\n if (element.type === 'form' || element.type === 'infoPage') {\n const form = await getForm(element.formId)\n const resolved = resolveEmbeddedFormElement(\n element,\n form,\n parentIds,\n injectAuthenticatedForms,\n )\n\n switch (resolved.type) {\n case 'replace': {\n newElements.push(resolved.element)\n break\n }\n case 'skip': {\n break\n }\n case 'continue': {\n // Clone so inject mutations (e.g. containers rewritten when cycle\n // edges are omitted) do not alter the source form, which may be\n // cached or a later batch root.\n element.elements = await injectFormElementsAsync(\n structuredClone(resolved.form.elements),\n getForm,\n [...parentIds, resolved.form.id],\n injectAuthenticatedForms,\n )\n newElements.push(element)\n break\n }\n }\n } else {\n newElements.push(element)\n }\n }\n\n return newElements\n}\n\nexport {\n forEachFormElement,\n forEachFormElementWithOptions,\n findFormElement,\n parseDynamicFormElementOptions,\n flattenFormElements,\n determineIsInfoPage,\n fixElementName,\n injectFormElementsIntoForm,\n injectFormElementsIntoForms,\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oneblink/sdk-core",
|
|
3
3
|
"description": "OneBlink SDK for JavaScript (works in Browsers and NodeJS)",
|
|
4
|
-
"version": "10.1.0-beta.
|
|
4
|
+
"version": "10.1.0-beta.9",
|
|
5
5
|
"author": "OneBlink <developers@oneblink.io> (https://oneblink.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/oneblink/sdk-core-js/issues"
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@eslint/js": "^9.39.2",
|
|
15
15
|
"@microsoft/eslint-plugin-sdl": "^1.1.0",
|
|
16
16
|
"@microsoft/microsoft-graph-types": "^2.43.1",
|
|
17
|
-
"@oneblink/release-cli": "^
|
|
17
|
+
"@oneblink/release-cli": "^5.0.1",
|
|
18
18
|
"@oneblink/types": "github:oneblink/types",
|
|
19
19
|
"@salesforce/types": "^1.6.0",
|
|
20
20
|
"@spotto/contract": "^1.0.69-alpha.13",
|
|
@@ -64,12 +64,12 @@
|
|
|
64
64
|
"format": "prettier --write .",
|
|
65
65
|
"prepare": "npm run build",
|
|
66
66
|
"pretest": "npm run fixpack && npm run eslint",
|
|
67
|
-
"release": "oneblink-release repository --no-name",
|
|
67
|
+
"release": "oneblink-release repository --no-name --update-dependents --force-update-dependency",
|
|
68
68
|
"start": "tsc-watch",
|
|
69
69
|
"test": "vitest run",
|
|
70
70
|
"types": "npm install --save-dev github:oneblink/types",
|
|
71
71
|
"typescript": "tsc --noEmit",
|
|
72
|
-
"update-dependents": "oneblink-release update-dependents --force"
|
|
72
|
+
"update-dependents": "oneblink-release update-dependents --force-update-dependency"
|
|
73
73
|
},
|
|
74
74
|
"type": "module"
|
|
75
75
|
}
|