adaptorex
Version:
Connect all your live interactive storytelling devices and software
1,210 lines (1,116 loc) • 38.8 kB
JavaScript
/**
* 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