From 3fd67df2db8e182de18b4823020e16af1cb10264 Mon Sep 17 00:00:00 2001 From: Kyriakos Barbounakis Date: Wed, 22 Jan 2025 17:21:57 +0200 Subject: [PATCH] Merge value formatter (#197) * use value formatter for calculating values * validate $user dialect * 2.6.75 --- .eslintrc | 2 +- ValueFormatter.d.ts | 44 ++ ValueFormatter.js | 771 +++++++++++++++++++++ data-listeners.js | 33 +- index.d.ts | 1 + index.js | 6 +- model-schema.json | 726 ++++++++++++++++++- package-lock.json | 79 ++- package.json | 12 +- spec/DataObjectAssociationListener.spec.ts | 60 ++ spec/ValueFormatter.spec.ts | 439 ++++++++++++ spec/test2/config/models/Thing.json | 32 +- spec/test2/db/local.db | Bin 1089536 -> 1101824 bytes 13 files changed, 2161 insertions(+), 44 deletions(-) create mode 100644 ValueFormatter.d.ts create mode 100644 ValueFormatter.js create mode 100644 spec/ValueFormatter.spec.ts diff --git a/.eslintrc b/.eslintrc index c0e0751..3a85d71 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,7 +4,7 @@ "plugin:node/recommended" ], "parserOptions": { - "ecmaVersion": 2015, + "ecmaVersion": 2018, "sourceType": "module" }, "env": { diff --git a/ValueFormatter.d.ts b/ValueFormatter.d.ts new file mode 100644 index 0000000..e77fdf4 --- /dev/null +++ b/ValueFormatter.d.ts @@ -0,0 +1,44 @@ +import { DataModelBase } from "@themost/common"; +import { AsyncSeriesEventEmitter } from '@themost/events'; +import { DataContext } from "./types"; +import { DataModel } from './data-model'; + +export declare interface ResolvingVariable { + name: string; + model?: DataModelBase; + context?: DataContext; + target?: any; + value?: any; +} + + +/** + * Class responsible for formatting values within a given data context. + */ +export declare class ValueFormatter { + + readonly resolvingVariable: AsyncSeriesEventEmitter; + /** + * Creates an instance of ValueFormatter. + * @param context - The data context in which the formatter operates. + */ + constructor(context: DataContext, model?: DataModel, target?: any); + + /** + * Formats the given value according to the rules defined in the data context. + * @param value - The value to be formatted. + * @returns The formatted value. + */ + format(value: unknown): any; + + static register(name: string, definition: { [k: string]: (...value: any[]) => Promise }): void; +} + +export declare class ValueDialect { + /** + * Creates an instance of ValueDialect. + * @param context - The data context in which the dialect operates. + */ + constructor(context: DataContext, model: DataModel, target: any); + +} diff --git a/ValueFormatter.js b/ValueFormatter.js new file mode 100644 index 0000000..855048f --- /dev/null +++ b/ValueFormatter.js @@ -0,0 +1,771 @@ +const { Args, Guid, DataError } = require('@themost/common'); +const moment = require('moment'); +const { v4 } = require('uuid'); +const {isObjectDeep} = require('./is-object'); +const random = require('lodash/random'); +const getProperty = require('lodash/get'); +const esprima = require('esprima'); +require('@themost/promise-sequence'); +const { AsyncSeriesEventEmitter } = require('@themost/events'); +const { round } = require('@themost/query'); +const MD5 = require('crypto-js/md5'); +const { DataAttributeResolver } = require('./data-attribute-resolver'); + +const testFieldRegex = /^\$\w+(\.\w+)*$/g; + +/** + * @param {ValueFormatter} formatter + * @param {import('./data-model').DataModel} model + * @param {import('./data-queryable').DataQueryable} emitter + * @returns + */ +function getValueReplacer(formatter, model, emitter) { + return function(key, value) { + if (typeof value === 'string') { + if (/^\$\$/.test(value)) { + return formatter.formatVariableSync(value); + } + if (testFieldRegex.test(value)) { + const name = value.replace(/^\$/, ''); + const parts = name.split('.'); + if (parts.length === 1) { + const { viewAdapter: collection } = model; + const field = model.getAttribute(name); + if (field) { + return { + $name: collection + '.' + name + } + } + throw new DataError('An expression contains an attribute that cannot be found', null, model.name, name); + } else { + const result = new DataAttributeResolver().resolveNestedAttribute.bind(emitter)(name.replace(/\./g, '/')); + if (result) { + return result; + } + throw new DataError('An nested expression contains an attribute that cannot be found', null, model.name, name); + } + } + } + return value; + } +} + + +function getFunctionArguments(fn) { + if (typeof fn !== 'function') { + throw new Error('Invalid parameter. Expected function.'); + } + // if dialect function params are already cached + if (Object.prototype.hasOwnProperty.call(fn, 'dialectFunctionParams')) { + // return cached function params + return fn.dialectFunctionParams; + } + const fnString = fn.toString().trim(); + let ast; + if (/^function\s+/i.test(fnString) === false) { + if (/^async\s+/i.test(fnString)) { + ast = esprima.parseScript(fnString.replace(/^async\s+/, 'async function ')); + } else { + ast = esprima.parseScript('function ' + fnString); + } + } else { + ast = esprima.parseScript(fnString); + } + const params = ast.body[0].params.map((param) => param.name); + // cache function params + Object.defineProperty(fn, 'dialectFunctionParams', { + configurable: true, + enumerable: false, + writable: true, + value: params + }); + return params; +} + +class ValueDialect { + /** + * @param {import('./types').DataContext} context + * @param {*} target + */ + constructor(context, model, target) { + this.context = context; + this.model = model; + this.target = target; + } + + /** + * Get the current date and time + * @returns {Promise} + */ + async $date() { + return new Date(); + } + + /** + * Add the specified amount of time to the specified date + * @returns {Promise} + */ + async $dateAdd(startDate, unit, amount) { + return moment(startDate).add(amount, unit).toDate(); + } + + /** + * Add the specified amount of time to the specified date + * @returns {Promise} + */ + async $dateSubtract(startDate, unit, amount) { + return moment(startDate).subtract(amount, unit).toDate(); + } + + /** + * A shorthand for $date method + * @returns {Promise} + */ + async $now() { + return new Date(); + } + + /** + * Get the current date + * @returns {Promise} + */ + async $today() { + return moment(new Date()).startOf('day').toDate(); + } + + async $year(date) { + return moment(date).year(); + } + + async $month(date) { + return moment(date).month(); + } + + async $dayOfMonth(date) { + return moment(date).date(); + } + + async $dayOfWeek(date) { + return moment(date).day(); + } + + async $hour(date) { + return moment(date).hour(); + } + + async $minutes(date) { + return moment(date).minutes(); + } + + async $seconds(date) { + return moment(date).seconds(); + } + + /** + * Get current user identifier or the value of the specified attribute + * @param {string=} property + * @returns Promise + */ + $user(property) { + const selectAttribute = property || 'id'; + let name = this.context.user && this.context.user.name; + if (Object.prototype.hasOwnProperty.call(this.context, 'interactiveUser') === true) { + name = this.context.interactiveUser && this.context.interactiveUser.name; + } + return this.context.model('User').asQueryable().where((x, username) => { + return x.name === username && x.name != null && x.name != 'anonymous'; + }, name).select(selectAttribute).value().then((result) => { + if (typeof result === 'undefined') { + return null; + } + return result; + }); + } + /** + * A shorthand for $user method + * @param {string=} property + * @returns Promise + */ + $me(property) { + return this.$user(property); + } + + /** + * Get a new GUID value + * @returns {Promise} + */ + $newGuid() { + return Promise.resolve(v4().toString()); + } + + /** + * Get a new GUID value + * @returns {Promise} + */ + $uuid() { + return Promise.resolve(v4().toString()); + } + + /** + * Get a new identifier value for the current data model + * @returns {Promise} + */ + $newid() { + return new Promise((resolve, reject) => { + this.model.context.db.selectIdentity(this.model.sourceAdapter, this.model.primaryKey, (err, result) => { + if (err) { + return reject(err); + } + resolve(result); + }); + }); + } + + /** + * @param {...*} args + */ + $concat() { + return Promise.resolve(Array.from(arguments).join('')); + } + + /** + * @param {*} value + */ + $toLowerCase(value) { + return Promise.resolve(value == null ? null : String(value).toLowerCase()); + } + + $toLower(value) { + return this.$toLowerCase(value); + } + + /** + * @param {*} value + */ + $toUpperCase(value) { + return Promise.resolve(value == null ? null : String(value).toUpperCase()); + } + + $toUpper(value) { + return this.$toUpperCase(value); + } + + $length(value) { + return value == null ? 0 : value.length; + } + + $substring(value, start, length) { + return value == null ? null : value.substring(start, length); + } + + /** + * Returns a random string with the specified length + * @param {number=} length + * @returns + */ + $randomString(length) { + return new Promise((resolve, reject) => { + try { + length = length || 8; + var chars = 'abcdefghkmnopqursuvwxz2456789ABCDEFHJKLMNPQURSTUVWXYZ'; + var str = ''; + var rnd; + for(var i = 0; i < length; i++) { + rnd = random(0, chars.length - 1); + str += chars.substring(rnd, rnd + 1); + } + resolve(str); + } + catch (err) { + reject(err); + } + }); + } + + /** + * A shorthand for $randomString method + * @param {number=} length + * @returns {Promise} + */ + $chars(length) { + return this.$randomString(length); + } + + /** + * Returns a random integer value + * @param {number=} min + * @param {number=} max + * @returns + */ + $randomInt(min, max) { + return new Promise((resolve, reject) =>{ + try { + resolve(random(min, max)); + } + catch (err) { + reject(err); + } + }); + } + + /** + * + * @param {*} min + * @param {*} max + * @returns + */ + $int(min, max) { + return this.$randomInt(min, max); + } + + $abs(value) { + return value != null ? Math.abs(value) : null; + } + + /** + * Returns a random string containing only numbers with the specified length + * @param {number=} length + * @returns + */ + $randomNumbers(length) { + return new Promise((resolve, reject) => { + try { + length = length || 8; + var chars = '0123456789'; + var str = ''; + var rnd; + for(var i = 0; i < length; i++) { + rnd = random(0, chars.length - 1); + str += chars.substring(rnd, rnd + 1); + } + resolve(str); + } + catch (err) { + reject(err); + } + }); + } + + /** + * A shorthand for $randomNumbers method + * @param {number=} length + * @returns + */ + $numbers(length) { + return this.$randomNumbers(length); + } + + /** + * Returns a random password with the specified length + * @param {number} length + */ + $randomPassword(length) { + return new Promise((resolve, reject) => { + try { + length = length || 16; + const chars = 'abcdefghkmnopqursuvwxz2456789ABCDEFHJKLMNPQURTUVWXYZ'; + const numberChars = '0123456789'; + let requiredNumberChars = length < 8 ? 1 : 2; + const specialChars = '!@#$%^&*()_+'; + let requiredSpecialChars = length < 8 ? 1 : 2; + let str = ''; + let rnd; + for(var i = 0; i < length; i++) { + if (requiredNumberChars > 0 && random(0, 1) === 1) { + rnd = random(0, numberChars.length - 1); + str += numberChars.substring(rnd, rnd + 1); + requiredNumberChars--; + continue; + } + if (requiredSpecialChars > 0 && random(0, 1) === 1) { + rnd = random(0, specialChars.length - 1); + str += specialChars.substring(rnd, rnd + 1); + requiredSpecialChars--; + continue; + } + rnd = random(0, chars.length - 1); + str += chars.substring(rnd, rnd + 1); + } + resolve(str); + } + catch (err) { + reject(err); + } + }); + } + + /** + * A shorthand for $randomPassword method + * @param {number=} length + * @returns + */ + $password(length) { + return this.$randomPassword(length); + } + + /** + * Rounds the specified value to the nearest integer + * @param {*} value The value to round + * @param {*} place The number of decimal places to round + * @returns + */ + async $round(value, place) { + return round(value, place); + } + + /** + * Converts the specified value to a number and returns the absolute value + * @param {*} value + * @returns + */ + async $ceil(value) { + return Math.ceil(value); + } + + /** + * Converts the specified value to a number and returns the lowest integer value + * @param {*} value + * @returns + */ + async $floor(value) { + return Math.floor(value); + } + + async $add() { + return Array.from(arguments).reduce((a, b) => a + b, 0); + } + + async $subtract() { + return Array.from(arguments).reduce((a, b) => a - b, 0); + } + + async $multiply() { + return Array.from(arguments).reduce((a, b) => a * b, 1); + } + + async $divide() { + return Array.from(arguments).reduce((a, b) => a / b, 1); + } + + async $mod() { + return Array.from(arguments).reduce((a, b) => a % b, 0); + } + + async $toString(value) { + if (value == null) { + return null; + } + return String(value).toString(); + } + + async $trim(value) { + if (value == null) { + return null; + } + return String(value).trim(); + } + + async $toInt(value) { + return parseInt(value, 10); + } + + async $toDouble(value) { + if (typeof value === 'number') { + return Promise.resolve(value); + } + return parseFloat(value); + } + + async $toDecimal(value) { + if (typeof value === 'number') { + return Promise.resolve(value); + } + return parseFloat(value); + } + + /** + * Converts the specified value to a UUID + * @param {*} value + * @returns + */ + async $toGuid(value) { + if (Guid.isGuid(value)) { + return Promise.resolve(value); + } + var str = MD5(value).toString(); + return new Guid([ + str.substring(0, 8), + str.substring(8, 12), + str.substring(12, 16), + str.substring(16, 20), + str.substring(20, 32) + ].join('-')); + } + + /** + * A shorthand for $toGuid method + * @param {*} value + * @returns + */ + async $toUUID(value) { + return this.$toGuid(value); + } + + async $eq() { + const [a,b] = Array.from(arguments); + return a === b; + } + + async $gt() { + const [a,b] = Array.from(arguments); + return a > b; + } + + async $lt() { + const [a,b] = Array.from(arguments); + return a < b; + } + + async $gte() { + const [a,b] = Array.from(arguments); + return a >= b; + } + + async $lte() { + const [a,b] = Array.from(arguments); + return a <= b; + } + + async $ne() { + const [a,b] = Array.from(arguments); + return a !== b; + } + + async $or() { + return Array.from(arguments).reduce((a, b) => a || b, false); + } + + async $and() { + return Array.from(arguments).reduce((a, b) => a && b, true); + } + + async $cond(ifExpr, thenExpr, elseExpr) { + return ifExpr ? thenExpr : elseExpr; + } + + async $replaceOne(input, find, replacement) { + if (input == null) { + return null; + } + return input.replace(find, replacement); + } + + /** + * @param {string} input + * @param {string} find + * @param {string} replacement + */ + async $replaceAll(input, find, replacement) { + if (input == null) { + return null; + } + return input.replaceAll(new RegExp(find, 'g'), replacement); + } + +} + +class ValueFormatter { + + /** + * + * @param {import('./types').DataContext} context + * @param {import('@themost/common').DataModelBase=} model + * @param {*=} target + */ + constructor(context, model, target) { + this.context = context; + this.model = model; + this.target = target; + this.dialect = new ValueDialect(context, model, target); + this.resolvingVariable = new AsyncSeriesEventEmitter(); + } + + /** + * @param {string} value + * @returns Promise + */ + async formatVariable(value) { + const propertyPath = value.substring(2).split('.'); + const property = propertyPath.shift(); + if (Object.prototype.hasOwnProperty.call(this.dialect, property)) { + return getProperty(this.dialect[property], propertyPath.join('.')); + } else { + const event = { + name: value, + model: this.model, + context: this.context, + target: this.target + } + await this.resolvingVariable.emit('resolve', event); + if (Object.prototype.hasOwnProperty.call(event, 'value')) { + return event.value; + } + throw new Error(`Variable '${property}' not found.`); + } + } + + formatVariableSync(value) { + const propertyPath = value.substring(2).split('.'); + const property = propertyPath.shift(); + if (Object.prototype.hasOwnProperty.call(this.dialect, property)) { + return getProperty(this.dialect[property], propertyPath.join('.')); + } else { + throw new Error(`Variable '${property}' not found.`); + } + } + + /** + * + * @param {{$collection: string, $select: { value: string }, $where: *, $sort: *=, $order: Array<*>=, $group: Array<*>=}} query + */ + async formatQuery(query) { + const model = this.context.model(query.$collection); + const q = model.asQueryable(); + if (Object.prototype.hasOwnProperty.call(query, '$select') === false) { + throw new Error('Query expression $select statement not found.'); + } + if (Object.prototype.hasOwnProperty.call(query.$select, 'value') === false) { + throw new Error('Query expression $select statement should a value property.'); + } + // use select expression + // get value property + const { value: attribute } = query.$select; + // parse select expression + q.select(attribute.replace(/^\$/, '').replace(/^\./, '/')); + if (Object.prototype.hasOwnProperty.call(query, '$where') === false) { + throw new Error('Query expression $where statement not found.'); + } + /** + * @returns {function(string, *)} + */ + const nameReplacer = getValueReplacer(this, model, q); + const $where = JSON.parse(JSON.stringify(query.$where, function(key, value) { + return nameReplacer(key, value); + })); + Object.assign(q.query, { + $where + }); + if (Array.isArray(query.$order)) { + const $order = JSON.parse(JSON.stringify(query.$order, function(key, value) { + return nameReplacer(key, value); + })); + Object.assign(q.query, { + $order + }); + } + if (Array.isArray(query.$group)) { + const $group = JSON.parse(JSON.stringify(query.$group, function(key, value) { + return nameReplacer(key, value); + })); + Object.assign(q.query, { + $group + }); + } + return q.value(); + } + + /** + * @param {*} value + * @returns Promise + */ + format(value) { + if (isObjectDeep(value) === false) { + if (typeof value === 'string' && value.startsWith('$$')) { + return this.formatVariable(value); + } + return Promise.resolve(value); + } + // get property + const [property] = Object.keys(value); + // check if method is $value e.g. $value: 'Hello World' + if (property === '$value') { + const val = value[property]; + if (typeof val === 'string' && val.startsWith('$$')) { + return this.formatVariable(val); + } + return Promise.resolve(this.format(value)); + } + if (property.startsWith('$$')) { + return this.formatVariable(value); + } + // exception $cond method which is a special case of formatting method + if (property === '$cond') { + // use language keywords if, then, else + const cond = value[property]; + if (Object.prototype.hasOwnProperty.call(cond, 'if') && Object.prototype.hasOwnProperty.call(cond, 'then') && Object.prototype.hasOwnProperty.call(cond, 'else')) { + return Promise.all([ + this.format(cond.if), + this.format(cond.then), + this.format(cond.else) + ]).then(([ifExpr, thenExpr, elseExpr]) => { + return this.dialect.$cond(ifExpr, thenExpr, elseExpr); + }); + } + } + + if (property === '$query') { + return this.formatQuery(value[property]); + } + + // check if method exists + const propertyDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this.dialect), property); + if (propertyDescriptor) { + Args.check(propertyDescriptor.value instanceof Function, 'Dialect method must be a function.'); + // get arguments + const args = value[property]; + if (args == null) { + return Promise.resolve(null); + } + if (Array.isArray(args)) { + return Promise.sequence(args.map((arg) => { + return () => this.format(arg); + })).then((args) => { + // call dialect method + const invoke = propertyDescriptor.value; + return invoke.apply(this.dialect, args); + }); + } else { + const params = getFunctionArguments(propertyDescriptor.value); + return Promise.sequence(params.map((param) => { + if (Object.prototype.hasOwnProperty.call(args, param)) { + return args[param]; + } + return null; + }).map((arg) => { + return () => this.format(arg); + })).then((args) => { + // call dialect method + const invoke = propertyDescriptor.value; + return invoke.apply(this.dialect, args); + }); + } + + } else { + Promise.reject(new Error(`Dialect method '${property}' not found.`)); + } + } + + /** + * @param {*} name + * @param {*} definition + */ + static register(name, definition) { + Object.assign(ValueDialect.prototype, definition) + } + +} +//** @ts-ignore **/ +module.exports = { + ValueDialect, + ValueFormatter +}; \ No newline at end of file diff --git a/data-listeners.js b/data-listeners.js index b28846b..74254a2 100644 --- a/data-listeners.js +++ b/data-listeners.js @@ -13,6 +13,8 @@ var {TextUtils} = require('@themost/common'); var {DataCacheStrategy} = require('./data-cache'); var {DataFieldQueryResolver} = require('./data-field-query-resolver'); var {FunctionContext} = require('./functions'); +var {isObjectDeep} = require('./is-object'); +var {ValueFormatter} = require('./ValueFormatter'); /** * @classdesc Represents an event listener for validating not nullable fields. This listener is automatically registered in all data models. @@ -228,8 +230,19 @@ CalculatedValueListener.prototype.beforeSave = function(event, callback) { functionContext.context = event.model.context; //find all attributes that have a default value var attrs = event.model.attributes.filter(function(x) { return (x.calculation!==undefined); }); + // create an instance of ValueFormatter + var valueFormatter = new ValueFormatter(event.model.context, event.model, event.target); async.eachSeries(attrs, function(attr, cb) { var expr = attr.calculation; + // use value formatter for object-like expressions + if (isObjectDeep(expr)) { + return valueFormatter.format(expr).then(function(result) { + event.target[attr.name] = result; + void cb(); + }).catch(function(err) { + void cb(err); + }); + } //validate expression if (typeof expr !== 'string') { event.target[attr.name] = expr; @@ -559,14 +572,24 @@ DefaultValueListener.prototype.beforeSave = function(event, callback) { _.assign(functionContext, event); //find all attributes that have a default value var attrs = event.model.attributes.filter(function(x) { return (typeof x.value!== 'undefined'); }); + // create an instance of ValueFormatter + var valueFormatter = new ValueFormatter(event.model.context, event.model, event.target); async.eachSeries(attrs, function(attr, cb) { try { var expr = attr.value; - //if attribute is already defined - if (typeof event.target[attr.name] !== 'undefined') { - //do nothing - cb(null); - return; + // if attribute is already defined + if (Object.prototype.hasOwnProperty.call(event.target, attr.name)) { + // do nothing + return cb(); + } + // use value formatter for object-like expressions + if (isObjectDeep(expr)) { + return valueFormatter.format(expr).then(function(result) { + event.target[attr.name] = result; + void cb(); + }).catch(function(err) { + void cb(err); + }); } //validate expression if (typeof expr !== 'string') { diff --git a/index.d.ts b/index.d.ts index e4b34ef..d6f42e4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -21,3 +21,4 @@ export * from './data-associations'; export * from './data-application'; export * from './data-errors'; export * from './UnattendedMode'; +export * from './ValueFormatter'; diff --git a/index.js b/index.js index 61223a6..878955a 100644 --- a/index.js +++ b/index.js @@ -89,6 +89,8 @@ var { UnknownAttributeError } = require('./data-errors'); var { executeInUnattendedModeAsync, executeInUnattendedMode, enableUnattendedExecution, disableUnattendedExecution } = require('./UnattendedMode'); +var { ValueFormatter, ValueDialect } = require('./ValueFormatter'); + module.exports = { TypeParser, PrivilegeType, @@ -174,6 +176,8 @@ module.exports = { enableUnattendedExecution, disableUnattendedExecution, executeInUnattendedMode, - executeInUnattendedModeAsync + executeInUnattendedModeAsync, + ValueFormatter, + ValueDialect }; diff --git a/model-schema.json b/model-schema.json index 47a3db5..5aabc03 100644 --- a/model-schema.json +++ b/model-schema.json @@ -16,6 +16,678 @@ "not": { "$ref": "#/definitions/types" } + }, + "stringFunction": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field1" + } + }, + "required": [ + "value" + ], + "default": { + "value": "$field1" + }, + "additionalProperties": false + }, + "anyFunctionWithValueParam": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field" + } + }, + "required": [ + "value" + ], + "default": { + "value": "$field" + }, + "additionalProperties": false + }, + "comparisonFunction": { + "type": "array", + "items": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ] + }, + "minLength": 2, + "maxLength": 2, + "default": [ + "$field1", + "$field2" + ], + "additionalProperties": false + }, + "anyFunctionWithParams": { + "type": "array", + "items": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ] + }, + "default": [ + "$field1", + "$field2" + ] + }, + "logicalFunction": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/comparisonFunction" + } + } + }, + { + "type": "object", + "properties": { + "$ne": { + "$ref": "#/definitions/comparisonFunction" + } + } + }, + { + "type": "object", + "properties": { + "$gt": { + "$ref": "#/definitions/comparisonFunction" + } + } + }, + { + "type": "object", + "properties": { + "$gte": { + "$ref": "#/definitions/comparisonFunction" + } + } + }, + { + "type": "object", + "properties": { + "$lt": { + "$ref": "#/definitions/comparisonFunction" + } + } + }, + { + "type": "object", + "properties": { + "$lte": { + "$ref": "#/definitions/comparisonFunction" + } + } + } + ] + }, + "minLength": 2, + "default": [ + { + "$eq": [ + "$field1", + "$field2" + ] + }, + { + "$eq": [ + "$field3", + "$field4" + ] + } + ], + "additionalProperties": false + }, + "dateFunctionWithValueParam": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$dateField1" + } + }, + "required": [ + "value" + ], + "default": { + "value": "$dateField1" + }, + "additionalProperties": false + }, + "valueType": { + "type": "object", + "properties": { + "$date": { + "type": "number", + "default": 1 + }, + "$now": { + "type": "number", + "default": 1 + }, + "$today": { + "type": "number", + "default": 1 + }, + "$year": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$month": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$dayOfMonth": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$hour": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$minutes": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$seconds": { + "$ref": "#/definitions/dateFunctionWithValueParam" + }, + "$dateAdd": { + "type": "object", + "properties": { + "startDate": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$dateField" + }, + "unit": { + "type": "string", + "default": "day" + }, + "amount": { + "type": "number", + "default": 1 + } + }, + "required": [ + "startDate", "unit", "amount" + ], + "default": { + "startDate": "$dateField", + "unit": "day", + "amount": 1 + }, + "additionalProperties": false + }, + "$dateSubtract": { + "type": "object", + "properties": { + "startDate": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$dateField" + }, + "unit": { + "type": "string", + "default": "day" + }, + "amount": { + "type": "number", + "default": 1 + } + }, + "required": [ + "startDate", "unit", "amount" + ], + "default": { + "startDate": "$dateField", + "unit": "day", + "amount": 1 + }, + "additionalProperties": false + }, + "$user": { + "oneOf": [ + { + "type": "number", + "default": 1 + }, + { + "type": "object", + "properties": { + "property": { + "type": "string", + "default": "name" + } + } + } + ] + }, + "$me": { + "oneOf": [ + { + "type": "number", + "default": 1 + }, + { + "type": "object", + "properties": { + "property": { + "type": "string", + "default": "name" + } + } + } + ] + }, + "$randomString": { + "type": "object", + "properties": { + "length": { + "type": "number", + "default": 8 + } + }, + "required": [ + "length" + ], + "default": { + "length": 8 + }, + "additionalProperties": false + }, + "$chars": { + "type": "object", + "properties": { + "length": { + "type": "number", + "default": 8 + } + }, + "required": [ + "length" + ], + "default": { + "length": 8 + }, + "additionalProperties": false + }, + "$randomNumber": { + "type": "object", + "properties": { + "length": { + "type": "number", + "default": 8 + } + }, + "required": [ + "length" + ], + "default": { + "length": 8 + }, + "additionalProperties": false + }, + "$randomPassword": { + "type": "object", + "properties": { + "length": { + "type": "number", + "default": 8 + } + }, + "required": [ + "length" + ], + "default": { + "length": 8 + }, + "additionalProperties": false + }, + "$randomInt": { + "type": "object", + "properties": { + "min": { + "type": "number", + "default": 0 + }, + "max": { + "type": "number", + "default": 100 + } + }, + "required": [ + "min", "max" + ], + "default": { + "min": 0, + "max": 100 + }, + "additionalProperties": false + }, + "$int": { + "type": "object", + "properties": { + "min": { + "type": "number", + "default": 0 + }, + "max": { + "type": "number", + "default": 100 + } + }, + "required": [ + "min", "max" + ], + "default": { + "min": 0, + "max": 100 + }, + "additionalProperties": false + }, + "$query": { + "type": "object", + "properties": { + "$collection": { + "type": "string" + }, + "$select": { + "type": "object" + }, + "$where": { + "type": "object" + }, + "$order": { + "type": "object" + } + } + }, + "$newGuid": { + "type": "number", + "default": 1 + }, + "$uuid": { + "type": "number", + "default": 1 + }, + "$newId": { + "type": "number", + "default": 1 + }, + "$concat": { + "type": "array", + "items": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ] + }, + "default": [ + "$stringField1", + "$stringField2" + ] + }, + "$toLower": { + "$ref": "#/definitions/stringFunction" + }, + "$toLowerCase": { + "$ref": "#/definitions/stringFunction" + }, + "$toUpper": { + "$ref": "#/definitions/stringFunction" + }, + "$toUpperCase": { + "$ref": "#/definitions/stringFunction" + }, + "$trim": { + "$ref": "#/definitions/stringFunction" + }, + "$length": { + "$ref": "#/definitions/stringFunction" + }, + "$substring": { + "type": "object", + "properties": { + "value": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field" + }, + "start": { + "type": "number", + "default": 0 + }, + "length": { + "type": "number", + "default": 1 + } + }, + "required": [ + "value" + ], + "default": { + "value": "$field", + "start": 0, + "length": 1 + }, + "additionalProperties": false + }, + "$abs": { + "$ref": "#/definitions/anyFunctionWithValueParam" + }, + "$ceil": { + "$ref": "#/definitions/anyFunctionWithValueParam" + }, + "$floor": { + "$ref": "#/definitions/anyFunctionWithValueParam" + }, + "$add": { + "$ref": "#/definitions/anyFunctionWithParams" + }, + "$subtract": { + "$ref": "#/definitions/anyFunctionWithParams" + }, + "$multiply": { + "$ref": "#/definitions/anyFunctionWithParams" + }, + "$divide": { + "$ref": "#/definitions/anyFunctionWithParams" + }, + "$mod": { + "$ref": "#/definitions/anyFunctionWithParams" + }, + "$round": { + "type": "object", + "properties": { + "value": { + "$ref": "#/definitions/valueType", + "default": "$field1" + }, + "place": { + "type": "number", + "default": 2 + } + }, + "required": [ + "value", "place" + ], + "default": { + "value": "$field1", + "place": 2 + }, + "additionalProperties": false + }, + "$toString": { + "$ref": "#/definitions/stringFunction" + }, + "$toGuid":{ + "$ref": "#/definitions/stringFunction" + }, + "$toUUID":{ + "$ref": "#/definitions/stringFunction" + }, + "$toInt":{ + "$ref": "#/definitions/stringFunction" + }, + "$toDouble":{ + "$ref": "#/definitions/stringFunction" + }, + "$toDecimal":{ + "$ref": "#/definitions/stringFunction" + }, + "$eq": { + "$ref": "#/definitions/comparisonFunction" + }, + "$ne": { + "$ref": "#/definitions/comparisonFunction" + }, + "$gt": { + "$ref": "#/definitions/comparisonFunction" + }, + "$gte": { + "$ref": "#/definitions/comparisonFunction" + }, + "$lt": { + "$ref": "#/definitions/comparisonFunction" + }, + "$lte": { + "$ref": "#/definitions/comparisonFunction" + }, + "$and": { + "$ref": "#/definitions/logicalFunction" + }, + "$or": { + "$ref": "#/definitions/logicalFunction" + }, + "$cond": { + "type": "object", + "properties": { + "if": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field" + }, + "then": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": true + }, + "else": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": false + } + }, + "required": [ + "if", "then", "else" + ], + "default": { + "if": "$field", + "start": 0, + "length": 1 + }, + "additionalProperties": false + }, + "$replaceAll": { + "type": "object", + "properties": { + "input": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field" + }, + "find": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "" + }, + "replacement": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "" + } + }, + "required": [ + "input", "find", "replacement" + ], + "default": { + "input": "$field", + "find": "", + "replacement": "" + }, + "additionalProperties": false + }, + "$replaceOne": { + "type": "object", + "properties": { + "input": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "$field" + }, + "find": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "" + }, + "replacement": { + "anyOf": [ + { "type": "string" }, + { "$ref": "#/definitions/valueType" } + ], + "default": "" + } + }, + "required": [ + "input", "find", "replacement" + ], + "default": { + "input": "$field", + "find": "", + "replacement": "" + }, + "additionalProperties": false + } + }, + "additionalProperties": true } }, "properties": { @@ -133,20 +805,39 @@ "description": "A boolean which indicates whether this attribute is a key column or not." }, "value": { - "type": "string", - "description": "An expression which represents the default value for this attribute." + "description": "An expression which represents the default value for this attribute.", + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/valueType" + } + ] }, "calculation": { - "type": "string", - "description": "An expression which represents the calculated value for this attribute." + "description": "An expression which represents the calculated value for this attribute.", + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/valueType" + } + ] }, "readonly": { "default": false, "type": "boolean", "description": "A boolean which indicates whether this attribute is readonly or not. A readonly value must have a default value or a calculated value." }, + "insertable": { + "default": true, + "type": "boolean", + "description": "A boolean which indicates whether this attribute is insertable or not." + }, "editable": { - "default": false, + "default": true, "type": "boolean", "description": "A boolean which indicates whether this attribute is editable or not." }, @@ -212,7 +903,9 @@ "type": "string", "enum": [ "delete", - "none" + "none", + "null", + "default" ] }, "options": { @@ -265,6 +958,13 @@ "filter": { "type": "string", "description": "A string which represents a filter expression for this privilege. This attribute is used for self privileges which are commonly derived from user's attributes e.g. 'owner eq me()' or 'orderStatus eq 1 and customer eq me()' etc." + }, + "scope": { + "type": "array", + "items": { + "type":"string" + }, + "description": "An array of OAuth2 client scopes as described here https://oauth.net/2/scope/. If current context does not have any of the provided scopes this privilege will be excluded. This option may be used in OAuth2 authorized environments or in any environment which implements such protocols." } }, "required": [ @@ -554,6 +1254,13 @@ "filter": { "type": "string", "description": "A string which represents a filter expression for this privilege. This attribute is used for self privileges which are commonly derived from user's attributes e.g. 'owner eq me()' or 'orderStatus eq 1 and customer eq me()' etc." + }, + "scope": { + "type": "array", + "items": { + "type":"string" + }, + "description": "An array of OAuth2 client scopes as described here https://oauth.net/2/scope/. If current context does not have any of the provided scopes this privilege will be excluded. This option may be used in OAuth2 authorized environments or in any environment which implements such protocols." } }, "required": [ @@ -597,6 +1304,13 @@ "filter": { "type": "string", "description": "A string which represents a filter expression for this privilege. This attribute is used for self privileges which are commonly derived from user's attributes e.g. 'owner eq me()' or 'orderStatus eq 1 and customer eq me()' etc." + }, + "scope": { + "type": "array", + "items": { + "type":"string" + }, + "description": "An array of OAuth2 client scopes as described here https://oauth.net/2/scope/. If current context does not have any of the provided scopes this privilege will be excluded. This option may be used in OAuth2 authorized environments or in any environment which implements such protocols." } }, "required": [ diff --git a/package-lock.json b/package-lock.json index 630b1c9..315008c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,41 @@ { "name": "@themost/data", - "version": "2.6.74", + "version": "2.6.75", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@themost/data", - "version": "2.6.74", + "version": "2.6.75", "license": "BSD-3-Clause", "dependencies": { "@themost/events": "^1.3.0", "@themost/promise-sequence": "^1.0.1", "ajv": "^8.17.1", "async": "^2.6.4", + "crypto-js": "^4.2.0", + "esprima": "^4.0.1", "lodash": "^4.17.21", + "moment": "^2.30.1", "node-cache": "^4.2.1", "pluralize": "^7.0.0", "q": "^1.4.1", "sprintf-js": "^1.1.2", - "symbol": "^0.3.1" + "symbol": "^0.3.1", + "uuid": "^11.0.5" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.17.2", "@babel/preset-env": "^7.23.8", "@babel/preset-typescript": "^7.16.7", - "@themost/common": "^2.5.11", + "@themost/common": "^2.10.4", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", "@themost/query": "^2.6.73", "@themost/sqlite": "^2.8.4", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", + "@types/crypto-js": "^4.2.2", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.149", "@types/node": "^10.12.0", @@ -43,7 +48,6 @@ "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", "jest-standard-reporter": "^2.0.0", - "moment": "^2.30.1", "sql.js": "^1.4.0", "ts-node": "^9.1.1", "typescript": "^4.2.3" @@ -5174,10 +5178,11 @@ } }, "node_modules/@themost/common": { - "version": "2.5.11", - "resolved": "https://registry.npmjs.org/@themost/common/-/common-2.5.11.tgz", - "integrity": "sha512-n+e/fGodPeDSpDkMxOZHeoUR7ocOhEttBB7mydAiaLm4o5wsD1mLYRc+CJDJjgFb2wiznB4KUZ6oG+fGbn8urw==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@themost/common/-/common-2.10.4.tgz", + "integrity": "sha512-uR2qQfc/GbzgnNjaZ2HaGQ0QRCRMsIY6HdNyLTqWR3Oyg4BSfXzv7Yod/Y96KsjpV+6YAD0bCyc/kuFIGXY+WQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "async": "^2.6.4", "blueimp-md5": "^2.7.0", @@ -5380,6 +5385,13 @@ "integrity": "sha512-C4vwOHrhsvxn7UFyk4NDQNUpgNKdWsT/bL39UWyD75KSEOObZSKa9mYDOCM5FGeJG2qtbG0XiEbUKND2+j0WOg==", "dev": true }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/emscripten": { "version": "1.39.6", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", @@ -6407,6 +6419,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/date-and-time": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.1.1.tgz", @@ -7022,7 +7040,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -10657,7 +10675,7 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "dev": true, + "license": "MIT", "engines": { "node": "*" } @@ -12186,6 +12204,19 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -16199,9 +16230,9 @@ } }, "@themost/common": { - "version": "2.5.11", - "resolved": "https://registry.npmjs.org/@themost/common/-/common-2.5.11.tgz", - "integrity": "sha512-n+e/fGodPeDSpDkMxOZHeoUR7ocOhEttBB7mydAiaLm4o5wsD1mLYRc+CJDJjgFb2wiznB4KUZ6oG+fGbn8urw==", + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/@themost/common/-/common-2.10.4.tgz", + "integrity": "sha512-uR2qQfc/GbzgnNjaZ2HaGQ0QRCRMsIY6HdNyLTqWR3Oyg4BSfXzv7Yod/Y96KsjpV+6YAD0bCyc/kuFIGXY+WQ==", "dev": true, "requires": { "async": "^2.6.4", @@ -16369,6 +16400,12 @@ "integrity": "sha512-C4vwOHrhsvxn7UFyk4NDQNUpgNKdWsT/bL39UWyD75KSEOObZSKa9mYDOCM5FGeJG2qtbG0XiEbUKND2+j0WOg==", "dev": true }, + "@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "@types/emscripten": { "version": "1.39.6", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", @@ -17158,6 +17195,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "date-and-time": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.1.1.tgz", @@ -17610,8 +17652,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.4.0", @@ -20362,8 +20403,7 @@ "moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "dev": true + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" }, "ms": { "version": "2.1.2", @@ -21482,6 +21522,11 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==" + }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index b5f1471..998320f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@themost/data", - "version": "2.6.74", + "version": "2.6.75", "description": "MOST Web Framework Codename Blueshift - Data module", "main": "index.js", "types": "index.d.ts", @@ -30,24 +30,29 @@ "@themost/promise-sequence": "^1.0.1", "ajv": "^8.17.1", "async": "^2.6.4", + "crypto-js": "^4.2.0", + "esprima": "^4.0.1", "lodash": "^4.17.21", + "moment": "^2.30.1", "node-cache": "^4.2.1", "pluralize": "^7.0.0", "q": "^1.4.1", "sprintf-js": "^1.1.2", - "symbol": "^0.3.1" + "symbol": "^0.3.1", + "uuid": "^11.0.5" }, "devDependencies": { "@babel/plugin-proposal-decorators": "^7.17.2", "@babel/preset-env": "^7.23.8", "@babel/preset-typescript": "^7.16.7", - "@themost/common": "^2.5.11", + "@themost/common": "^2.10.4", "@themost/json-logger": "^1.1.0", "@themost/peers": "^1.0.2", "@themost/query": "^2.6.73", "@themost/sqlite": "^2.8.4", "@themost/xml": "^2.5.2", "@types/core-js": "^2.5.0", + "@types/crypto-js": "^4.2.2", "@types/jest": "^27.4.1", "@types/lodash": "^4.14.149", "@types/node": "^10.12.0", @@ -60,7 +65,6 @@ "eslint-plugin-node": "^11.1.0", "jest": "^29.7.0", "jest-standard-reporter": "^2.0.0", - "moment": "^2.30.1", "sql.js": "^1.4.0", "ts-node": "^9.1.1", "typescript": "^4.2.3" diff --git a/spec/DataObjectAssociationListener.spec.ts b/spec/DataObjectAssociationListener.spec.ts index 1d4eb4e..19ee0dc 100644 --- a/spec/DataObjectAssociationListener.spec.ts +++ b/spec/DataObjectAssociationListener.spec.ts @@ -341,4 +341,64 @@ describe('DataObjectAssociationListener', () => { }); }); + it('should validate foreign key constraint while inserting object', async () => { + await TestUtils.executeInTransaction(context, async () => { + const product = await context.model('Product').asQueryable().where((x: any) => x.name === 'Samsung Galaxy S4').getItem(); + expect(product).toBeTruthy(); + let newOffer: any = { + itemOffered: -400, + price: 999, + validFrom: new Date('2021-12-20'), + validThrough: new Date('2021-12-31') + } + try { + await context.model('Offer').silent().save(newOffer); + throw new Error('An error is expected'); + } catch (error) { + expect(error).toBeInstanceOf(DataObjectAssociationError); + expect(error.model).toEqual('Offer'); + expect(error.field).toEqual('itemOffered'); + } + }); + }); + + it('should validate foreign key constraint while updating object', async () => { + await TestUtils.executeInTransaction(context, async () => { + const product = await context.model('Product').asQueryable().where((x: any) => x.name === 'Samsung Galaxy S4').getItem(); + expect(product).toBeTruthy(); + let newOffer: any = { + itemOffered: product, + price: 999, + validFrom: new Date('2021-12-20'), + validThrough: new Date('2021-12-31') + } + await context.model('Offer').silent().save(newOffer); + newOffer.offeredBy = -400; + try { + await context.model('Offer').silent().save(newOffer); + throw new Error('An error is expected'); + } catch (error) { + expect(error).toBeInstanceOf(DataObjectAssociationError); + expect(error.model).toEqual('Offer'); + expect(error.field).toEqual('offeredBy'); + } + }); + }); + + it('should validate foreign key constraint of readonly fields without calculating values', async () => { + await TestUtils.executeInTransaction(context, async () => { + const product = await context.model('Product').asQueryable().where((x: any) => x.name === 'Samsung Galaxy S4').getItem(); + expect(product).toBeTruthy(); + product.createdBy = -400; + try { + await context.model('Product').silent().save(product); + throw new Error('An error is expected'); + } catch (error) { + expect(error).toBeInstanceOf(DataObjectAssociationError); + expect(error.model).toEqual('Thing'); + expect(error.field).toEqual('createdBy'); + } + }); + }); + }); diff --git a/spec/ValueFormatter.spec.ts b/spec/ValueFormatter.spec.ts new file mode 100644 index 0000000..5a38912 --- /dev/null +++ b/spec/ValueFormatter.spec.ts @@ -0,0 +1,439 @@ +import {TestApplication} from './TestApplication'; +import { DataContext, executeInUnattendedMode, executeInUnattendedModeAsync, ValueFormatter, ValueDialect } from '@themost/data'; +import moment from 'moment'; +import MD5 from 'crypto-js/md5'; +import { resolve } from 'path'; +import { TestUtils } from './adapter/TestUtils'; + +describe('ValueFormatter', () => { + let app: TestApplication; + let context: DataContext; + beforeAll((done) => { + app = new TestApplication(resolve(process.cwd(), 'spec/test2')); + context = app.createContext(); + return done(); + }); + afterAll(async () => { + await context.finalizeAsync(); + await app.finalize(); + }); + + it('should use $now function', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value = await formatter.format({ + $now: [] + }); + expect(value instanceof Date).toBeTruthy(); + }); + + it('should use $randomString function', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $randomString: [ + 10 + ] + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value.length).toBe(10); + }); + + it('should use $newGuid function', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $newGuid: 1 + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value.length).toBe(36); + }); + + it('should use $randomString with named params', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $randomString: { + length: 10 + } + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value.length).toBe(10); + }); + + it('should use $randomInt with named params', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $randomInt: { + min: 10, + max: 20 + } + }); + expect(typeof value === 'number').toBeTruthy(); + expect(value).toBeGreaterThanOrEqual(10); + expect(value).toBeLessThanOrEqual(20); + }); + + it('should use $randomPassword function', async () => { + const formatter = new ValueFormatter(context); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $randomPassword: [ + 16 + ] + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value.length).toBe(16); + }); + + + it('should use property syntax', async () => { + const formatter = new ValueFormatter(context, null, { + name: 'Macbook Pro 13', + additionalType: 'Product', + productDimensions: { + width: 30, + height: 20, + depth: 5 + } + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $value: '$$target.additionalType' + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value).toBe('Product'); + value = await formatter.format({ + $value: '$$target.productDimensions.width' + }); + expect(typeof value === 'number').toBeTruthy(); + expect(value).toBe(30); + }); + + it('should use $$model function', async () => { + const formatter = new ValueFormatter(context, context.model('Product')); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $value: '$$model.name' + }); + expect(typeof value === 'string').toBeTruthy(); + expect(value).toBe('Product'); + }); + + it('should use $newid function', async () => { + const formatter = new ValueFormatter(context, context.model('Product')); + expect(formatter).toBeTruthy(); + const value: string = await formatter.format({ + $newid: 1 + }); + expect(typeof value === 'number').toBeTruthy(); + }); + + it('should use date functions', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14) + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $dateAdd: { + startDate: '$$target.dateReleased', + unit: 'day', + amount: 10 + } + }); + expect(value).toStrictEqual(moment(new Date(2024, 11, 24)).toDate()); + value = await formatter.format({ + $dateSubtract: { + startDate: '$$target.dateReleased', + unit: 'day', + amount: 10 + } + }); + expect(value).toStrictEqual(moment(new Date(2024, 11, 4)).toDate()); + }); + + it('should use math functions', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + price: 949.458, + dateReleased: new Date(2024, 11, 14) + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $floor: { + value: '$$target.price' + } + }); + expect(value).toBe(949); + value = await formatter.format({ + $round: { + value: '$$target.price', + place: 2 + } + }); + expect(value).toBe(949.46); + value = await formatter.format({ + $ceil: { + value: '$$target.price' + } + }); + expect(value).toBe(950); + + }); + + it('should use comparison operators', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14) + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $gt: [ + new Date(2024, 11, 1), + '$$target.dateReleased', + ] + }); + expect(value).toBeFalsy(); + }); + + it('should use $or operator', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458 + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $or: [ + { + $gt: [ + '$$target.dateReleased', + new Date(2024, 11, 1) + ] + }, + { + $lt: [ + '$$target.price', + 500 + ] + } + ] + }); + expect(value).toBeTruthy(); + }); + + it('should use $and operator', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458 + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $and: [ + { + $gt: [ + '$$target.dateReleased', + new Date(2024, 11, 1) + ] + }, + { + $lt: [ + '$$target.price', + 500 + ] + } + ] + }); + expect(value).toBeFalsy(); + }); + + it('should use $cond operator', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458 + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $cond: { + if: { + $gt: [ + '$$target.dateReleased', + new Date(2024, 11, 1) + ] + }, + then: 'Released', + else: 'Not Released' + } + }); + expect(value).toBe('Released'); + }); + + it('should use $replaceAll operator', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458 + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $replaceAll: { + input: { + $newGuid: 1 + }, + find: '-', + replacement: '' + } + }); + expect(value).toMatch(/^[a-f0-9]{32}$/); + }); + + it('should use $replaceOne operator', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458 + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $replaceOne: { + input: { + $newGuid: 1 + }, + find: '-', + replacement: '' + } + }); + expect(value).toMatch(/^[a-f0-9\-]{35}$/); + }); + + it('should use $query function', async () => { + const formatter = new ValueFormatter(context, context.model('Product'), { + name: 'Macbook Pro 13', + dateReleased: new Date(2024, 11, 14), + price: 949.458, + defaultOrderStatus: { + name: 'Processing', + } + }); + expect(formatter).toBeTruthy(); + let value: any = await formatter.format({ + $query: { + $collection: 'OrderStatusType', + $select: { + value: '$alternateName' + }, + $where: { + $eq: [ + '$name', + '$$target.defaultOrderStatus.name' + ] + }, + $order: [ + { + $asc: '$name' + } + ] + } + }); + expect(value).toBe('OrderProcessing'); + }); + + it('should use $query function with nested joins', async () => { + await executeInUnattendedModeAsync(context, async () => { + const product = await context.model('Product').where('name').equal('Microsoft Sculpt Mobile Mouse').getItem(); + expect(product).toBeTruthy(); + const formatter = new ValueFormatter(context, context.model('Product'), product); + let lastOrder: any = await formatter.format({ + $query: { + $collection: 'Order', + $select: { + value: '$orderDate' + }, + $where: { + $and: [ + { + $eq: [ + '$orderedItem', + '$$target.id' + ] + }, + { + $eq: [ + '$orderStatus.alternateName', + 'OrderDelivered' + ] + } + ] + }, + $order: [ + { + $desc: '$orderDate' + } + ] + } + }); + expect(lastOrder).toBeTruthy(); + }); + }); + + it('should use custom functions', async () => { + Object.assign(ValueDialect.prototype, { + async $initials(first: string, last: string) { + return `${first.charAt(0)}${last.charAt(0)}`; + } + }); + const formatter = new ValueFormatter(context, context.model('Person'), { + givenName: 'John', + familyName: 'Doe' + }); + const initials = await formatter.format({ + $initials: { + first: '$$target.givenName', + last: '$$target.familyName' + } + }); + expect(initials).toBe('JD'); + }); + + it('should use register for custom dialect functions', async () => { + ValueFormatter.register('initials', { + async $md5 (value: any) { + return MD5(value).toString(); + } + }); + const formatter = new ValueFormatter(context, context.model('Person'), { + givenName: 'John', + familyName: 'Doe' + }); + const hash = await formatter.format({ + $md5: { + value: 'Hello World' + } + }); + expect(hash).toBe(MD5('Hello World').toString()); + }); + + it('should use $user function', async () => { + await TestUtils.executeInTransaction(context, async () => { + context.user = { + name: 'angela.parry@example.com' + }; + const user = await context.model('User').asQueryable().where((x: { id: number, name: string}, username: string) => { + return x.name === username; + }, context.user.name).getItem() + const Products = context.model('Product'); + const formatter = new ValueFormatter(context, Products); + const value: number = await formatter.format({ + $user: 1 + }); + expect(value).toBeTruthy(); + expect(value).toEqual(user.id); + }); + }); + + +}); \ No newline at end of file diff --git a/spec/test2/config/models/Thing.json b/spec/test2/config/models/Thing.json index 013e889..4de6512 100644 --- a/spec/test2/config/models/Thing.json +++ b/spec/test2/config/models/Thing.json @@ -1,12 +1,12 @@ { - "$schema": "https://themost-framework.github.io/themost/models/2018/2/schema.json", + "$schema": "../../../../model-schema.json", "@id": "http://schema.org/Thing", "name": "Thing", "description": "The most generic type of item.", "title": "Thing", "abstract": false, "sealed": false, - "hidden": false, + "hidden": true, "version": "2.0", "fields": [{ "@id": "http://schema.org/sameAs", @@ -88,7 +88,9 @@ "description": "The date on which this item was created.", "type": "DateTime", "readonly": true, - "value": "javascript:return new Date();" + "value": { + "$now": 1 + } }, { "@id": "https://themost.io/schemas/dateModified", @@ -97,27 +99,37 @@ "description": "The date on which this item was most recently modified.", "type": "DateTime", "readonly": true, - "value": "javascript:return (new Date());", - "calculation": "javascript:return (new Date());" + "value": { + "$now": 1 + }, + "calculation": { + "$now": 1 + } }, { "@id": "https://themost.io/schemas/createdBy", "name": "createdBy", "title": "createdBy", "description": "Created by user.", - "type": "Integer", + "type": "User", "readonly": true, - "value": "javascript:return this.user();" + "value": { + "$user": 1 + } }, { "@id": "https://themost.io/schemas/modifiedBy", "name": "modifiedBy", "title": "modifiedBy", "description": "Last modified by user.", - "type": "Integer", + "type": "User", "readonly": true, - "value": "javascript:return this.user();", - "calculation": "javascript:return this.user();" + "value": { + "$user": 1 + }, + "calculation": { + "$user": 1 + } } ], "privileges": [{ diff --git a/spec/test2/db/local.db b/spec/test2/db/local.db index 970892dad0344b6a691145e9d2dd1dded25a9339..0c03f49d0443a242c604ec1318fae723c1caeaf2 100644 GIT binary patch delta 12345 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 delta 9213 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