/**
* @module models/modelBaseMongoose
* @author jesse reichler <mouser@donationcoder.com>
* @copyright 5/1/19
* @description
* The main base class we use to derive all database model objects
* NOTE: Always be on the lookout for find queries that use the "lean" option to not instantiate full objects when querying database
*/
"use strict";
// modules
const mongoose = require("mongoose");
// requirement service locator
const jrequire = require("../helpers/jrequire");
// controllers
const arserver = jrequire("arserver");
const appdef = jrequire("appdef");
// our helper modules
const jrdebug = require("../helpers/jrdebug");
const jrhMisc = require("../helpers/jrh_misc");
const jrhMongo = require("../helpers/jrh_mongo");
const jrhText = require("../helpers/jrh_text");
const jrhValidate = require("../helpers/jrh_validate");
const jrhCrypto = require("../helpers/jrh_crypto");
/**
*The main base class we use to derive all database model objects
*
* @class ModelBaseMongoose
*/
class ModelBaseMongoose {
// CLASS data, which can be prolematic when we try to access from an instance (via getModelClass())
// this.mongooseclass <-- this is the one we get caught on as being undefined
// this.crudBaseUrl
// this.schema
// this.modelSchema
// this.modelObjPropertyList
//---------------------------------------------------------------------------
// subclasses implement these
/*
// global static version info
static getVersion() { return 1; }
// collection name for this model
static getCollectionName() {
return "basemodel";
}
// nice name for display
static getNiceName() {
return "BaseModel";
}
// name for acl lookup
static getAclName() {
return "basemodel";
}
// name for logging
static getLoggingString() {
return "Basemodel";
}
*/
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
getModelClass() {
// subclass overriding function that returns class instance (each subclass MUST implement this)
// useful when we want to invoke a static function from instance.
return ModelBaseMongoose;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static getBaseSchemaDefinition() {
// some base schema properties for ALL models
// this helps us keep track of some basic stuff for everything
const UserModel = jrequire("models/user");
return {
_id: {
label: "Id",
readOnly: true,
filterSize: 25,
mongoose: {
type: mongoose.Schema.ObjectId,
auto: true,
},
},
version: {
label: "Ver.",
readOnly: true,
filterSize: 5,
mongoose: {
type: Number,
},
},
creator: {
label: "Creator",
hide: true,
valueFunction: this.makeModelValueFunctionObjectId(UserModel),
mongoose: {
type: mongoose.Schema.ObjectId,
},
},
creationDate: {
label: "Date created",
// hide: true,
readOnly: true,
format: "date",
mongoose: {
type: Date,
},
},
modificationDate: {
label: "Date modified",
readOnly: true,
format: "date",
mongoose: {
type: Date,
},
},
disabled: {
label: "Disabled?",
format: "choices",
choices: appdef.DefStateModeLabels,
choicesEdit: appdef.DefStateModeLabelsEdit,
filterSize: 8,
defaultValue: appdef.DefMdbEnable,
mongoose: {
type: Number,
},
},
extraData: {
label: "Extra data",
valueFunction: this.makeModelValueFunctionExtraData(),
filterSize: 0,
mongoose: {
type: mongoose.Mixed,
},
},
notes: {
label: "Notes",
format: "textarea",
mongoose: {
type: String,
},
},
};
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// defaults for crud list
static getCrudDefaults() {
return {
sortField: "_id",
sortDir: "asc",
};
}
// override this to default to real delete for some models
static getDefaultDeleteDisableMode() {
return appdef.DefMdbVirtDelete;
}
static getDefaultDeleteDisableModeIsVirtual() {
return (this.getDefaultDeleteDisableMode() === appdef.DefMdbVirtDelete);
}
static supportsVirtualDelete() {
return (this.getBaseSchemaDefinition().disabled !== undefined);
}
static getDefaultDeleteDisableModeAsAclAction() {
const deleteDisableMode = this.getDefaultDeleteDisableMode();
if (deleteDisableMode === appdef.DefMdbVirtDelete) {
return appdef.DefAclActionDelete;
}
return appdef.DefAclActionPermDelete;
}
/**
* Should some user ACL own each instance?
* Subclasses can override this (rooms, apps) to say that there should be someone who OWNS this resource
*
* @static
* @returns true or false
* @memberof ModelBaseMongoose
*/
static getShouldBeOwned() {
return false;
}
/**
* Should we log database actions on instances of this model?
* Subclasses can override this (logModel) to say that we shouldnt create log entries when they are deleted etc.
*
* @static
* @returns true or false
* @memberof ModelBaseMongoose
*/
static getShouldLogDbActions() {
return true;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static extractMongooseDbSchemaDefintion() {
// get the full schema definition
const schemaDefinition = this.getSchemaDefinition();
// now build a new object with only the key mongoose values
const mongooseDbSchemaDefinition = {};
for (const key in schemaDefinition) {
if (schemaDefinition[key].mongoose) {
mongooseDbSchemaDefinition[key] = schemaDefinition[key].mongoose;
}
}
// return it
return mongooseDbSchemaDefinition;
}
// User model mongoose db schema
static buildMongooseDbSchema(mongooser) {
const mongooseDbSchemaDefinition = this.extractMongooseDbSchemaDefintion();
this.schema = new mongooser.Schema(mongooseDbSchemaDefinition, {
collection: this.getCollectionName(),
});
return this.schema;
}
// subbclasses implement this
static calcSchemaDefinition() {
return {};
}
static getSchemaDefinition() {
// this is the one that should be called
// returns cached value
if (!this.cachedSchemaDefinition) {
this.cachedSchemaDefinition = this.calcSchemaDefinition();
}
return this.cachedSchemaDefinition;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static getBaseSchemaType(fieldname) {
const baseSchemaDefinition = this.getBaseSchemaDefinition();
if (baseSchemaDefinition[fieldname]) {
return baseSchemaDefinition[fieldname].type;
}
return null;
}
// ATTN: TODO -- cache the schema definition and extras
static getSchemaFieldVal(fieldName, key, defaultVal) {
const modelSchemaDefinition = this.getSchemaDefinition();
if (modelSchemaDefinition[fieldName] && modelSchemaDefinition[fieldName][key] !== undefined) {
return modelSchemaDefinition[fieldName][key];
}
return defaultVal;
}
static async calcHiddenSchemaKeysForView(jrContext, viewType) {
const retKeys = [];
const modelSchemaDefinition = this.getSchemaDefinition();
const keys = Object.keys(modelSchemaDefinition);
let keyHideArray;
let visibleFunction, isVisible;
await jrhMisc.asyncAwaitForEachFunctionCall(keys, async (fieldName) => {
keyHideArray = modelSchemaDefinition[fieldName].hide;
if ((keyHideArray === true) || (jrhMisc.isInAnyArray(viewType, keyHideArray))) {
retKeys.push(fieldName);
} else {
visibleFunction = modelSchemaDefinition[fieldName].visibleFunction;
if (visibleFunction) {
isVisible = await visibleFunction(jrContext, viewType, fieldName, null, null, null);
if (!isVisible) {
retKeys.push(fieldName);
}
}
}
});
return retKeys;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static setCrudBaseUrl(urlPath) {
this.crudBaseUrl = urlPath;
}
static getCrudUrlBase(suburl, id) {
// return url for crud access, adding suburl and id
if (id && !jrhMongo.isValidMongooseObjectId(id)) {
// invalid id
return "";
}
let url = this.crudBaseUrl;
if (suburl) {
url += "/" + suburl;
}
if (id) {
url += "/" + id.toString();
}
return url;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
getExtraDataField(key, defaultValue) {
if (this.extraData === undefined || this.extraData[key] === undefined) {
return defaultValue;
}
return this.extraData[key];
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// create new obj
static createModel(inobj) {
const obj = {
version: this.getVersion(),
creator: null,
creationDate: new Date(),
modificationDate: null,
disabled: 0,
notes: "",
...inobj,
};
const model = this.newMongooseModel(obj);
return model;
}
// cacheable list of schema keys
// ATTN: TODO - this is messy and confusing, fix it
getModelObjPropertyList() {
// cached value
if (this.getModelClass().modelObjPropertyList) {
return this.getModelClass().modelObjPropertyList;
}
const propkeys = Object.keys(this.getModelClass().getSchemaDefinition());
return propkeys;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static async setupModelSchema(mongooser) {
// we only do this IF it"s not yet been done
if (this.modelSchema) {
jrdebug.cdebug("misc", "Skipping model rebuild for " + this.getCollectionName());
return;
}
// jrdebug.debug("Setting up model schema for " + this.getCollectionName());
// compile the model scheme
this.modelSchema = this.buildMongooseDbSchema(mongooser);
// this is an attempt to cache this information but it doesn't seem to work
this.modelObjPropertyList = Object.keys(this.getSchemaDefinition());
// 5/8/19 trying to tie our model class to the mongoose model
// see https://mongoosejs.com/docs/advanced_schemas.html
// this idea is that this transfers the functions and properties from the model class to the schema
// This lets us do things like load a user document, and then call methods on the document returned (see user password checking)
// We can pass lean option in queries to bypass this on a case-by-case-basis
await this.modelSchema.loadClass(this);
// create the mongoose model
const collectionName = this.getCollectionName();
this.setMongooseModel(await mongooser.model(collectionName, this.modelSchema));
// ensure the collection is created now even though it's blank
// ATTN: 5/11/19 - mongoose/mongodb is having a weird fit here, where it is throwing an error
// about connection already exists if while making schema it is creating indexes, even if strict = false
// so we are going to try to check for collection manually before creating it.
// note that even with this check, we must use the default strict:false, otherwise we still get a complaint
if (!await this.collectionExists(mongooser, collectionName)) {
await this.getMongooseModel().createCollection({ strict: false });
}
// any database initialization to be done (e.g. create initial objects/documents, etc.)
await this.dbInit();
}
static async collectionExists(mongooser, collectionName) {
// return true if collection already exists
const list = await mongooser.connection.db.listCollections({ name: collectionName }).toArray();
if (list.length > 0) {
return true;
}
// not found
return false;
}
static async dbInit() {
// nothing to do in base class
}
async dbSave(jrContext) {
// simple wrapper (for now) around mongoose model save
// NOTE: this should always be used instead of superclass built-in mongoose save() function
// NOTE: if jrContext is specified then exceptions are not thrown and errors are added to it; otherwise exception is thrown
// ATTN: TODO it might be better to use an explicit flag regarding whether to throw exceptions and always allow passing in of jrContext
// ATTN: TODO log the errors here as db.severe if we have jrContext to do so?
// update modification date
this.updateModificationDate();
// save and we catch any exceptions and convert to jrResults
let retv;
let serr;
try {
retv = await await this.save();
} catch (err) {
// just set serr and drop down
serr = err;
}
if (serr !== undefined) {
if (jrContext === undefined) {
// just let exceptions percolate up
console.log("ATTN: Unexpected error while trying to mongoose save object:");
console.log(serr);
throw serr;
}
// if jrContext *is* set, we add error to it and do NOT throw exception
jrContext.pushError("Failed to save " + this.getModelClass().getNiceName() + ". " + serr.toString());
return null;
}
// success
// we don't push a success result here because we would see it in operations we dont want messages on
// jrContext.pushSuccess(this.getModelClass().getNiceName() + " saved on " + jrhMisc.getNiceNowString() + ".");
return retv;
}
//---------------------------------------------------------------------------
/**
* A wrapper around doValidateAndSave that can set ownership of the object when appropriate
*
* @static
* @param {*} jrResult
* @param {*} options
* @param {*} flagSave
* @param {*} user
* @param {*} source
* @param {*} saveFields
* @param {*} preValidatedFields
* @param {*} ignoreFields
* @param {*} obj
* @param {*} flagUpdateUserRolesForNewObject
* @memberof ModelBaseMongoose
*/
static async validateSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, obj, flagUpdateUserRolesForNewObject) {
// is this a new object?
const flagIsNew = obj.isNew;
// call validate and save
const savedObj = await this.doValidateAndSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, obj);
// success?
if (flagUpdateUserRolesForNewObject && flagIsNew && !jrContext.isError() && user) {
// successful save and it was a new object, and caller wants us to set roles of owner
await user.addOwnerCreatorRolesForNewObject(jrContext, obj);
if (jrContext.isError()) {
// error setting roles, which means we would like to DESTROY the object and reset it..
// ATTN: unfinished
const emsg = "ATTN: Failed to set ownership roles on " + obj.getLogIdString();
arserver.logr(jrContext, "error.imp", emsg);
jrdebug.debug(emsg);
}
}
return savedObj;
}
static async validateAndSaveNewWrapper(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, flagUpdateUserRolesForNewObject) {
const newObj = this.createModel({});
await this.validateSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, newObj, flagUpdateUserRolesForNewObject);
return newObj;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// subclasses implement this
static async doValidateAndSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, obj) {
jrContext.pushError("Internal error: No subclassed procedure to handle doValidateAndSave() for " + this.getCollectionName() + " model");
return null;
}
static async validateAndSaveNew(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields) {
const newObj = this.createModel({});
await this.doValidateAndSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, newObj);
return newObj;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static getSaveFields(operationType) {
// operationType is commonly "crudAdd", "crudEdit"
// return an array of field names that the user can modify when saving an object
// this is a safety check to allow us to handle form data submitted flexibly and still keep tight control over what data submitted is used
// subclasses implement; by default we return empty array
// NOTE: this list can be generated dynamically based on logged in user
return [];
}
// ATTN: this function typically does not have to run async so its cpu inefficient to make it async but rather than have 2 copies of this function to maintain, we use just async one
// ATTN: TODO in future make a version of this that is sync; or find some better way to handle it
static async validateMergeAsync(jrContext, fieldNameSource, fieldNameTarget, source, saveFields, preValidatedFields, obj, flagRequired, validateFunction) {
//
// source and target field names might be different (for example, password is plaintext hashed into a different target fieldname)
if (fieldNameTarget === "") {
fieldNameTarget = fieldNameSource;
}
// first see if value was pre-validated
let validatedVal, unvalidatedVal;
let fieldNameUsed;
if (preValidatedFields && (preValidatedFields === "*" || (preValidatedFields.includes(fieldNameTarget)))) {
// field is pre-validated, so just grab its prevalidated value
fieldNameUsed = fieldNameTarget;
unvalidatedVal = source[fieldNameSource];
validatedVal = source[fieldNameTarget];
} else {
fieldNameUsed = fieldNameSource;
unvalidatedVal = source[fieldNameSource];
}
// if value isnt set, but a fieldname_checkbox value is, then we know this is a case of html form processing not parsing the checkbox unchecked
if (validatedVal === undefined && unvalidatedVal === undefined) {
// no value found
if (source[fieldNameSource + "_checkbox"]) {
// found checkbox, so unvalidated value should be considered set to false
unvalidatedVal = false;
}
}
// check if the value is even set (!== undefined). this is either an error, or a case where we return doing nothing
if (validatedVal === undefined && unvalidatedVal === undefined) {
// no value found
// throw error if required and it's not ALREADY in the object we are merging into
if (flagRequired && obj[fieldNameTarget] === undefined) {
// it's an error that its not provided and not set in obj already
// ATTN: note that this test does *NOT* require that the field be set in source, just that it already be set in obj if not
jrContext.pushError("Required value not provided for: " + fieldNameSource);
}
// ATTN: do not let validator have a chance to run??
return undefined;
}
// ok its set. if we aren't allowed to save this field, its an error
if (saveFields !== "*" && !(saveFields.includes(fieldNameUsed))) {
jrContext.pushError("Permission denied to save value for: " + fieldNameUsed);
return undefined;
}
// now resolve it if its not yet resolved
if (validatedVal === undefined) {
validatedVal = await validateFunction(jrContext.result, fieldNameSource, unvalidatedVal, flagRequired);
// if its an error, for example during validation, we are done
if (jrContext.isError()) {
return undefined;
}
}
// secondary check for missing value, after we run the valueFunction function
if (validatedVal === undefined) {
// if undefined is returned, we do NOT save the value
if (flagRequired && obj[fieldNameTarget] === undefined) {
// it's an error that its not provided and not set in obj already
// ATTN: note that this test does *NOT* require that the field be set in source, just that it already be set in obj if not
jrContext.pushError("Required value not provided for: " + fieldNameUsed);
}
// should we return undefined, OR should we return obj[fieldNameTarget] if its already in there
return undefined;
}
// null value will also cause error if we're not allowed to be blank
if (flagRequired && validatedVal === null) {
// error if they are trying to save a NULL value and we've been told that the field is required
// above we check for undefined, which means DONT CHANGE the value; null means CHANGE The value to null
jrContext.pushError("Required value not provided for: " + fieldNameUsed);
}
// success, set it
obj[fieldNameTarget] = validatedVal;
// tell object we have set it
obj.notifyValueModified(fieldNameTarget, validatedVal);
return validatedVal;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
notifyValueModified(key, val) {
// ATTN: 4/18/20 -- despite documentation, this doesnt ACTUALLY seem to be needed even for mixed schematype..
if (false) {
// some mongoose models need this to be called, like those with Mixed schema, like extraData
// doesn't seem to be any harm in calling for all vars
// this.markModified(key);
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
/**
* Complain about fields we found that we are refusing to save
*
* @static
* @param {*} jrResult
* @param {*} options
* @param {*} source
* @param {*} saveFields
* @param {*} preValidatedFields
* @memberof ModelBaseMongoose
*/
static async validateComplainExtraFields(jrContext, options, source, saveFields, preValidatedFields, ignoreFields) {
// walk the properties in source, and complain if not found in saveFields, preValidatedFields, and ignoreFields
for (const prop in source) {
if (Object.prototype.hasOwnProperty.call(source, prop)) {
if ((saveFields && saveFields.includes(prop)) || (preValidatedFields && preValidatedFields.includes(prop)) || (ignoreFields && ignoreFields.includes(prop))) {
// good
continue;
} else {
// not found, first check if its a _checkbox version of an allowed field
if (prop.endsWith("_checkbox")) {
const preprop = prop.substr(0, prop.length - 9);
if ((saveFields && saveFields.includes(preprop)) || (preValidatedFields && preValidatedFields.includes(preprop)) || (ignoreFields && ignoreFields.includes(preprop))) {
// good
continue;
}
}
// error
jrContext.pushFieldError(prop, "Not allowed to save this field (" + prop + ").");
}
}
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static async validateAddEditFormId(jrContext, formTypeStr) {
// push error into jrResult on error
// return {id, existingModel}
// get id from form
const id = jrContext.req.body._editId;
// add form should not have shortcode specified
if (formTypeStr === "add") {
// id should be blank in this case
if (id) {
jrContext.pushError("Unexpected Id specified in " + this.getNiceName() + " ADD submission.");
return {};
}
// fine
return {
id: null,
existingModel: null,
};
}
// non-add form MUST have shortcode specified
if (!id) {
jrContext.pushError("Id for " + this.getNiceName() + " missing from NON-ADD submission.");
return {};
}
// now try to look it up
const existingModel = await this.mFindOneById(id);
if (!existingModel) {
jrContext.pushError("Lookup of " + this.getNiceName() + " not found for id specified.");
return {};
}
// success
return {
id,
existingModel,
};
}
static async validateAddEditFormIdMakeObj(jrContext, formTypeStr) {
// return an object with validated properties
// OR an instance of jrResult if error
// get any existing model
const { id, existingModel } = await this.validateAddEditFormId(jrContext, formTypeStr);
if (jrContext.isError()) {
return null;
}
if (!existingModel) {
// create new one (doesn't save it yet)
return this.createModel();
}
return existingModel;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// validate. push error to jrResult on error, return good value on success
static async validateModelFieldUnique(jrContext, key, val, existingModel) {
if (!val) {
jrContext.pushFieldError(key, "Value for " + key + " cannot be blank (must be unique).");
}
// must be unique so we search for collissions
let criteria;
if (existingModel._id) {
// there is an id for the object we are working on, so DONT include that one when searching for a colission
criteria = {
[key]: val,
_id: { $ne: existingModel._id },
};
} else {
criteria = {
[key]: val,
};
}
const clashObj = await this.mFindOne(criteria);
if (clashObj) {
// error
jrContext.pushFieldError(key, "Duplicate " + key + " entry found for another " + this.getNiceName());
// doesnt matter what we return?
return null;
}
return val;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static validateShortcodeSyntax(jrContext, key, val) {
if (!val) {
if (jrContext) {
const sstr = (key === "shortcode") ? "shortcode" : "shortcode (" + key + ")";
jrContext.pushFieldError(key, sstr + " cannot be left blank");
}
return null;
}
// uppercase it
val = val.toUpperCase();
// simple regex test it should only contain letters and numbers and a few basic syboles
const regexPat = /^[A-Z0-9_\-.]*$/;
if (!regexPat.test(val)) {
if (jrContext) {
const sstr2 = (key === "shortcode") ? "shortcode" : "shortcode (" + key + ")";
jrContext.pushFieldError(key, "Syntax error in " + sstr2 + " value; it should be uppercase, and shouold contain only the characters A-Z 0-9 _-. (no spaces).");
}
return null;
}
return val;
}
static async validateShortcodeUnique(jrContext, key, val, existingModel) {
// first basic validation (and fixing) of shortcode syntax
val = this.validateShortcodeSyntax(jrContext, key, val);
if (!val) {
return val;
}
// must be unique so we search for collissions
let criteria;
if (existingModel._id) {
// there is an id for the object we are working on, so DONT include that one when searching for a colission
criteria = {
[key]: val,
_id: { $ne: existingModel._id },
};
} else {
criteria = {
[key]: val,
};
}
if (await this.isShortcodeInUse(criteria)) {
jrContext.pushFieldError(key, "Duplicate " + key + " entry found for another " + this.getNiceName());
// doesnt matter what we return?
return null;
}
return val;
}
static async isShortcodeInUse(criteria) {
const clashObj = await this.mFindOne(criteria);
if (clashObj) {
return true;
}
return false;
}
static async makeRandomShortcode(key) {
// try to make an unused random shortcode
const maxTrycount = 100;
const shortcodeLen = 9;
let shortcode;
const criteria = {};
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (let trycount = 0; trycount < maxTrycount; trycount += 1) {
// random shortcode
shortcode = "RND" + jrhCrypto.genRandomStringFromCharSet(charset, shortcodeLen);
criteria[key] = shortcode;
// see if it's in use
if (!(await this.isShortcodeInUse(criteria))) {
// found one not in use
return shortcode;
}
}
// not found
return null;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static validateModelFielDisbled(jrContext, key, val, flagRequired) {
// the disabled field for resource models must be a postitive integer (0 meaning not disabled, higher than 0 various flavors of being a disabled resource)
return jrhValidate.validateIntegerRange(jrContext, key, val, 0, 999999, flagRequired);
}
static validateModelFieldId(jrContext, val) {
if (!jrhMongo.isValidMongooseObjectId(val)) {
jrContext.pushError("No valid id specified.");
return null;
}
return val;
}
static async validateModelFieldAppId(jrContext, key, val, user) {
const AppModel = jrequire("models/app");
const appIds = await AppModel.buildSimpleAppIdListUserTargetable(user);
if (val === "") {
jrContext.pushFieldError(key, "app id may not be blank.");
return null;
}
if (!appIds || appIds.indexOf(val) === -1) {
jrContext.pushFieldError(key, "specified app id is inaccessible.");
console.log("ATTN:DEBUG APPIDS");
console.log(appIds);
return null;
}
// valid
return val;
}
static async validateModelFieldRoomId(jrContext, key, val, user) {
const RoomModel = jrequire("models/room");
const roomIds = await RoomModel.buildSimpleRoomIdListUserTargetable(user);
if (val === "") {
jrContext.pushFieldError(key, "room id may not be blank.");
return null;
}
if (!roomIds || roomIds.indexOf(val) === -1) {
jrContext.pushFieldError(key, "specified room id is inaccessible.");
return null;
}
// valid
return val;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// accessors
getIdAsM() {
return this._id;
}
getIdAsString() {
if (true) {
// ATTN: does this work?
return this.id;
}
// old
if (!this._id) {
return "";
}
return this._id.toString();
}
getLogIdString() {
// human readable id string for use in log messages that we could parse to get a link
// ATTN: note we use the "this.constructor.staticfunc" syntax to access static class function from member
// return this.getModelClass().getLoggingString() + "#" + this.getIdAsString();
return this.getModelClass().getLogStringFromId(this.getIdAsString());
}
static getLogStringFromId(id) {
return this.getLoggingString() + "#" + id;
}
isRealObjectInDatabase() {
return !this.getIsNew();
}
getIsNew() {
// return TRUE if it is new and not yet saved
return this.isNew;
}
getCreationDate() {
return this.creationDate;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
updateModificationDate() {
this.modificationDate = new Date();
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// isolate use of this.mongooseModel
static getMongooseModel() {
return this.mongooseModel;
}
static setMongooseModel(val) {
this.mongooseModel = val;
}
static newMongooseModel(obj) {
const retv = new this.mongooseModel(obj);
return retv;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// rather than letting different models call mongoose directly, we try to put a thin wrapper of our own
static async mFindOne(...args) {
// actually call mongooseModel mFindOne
const retv = await this.mongooseModel.findOne(...args).exec();
return retv;
}
static async mFindOneAndUpdate(criteria, setObject) {
const retv = await this.mongooseModel.findOneAndUpdate(criteria, setObject).exec();
return retv;
}
static async mFindAll(criteria) {
const retv = await this.mongooseModel.find(criteria).exec();
return retv;
}
static async mFindAllAndSelect(criteria, projection) {
// pass null as criteria to get full set
// ATTN: we dont exec when we select?
const retv = await this.mongooseModel.find(criteria).select(projection);
return retv;
}
static async mFindMongoose(...args) {
// just pass through to mongoose find
const retv = await this.mongooseModel.find(...args).exec();
return retv;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// more elaborate helpers
/**
* Find some items (possibly paginated)
* This is used in our crud system
*
* @static
* @param {object} query
* @param {object} queryOptions
* @param {object} jrResult
* @returns a tuble [items, fullQueryResultCount] - where fullQueryResultCount may be larger than items.length if pagination is only bringing us some of the reulst
* @memberof ModelBaseMongoose
*/
static async mFindAllByQuery(jrContext, query, queryOptions, flagDoLeanRequestNotFullClasses) {
// fetch the array of items to be displayed in grid
// see https://thecodebarbarian.com/how-find-works-in-mongoose
// ATTN: IMPORTANT -- when this is set, we don't instantiate full model classes when retrieving
// we force caller to specify this explicitly instead of embedding it in queryOptions so that it sticks out more like a sore thumb since it can have important ramifications
if (flagDoLeanRequestNotFullClasses) {
queryOptions.lean = true;
}
const queryProjection = null;
try {
const items = await this.mongooseModel.find(query, queryProjection, queryOptions).exec();
let resultCount;
const isQueryEmpty = ((Object.keys(query)).length === 0);
if (isQueryEmpty) {
resultCount = await this.mongooseModel.countDocuments();
} else {
resultCount = await this.mongooseModel.countDocuments(query).exec();
}
return [items, resultCount];
} catch (err) {
jrContext.pushError("Error executing find filter: " + JSON.stringify(query, null, " ") + ":" + err.message);
return [[], 0];
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static async mFindAndDeleteMany(criteria) {
await this.getMongooseModel().deleteMany(criteria).exec();
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// shortcuts that call above
static async mFindOneByShortcode(shortcodeval) {
// return null if not found
if (!shortcodeval) {
return null;
}
return await this.mFindOne({ shortcode: shortcodeval });
}
// lookup user by their id
static async mFindOneById(id) {
// return null if not found
if (!id) {
return null;
}
return await this.mFindOne({ _id: id });
}
static async mFindOneByKeyValue(key, val) {
return await this.mFindOne({ [key]: val });
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
modelObjPropertyCopy(flagIncludeId) {
// copy the properties in schema
const obj = {};
const keylist = this.getModelObjPropertyList();
keylist.forEach((key) => {
if (key in this) {
obj[key] = this[key];
}
});
if (flagIncludeId) {
obj._id = this._id;
}
return obj;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// subclasses can subclass this for crud add/edit
static async calcCrudEditHelperData(jrContext, id) {
return undefined;
}
// subclasses can subclass this for crud view
static async calcCrudViewHelperData(jrContext, id, obj) {
return undefined;
}
// subclasses can subclass this list grid helper
static async calcCrudListHelperData(jrContext, user, baseUrl, protectedFields, hiddenFields) {
// perform a find filter and create table grid
// schema for obj
const gridSchema = this.getSchemaDefinition();
// force add the invisible id field to schema for display
// we shouldn't have to do this anymore, we found out had to add it to the model schema
if (false) {
gridSchema._id = { type: "id" };
}
// headers for list grid
const gridHeaders = [];
// default sorting?
const crudDefaults = this.getCrudDefaults();
// options for filter construction
const filterOptions = {
defaultPageSize: 10,
minPageSize: 1,
maxPageSize: 1000,
defaultSortField: jrhMisc.getNonNullValueOrDefault(crudDefaults.sortField, "_id"),
defaultSortDir: jrhMisc.getNonNullValueOrDefault(crudDefaults.sortDir, "desc"),
alwaysFilter: [],
protectedFields,
hiddenFields,
};
// convert filter into query and options
const jrhMongoFilter = require("../helpers/jrh_mongo_filter");
const { query, queryOptions, queryUrlData } = jrhMongoFilter.buildMongooseQueryFromReq(jrContext, filterOptions, gridSchema);
// ATTN: IMPORTANT! 5/20/20
// Force the lean option to speed up retrieving of results, since we only need for read-only display here; see https://mongoosejs.com/docs/tutorials/lean.html
// Note that if we wanted to call methods on the model class we couldn't do this, as it returns results as plain generic objects
const flagDoLeanRequestNotFullClasses = false;
// add filter to not show vdeletes if appropriate
await this.addUserDisabledVisibilityToQuery(jrContext, user, query);
// get the items using query
const [gridItems, resultcount] = await this.mFindAllByQuery(jrContext, query, queryOptions, flagDoLeanRequestNotFullClasses);
queryUrlData.resultCount = resultcount;
// store other stuff in queryUrl data to aid in making urls for pager and grid links, etc.
queryUrlData.baseUrl = baseUrl;
queryUrlData.tableId = this.getCollectionName();
// return constructed object -- this is listHelperData in template
return {
modelClass: this,
gridSchema,
gridHeaders,
query,
queryOptions,
queryUrlData,
gridItems,
filterOptions,
};
}
static async calcCrudStatsHelperData(jrContext) {
return undefined;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
/**
* This adds to a query a filter that makes virtually deleted objects inaccessible if the user doesn't have permission.
* Modifies the passed query.
*
* @static
* @param {object} user
* @param {object} query
* @memberof ModelBaseMongoose
*/
static async addUserDisabledVisibilityToQuery(jrContext, user, query) {
if (await user.aclHasPermissionSeeVDeletes(jrContext, this)) {
// they are allowed to see the virtually deleted, so just return
return;
}
// we need to filter out virtual deletes
const addFilter = {
$ne: 2,
};
if (!query.disabled) {
// just add the filter
query.disabled = addFilter;
} else {
// more complicated, we have to inject it
const oldDisabled = query.disabled;
delete query.disabled;
if (query.$and) {
// there is an and we need to add it to
query.$and.push(oldDisabled);
query.$and.push(addFilter);
} else {
// there is no and, so create one
query.$and = [
{ disabled: oldDisabled },
{ disabled: addFilter },
];
}
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static async validateGetObjByIdDoAclRenderErrorPageOrRedirect(jrContext, user, val, aclTestName) {
// get a model object, performing acl access check first
// if not, render an error and return null
let obj;
const id = this.validateModelFieldId(jrContext, val);
if (!jrContext.isError()) {
// acl test
if (!await arserver.aclRequireModelAccessRenderErrorPageOrRedirect(jrContext, user, this, aclTestName, id)) {
// ATTN: note that in thie case, callee will have ALREADY rendered an error to the user about permissions, which is why we need to not drop down and re-render acl access error
// but we DO need to push an error onto jrresult for our return check; note that text of error message is irrelevant
jrContext.pushError("model access denied");
return null;
}
// permission was granted
// get object being edited
obj = await this.mFindOneById(id);
if (!obj) {
jrContext.pushError("Could not find " + this.getNiceName() + " with that Id.");
}
}
//
if (jrContext.isError()) {
// render error
arserver.renderAclAccessErrorResult(jrContext, this);
return null;
}
return obj;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static async validateMergeAsyncBaseFields(jrContext, options, flagSave, source, saveFields, preValidatedFields, obj) {
// base fields shared among most models
await this.validateMergeAsync(jrContext, "disabled", "", source, saveFields, preValidatedFields, obj, true, (jrr, keyname, inVal, flagRequired) => this.validateModelFielDisbled(jrr, keyname, inVal, flagRequired));
await this.validateMergeAsync(jrContext, "notes", "", source, saveFields, preValidatedFields, obj, false, (jrr, keyname, inVal, flagRequired) => jrhValidate.validateString(jrr, keyname, inVal, flagRequired));
// extraData json
await this.validateMergeAsync(jrContext, "extraData", "", source, saveFields, preValidatedFields, obj, false, (jrr, keyname, inVal, flagRequired) => jrhValidate.validateJsonObjOrStringToObj(jrr, keyname, inVal, flagRequired));
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// value function helpers
/**
* Helper function that makes a value function where only admin can see
*
* @static
* @param {*} flagRequired
* @returns async value function
* @memberof ModelBaseMongoose
*/
static makeModelValueFunctionPasswordAdminEyesOnly(flagRequired) {
// a value function usable by model definitions
return async (jrContext, viewType, fieldName, obj, editData, helperData) => {
let retv;
const flagExistingIsNonBlank = (obj && (obj.passwordHashed !== undefined && obj.passwordHashed !== null && obj.password !== ""));
if (editData && fieldName in editData) {
// they are editing this field on a crud form, return the current editing value (not any previous val of object)
retv = jrhText.jrHtmlFormInputPassword("password", editData, flagRequired, flagExistingIsNonBlank);
return retv;
}
const isLoggedInUserSiteAdmin = await arserver.isLoggedInUserSiteAdmin(jrContext);
if (viewType === "view" && obj) {
if (isLoggedInUserSiteAdmin) {
// for debuging
retv = obj.passwordHashed;
} else {
// safe
retv = this.safeDisplayPasswordInfoFromPasswordHashed(obj.passwordHashed);
}
} else if (viewType === "add" || viewType === "edit") {
retv = jrhText.jrHtmlFormInputPassword("password", obj, flagRequired, flagExistingIsNonBlank);
} else if (viewType === "list" && obj) {
if (isLoggedInUserSiteAdmin) {
retv = obj.passwordHashed;
} else if (!obj.passwordHashed) {
retv = "";
} else {
retv = "[HIDDEN]";
}
}
//
if (retv === undefined) {
return "";
}
return retv;
};
}
/**
* Helper function to make a value function for the extraData field
*
* @static
* @returns async value function
* @memberof ModelBaseMongoose
*/
static makeModelValueFunctionExtraData() {
// a value function usable by model definitions
return async (jrContext, viewType, fieldName, obj, editData, helperData) => {
let str;
if (editData && fieldName in editData) {
// they are editing this field on a crud form, return the current editing value (not any previous val of object)
str = editData[fieldName];
} else if (obj && obj.extraData) {
if (typeof obj.extraData === "string") {
// already a string -- this is used when form error being reshown..
str = obj.extraData;
} else {
str = JSON.stringify(obj.extraData, null, " ");
}
} else {
// str will be undefined, which is handled in sanitizeUnsafeText
}
// let them edit the json string
if (viewType === "add" || viewType === "edit") {
// sanitize html
str = jrhText.sanitizeUnsafeText(str, false, false);
// wrap in input textarea
str = `<textarea name="${fieldName}" rows="4" cols="80">${str}</textarea>`;
} else {
// just for display sanitize html
str = jrhText.sanitizeUnsafeText(str, true, true);
}
// return it
return str;
};
}
/**
* Helper function to make a value function for an object's id crud field
*
* @static
* @param {*} modelClass
* @param {*} fieldId
* @param {*} fieldLabel
* @param {*} fieldList
* @returns an async value function
* @memberof ModelBaseMongoose
*/
static makeModelValueFunctionObjectId(modelClass) {
return async (jrContext, viewType, fieldName, obj, editData, helperData) => {
if (editData && fieldName in editData) {
// no way to edit this
}
if (obj) {
const objid = obj[fieldName];
if (objid) {
// jrdebug.debugObj(obj, "Obj test");
const alink = modelClass.getCrudUrlBase("view", objid);
return `<a href="${alink}">${objid}</a>`;
}
}
return "";
};
}
/**
* Helper function to make a value function for an object's id crud field, where there are a list of choices from helperdata
*
* @static
* @param {*} modelClass
* @param {*} fieldId
* @param {*} fieldLabel
* @param {*} fieldList
* @returns an async value function
* @memberof ModelBaseMongoose
*/
static makeModelValueFunctionCrudObjectIdFromList(modelClass, fieldId, fieldLabel, fieldList) {
return async (jrContext, viewType, fieldName, obj, editData, helperData) => {
let viewUrl, oLabel, rethtml, oid;
if (editData && editData[fieldId]) {
// they are editing this field on a crud form, return the current editing value (not any previous val of object)
oid = editData[fieldId];
rethtml = jrhText.jrHtmlFormOptionListSelect(fieldId, helperData[fieldList], oid, true);
return rethtml;
}
if (viewType === "view" && obj) {
viewUrl = modelClass.getCrudUrlBase("view", obj[fieldId]);
oLabel = helperData[fieldLabel];
rethtml = `${oLabel} (<a href="${viewUrl}">#${obj[fieldId]}</a>)`;
return rethtml;
}
if (viewType === "add" || viewType === "edit") {
oid = obj ? obj[fieldId] : null;
rethtml = jrhText.jrHtmlFormOptionListSelect(fieldId, helperData[fieldList], oid, true);
return rethtml;
}
if (viewType === "list" && obj) {
viewUrl = modelClass.getCrudUrlBase("view", obj[fieldId]);
rethtml = `<a href="${viewUrl}">${obj[fieldId]}</a>`;
return rethtml;
}
return undefined;
};
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
/**
* Return a value function for showing the roles held by all users on a specific object
*
* @static
* @param {*} modelClass
* @returns async value function
* @memberof ModelBaseMongoose
*/
static makeModelValueFunctionRoleOnObjectList(modelClass) {
const RoleModel = jrequire("models/role");
return async (jrContext, viewType, fieldName, obj, editData, helperData) => {
if (editData && fieldName in editData) {
// no way to edit this
}
// can't get roles?
if (!obj || (!obj.getAllRolesOnThisObject && !obj._id)) {
return "n/a";
}
if (false && viewType === "list") {
// too heavy to retrieve in this mode
return "...";
}
//
if (obj.getAllRolesOnThisObject) {
// it's a full object so we can resolve it
// ATTN: 5/13/20 -- because this needs a valid object, it doesnt work in crud edit mode only crud view mode
const roles = await obj.getAllRolesOnThisObject();
return RoleModel.stringifyRoles(roles, true, false);
}
// a thin json object, but we still know how to do this
if (obj._id) {
const roles = await this.getAllRolesOnObjectById(obj._id);
return RoleModel.stringifyRoles(roles, true, false);
}
// should never be able to get here
return "n/a";
};
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
static getNiceNamePluralized(num) {
if (num === 1) {
return num.toString() + " " + this.getNiceName();
}
return num.toString() + " " + this.getNiceName() + "s";
}
static getNiceNameWithId(id) {
return this.getNiceName() + " #" + id;
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
/**
* Delete the object AND do any cleanup, like deleteing accessory objects, removing references, etc.
* Just hand off to static class method
*
* @param {string} mode
* @param {object} jrResult
*/
async doChangeMode(jrContext, mode) {
// just hand off to static class version
await this.getModelClass().doChangeModeById(jrContext, this.getIdAsM(), mode);
}
/**
* change mode (delete) the object AND do any cleanup, like deleteing accessory objects, removing references, etc.
*
* @static
* @param {string} id
* @param {string} mode
* @param {object} jrResult
*/
static async doChangeModeById(jrContext, id, mode) {
if (mode === appdef.DefMdbRealDelete) {
// direct database delete
await this.getMongooseModel().deleteOne({ _id: id }, (err) => {
if (err) {
const msg = "Error while tryign to delete " + this.getNiceNameWithId(id) + ": " + err.message;
jrContext.pushError(msg);
} else {
// log the action
if (this.getShouldLogDbActions()) {
arserver.logr(jrContext, "db.delete", "Deleted " + this.getNiceNameWithId(id));
}
}
});
} else {
// change mode (enable, disable, vdelete, etc.)
// just sets the field "disabled" to mode value
// see https://mongoosejs.com/docs/documents.html#updating
const nowDate = new Date();
await this.getMongooseModel().updateOne({ _id: id }, { $set: { disabled: mode, modificationDate: nowDate } }, (err) => {
if (err) {
const msg = "Error while changing to " + appdef.DefStateModeLabels[mode] + " " + this.getNiceNameWithId(id) + ": " + err.message;
jrContext.pushError(msg);
} else {
if (this.getShouldLogDbActions()) {
// log the action
arserver.logr(jrContext, "db.modify", "Changing to " + appdef.DefStateModeLabels[mode] + " " + this.getNiceNameWithId(id));
}
}
});
}
if (jrContext.isError()) {
return;
}
// success, now handle any post change operations (like deleting accessory objects, etc.)
await this.auxChangeModeById(jrContext, id, mode);
}
static async doChangeModeByIdList(jrContext, idList, mode, flagSupressSuccessMessage) {
// delete/disable a bunch of items
let successCount = 0;
// walk the list and do a deep delete of each
let id;
for (let i = 0; i < idList.length; ++i) {
id = idList[i];
await this.doChangeModeById(jrContext, id, mode);
if (jrContext.isError()) {
break;
}
++successCount;
}
const modeLabel = jrhText.capitalizeFirstLetter(appdef.DefStateModeLabels[mode]);
if (!jrContext.isError()) {
if (!flagSupressSuccessMessage) {
jrContext.pushSuccess(modeLabel + " " + this.getNiceNamePluralized(successCount) + ".");
}
} else {
if (successCount > 0) {
jrContext.pushError(modeLabel + " " + this.getNiceNamePluralized(successCount) + " before error occurred.");
}
}
}
// delete any ancillary deletions AFTER the normal delete
// this would normally be subclassed by specific model
static async auxChangeModeById(jrContext, id, mode) {
// by default, nothing to do; subclasses can replace this
// roles delete IF the object is really deleted (and the object was succesfully deletd)
if (mode === appdef.DefMdbRealDelete) {
await this.deleteAllRolesRelatedToObject(jrContext, id);
}
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
async getAllRolesOnThisObject() {
// get all roles held by all users on this object
return await this.getModelClass().getAllRolesOnObjectById(this.getIdAsString());
}
static async getAllRolesOnObjectById(objectIdString) {
const cond = {
objectType: this.getAclName(),
objectId: objectIdString,
};
const RoleModel = jrequire("models/role");
const roles = await RoleModel.mFindRolesByCondition(cond);
return roles;
}
static async deleteAllRolesRelatedToObject(jrContext, id) {
const cond = {
objectType: this.getAclName(),
objectId: id,
};
const RoleModel = jrequire("models/role");
// await RoleModel.deleteRolesByCondition(jrContext, cond);
await RoleModel.mFindAndDeleteMany(cond);
// log it
await arserver.logr(jrContext, "acl.deleteRoles", "delete roles related to deleted object " + this.getLogStringFromId(id));
}
//---------------------------------------------------------------------------
//---------------------------------------------------------------------------
// ATTN: first stab at extracting a function to do what crudaid does
static async renderFieldValueHtml(jrContext, obj, editData, fieldName, crudSubType, helperData) {
let isReadOnly = false;
let val, valHtml;
// editing or read only?
if (crudSubType === "view" || crudSubType === "list") {
isReadOnly = true;
} else {
// might be editable
const readOnlyList = this.getSchemaFieldVal(fieldName, "readOnly", undefined);
isReadOnly = ((readOnlyList === true) || jrhMisc.isInAnyArray(crudSubType, readOnlyList));
}
// now compute value
// is there a custom value function? if so use that to grab value
const valueFunction = this.getSchemaFieldVal(fieldName, "valueFunction");
if (valueFunction) {
// ok we have a custom function to call to get html to show for value (only pass in potential editData if not readOnly)
valHtml = await valueFunction(jrContext, crudSubType, fieldName, obj, isReadOnly ? null : editData, helperData);
}
// if we havent yet set a value using valueFunctions (or if that returns undefined) then use default value
if (valHtml === undefined) {
let choices;
let extra;
let url;
const format = this.getSchemaFieldVal(fieldName, "format", undefined);
// compact view mode?
const isCompact = (crudSubType === "list");
// get the raw value we are going to use
if (isReadOnly) {
// read only just use obj value (ignore editData)
if (obj && fieldName in obj) {
val = obj[fieldName];
}
} else {
// it's editable, check for value in reqbody
if (editData && fieldName in editData) {
// value set in editData
val = editData[fieldName];
} else if (obj && fieldName in obj) {
// value not set in editData, fall back on obj value if available
val = obj[fieldName];
} else {
// not found in obj or editData (perhaps new object so obj is null)
val = this.getSchemaFieldVal(fieldName, "defaultValue", undefined);
}
}
// we have the raw value, now we need to format it nicely depending on format, etc.
// is it multiple choice type?
if (!isReadOnly) {
// try to get editing choices
choices = this.getSchemaFieldVal(fieldName, "choicesEdit", null);
// if not found, drop down and fall back on choices
}
if (!choices) {
choices = this.getSchemaFieldVal(fieldName, "choices", null);
}
// how we format will depend on whether its read only or editable input
if (isReadOnly) {
// read only value
if (choices) {
if (isCompact) {
valHtml = jrhText.jrHtmlDisplayOptionListChoice(val, choices);
} else {
valHtml = jrhText.jrHtmlNiceOptionFromList(choices, val);
}
} else if (format === "textarea") {
valHtml = jrhText.sanitizeUnsafeText(val, true, true);
} else if (format === "checkbox") {
// checkbox (note that we display null and undefined as false here)
if (val) {
valHtml = "true";
} else {
valHtml = "false";
}
} else if (format === "date") {
// format as compact date?
valHtml = jrhText.formatDateNicely(val, isCompact);
}
// fallback default
if (valHtml === undefined) {
// just coerce to a string for display
valHtml = jrhText.sanitizeUnsafeText(jrhText.coerceToString(val, true), true, false);
}
// can we link to another object crud page for this field?
if (!url && val) {
// this field refers to another model so we can link to it
const refModelClass = this.getSchemaFieldVal(fieldName, "refModelClass");
if (refModelClass) {
url = refModelClass.getCrudUrlBase("view", val);
}
}
// wrap in url?
if (url) {
valHtml = `<a href="${url}">${valHtml}</a>`;
}
} else {
// not read only, editable
if (choices) {
const flagShowBlank = true;
valHtml = jrhText.jrHtmlFormOptionListSelect(fieldName, choices, val, flagShowBlank);
} else if (format === "textarea") {
// textview block (note in this case we pass false, false to sanitize, so that we edit "" if its undefined)
val = jrhText.sanitizeUnsafeText(val, false, false);
valHtml = `<textarea name="${fieldName}" rows="4" cols="80">${val}</textarea>`;
} else if (format === "checkbox") {
// checkbox
if (val) {
extra = "checked";
} else {
extra = "";
}
valHtml = `<input type="checkbox" name="${fieldName}" ${extra}>`;
// add a hidden var to handle the cases where unchecked checkbox is not sent, stupid html form processing of checkboxes
valHtml += `<input type="hidden" name="${fieldName}_checkbox" value="true">`;
}
// fallback default - simple text input
if (valHtml === undefined) {
// just show text in input (note in this case we pass false, false to sanitize, so that we edit "" if its undefined)
val = jrhText.sanitizeUnsafeText(jrhText.coerceToString(val, true), false, false);
valHtml = `<input type="text" name="${fieldName}" value="${val}" size="80"/>`;
}
}
}
return valHtml;
}
//---------------------------------------------------------------------------
}
// export the class as the sole export
module.exports = ModelBaseMongoose;