UNPKG

@flowfuse/flowfuse

Version:

An open source low-code development platform

323 lines (312 loc) • 12.4 kB
/* eslint-disable no-useless-escape */ // Extracted from @node-red/util/util.js to avoid dependency on @node-red/util package function createError (code, message) { const e = new Error(message) e.code = code return e } /** * Parses a property expression, such as `msg.foo.bar[3]` to validate it * and convert it to a canonical version expressed as an Array of property * names. * * For example, `a["b"].c` returns `['a','b','c']` * * If `msg` is provided, any internal cross-references will be evaluated against that * object. Otherwise, it will return a nested set of properties * * For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]` * But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]` * * @param {String} str - the property expression * @return {Array} the normalised expression * @memberof @node-red/util_util */ function normalisePropertyExpression (str, msg, toString) { // This must be kept in sync with validatePropertyExpression // in editor/js/ui/utils.js const length = str.length if (length === 0) { throw createError('INVALID_EXPR', 'Invalid property expression: zero-length') } const parts = [] let start = 0 let inString = false let inBox = false let quoteChar let v for (let i = 0; i < length; i++) { const c = str[i] if (!inString) { if (c === "'" || c === '"') { if (i !== start) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) } inString = true quoteChar = c start = i + 1 } else if (c === '.') { if (i === 0) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected . at position 0') } if (start !== i) { v = str.substring(start, i) if (/^\d+$/.test(v)) { parts.push(parseInt(v)) } else { parts.push(v) } } if (i === length - 1) { throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') } // Next char is first char of an identifier: a-z 0-9 $ _ if (!/[a-z0-9\$\_]/i.test(str[i + 1])) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1)) } start = i + 1 } else if (c === '[') { if (i === 0) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) } if (start !== i) { parts.push(str.substring(start, i)) } if (i === length - 1) { throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') } // Start of a new expression. If it starts with msg it is a nested expression // Need to scan ahead to find the closing bracket if (/^msg[.\[]/.test(str.substring(i + 1))) { let depth = 1 let inLocalString = false let localStringQuote for (let j = i + 1; j < length; j++) { if (/["']/.test(str[j])) { if (inLocalString) { if (str[j] === localStringQuote) { inLocalString = false } } else { inLocalString = true localStringQuote = str[j] } } if (str[j] === '[') { depth++ } else if (str[j] === ']') { depth-- } if (depth === 0) { try { if (msg) { const crossRefProp = getMessageProperty(msg, str.substring(i + 1, j)) if (crossRefProp === undefined) { throw createError('INVALID_EXPR', 'Invalid expression: undefined reference at position ' + (i + 1) + ' : ' + str.substring(i + 1, j)) } parts.push(crossRefProp) } else { parts.push(normalisePropertyExpression(str.substring(i + 1, j), msg)) } inBox = false i = j start = j + 1 break } catch (err) { throw createError('INVALID_EXPR', 'Invalid expression started at position ' + (i + 1)) } } } if (depth > 0) { throw createError('INVALID_EXPR', "Invalid property expression: unmatched '[' at position " + i) } continue } else if (!/["'\d]/.test(str[i + 1])) { // Next char is either a quote or a number throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1)) } start = i + 1 inBox = true } else if (c === ']') { if (!inBox) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) } if (start !== i) { v = str.substring(start, i) if (/^\d+$/.test(v)) { parts.push(parseInt(v)) } else { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start) } } start = i + 1 inBox = false } else if (c === ' ') { throw createError('INVALID_EXPR', "Invalid property expression: unexpected ' ' at position " + i) } } else { if (c === quoteChar) { if (i - start === 0) { throw createError('INVALID_EXPR', 'Invalid property expression: zero-length string at position ' + start) } parts.push(str.substring(start, i)) // If inBox, next char must be a ]. Otherwise it may be [ or . if (inBox && !/\]/.test(str[i + 1])) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start) } else if (!inBox && i + 1 !== length && !/[\[\.]/.test(str[i + 1])) { throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' expression at position ' + (i + 1)) } start = i + 1 inString = false } } } if (inBox || inString) { // eslint-disable-next-line new-cap throw new createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') } if (start < length) { parts.push(str.substring(start)) } if (toString) { let result = parts.shift() while (parts.length > 0) { let p = parts.shift() if (typeof p === 'string') { if (/"/.test(p)) { p = "'" + p + "'" } else { p = '"' + p + '"' } } result = result + '[' + p + ']' } return result } return parts } /** * Gets a property of a message object. * * Unlike {@link @node-red/util-util.getObjectProperty}, this function will strip `msg.` from the * front of the property expression if present. * * @param {Object} msg - the message object * @param {String} expr - the property expression * @return {any} the message property, or undefined if it does not exist * @throws Will throw an error if the *parent* of the property does not exist * @memberof @node-red/util_util */ function getMessageProperty (msg, expr) { if (expr.indexOf('msg.') === 0) { expr = expr.substring(4) } return getObjectProperty(msg, expr) } /** * Gets a property of an object. * * Given the object: * * { * "pet": { * "type": "cat" * } * } * * - `pet.type` will return `"cat"`. * - `pet.name` will return `undefined` * - `car` will return `undefined` * - `car.type` will throw an Error (as `car` does not exist) * * @param {Object} msg - the object * @param {String} expr - the property expression * @return {any} the object property, or undefined if it does not exist * @throws Will throw an error if the *parent* of the property does not exist * @memberof @node-red/util_util */ function getObjectProperty (msg, expr) { let result = null const msgPropParts = normalisePropertyExpression(expr, msg) msgPropParts.reduce(function (obj, key) { result = (typeof obj[key] !== 'undefined' ? obj[key] : undefined) return result }, msg) return result } /** * Sets a property of an object. * * @param {Object} msg - the object * @param {String} prop - the property expression * @param {any} value - the value to set * @param {boolean} createMissing - whether to create missing parent properties * @memberof @node-red/util_util */ function setObjectProperty (msg, prop, value, createMissing) { if (typeof createMissing === 'undefined') { createMissing = (typeof value !== 'undefined') } const msgPropParts = normalisePropertyExpression(prop, msg) const length = msgPropParts.length let obj = msg let key for (let i = 0; i < length - 1; i++) { key = msgPropParts[i] if (typeof key === 'string' || (typeof key === 'number' && !Array.isArray(obj))) { if (hasOwnProperty.call(obj, key)) { if (length > 1 && ((typeof obj[key] !== 'object' && typeof obj[key] !== 'function') || obj[key] === null)) { // Break out early as we cannot create a property beneath // this type of value return false } obj = obj[key] } else if (createMissing) { if (typeof msgPropParts[i + 1] === 'string') { obj[key] = {} } else { obj[key] = [] } obj = obj[key] } else { return false } } else if (typeof key === 'number') { // obj is an array if (obj[key] === undefined) { if (createMissing) { if (typeof msgPropParts[i + 1] === 'string') { obj[key] = {} } else { obj[key] = [] } obj = obj[key] } else { return false } } else { obj = obj[key] } } } key = msgPropParts[length - 1] if (typeof value === 'undefined') { if (typeof key === 'number' && Array.isArray(obj)) { obj.splice(key, 1) } else { delete obj[key] } } else { if (typeof obj === 'object' && obj !== null) { obj[key] = value } else { // Cannot set a property of a non-object/array return false } } return true } module.exports = { getObjectProperty, setObjectProperty }