UNPKG

adaptorex

Version:

Connect all your live interactive storytelling devices and software

1,210 lines (1,116 loc) 38.8 kB
/** * Get and Set variables and items in the current session and state * * provides functions to find and replace variables and to update or remove variables and store the changes to the database * * @requires json5 * * @module variables * @copyright Lasse Marburg 2022 * @license MIT */ const JSON5 = require("json5") /** @typedef {import('./types').SessionInterface} Session */ /** @typedef {import('./types').Game} Game */ /** * resolve variables using session references. * * Each Variable instance is assigned to an action context of a certain state in a session */ class Variables { /** * @param {Session} session * @param {Game} game */ constructor(session, game) { /** * @type {Session} * @private */ this.session = session /** * @type {Game} * @private */ this.game = game this.name = session.level.name + " " + session.name + " " + this.session.state.name + " " + this.session.action.plugin + " " + this.session.action.name } /** * Write update to local session state data. The data will be accessible via state and action name. * * Top level object properties will be appended to existing properties on subsequent store calls * * If you want the action data to persist if the action is triggered again, you can set persistent=true. * You can get persistent action data with load() * * @param {*} data - state data to be stored by action * @param {boolean} persistent - wether to store a copy of the action data that is not cleared if action is retriggered * * @returns {Promise} mongodb update result */ store(data, persistent) { return new Promise((resolve, reject) => { let updt = {} let properties = ["state_data"] if (persistent) { properties.push("persistent_state_data") } for (let property of properties) { let path = property + "." + this.session.state.name + "." + this.session.action.name if (adaptor.isObject(data)) { for (let d in data) { updt[path + "." + d] = data[d] } } else { updt[path] = data } } this.game.db.sessions .set({ _id: this.session._id }, updt) .then((result) => { return resolve(result) }) .catch((error) => { return reject( new Error( "couldn't store data for " + this.session.action.name + "." + this.session.state.name + " in session " + this.session.name, { cause: error } ) ) }) }) } /** * Load current action data that has been saved with store() function * * @returns {Promise} - Action data object as saved using store() */ async load() { let result = await this.game.db.sessions.find({ _id: this.session._id }) if (result.length) { let action_data = this.getPath( result[0], "persistent_state_data." + this.session.state.name + "." + this.session.action.name ) if (action_data) { return action_data } else { action_data = this.getPath( result[0], "state_data." + this.session.state.name + "." + this.session.action.name ) return action_data } } } /** * Set a variable to a new value. * * If variable references an item you can also update multiple fields if you provide an object as value. * * @param {string} variable - variable reference (e.g. Player.name or [[local_var]]) * @param {*} value - the data that will be stored to the variable */ async set(variable, value, options) { await this.edit("set", variable, value, options) } /** * Append value to a variable list. * * @param {string} variable - variable reference (e.g. Player.name or [[local_var]]) * @param {*} value - the data that will be appended to the variable list */ async push(variable, value, options) { await this.editArray("push", variable, value, options) } /** * Append value to a variable list if it does not contain the same value (avoid duplicates). * * @param {string} variable - variable reference (e.g. Player.name or [[local_var]]) * @param {*} value - the data that will be appended to the variable list */ async add(variable, value, options) { await this.editArray("add", variable, value, options) } /** * Remove value from variable list. * * @param {string} variable - variable reference (e.g. Player.name or [[local_var]]) * @param {*} value - the data that will be removed from the variable list */ async pull(variable, value, options) { await this.editArray("pull", variable, value, options) } /** * Make an array update. if value is an array the arrays will be merged not inserted. * * @param {string} operator - Array update operator pull, push or add * @param {string} variable - variable reference (e.g. Player.name or [[local_var]]) * @param {*} value - value the array will be updated with */ async editArray(operator, variable, value, options) { let modifier = "$each" if (operator == "pull") { modifier = "$in" } if (Array.isArray(value)) { if (value.length == 1) { await this.edit(operator, variable, value[0], options) } else { options.modifier = modifier await this.edit(operator, variable, value, options) } } else { await this.edit(operator, variable, value, options) } } /** * Make a DB update operation through the associated API Topic. * * @param {string} operator - mongo db update operator (e.g. $set) * @param {string} variable - the adaptor variable that will be updated * @param {any} value - value the variable will be updated with * @param {object} [options] - optional update options * @param {boolean} [options.multiple=true] - allow to edit multiple docs * @param {string} [options.modifier] - optional update modifier */ async edit( operator, variable, value, { multiple = true, modifier } = { multiple: true } ) { if (typeof variable !== "string") { throw new Error("Can not edit. Variable Reference has to be string type") } let reference = await this.getVariableReference(variable) value = await this.review(value) value = this.parseJSON(value) let update = {} if (modifier && reference.field) { update[reference.field] = {} update[reference.field][modifier] = value } else if (reference.field) { update[reference.field] = value } else if (typeof value === "object") { update = value } else { throw new adaptor.InvalidError( "Can not update " + variable + ": not a valid variable." ) } let result if (multiple) { result = await this.game.topics[reference.topic].edit( reference.query, operator, update, { user: { login: this.name } } ) } else { result = await this.game.topics[reference.topic].editOne( reference.query, operator, update, { user: { login: this.name } } ) } if (result) { this.session.log( `${operator} value '${value}' to/from variable '${reference.variable}' in '${reference.collection}'. ${result.changed} item(s) changed.` ) return result } throw new adaptor.NotFoundError( `${operator} failed. Could not find document with ${JSON.stringify(reference.query)} in ${reference.collection}` ) } /** * Make an update operation to a variable item document. Maybe any kind of mongodb update query * * @param {string} variable - variable reference that references an item (e.g. Player or [[chats.MyChat]]) * @param {*} data - Mongodb style update query * @param {object} [options] - optional update options * @param {boolean} [options.multiple=true] - allow to update multiple docs */ async update(variable, data, { multiple = true } = { multiple: true }) { let reference = await this.getVariableReference(variable) if (!multiple) { let document = await this.game.topics[reference.topic].collection.find( reference.query ) reference.query = { _id: document[0]._id } } data = await this.review(data) let update = this.parseJSON(data, true) let response = await this.game.topics[reference.topic].collection.update( reference.query, update ) let result = await this.game.topics[reference.topic].reportChanges( response, reference.query, { login: this.name } ) if (!result) { throw new adaptor.NotFoundError( `update failed. Could not find document with ${JSON.stringify(reference.query)} in ${reference.topic}.` ) } else if (!result.changed) { this.session.log(`No changes made when updating ${variable}.`) } else { this.session.log( result.changed + " item(s) updated with " + data + " in " + reference.topic + " query: " + JSON.stringify(reference.query) ) } } /** * Create a new item document. * * Use reference to make it more easy to access it in the current session. * * @param {string} collection - Database collection the item will be created in * @param {Object} variables - The Data that will be inserted * @param {*} [reference] - Name of reference inside the current session * @returns {Promise<object>} Result of insert operation contains created_id, created_at and created_by */ async create(collection, variables, reference) { variables = await this.findAndReplace(variables, true) if (!this.game.topics[collection]) { throw new adaptor.NotFoundError( collection + " data collection or topic not found." ) } let result = await this.game.topics[collection].create(variables, { user: { login: this.name } }) if (reference) { reference = await this.review(reference) await this.session.createReference( collection, { _id: result.created_id }, reference ) } this.session.log( `Created new ${collection} item ${variables.name}. id: ${result.created_id}` ) return result } /** * Remove variable from item document or delete the whole document. * * @param {string} variable * @param {object} [options] - optional update options * @param {boolean} [options.multiple] - allow to delete multiple docs */ async delete(variable, { multiple = true } = { multiple: true }) { let reference = await this.getVariableReference(variable) if (reference.field) { let update_query = {} update_query[reference.field] = "" let result if (multiple) { result = await this.game.topics[reference.topic].edit( reference.query, "remove", update_query, { user: { login: this.name } } ) } else { result = await this.game.topics[reference.topic].editOne( reference.query, "remove", update_query, { user: { login: this.name } } ) } if (!result) { this.session.log.warn( `Could not remove ${reference.field}. ${reference.source} does not reference any exiting Item.` ) } else if (!result.changed) { this.session.log( `Could not remove ${reference.field} from ${reference.source}. ${reference.field} does not exist.` ) } else { this.session.log( `Removed variable ${reference.field} from ${reference.source} in ${result.changed} Item(s).` ) } return result } else { let result = await this.game.topics[reference.topic].delete( reference.query, { multiple: multiple } ) if (result) { this.session.log( `Removed Item ${variable} from ${reference.collection} collection. ${result}` ) } else { this.session.log.warn( `Could not remove ${variable} from ${reference.collection} collection. Item not found.` ) } return result } } /** * Get document based on variable reference or query * * @example * // returns document that references to the current session as "teamcaptain" * get("teamcaptain") * * @param {string} variable - value to check against being a reference variable * @param {boolean} [return_empty=false] - get() rejects promise (throw error) if no document can be resolved. If return_empty is set to true, get() resolves undefined instead * * @returns {Promise} - resolves with (first) referred document. rejects if no document could be found. */ get(variable, return_empty) { return new Promise((resolve, reject) => { try { var ref = this.getReference(variable) } catch (error) { if (return_empty) { return resolve() } else { return reject( "reference couldn't be resolved: " + variable + "\n" + error ) } } if (this.game.db.hasOwnProperty(ref.collection)) { this.game.db[ref.collection] .find(ref.query) .then((result) => { if (result.length) { result = this.sortReferencedDocuments(result, ref.query) return resolve(result[0]) } else if (return_empty) { return resolve() } else { return reject("no document found with reference: " + variable) } }) .catch((error) => { return reject(error) }) } else { return reject( `Can not get variable ${variable} collection ${ref.collection} does not exist` ) } }) } /** * Get documents based on variable reference or query * * @param {string} variable - value to check against being a reference variable * @param {boolean} [return_empty=false] - An error is thrown if no document can be resolved. If return_empty is set to true, getMany() resolves with empty array instead. * @returns {Promise<array>} - list of all documents referred to by variable. */ async getMany(variable, return_empty) { let ref = await this.getVariableReference(variable) let documents = await this.game.db[ref.collection].find(ref.query) if (!documents.length && !return_empty) { throw new adaptor.NotFoundError( `Could not find any items in collection ${ref.collection} with ${variable}` ) } documents = this.sortReferencedDocuments(documents, ref.query) return documents } /** * Get the item instance that is bound to a variable object. * * Could return a Data Item, Plugin Item, a Session or the Game API Object itself * * variable reference should always be explicit, since getItem will, other than get, getMany and getValue, ignore sort option for variable references * * @param {*} variable - string reference to variable object. * * @returns {Promise<object>} - returns the class instance object of an item, session or the current game */ async getItem(variable) { let reference = await this.getVariableReference(variable) if (reference.topic == "game") { return this.game } if (this.game.topics.hasOwnProperty(reference.topic)) { if (reference.topic == "session") { let doc = await this.game.db.sessions.find(reference.query) return this.game.getSession(doc[0]._id) } if (reference.topic == "plugin") { let doc = await this.game.db.plugins.find(reference.query) return this.game.findPlugin("_id", doc[0]._id) } return this.game.topics[reference.topic].queryItem(reference.query) } else { throw new adaptor.NotFoundError( `Can not get Item ${variable} collection ${reference.collection} does not exist` ) } } /** * @typedef VariableReference * @property {string} collection - name of database collection the variable is referring to. * @property {string} topic - name of game api topic the variable is referring to. Similar to 'collection' but night differ. * @property {Object<string, any} query - find query to get the document that is or contains the variable value * @property {string} [field] - the property inside the document that contains the referenced value * @property {string} source - a descriptive name for the variable origin to use in log or other feedback context * @property {string} [variable] - the original variable the reference is based on * * @example * {"collection":"levels", "topic":""level", "query":{"_id":"abcd1234"}, field:"config.comment", source:"level attributes", "variable":"level.comment"} */ /** * Create reference to database document . * * First part of string path indicates collection and query of reference and may be a: * * - level argument like `Player` * - the `state` keyword * - the `action` keyword * - the `level` keyword * - a plugin item reference `<plugin>.<collection>.<item name or query string>` eg `telegram.client.MyClient` * - a data item reference `<collection>.<item name or query string>` e.g. `player.MyPlayer` * * All segments that might append the above define the field of the value inside the referenced document. * * If first part of path string is none of the above, the referenced document defaults to `variables` property in the current session document (local variable) * * `level` keyword will always refer to 'config' property inside level document except for the name, _id or instances field. * * @param {string} path - dot notated path string * * @returns {VariableReference} collection name, topic name, Mongo style find query and value field: `{collection:<collection>,topic:<topic>,query:<query>,field:<field>,source:<source name>}` */ getReference(path) { let segments = path.match(/\{.*\}|[^\.]+/g) // /\{[^\{^\}]+\}|[^\.]+/g <= regex until 13.02.2024 function getField(depth) { let field_index = 0 for (let i = 0; i < depth; i++) { field_index += segments[i].length + 1 } return path.slice(field_index) } // Level Argument or Loaded Item for (let ref of this.session.references) { if (ref.name == segments[0]) { let query = { sessions: { $elemMatch: { _id: this.session._id, reference: segments[0] } } } let topic = ref.collection if (ref.collection == "sessions") { topic = "session" } return { collection: ref.collection, topic: topic, query: query, field: getField(1), source: segments[0] } } } // State Data if (segments[0] == "state") { let field = "state_data" if (segments.length > 1) { field += "." + getField(1) } return { collection: "sessions", topic: "session", query: { _id: this.session._id }, field: field, source: "states" } } // Local State/Action Data if (segments[0] == "action") { let field = "state_data." + this.session.state.name + "." + this.session.action.name if (segments.length > 1) { field += "." + getField(1) } return { collection: "sessions", topic: "session", query: { _id: this.session._id }, field: field, source: "states" } } // Current Level Document or Attribute if (segments[0] == "level") { let field if (segments.length > 1) { let attr = getField(1) if (!["name", "_id", "instances"].includes(attr)) { field = "config." + attr } else { field = attr } } return { collection: "levels", topic: "level", query: { _id: this.session.level._id }, field: field, source: "level attributes" } } // Any Level Document or Attribute if (segments[0] == "levels") { if (segments.length < 2) { throw new adaptor.InvalidError( "Levels reference is missing level name or query." ) } let field if (segments.length > 2) { let attr = getField(2) if (!["name", "_id", "instances"].includes(attr)) { field = "config." + attr } else { field = attr } } return { collection: "levels", topic: "level", query: this.parseQuery(segments[1]), field: field, source: "levels attributes" } } // Plugins if (this.game.getPlugin(segments[0])) { // Plugin Item if ( segments.length >= 2 && this.game.db[segments[0] + "_" + segments[1]] ) { if (segments.length < 3) { throw new adaptor.InvalidError( "Plugin item reference is missing item name or query." ) } let coll_name = segments[0] + "_" + segments[1] return { collection: coll_name, topic: coll_name, query: this.parseQuery(segments[2]), field: getField(3), source: segments[0] + "." + segments[1] } } // Plugin return { collection: "plugins", topic: "plugin", query: { name: segments[0] }, field: getField(1), source: segments[0] } } // Data Item if (this.game.setup.collections.includes(segments[0])) { if (segments.length < 2) { throw new adaptor.InvalidError( "Data item reference is missing item name or query." ) } return { collection: segments[0], topic: segments[0], query: this.parseQuery(segments[1]), field: getField(2), source: segments[0] + "." + segments[1] } } // Game Setup Document if (segments[0] == "game") { return { collection: "setup", topic: "game", query: {}, field: getField(1), source: "game" } } // Local Session variable return { collection: "sessions", topic: "session", query: { _id: this.session._id }, field: "variables." + path, source: "session" } } /** * Get variable reference (see getReference) independent of square brackets `[[]]`. * * Encapsulating square brackets are ignored. * * If there are square brackets in midst of the string they are resolved using variables.review() * * @param {string} variable - an adaptor variable reference string * * @returns {Promise<VariableReference>} - the DB reference to the respective variable: {collection:<collection>,topic:<topic>,query:<query>,field:<field>,source:<source name>, variable:<original_variable>} */ async getVariableReference(variable) { variable = variable.replace(/(^\[\[)(.*)(\]\]$)/, "$2") // remove [[ at beginning and ]] at end of string if both. variable = await this.review(variable) let reference = this.getReference(variable) reference["variable"] = variable return reference } /** * Sort Data and Plugin Item references based on the (optional) 'sessions.index' property that * has been assigned to the documents when the reference was created (using sort option) * * If you want to make sure references that have been resolved using getReference or getVariableReference are in the correct sort order * you need to call this function with the documents that have been received using the respective query and collection. * * Will return the original documents in their original sort order if they do not have a 'session.index' property * * @param {Array} docs - list of collection documents (data or plugin items) * @param {object} query - respective mongodb query that was used to find the docs * @returns {Object} the original documents in the correct sort order based on their 'session.index' property (if any) */ sortReferencedDocuments(docs, query) { if (!query || !query.hasOwnProperty("sessions")) { return docs } return docs.sort((a, b) => { let index_a = this.getReferenceIndex(a, query) let index_b = this.getReferenceIndex(b, query) if (index_a < index_b) { return -1 } if (index_a > index_b) { return 1 } }) } /** * Get the index property ('session.index') that defines the documents position of the sort order of a reference. * * @param {Object} doc - collection document * @param {*} query - query that was used to find the document * @returns */ getReferenceIndex(doc, query) { let ref = doc.sessions.find((session) => { return ( session.reference == query.sessions.$elemMatch.reference && this.game.db.sessions.compareIDs( session._id, query.sessions.$elemMatch._id ) ) }) if (!ref) { throw new Error( `Could not sort entries for ${JSON.stringify(doc)} with query ${JSON.stringify(query)}` ) } return ref.index } /** * Use JSON5 parse to convert to valid query object. * * Convert regex `/regexpr/igm` to {"$regex":"regexpr","$options":"igm"} so it matches JSON syntax and will be recognized by mongo db. * * If query string is not json (no curly brackets `{ }` or colons `:`) return name property query like so: * `{name: <query string>}` * * @param {string} query * @returns {*} - Valid json object query or, if query is not of type string, the original value */ static parseQuery(query) { if (typeof query === "string") { if (/.*[{}:].*/.test(query)) { let done = false while (!done) { let hasReg = query.search(/: *\/[^\/]*\/[ ,igm]*$/) if (hasReg >= 0) { let regStart = query.indexOf("/", hasReg + 1) let regEnd = query.indexOf("/", regStart + 1) if (regEnd >= 0) { let front = query.substring(0, regStart) let regex = query.substring(regStart + 1, regEnd) let rear = query.substring(regEnd + 1, query.length) let regOptions = rear.match(/^[igm]+/) if (regOptions) { query = `${front}{"$regex":"${regex}","$options":"${regOptions[0]}"}${rear.substring(regOptions[0].length, query.length)}` } else { query = `${front}{"$regex":"${regex}"}${rear}` } } else { done = true } } else { done = true } } return this.parseJSON(query, true) } else { return { name: query } } } else { return query } } /** * Use JSON5 parse to convert to valid query object. * * If query string is not json (no curly brackets `{ }` or colons `:`) return name property query like so: * `{name: <query string>}` * * @param {string} query * @returns {Object} - valid json object query */ parseQuery(query) { return Variables.parseQuery(query) } /** * Use JSON5 to parse json formatted string to js object. * Only convert if starts and ends with curly brackets * Use `force_json=true` to add encapsulating curly brackets '{}' if missing and try to convert any string that contains at least 1 colon `:` * * @param {string} json_string - string to be converted to js object * @param {boolean} [force_json] - try to convert even if string is not encapsulated by curly brackets '{}'. Will allow anything as json that contains at least 1 colon `:` * @returns {*} either converted JS object or the input argument if it is not of type string or does not begin and end with curly bracket '{' (exception see `force_json`) */ static parseJSON(json_string, force_json) { if (typeof json_string === "string") { if (!["{", "[", '"'].includes(json_string.trim()[0])) { if (force_json) { json_string = "{" + json_string + "}" } else { return json_string } } try { let json = JSON5.parse(json_string) return json } catch (err) { if (err instanceof SyntaxError) { throw new adaptor.InvalidError( 'syntax error in json string "' + json_string + '"', { cause: err } ) } else { throw err } } } else { return json_string } } /** * Use JSON5 to parse json formatted string to js object. * Only convert if starts and ends with curly brackets * Use `force_json=true` to add encapsulating curly brackets '{}' if missing and try to convert any string that contains at least 1 colon `:` * * @param {string} json_string - string to be converted to js object * @param {boolean} [force_json] - try to convert even if string is not encapsulated by curly brackets '{}'. Will allow anything as json that contains at least 1 colon `:` * @returns {*} either converted JS object or the input argument if it is not of type string or does not begin and end with curly bracket '{' (exception see `force_json`) */ parseJSON(json_string, force_json) { return Variables.parseJSON(json_string, force_json) } /** * address object property with '.' separated string * Might be handy especially when working with Mongodb and nested document find queries. * Mongo uses [dot notation]{@link https://docs.mongodb.com/manual/core/document/#document-dot-notation} * from: * https://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key/6491621#6491621 * * @param {Object} o - some Object * @param {string} s - some string that finds nested properties with '.' notation */ getPath(o, s) { s = s.replace(/\[(\w+)\]/g, ".$1") // convert indexes to properties s = s.replace(/^\./, "") // strip a leading dot var a = s.split(".") for (var i = 0, n = a.length; i < n; ++i) { var k = a[i] if (k in o) { o = o[k] } else { return } } return o } /** * Resolve dot notated string path to the value it references. * * 1. Variables * * Uses variables.getReference to find the value behind a level variable, plugin item, data item, local variable or state variable * * 2. Functions * * Call a function from game functions. * * `function.<function_name>(<arg1>,<arg2>,...)` * * function args are comma separated * @param {string} path - dot notated path * * @returns {Promise} the value, document or function return value. else undefined */ async getValue(path) { if (typeof path === "string") { let first = path.split(".", 1)[0] // Resolve Function if (first == "functions") { let second = path.slice(first.length + 1) let function_name = second.substring(0, second.indexOf("(")) let function_args = second.substring( second.indexOf("(") + 1, second.indexOf(")") ) log.debug( this.name, "resolve function " + function_name + " with args: " + function_args ) let functions = this.game.getFunctions() if (typeof functions[function_name] === "function") { function_args = function_args.split(",") for (let i = 0; i < function_args.length; i++) { function_args[i] = function_args[i].trim() if ( (function_args[i][0] == "'" && function_args[i][function_args[i].length - 1] == "'") || (function_args[i][0] == '"' && function_args[i][function_args[i].length - 1] == '"') ) { function_args[i] = function_args[i].slice(1, -1) } else if (!isNaN(function_args[i])) { function_args[i] = Number(function_args[i]) } else { function_args[i] = await this.getValue(function_args[i]) } } try { let session = Object.assign(this.session, { variables: this }) let result = await functions[function_name](function_args, { game: this.game, session: session }) return result } catch (error) { log.error(this.name, error) throw new Error( `Failed to execute javascript function ${function_name}: \n${error.stack}` ) } } else { throw new Error(`No such function ${second} at ${this.name}`) } } else { // Resolve Variable let ref = this.getReference(path) let result = await this.game.db[ref.collection].find(ref.query) if (result.length) { result = this.sortReferencedDocuments(result, ref.query) if (ref.field) { var value = this.getPath(result[0], ref.field) } else { var value = result[0] } if (typeof value === "undefined") { this.session.log.warn( ref.field + " could not be found in " + ref.source ) } return value } else { log.error( this.name, "Variable reference '" + path + "' could not be resolved" ) return } } } else { throw new adaptor.InvalidError( "variable query from path only allows string path." ) } } /** * find variables and replace them with value from database. Takes different types: * - String: review string * - Array: walk through all elements of array and review them * @todo will only replace variables in strings on first array level * - Object: Iterate through (nested) object and parse [[]] variables using review function * * @param {Object|array|string} element - js Object. May be string or array * @param {boolean} parseJSON - optionally try to parse json for each string that starts with curly brackets '{' * * @returns {Promise} Element with replaced variables */ findAndReplace(element, parseJSON) { return new Promise((resolve, reject) => { let type = "" let review_promises = [] if (typeof element === "string") { review_promises.push(this.review(element)) type = "string" } else if (Array.isArray(element)) { for (let elem of element) { review_promises.push(this.review(elem)) } type = "array" } else { review_promises.push(this.reviewObject(element, parseJSON)) type = "object" } Promise.all(review_promises) .then((result) => { switch (type) { case "string": if (parseJSON) { result[0] = this.parseJSON(result[0]) } return resolve(result[0]) case "array": return resolve(result) case "object": return resolve(element) } }) .catch((error) => { return reject(error) }) }) } /** * Walk through (nested) object properties and check for variables and replace them with current value * * Optional: try and parse any string that starts with curly bracket `{` to json. * * @param {Object} payload - Object to be reviewed. No effect if its not an object * @param {boolean} parseJSON - wether or not to try and parse string to json if it starts with curly bracket `{` */ async reviewObject(payload, parseJSON) { if (typeof payload === "object") { for (const field in payload) { if (typeof payload[field] === "string") { payload[field] = await this.review(payload[field]) if (parseJSON) { payload[field] = this.parseJSON(payload[field]) } continue } else { await this.reviewObject(payload[field], parseJSON) } } } } /** * check string for `[[ ]]` and replace the content in case. * if `[[ ]]` embraces the whole string, it resolves to its original data type of the variable and will not be parsed to string in case! * * if review is supposed to check anything thats not a string it returns the argument itself * * @example * // resolve player variables using the player reference, a database query and the level reference. * this.review("your score is: [[Player.score]] and your phone number is [[player.{'friends.good':'tom'}.phone_number]] and your level is [[level.name]]") * .then(res => { * log.info("VARIABLES EXAMPLE", res) * }) * * * @param {string} str - string that might contain replaceable values. Other datatypes are allowed and simply returned the way they came in. * * @returns {Promise} the string with replaced variables. */ review(str) { return new Promise((resolve, reject) => { if (typeof str === "string") { var regex = /\[\[([\+\w\u00C0-\u017F\.\,\-\{\}\(\)\$\:\'\"\\\s\/\*]+)\]\]/g var matches = str.match(regex) if (!matches) { return resolve(str) } let values = matches.map((match, index) => { return this.getValue(match.slice(2, -2)) }) Promise.all(values) .then((result) => { if (matches[0] == str) { return resolve(result[0]) } var i = -1 let replaced_str = str.replace(regex, (tag, match) => { i++ return result[i] }) return resolve(replaced_str) }) .catch((err) => { if (err instanceof adaptor.AdaptorError) { return reject(err) } return reject( new Error( `Failed to resolve variables in "${str.substring(0, 35)}"`, { cause: err } ) ) }) } else { return resolve(str) } }) } } module.exports.Variables = Variables