UNPKG

@player-ui/player

Version:

1 lines 379 kB
{"version":3,"sources":["../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding-grammar/ast.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding-grammar/custom/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding/binding.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/binding/resolver.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/data/dependency-tracker.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/data/model.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/data/noop-model.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/data/local-model.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/expressions/evaluator.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/expressions/types.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/expressions/parser.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/expressions/evaluator-functions.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/expressions/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/logger/types.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/logger/consoleLogger.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/logger/noopLogger.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/logger/tapableLogger.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/logger/proxyLogger.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/schema/schema.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/string-resolver/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/validator/validation-middleware.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/validator/binding-map-splice.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/validator/registry.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/view.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/resolver/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/parser/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/parser/types.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/parser/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/resolver/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/builder/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/template.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/string-resolver.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/applicability.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/switch.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/multi-node.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/view/plugins/asset.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/player.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/flow/flow.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/flow/controller.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/validation/controller.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/utils/replaceParams.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/validation/binding-tracker.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/view/store.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/view/asset-transform.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/view/controller.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/data/controller.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/data/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/constants/utils.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/controllers/constants/index.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/plugins/flow-exp-plugin.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/plugins/default-exp-plugin.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/types.ts","../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/core/player/src/plugins/default-view-plugin.ts"],"sourcesContent":["// Add the types export first so it's naming takes precedence\nexport * from \"@player-ui/types\";\nexport * from \"./binding/index\";\nexport * from \"./data/index\";\nexport * from \"./expressions/index\";\nexport * from \"./logger/index\";\nexport * from \"./schema/index\";\nexport * from \"./string-resolver/index\";\nexport * from \"./validator/index\";\nexport * from \"./view/index\";\n\nexport * from \"./player\";\nexport * from \"./controllers/index\";\nexport * from \"./types\";\nexport * from \"./plugins/flow-exp-plugin\";\n","import { SyncBailHook, SyncWaterfallHook } from \"tapable-ts\";\nimport { NestedError } from \"ts-nested-error\";\nimport type { ParserResult, AnyNode } from \"../binding-grammar/index\";\nimport {\n // We can swap this with whichever parser we want to use\n parseCustom as parseBinding,\n} from \"../binding-grammar\";\nimport type { BindingParserOptions, BindingLike } from \"./binding\";\nimport { BindingInstance } from \"./binding\";\nimport { isBinding } from \"./utils\";\nimport type { NormalizedResult, ResolveBindingASTOptions } from \"./resolver\";\nimport { resolveBindingAST } from \"./resolver\";\n\nexport * from \"./utils\";\nexport * from \"./binding\";\n\nexport const SIMPLE_BINDING_REGEX = /^[\\w\\-@]+(\\.[\\w\\-@]+)*$/;\nexport const BINDING_BRACKETS_REGEX = /[\\s()*=`{}'\"[\\]]/;\nconst LAZY_BINDING_REGEX = /^[^.]+(\\..+)*$/;\n\nconst DEFAULT_OPTIONS: BindingParserOptions = {\n get: () => {\n throw new Error(\"Not Implemented\");\n },\n set: () => {\n throw new Error(\"Not Implemented\");\n },\n evaluate: () => {\n throw new Error(\"Not Implemented\");\n },\n};\n\ntype BeforeResolveNodeContext = Required<NormalizedResult> &\n ResolveBindingASTOptions;\n\n/** A parser for creating bindings from a string */\nexport class BindingParser {\n private cache: Record<string, BindingInstance>;\n private parseCache: Record<string, ParserResult>;\n private parserOptions: BindingParserOptions;\n\n public hooks = {\n skipOptimization: new SyncBailHook<[string], boolean>(),\n beforeResolveNode: new SyncWaterfallHook<\n [AnyNode, BeforeResolveNodeContext]\n >(),\n };\n\n constructor(options?: Partial<BindingParserOptions>) {\n this.parserOptions = { ...DEFAULT_OPTIONS, ...options };\n this.cache = {};\n this.parseCache = {};\n this.parse = this.parse.bind(this);\n }\n\n /**\n * Takes a binding path, parses it, and returns an equivalent, normalized\n * representation of that path.\n */\n private normalizePath(\n path: string,\n resolveOptions: ResolveBindingASTOptions,\n ) {\n /**\n * Ensure no binding characters exist in path and the characters remaining\n * look like a binding format.\n */\n if (\n !BINDING_BRACKETS_REGEX.test(path) &&\n LAZY_BINDING_REGEX.test(path) &&\n this.hooks.skipOptimization.call(path) !== true\n ) {\n return { path: path.split(\".\"), updates: undefined } as NormalizedResult;\n }\n\n const ast = this.parseCache[path] ?? parseBinding(path);\n this.parseCache[path] = ast;\n\n if (typeof ast !== \"object\" || !ast?.status) {\n throw new TypeError(\n `Cannot normalize path \"${path}\": ${ast?.error ?? \"Unknown Error.\"}`,\n );\n }\n\n try {\n return resolveBindingAST(ast.path, resolveOptions, this.hooks);\n } catch (e: any) {\n throw new NestedError(`Cannot resolve binding: ${path}`, e);\n }\n }\n\n private getBindingForNormalizedResult(\n normalized: NormalizedResult,\n ): BindingInstance {\n const normalizedStr = normalized.path.join(\".\");\n\n if (this.cache[normalizedStr]) {\n return this.cache[normalizedStr];\n }\n\n const created = new BindingInstance(\n normalizedStr === \"\" ? [] : normalized.path,\n this.parse,\n );\n this.cache[normalizedStr] = created;\n\n return created;\n }\n\n public parse(\n rawBinding: BindingLike,\n overrides: Partial<BindingParserOptions> = {},\n ): BindingInstance {\n if (isBinding(rawBinding)) {\n return rawBinding;\n }\n\n const options = {\n ...this.parserOptions,\n ...overrides,\n };\n\n let updates: Record<string, any> = {};\n\n const joined = Array.isArray(rawBinding)\n ? rawBinding.join(\".\")\n : String(rawBinding);\n\n const normalizeConfig: ResolveBindingASTOptions = {\n getValue: (path: Array<string | number>) => {\n const normalized = this.normalizePath(path.join(\".\"), normalizeConfig);\n\n return options.get(this.getBindingForNormalizedResult(normalized));\n },\n evaluate: (exp) => {\n return options.evaluate(exp);\n },\n convertToPath: (path: any) => {\n if (path === undefined) {\n throw new Error(\n \"Attempted to convert undefined value to binding path\",\n );\n }\n\n if (\n typeof path !== \"string\" &&\n typeof path !== \"number\" &&\n typeof path !== \"boolean\"\n ) {\n throw new Error(\n `Attempting to convert ${typeof path} to a binding path.`,\n );\n }\n\n const normalized = this.normalizePath(String(path), normalizeConfig);\n\n if (normalized.updates) {\n updates = {\n ...updates,\n ...normalized.updates,\n };\n }\n\n const joinedNormalizedPath = normalized.path.join(\".\");\n\n if (joinedNormalizedPath === \"\") {\n throw new Error(\"Nested path resolved to an empty path\");\n }\n\n return joinedNormalizedPath;\n },\n };\n\n const normalized = this.normalizePath(joined, normalizeConfig);\n\n if (normalized.updates) {\n updates = {\n ...updates,\n ...normalized.updates,\n };\n }\n\n const updateKeys = Object.keys(updates);\n\n if (!options.readOnly && updateKeys.length > 0) {\n const updateTransaction = updateKeys.map<[BindingInstance, any]>(\n (updatedBinding) => [\n this.parse(updatedBinding),\n updates[updatedBinding],\n ],\n );\n\n options.set(updateTransaction);\n }\n\n return this.getBindingForNormalizedResult(normalized);\n }\n}\n","export interface Node<T extends string> {\n /** The basic node type */\n name: T;\n}\n\n/**\n * An AST node that represents a nested path in the model\n * foo.{{bar}}.baz (this is {{bar}})\n */\nexport interface PathNode extends Node<\"PathNode\"> {\n /** The path in the model that this node represents */\n path: Array<AnyNode>;\n}\n\n/**\n * A segment representing a query\n * [foo=bar]\n */\nexport interface QueryNode extends Node<\"Query\"> {\n /** The key to query */\n key: AnyNode;\n\n /** The target value */\n value?: AnyNode;\n}\n\n/** A simple segment */\nexport interface ValueNode extends Node<\"Value\"> {\n /** The segment value */\n value: string | number;\n}\n\n/** A nested expression */\nexport interface ExpressionNode extends Node<\"Expression\"> {\n /** The expression */\n value: string;\n}\n\n/** Helper to create a value node */\nexport const toValue = (value: string | number): ValueNode => ({\n name: \"Value\",\n value,\n});\n\n/** Helper to create an expression node */\nexport const toExpression = (value: string): ExpressionNode => ({\n name: \"Expression\",\n value,\n});\n\n/** Helper to create a nested path node */\nexport const toPath = (path: Array<AnyNode>): PathNode => ({\n name: \"PathNode\",\n path,\n});\n\n/** Helper to create a query node */\nexport const toQuery = (key: AnyNode, value?: AnyNode): QueryNode => ({\n name: \"Query\",\n key,\n value,\n});\n\n/** Create a concat node */\nexport const toConcatenatedNode = (\n values: Array<PathNode | ValueNode | ExpressionNode>,\n): PathNode | ValueNode | ConcatenatedNode | ExpressionNode => {\n if (values.length === 1) {\n return values[0];\n }\n\n return {\n name: \"Concatenated\",\n value: values,\n };\n};\n\n/**\n * A binding segment that's multiple smaller ones\n * {{foo}}_bar_{{baz}}\n */\nexport interface ConcatenatedNode extends Node<\"Concatenated\"> {\n /** A list of nested paths, or value nodes to concat together to form a segment */\n value: Array<PathNode | ValueNode | ExpressionNode>;\n}\n\nexport type AnyNode =\n | PathNode\n | QueryNode\n | ValueNode\n | ConcatenatedNode\n | ExpressionNode;\nexport type Path = Array<AnyNode>;\n\nexport interface ParserSuccessResult {\n /** A successful parse result */\n status: true;\n\n /** The path the binding represents */\n path: PathNode;\n}\n\nexport interface ParserFailureResult {\n /** A failed parse result */\n status: false;\n\n /** The message representing the reason the parse result failed */\n error: string;\n}\n\nexport type ParserResult = ParserSuccessResult | ParserFailureResult;\n\nexport type Parser = (raw: string) => ParserResult;\n","import type {\n Parser,\n AnyNode,\n PathNode,\n ConcatenatedNode,\n ValueNode,\n QueryNode,\n ExpressionNode,\n} from \"../ast\";\nimport {\n toValue,\n toPath,\n toConcatenatedNode,\n toQuery,\n toExpression,\n} from \"../ast\";\n\nconst SEGMENT_SEPARATOR = \".\";\nconst OPEN_CURL = \"{\";\nconst CLOSE_CURL = \"}\";\nconst OPEN_BRACKET = \"[\";\nconst CLOSE_BRACKET = \"]\";\nconst EQUALS = \"=\";\nconst SINGLE_QUOTE = \"'\";\nconst DOUBLE_QUOTE = '\"';\nconst BACK_TICK = \"`\";\n// const IDENTIFIER_REGEX = /[\\w\\-@]+/;\n\n/** A _faster_ way to match chars instead of a regex. */\nconst isIdentifierChar = (char?: string): boolean => {\n if (!char) {\n return false;\n }\n\n const charCode = char.charCodeAt(0);\n\n const matches =\n charCode === 32 || // ' '\n charCode === 34 || // \"\n charCode === 39 || // '\n charCode === 40 || // (\n charCode === 41 || // )\n charCode === 42 || // *\n charCode === 46 || // .\n charCode === 61 || // =\n charCode === 91 || // [\n charCode === 93 || // ]\n charCode === 96 || // `\n charCode === 123 || // {\n charCode === 125; // }\n\n return !matches;\n};\n\n/** Parse out a binding AST from a path */\nexport const parse: Parser = (path) => {\n let index = 1;\n let ch = path.charAt(0);\n\n /** get the next char in the string */\n const next = (expected?: string) => {\n if (expected && ch !== expected) {\n throw new Error(`Expected char: ${expected} but got: ${ch}`);\n }\n\n ch = path.charAt(index);\n index += 1;\n return ch;\n };\n\n /** gobble all whitespace */\n const whitespace = () => {\n /* eslint-disable no-unmodified-loop-condition */\n while (ch === \" \") {\n next();\n }\n };\n\n /** get an identifier if you can */\n const identifier = (): ValueNode | undefined => {\n if (!isIdentifierChar(ch)) {\n return;\n }\n\n let value: string | number = ch;\n\n while (next()) {\n if (!isIdentifierChar(ch)) {\n break;\n }\n\n value += ch;\n }\n\n if (value) {\n const maybeNumber = Number(value);\n value = isNaN(maybeNumber) ? value : maybeNumber;\n return toValue(value);\n }\n };\n\n /** get an expression node if you can */\n const expression = (): ExpressionNode | undefined => {\n if (ch === BACK_TICK) {\n next(BACK_TICK);\n\n let exp = ch;\n\n while (next()) {\n if (ch === BACK_TICK) {\n break;\n }\n\n exp += ch;\n }\n\n next(BACK_TICK);\n\n if (exp) {\n return toExpression(exp);\n }\n }\n };\n\n /** Grab a value using a regex */\n const regex = (match: RegExp): ValueNode | undefined => {\n if (!ch?.match(match)) {\n return;\n }\n\n let value = ch;\n\n while (next()) {\n if (!ch?.match(match)) {\n break;\n }\n\n value += ch;\n }\n\n if (value) {\n return toValue(value);\n }\n };\n\n /** parse out a nestedPath if you can */\n const nestedPath = (): PathNode | undefined => {\n if (ch === OPEN_CURL) {\n next(OPEN_CURL);\n next(OPEN_CURL);\n\n /* eslint-disable-next-line @typescript-eslint/no-use-before-define */\n const modelRef = parsePath();\n next(CLOSE_CURL);\n next(CLOSE_CURL);\n return modelRef;\n }\n };\n\n /** get a simple segment node */\n const simpleSegment = () => nestedPath() ?? expression() ?? identifier();\n\n /** Parse a segment */\n const segment = ():\n | ConcatenatedNode\n | PathNode\n | ValueNode\n | ExpressionNode\n | undefined => {\n // Either a string, modelRef, or concatenated version (both)\n const segments: Array<ValueNode | PathNode | ExpressionNode> = [];\n let nextSegment = simpleSegment();\n\n while (nextSegment !== undefined) {\n segments.push(nextSegment);\n nextSegment = simpleSegment();\n }\n\n if (segments.length === 0) {\n return undefined;\n }\n\n return toConcatenatedNode(segments);\n };\n\n /** get an optionally quoted block */\n const optionallyQuotedSegment = ():\n | ValueNode\n | PathNode\n | ExpressionNode\n | undefined => {\n whitespace();\n\n // see if we have a quote\n\n if (ch === SINGLE_QUOTE || ch === DOUBLE_QUOTE) {\n const singleQuote = ch === SINGLE_QUOTE;\n next(singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE);\n const id = regex(/[^'\"]+/);\n next(singleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE);\n return id;\n }\n\n return simpleSegment();\n };\n\n /** eat equals signs */\n const equals = (): boolean => {\n if (ch !== EQUALS) {\n return false;\n }\n\n while (ch === EQUALS) {\n next();\n }\n\n return true;\n };\n\n /** Parse out a bracket */\n const parseBracket = ():\n | ValueNode\n | QueryNode\n | PathNode\n | ExpressionNode\n | undefined => {\n if (ch === OPEN_BRACKET) {\n next(OPEN_BRACKET);\n whitespace();\n let value: ValueNode | QueryNode | PathNode | ExpressionNode | undefined =\n optionallyQuotedSegment();\n if (value) {\n whitespace();\n if (equals()) {\n whitespace();\n const second = optionallyQuotedSegment();\n value = toQuery(value, second);\n whitespace();\n }\n } else {\n throw new Error(`Expected identifier`);\n }\n\n if (value) {\n next(CLOSE_BRACKET);\n }\n\n return value;\n }\n };\n\n /** Parse a segment and any number of brackets following it */\n const parseSegmentAndBrackets = (): Array<AnyNode> => {\n // try to parse a segment first\n\n const parsed: Array<AnyNode> = [];\n\n const firstSegment = segment();\n\n if (firstSegment) {\n parsed.push(firstSegment);\n\n let bracketSegment = parseBracket();\n\n if (bracketSegment?.name === \"Value\") {\n const maybeNumber = Number(bracketSegment.value);\n bracketSegment.value =\n isNaN(maybeNumber) || String(maybeNumber) !== bracketSegment.value\n ? bracketSegment.value\n : maybeNumber;\n }\n\n while (bracketSegment !== undefined) {\n parsed.push(bracketSegment);\n bracketSegment = parseBracket();\n }\n }\n\n return parsed;\n };\n\n /** Parse out a path segment */\n const parsePath = (): PathNode => {\n const parts: AnyNode[] = [];\n\n let nextSegment = parseSegmentAndBrackets();\n\n while (nextSegment !== undefined) {\n parts.push(...nextSegment);\n\n if (!ch || ch === CLOSE_CURL) {\n break;\n }\n\n if (nextSegment.length === 0 && ch) {\n throw new Error(`Unexpected character: ${ch}`);\n }\n\n next(SEGMENT_SEPARATOR);\n nextSegment = parseSegmentAndBrackets();\n }\n\n return toPath(parts);\n };\n\n try {\n const result = parsePath();\n\n return {\n status: true,\n path: result,\n };\n } catch (e: any) {\n return {\n status: false,\n error: e.message,\n };\n }\n};\n","import type { BindingLike, BindingInstance } from \"./binding\";\n\n/** Check if the parameter representing a binding is already of the Binding class */\nexport function isBinding(binding: BindingLike): binding is BindingInstance {\n return !(typeof binding === \"string\" || Array.isArray(binding));\n}\n\n/** Convert the string to an int if you can, otherwise just return the original string */\nexport function maybeConvertToNum(i: string): string | number {\n const asInt = parseInt(i, 10);\n\n if (isNaN(asInt)) {\n return i;\n }\n\n return asInt;\n}\n\n/**\n * utility to convert binding into binding segments.\n */\nexport function getBindingSegments(\n binding: BindingLike,\n): Array<string | number> {\n if (Array.isArray(binding)) {\n return binding;\n }\n\n if (typeof binding === \"string\") {\n return binding.split(\".\");\n }\n\n return binding.asArray();\n}\n\n/** Like _.findIndex, but ignores types */\nexport function findInArray<T extends Record<string | number, object>>(\n array: Array<T>,\n key: string | number,\n value: T,\n): number | undefined {\n return array.findIndex((obj) => {\n if (obj && typeof obj === \"object\") {\n // Intentional double-equals because we want '4' to be coerced to 4\n // eslint-disable-next-line eqeqeq\n return obj[key] == value;\n }\n\n return false;\n });\n}\n","import { getBindingSegments } from \"./utils\";\n\nexport interface BindingParserOptions {\n /** Get the value for a specific binding */\n get: (binding: BindingInstance) => any;\n\n /**\n * Set the values for bindings.\n * This is used when the query syntax needs to modify an object\n */\n set: (transaction: Array<[BindingInstance, any]>) => void;\n\n /**\n * Get the result of evaluating an expression\n */\n evaluate: (exp: string) => any;\n\n /**\n * Without readOnly, if a binding such as this is used: arr[key='does not exist'],\n * then an object with that key will be created.\n * This is done to make assignment such as arr[key='abc'].val = 'foo' work smoothly.\n * Setting readOnly to true will prevent this behavior, avoiding unintended data changes.\n */\n readOnly?: boolean;\n}\n\nexport type Getter = (path: BindingInstance) => any;\n\nexport type RawBindingSegment = number | string;\nexport type RawBinding = string | RawBindingSegment[];\nexport type BindingLike = RawBinding | BindingInstance;\nexport type BindingFactory = (\n raw: RawBinding,\n options?: Partial<BindingParserOptions>,\n) => BindingInstance;\n\n/**\n * A path in the data model\n */\nexport class BindingInstance {\n private split: RawBindingSegment[];\n private joined: string;\n private factory: BindingFactory;\n\n constructor(\n raw: RawBinding,\n factory = (rawBinding: RawBinding) => new BindingInstance(rawBinding),\n ) {\n const split = Array.isArray(raw) ? raw : raw.split(\".\");\n this.split = split.map((segment) => {\n if (typeof segment === \"number\") {\n return segment;\n }\n\n const tryNum = Number(segment);\n return isNaN(tryNum) ? segment : tryNum;\n });\n Object.freeze(this.split);\n this.joined = this.split.join(\".\");\n this.factory = factory;\n }\n\n asArray(): RawBindingSegment[] {\n return this.split;\n }\n\n asString(): string {\n return this.joined;\n }\n\n /**\n * Check to see if the given binding is a sub-path of the current one\n */\n contains(binding: BindingInstance): boolean {\n // need to account for partial key matches\n // [foo, bar] !== [foo, ba]\n const bindingAsArray = binding.asArray();\n\n if (bindingAsArray.length < this.split.length) {\n return false;\n }\n\n // Check every overlapping index to make sure they're the same\n // Intentionally use a for loop for speeeed\n for (let i = 0; i < this.split.length; i++) {\n if (this.split[i] !== bindingAsArray[i]) {\n return false;\n }\n }\n\n return true;\n }\n\n relative(binding: BindingInstance): RawBindingSegment[] {\n return this.asArray().slice(binding.asArray().length);\n }\n\n parent(): BindingInstance {\n return this.factory(this.split.slice(0, -1));\n }\n\n key(): RawBindingSegment {\n return this.split[this.split.length - 1];\n }\n\n /**\n * This is a utility method to get a binding that is a descendent of this binding\n *\n * @param relative - The relative path to descend to\n */\n descendent(relative: BindingLike): BindingInstance {\n const descendentSegments = getBindingSegments(relative);\n\n return this.factory(this.split.concat(descendentSegments));\n }\n}\n","import { NestedError } from \"ts-nested-error\";\nimport type { SyncWaterfallHook } from \"tapable-ts\";\nimport type { PathNode, AnyNode } from \"../binding-grammar\";\nimport { findInArray, maybeConvertToNum } from \"./utils\";\n\nexport interface NormalizedResult {\n /** The normalized path */\n path: Array<string | number>;\n\n /** Any new updates that need to happen for this binding to be resolved */\n updates?: Record<string, any>;\n}\n\nexport interface ResolveBindingASTOptions {\n /** Get the value of the model at the given path */\n getValue: (path: Array<string | number>) => any;\n\n /** Convert the value into valid path segments */\n convertToPath: (value: any) => string;\n\n /** Convert the value into valid path segments */\n evaluate: (exp: string) => any;\n}\n\nexport interface ResolveBindingASTHooks {\n /** A hook for transforming a node before fully resolving it */\n beforeResolveNode: SyncWaterfallHook<\n [AnyNode, Required<NormalizedResult> & ResolveBindingASTOptions]\n >;\n}\n\n/** Given a binding AST, resolve it */\nexport function resolveBindingAST(\n bindingPathNode: PathNode,\n options: ResolveBindingASTOptions,\n hooks?: ResolveBindingASTHooks,\n): NormalizedResult {\n const context: Required<NormalizedResult> = {\n updates: {},\n path: [],\n };\n\n // let updates: Record<string, any> = {};\n // const path: Array<string | number> = [];\n\n /** Get the value for any child node */\n function getValueForNode(node: AnyNode): any {\n if (node.name === \"Value\") {\n return node.value;\n }\n\n if (node.name === \"PathNode\") {\n const nestedResolvedValue = resolveBindingAST(node, options);\n\n if (nestedResolvedValue.updates) {\n context.updates = {\n ...context.updates,\n ...nestedResolvedValue.updates,\n };\n }\n\n try {\n return options.convertToPath(\n options.getValue(nestedResolvedValue.path),\n );\n } catch (e: any) {\n throw new NestedError(\n `Unable to resolve path segment: ${nestedResolvedValue.path}`,\n e,\n );\n }\n }\n\n if (node.name === \"Expression\") {\n try {\n const actualValue = options.evaluate(node.value);\n\n return options.convertToPath(actualValue);\n } catch (e: any) {\n throw new NestedError(`Unable to resolve path: ${node.value}`, e);\n }\n }\n\n throw new Error(`Unable to resolve value for node: ${node.name}`);\n }\n\n /** Handle when path segments are binding paths (foo.bar) or single segments (foo) */\n function appendPathSegments(segment: string | number) {\n if (typeof segment === \"string\" && segment.indexOf(\".\") > -1) {\n segment.split(\".\").forEach((i) => {\n context.path.push(maybeConvertToNum(i));\n });\n } else {\n context.path.push(segment);\n }\n }\n\n /** Compute the _actual_ binding val from the AST */\n function resolveNode(_node: AnyNode) {\n const resolvedNode =\n hooks?.beforeResolveNode.call(_node, { ...context, ...options }) ?? _node;\n\n switch (resolvedNode.name) {\n case \"Expression\":\n case \"PathNode\":\n appendPathSegments(getValueForNode(resolvedNode));\n break;\n\n case \"Value\":\n appendPathSegments(resolvedNode.value);\n break;\n\n case \"Query\": {\n // Look for an object at the path with the given key/val criteria\n const objToQuery: Record<string, any>[] =\n options.getValue(context.path) ?? [];\n\n const { key, value } = resolvedNode;\n\n const resolvedKey = getValueForNode(key);\n const resolvedValue = value && getValueForNode(value);\n\n const index = findInArray(objToQuery, resolvedKey, resolvedValue);\n\n if (index === undefined || index === -1) {\n context.updates[\n [...context.path, objToQuery.length, resolvedKey].join(\".\")\n ] = resolvedValue;\n context.path.push(objToQuery.length);\n } else {\n context.path.push(index);\n }\n\n break;\n }\n\n case \"Concatenated\":\n context.path.push(resolvedNode.value.map(getValueForNode).join(\"\"));\n break;\n\n default:\n throw new Error(`Unsupported node type: ${(resolvedNode as any).name}`);\n }\n }\n\n bindingPathNode.path.forEach(resolveNode);\n\n return {\n path: context.path,\n updates:\n Object.keys(context.updates ?? {}).length > 0\n ? context.updates\n : undefined,\n };\n}\n","import type { BindingInstance } from \"../binding\";\nimport type {\n BatchSetTransaction,\n DataModelImpl,\n DataModelMiddleware,\n DataModelOptions,\n Updates,\n} from \"./model\";\n\nexport type DependencySets = \"core\" | \"children\";\n\n/** A class to track usage of read/writes to/from a data model */\nexport class DependencyTracker {\n protected readDeps: Set<BindingInstance>;\n protected writeDeps: Set<BindingInstance>;\n protected namedSet: DependencySets;\n\n private namedDependencySets: Partial<\n Record<\n DependencySets,\n {\n /** readDeps */\n readDeps: Set<BindingInstance>;\n /** writeDeps */\n writeDeps: Set<BindingInstance>;\n }\n >\n >;\n\n constructor() {\n this.readDeps = new Set();\n this.writeDeps = new Set();\n this.namedDependencySets = {};\n this.namedSet = \"core\";\n\n this.createSubset(\"core\");\n this.createSubset(\"children\");\n }\n\n protected createSubset(name: DependencySets, force = false): void {\n if (force || !this.namedDependencySets[name]) {\n this.namedDependencySets[name] = {\n readDeps: new Set(),\n writeDeps: new Set(),\n };\n }\n }\n\n /** Grab all of the bindings that this depended on */\n public getDependencies(name?: DependencySets): Set<BindingInstance> {\n if (name !== undefined) {\n return this.namedDependencySets?.[name]?.readDeps ?? new Set();\n }\n\n return this.readDeps;\n }\n\n public trackSubset(name: DependencySets) {\n this.createSubset(name);\n this.namedSet = name;\n }\n\n public trackDefault() {\n this.namedSet = \"core\";\n }\n\n /** Grab all of the bindings this wrote to */\n public getModified(name?: DependencySets): Set<BindingInstance> {\n if (name !== undefined) {\n return this.namedDependencySets?.[name]?.writeDeps ?? new Set();\n }\n\n return this.writeDeps;\n }\n\n /**\n * Check to see if the dataModel has read the value at the given binding\n *\n * @param binding - The binding you want to check for\n */\n public readsBinding(binding: BindingInstance): boolean {\n return this.readDeps.has(binding);\n }\n\n /**\n * Check to see if the dataModel has written to the binding\n */\n public writesBinding(binding: BindingInstance): boolean {\n return this.writeDeps.has(binding);\n }\n\n /** Reset all tracking of dependencies */\n public reset() {\n this.readDeps = new Set();\n this.writeDeps = new Set();\n this.namedDependencySets = {};\n this.namedSet = \"core\";\n\n this.createSubset(\"core\", true);\n this.createSubset(\"children\", true);\n }\n\n protected addReadDep(\n binding: BindingInstance,\n namedSet = this.namedSet,\n ): void {\n if (namedSet) {\n this.namedDependencySets?.[namedSet]?.readDeps.add(binding);\n }\n\n this.readDeps.add(binding);\n }\n\n protected addWriteDep(\n binding: BindingInstance,\n namedSet = this.namedSet,\n ): void {\n if (namedSet) {\n this.namedDependencySets?.[namedSet]?.writeDeps.add(binding);\n }\n\n this.writeDeps.add(binding);\n }\n\n public addChildReadDep(binding: BindingInstance): void {\n this.addReadDep(binding, \"children\");\n }\n}\n\n/** Middleware that tracks dependencies of read/written data */\nexport class DependencyMiddleware\n extends DependencyTracker\n implements DataModelMiddleware\n{\n constructor() {\n super();\n this.get = this.get.bind(this);\n this.set = this.set.bind(this);\n }\n\n public set(\n transaction: BatchSetTransaction,\n options?: DataModelOptions,\n next?: DataModelImpl | undefined,\n ): Updates {\n transaction.forEach(([binding]) => this.addWriteDep(binding));\n\n return next?.set(transaction, options) ?? [];\n }\n\n public get(\n binding: BindingInstance,\n options?: DataModelOptions,\n next?: DataModelImpl | undefined,\n ) {\n this.addReadDep(binding);\n\n return next?.get(binding, options);\n }\n\n public delete(\n binding: BindingInstance,\n options?: DataModelOptions,\n next?: DataModelImpl | undefined,\n ) {\n this.addWriteDep(binding);\n return next?.delete(binding, options);\n }\n}\n\n/** A data-model that tracks dependencies of read/written data */\nexport class DependencyModel<Options = DataModelOptions>\n extends DependencyTracker\n implements DataModelImpl<Options>\n{\n private readonly rootModel: DataModelImpl<Options>;\n\n constructor(rootModel: DataModelImpl<Options>) {\n super();\n this.rootModel = rootModel;\n this.set = this.set.bind(this);\n this.get = this.get.bind(this);\n }\n\n public set(transaction: BatchSetTransaction, options?: Options): Updates {\n transaction.forEach(([binding]) => this.addWriteDep(binding));\n\n return this.rootModel.set(transaction, options);\n }\n\n public get(binding: BindingInstance, options?: Options) {\n this.addReadDep(binding);\n\n return this.rootModel.get(binding, options);\n }\n\n public delete(binding: BindingInstance, options?: Options) {\n this.addWriteDep(binding);\n return this.rootModel.delete(binding, options);\n }\n}\n","import { SyncHook } from \"tapable-ts\";\nimport type { BindingLike, BindingFactory } from \"../binding\";\nimport { BindingInstance, isBinding } from \"../binding\";\nimport { NOOP_MODEL } from \"./noop-model\";\n\nexport const ROOT_BINDING = new BindingInstance([]);\nexport type BatchSetTransaction = [BindingInstance, any][];\n\nexport type Updates = Array<{\n /** The updated binding */\n binding: BindingInstance;\n\n /** The old value */\n oldValue: any;\n\n /** The new value */\n newValue: any;\n\n /** Force the Update to be included even if no data changed */\n force?: boolean;\n}>;\n\n/** Options to use when getting or setting data */\nexport interface DataModelOptions {\n /**\n * The data (either to set or get) should represent a formatted value\n * For setting data, the data will be de-formatted before continuing in the pipeline\n * For getting data, the data will be formatted before returning\n */\n formatted?: boolean;\n\n /**\n * By default, fetching data will ignore any invalid data.\n * You can choose to grab the queued invalid data if you'd like\n * This is usually the case for user-inputs\n */\n includeInvalid?: boolean;\n\n /**\n * A flag to set to ignore any default value in the schema, and just use the raw value\n */\n ignoreDefaultValue?: boolean;\n\n /**\n * A flag to indicate that this update should happen silently\n */\n silent?: boolean;\n\n /** Other context associated with this request */\n context?: {\n /** The data model to use when getting other data from the context of this request */\n model: DataModelWithParser;\n };\n}\n\nexport interface DataModelWithParser<Options = DataModelOptions> {\n get(binding: BindingLike, options?: Options): any;\n set(transaction: [BindingLike, any][], options?: Options): Updates;\n delete(binding: BindingLike, options?: Options): void;\n}\n\nexport interface DataModelImpl<Options = DataModelOptions> {\n get(binding: BindingInstance, options?: Options): any;\n set(transaction: BatchSetTransaction, options?: Options): Updates;\n delete(binding: BindingInstance, options?: Options): void;\n}\n\nexport interface DataModelMiddleware {\n /** The name of the middleware */\n name?: string;\n\n set(\n transaction: BatchSetTransaction,\n options?: DataModelOptions,\n next?: DataModelImpl,\n ): Updates;\n\n get(\n binding: BindingInstance,\n options?: DataModelOptions,\n next?: DataModelImpl,\n ): any;\n\n delete?(\n binding: BindingInstance,\n options?: DataModelOptions,\n next?: DataModelImpl,\n ): void;\n\n reset?(): void;\n}\n\n/** Wrap the inputs of the DataModel with calls to parse raw binding inputs */\nexport function withParser<Options = unknown>(\n model: DataModelImpl<Options>,\n parseBinding: BindingFactory,\n): DataModelWithParser<Options> {\n /** Parse something into a binding if it requires it */\n function maybeParse(\n binding: BindingLike,\n readOnly: boolean,\n ): BindingInstance {\n const parsed = isBinding(binding)\n ? binding\n : parseBinding(binding, {\n get: model.get,\n set: model.set,\n readOnly,\n });\n\n if (!parsed) {\n throw new Error(\"Unable to parse binding\");\n }\n\n return parsed;\n }\n\n return {\n get(binding, options?: Options) {\n return model.get(maybeParse(binding, true), options);\n },\n set(transaction, options?: Options) {\n return model.set(\n transaction.map(([key, val]) => [maybeParse(key, false), val]),\n options,\n );\n },\n delete(binding, options?: Options) {\n return model.delete(maybeParse(binding, false), options);\n },\n };\n}\n\n/** Wrap a middleware instance in a DataModel compliant API */\nexport function toModel(\n middleware: DataModelMiddleware,\n defaultOptions?: DataModelOptions,\n next?: DataModelImpl,\n): DataModelImpl {\n if (!next) {\n return middleware as DataModelImpl;\n }\n\n return {\n get: (binding: BindingInstance, options?: DataModelOptions) => {\n const resolvedOptions = options ?? defaultOptions;\n\n if (middleware.get) {\n return middleware.get(binding, resolvedOptions, next);\n }\n\n return next?.get(binding, resolvedOptions);\n },\n set: (transaction: BatchSetTransaction, options?: DataModelOptions) => {\n const resolvedOptions = options ?? defaultOptions;\n\n if (middleware.set) {\n return middleware.set(transaction, resolvedOptions, next);\n }\n\n return next?.set(transaction, resolvedOptions);\n },\n delete: (binding: BindingInstance, options?: DataModelOptions) => {\n const resolvedOptions = options ?? defaultOptions;\n\n if (middleware.delete) {\n return middleware.delete(binding, resolvedOptions, next);\n }\n\n return next?.delete(binding, resolvedOptions);\n },\n };\n}\n\nexport type DataPipeline = Array<DataModelMiddleware | DataModelImpl>;\n\n/**\n * Given a set of steps in a pipeline, create the effective data-model\n */\nexport function constructModelForPipeline(\n pipeline: DataPipeline,\n): DataModelImpl {\n if (pipeline.length === 0) {\n return NOOP_MODEL;\n }\n\n if (pipeline.length === 1) {\n return toModel(pipeline[0]);\n }\n\n /** Default and propagate the options into the nested calls */\n function createModelWithOptions(options?: DataModelOptions) {\n const model: DataModelImpl =\n pipeline.reduce<DataModelImpl | undefined>(\n (nextModel, middleware) => toModel(middleware, options, nextModel),\n undefined,\n ) ?? NOOP_MODEL;\n\n return model;\n }\n\n return {\n get: (binding: BindingInstance, options?: DataModelOptions) => {\n return createModelWithOptions(options)?.get(binding, options);\n },\n set: (transaction, options) => {\n return createModelWithOptions(options)?.set(transaction, options);\n },\n delete: (binding, options) => {\n return createModelWithOptions(options)?.delete(binding, options);\n },\n };\n}\n\n/** A DataModel that manages middleware data handlers */\nexport class PipelinedDataModel implements DataModelImpl {\n private pipeline: DataPipeline;\n private effectiveDataModel: DataModelImpl;\n\n public readonly hooks = {\n onSet: new SyncHook<[BatchSetTransaction]>(),\n };\n\n constructor(pipeline: DataPipeline = []) {\n this.pipeline = pipeline;\n this.effectiveDataModel = constructModelForPipeline(this.pipeline);\n }\n\n public setMiddleware(handlers: DataPipeline) {\n this.pipeline = handlers;\n this.effectiveDataModel = constructModelForPipeline(handlers);\n }\n\n public addMiddleware(handler: DataModelMiddleware) {\n this.pipeline = [...this.pipeline, handler];\n this.effectiveDataModel = constructModelForPipeline(this.pipeline);\n }\n\n public reset(model = {}) {\n this.pipeline.forEach((middleware) => {\n if (\"reset\" in middleware) {\n middleware.reset?.();\n }\n });\n\n this.set([[ROOT_BINDING, model]]);\n }\n\n public set(\n transaction: BatchSetTransaction,\n options?: DataModelOptions,\n ): Updates {\n const appliedTransaction = this.effectiveDataModel.set(\n transaction,\n options,\n );\n this.hooks.onSet.call(transaction);\n return appliedTransaction;\n }\n\n public get(binding: BindingInstance, options?: DataModelOptions): any {\n return this.effectiveDataModel.get(binding, options);\n }\n\n public delete(binding: BindingInstance, options?: DataModelOptions): void {\n return this.effectiveDataModel.delete(binding, options);\n }\n}\n","import type { DataModelImpl } from \"./model\";\n\n/**\n * A model that does nothing\n * Helpful for testing and other default DataModel applications\n */\nexport class NOOPDataModel implements DataModelImpl {\n get() {\n return undefined;\n }\n\n set() {\n return [];\n }\n\n delete() {}\n}\n\n/** You only really need 1 instance of the NOOP model */\nexport const NOOP_MODEL = new NOOPDataModel();\n","import get from \"dlv\";\nimport { setIn, omit, removeAt } from \"timm\";\nimport type { BindingInstance } from \"../binding\";\nimport type { BatchSetTransaction, DataModelImpl, Updates } from \"./model\";\n\n/**\n * A data model that stores data in an in-memory JS object\n */\nexport class LocalModel implements DataModelImpl {\n public model: {\n [key: string]: any;\n };\n\n constructor(model = {}) {\n this.model = model;\n this.get = this.get.bind(this);\n this.set = this.set.bind(this);\n }\n\n public reset(model = {}) {\n this.model = model;\n }\n\n public get(binding?: BindingInstance) {\n if (!binding || !binding.asString()) {\n return this.model;\n }\n\n return get(this.model, binding.asArray() as string[]);\n }\n\n public set(transaction: BatchSetTransaction) {\n const effectiveOperations: Updates = [];\n transaction.forEach(([binding, value]) => {\n const oldValue = this.get(binding);\n this.model = setIn(this.model, binding.asArray(), value) as any;\n effectiveOperations.push({ binding, oldValue, newValue: value });\n });\n return effectiveOperations;\n }\n\n public delete(binding: BindingInstance) {\n const parentBinding = binding.parent();\n\n if (parentBinding) {\n const parentValue = this.get(parentBinding);\n\n if (parentValue !== undefined) {\n if (Array.isArray(parentValue)) {\n this.model = setIn(\n this.model,\n parentBinding.asArray(),\n removeAt(parentValue, binding.key() as number),\n ) as any;\n } else {\n this.model = setIn(\n this.model,\n parentBinding.asArray(),\n omit(parentValue, binding.key() as string),\n ) as any;\n }\n }\n }\n }\n}\n","import { SyncWaterfallHook, SyncBailHook } from \"tapable-ts\";\nimport { NestedError } from \"ts-nested-error\";\nimport { parseExpression } from \"./parser\";\nimport * as DEFAULT_EXPRESSION_HANDLERS from \"./evaluator-functions\";\nimport { isExpressionNode } from \"./types\";\nimport { isObjectExpression } from \"./utils\";\nimport type {\n ExpressionNode,\n BinaryOperator,\n UnaryOperator,\n ExpressionType,\n ExpressionContext,\n ExpressionHandler,\n} from \"./types\";\n\n/** a && b -- but handles short cutting if the first value is false */\nconst andandOperator: BinaryOperator = (ctx, a, b) => {\n return ctx.evaluate(a) && ctx.evaluate(b);\n};\n\nandandOperator.resolveParams = false;\n\n/** a || b -- but with short cutting if first value is true */\nconst ororOperator: BinaryOperator = (ctx, a, b) => {\n return ctx.evaluate(a) || ctx.evaluate(b);\n};\n\nororOperator.resolveParams = false;\n\nconst DEFAULT_BINARY_OPERATORS: Record<string, BinaryOperator> = {\n // TODO: A lot of these functions used to do type coercion. Not sure if we want to keep that behavior or not.\n \"+\": (a: any, b: any) => a + b,\n \"-\": (a: any, b: any) => a - b,\n \"*\": (a: any, b: any) => a * b,\n \"/\": (a: any, b: any) => a / b,\n \"%\": (a: any, b: any) => a % b,\n\n // eslint-disable-next-line\n \"==\": (a: any, b: any) => a == b,\n\n // eslint-disable-next-line\n \"!=\": (a: any, b: any) => a != b,\n \">\": (a: any, b: any) => a > b,\n \">=\": (a: any, b: any) => a >= b,\n \"<\": (a: any, b: any) => a < b,\n \"<=\": (a: any, b: any) => a <= b,\n \"&&\": andandOperator,\n \"||\": ororOperator,\n \"!==\": (a: any, b: any) => a !== b,\n \"===\": (a: any, b: any) => a === b,\n\n // eslint-disable-next-line\n \"|\": (a: any, b: any) => a | b,\n\n // eslint-disable-next-line\n \"&\": (a: any, b: any) => a & b,\n \"+=\": (a: any, b: any) => a + b,\n \"-=\": (a: any, b: any) => a - b,\n\n // eslint-disable-next-line\n \"&=\": (a: any, b: any) => a & b,\n\n // eslint-disable-next-line\n \"|=\": (a: any, b: any) => a | b,\n};\n\nconst DEFAULT_UNARY_OPERATORS: Record<string, UnaryOperator> = {\n \"-\": (a: any) => -a,\n \"+\": (a: any) => Number(a),\n \"!\": (a: any) => !a,\n};\n\nexport interface HookOptions extends ExpressionContext {\n /** Given an expression node */\n resolveNode: (node: ExpressionNode) => any;\n\n /** Enabling this flag skips calling the onError hook, and just throws errors back to the caller.\n * The caller is responsible for handling the error.\n */\n throwErrors?: boolean;\n\n /** Whether expressions should be parsed strictly or not */\n strict?: boolean;\n}\n\nexport type ExpressionEvaluatorOptions = Omit<\n HookOptions,\n \"resolveNode\" | \"evaluate\"\n>;\n\nexport type ExpressionEvaluatorFunction = (\n exp: ExpressionType,\n options?: ExpressionEvaluatorOptions,\n) => any;\n\n/**\n * The expression evaluator is responsible for parsing and executing anything in the custom expression language\n * */\nexport class ExpressionEvaluator {\n private readonly vars