import RESTError from '../rest-error.js';
import ModelUtility from './model-utility.js';
import StringUtility from '../utilities/string-utility.js';
/**
* A utility class for working with StashKu-compatible model objects.
*/
class ModelGenerator {
/**
* Attempts to format a generically formatted property name to a JavaScript camelCase property name format.
* @param {String} dirtyPropName - The property name value to be formatted.
* @returns {String}
*/
static formatPropName(dirtyPropName) {
return StringUtility.camelify(dirtyPropName);
}
/**
* Attempts to format a generic resource name into a model class name in PascalCase format.
* The resource name is always suffixed with the word "Model".
*
* This function leverages the `STASHKU_MODEL_NAME_REMOVE` environmental setting, which allows you to configure
* one or more regular expressions that are removed from a generated model's class name (derived from a resource
* name). By default the configured expressions will strip "dbo.", "etl.", and "rpt." prefixes from resource names.
* @param {String} dirtyResourceName - The resource name value to be formatted.
* @param {String} [suffix="Model"] - A suffix attached to the model name. Defaults to "Model".
* @returns {String}
*/
static formatModelName(dirtyResourceName, suffix = 'Model') {
let removes = ['/^\\[?dbo\\]?./i', '/^\\[?etl\\]?./i', '/^\\[?rpt\\]?./i'];
let classNameCase = 'pascal';
if (typeof process !== 'undefined' && typeof process.env === 'object') {
if (process.env.STASHKU_MODEL_NAME_REMOVE) {
removes = JSON.parse(process.env.STASHKU_MODEL_NAME_REMOVE);
}
if (process.env.STASHKU_MODEL_NAME_CASE) {
classNameCase = process.env.STASHKU_MODEL_NAME_CASE;
}
}
if (removes && Array.isArray(removes)) {
for (let rStr of removes) {
let reg = StringUtility.toRegExp(rStr);
dirtyResourceName = dirtyResourceName.replace(reg, '');
}
}
dirtyResourceName = dirtyResourceName.replace(/[[\]{}]/g, '');
return StringUtility.camelify(StringUtility.singular(dirtyResourceName), (classNameCase === 'pascal')) + (suffix ?? '');
}
/**
* Generates a model type class dynamically utilizing the given properties and configuration.
*
* @throws 500 `RESTError` if the "typeName" argument is missing.
* @throws 500 `RESTError` if the "properties" argument is missing.
* @throws 500 `RESTError` if the "properties" argument is not a Map instance.
* @param {String} resource - The name of the target resource.
* @param {Map.<String, Modeling.PropertyDefinition>} properties - The map of all properties definable for the model.
* @param {Modeling.Configuration} [configuration] - The $stashku model configuration.
* @param {String} [className] - Optional argument to utilize a specific class name instead of generating one
* from the resource name.
* @returns {Modeling.AnyModelType}
*/
static generateModelType(resource, properties, configuration, className) {
if (!resource) {
throw new RESTError(500, 'The "resource" argument is required.');
} else if (!properties) {
throw new RESTError(500, 'The "properties" argument is required.');
} else if ((properties instanceof Map) === false) {
throw new RESTError(500, 'The "properties" argument must be of type Map.');
}
let propertyCase = 'camel';
let reservedPrefix = 'Resource';
if (typeof process !== 'undefined' && typeof process.env === 'object') {
if (process.env.STASHKU_MODEL_PROPERTY_CASE) {
propertyCase = process.env.STASHKU_MODEL_PROPERTY_CASE;
}
if (process.env.STASHKU_MODEL_PROPERTY_RESERVED_PREFIX) {
reservedPrefix = process.env.STASHKU_MODEL_PROPERTY_RESERVED_PREFIX;
}
}
let sortedProperties = new Map();
//presort
properties = new Map([...properties.entries()].sort());
//pascal-case keys
for (let [k, v] of properties) {
//rename reserved words
if (/^name|prototype$/i.test(k)) {
k = reservedPrefix + k.substring(0, 1).toUpperCase() + k.substring(1);
}
let formattedKey = StringUtility.camelify(k, (propertyCase === 'pascal'));
if (formattedKey != k && sortedProperties.has(formattedKey) === false) {
sortedProperties.set(formattedKey, v);
} else {
sortedProperties.set(k, v);
}
}
//sort final
sortedProperties = new Map([...sortedProperties.entries()].sort());
//create model type closure
let mtConstructor = function () {
for (let [k, v] of sortedProperties) {
let typeOfValue = typeof v;
if (v === null || typeOfValue === 'undefined') {
this[k] = undefined;
} else if (typeOfValue === 'object') {
this[k] = typeof v.default !== 'undefined' ? v.default : null;
}
}
};
let mt = class DynamicModel {
constructor() { mtConstructor.call(this); }
validate() {
//build initial results using all static keys
let validationResults = {};
for (let k of Object.keys(this.constructor)) {
let inputType = typeof this.constructor[k];
if (/^([$_].+|prototype|name)$/.test(k) === false && (inputType === 'string' || inputType === 'object')) {
validationResults[k] = null;
}
}
//run validations
let validations = this.constructor?.$stashku?.validations;
if (typeof validations === 'object') {
for (let k in validations) {
let v = validations[k];
let res = null;
if (Array.isArray(v)) {
for (let func of v) {
res = func.call(this, this, k, this[k]);
if (res) {
break;
}
}
} else {
res = v.call(this, this, k, this[k]);
}
validationResults[k] = res ?? null; //ensure null instead of undefined
}
}
return validationResults;
}
};
if (!className) {
className = ModelGenerator.formatModelName(resource);
}
Object.defineProperty(mt, 'name', { value: className });
//add json stringification support
let toSchema = ModelUtility.schema;
mt.toJSON = (function () {
return toSchema(this);
}).bind(mt);
//add static properties
for (let [k, v] of sortedProperties) {
if (v === null || typeof v === 'undefined') {
mt[k] = {};
} else {
mt[k] = v;
}
}
//add $stashku configuration static property
if (!configuration) {
mt.$stashku = {};
} else {
mt.$stashku = configuration;
}
//add helper names to configration if missing
if (!mt.$stashku.resource) {
mt.$stashku.resource = resource;
}
if (!mt.$stashku.validations) {
mt.$stashku.validations = {};
}
if (!mt.$stashku.name) {
mt.$stashku.name = ModelGenerator.formatModelName(resource, '');
}
if (!mt.$stashku.slug) {
mt.$stashku.slug = StringUtility.slugify(mt.$stashku.name, '-', true, true);
}
if (!mt.$stashku.plural) {
mt.$stashku.plural = {};
}
if (!mt.$stashku.plural.name) {
mt.$stashku.plural.name = StringUtility.camelify(StringUtility.plural(mt.$stashku.name), true);
}
if (!mt.$stashku.plural.slug) {
mt.$stashku.plural.slug = StringUtility.slugify(mt.$stashku.plural.name, '-', true, true);
}
return mt;
}
}
export default ModelGenerator;
source