models/room.js

/**
 * @module models/room
 * @author jesse reichler <mouser@donationcoder.com>
 * @copyright 5/15/19
 * @description
 * All data in our system is organized at the highest level into a collection of "Apps", and then for each App we have "Rooms", which allow a number of users to communicate / share data with each other.
 * The Room model manages the data for each virtual room.
 */

"use strict";


// modules
const mongoose = require("mongoose");


// requirement service locator
const jrequire = require("../helpers/jrequire");

// models
const ModelBaseMongoose = jrequire("models/model_base_mongoose");

// controllers
const arserver = jrequire("arserver");
const appdef = jrequire("appdef");


// our helper modules
const jrhText = require("../helpers/jrh_text");
const jrhValidate = require("../helpers/jrh_validate");
const jrhMongo = require("../helpers/jrh_mongo");



/**
 * The Room model manages the data for each virtual room.
 *
 * @class RoomModel
 * @extends {ModelBaseMongoose}
 */
class RoomModel extends ModelBaseMongoose {

	//---------------------------------------------------------------------------
	getModelClass() {
		// subclass overriding function that returns class instance (each subclass MUST implement this)
		return RoomModel;
	}
	//---------------------------------------------------------------------------


	//---------------------------------------------------------------------------
	// global static version info
	static getVersion() { return 1; }

	// collection name for this model
	static getCollectionName() {
		return "rooms";
	}

	static getNiceName() {
		return "Room";
	}

	// name for acl lookup
	static getAclName() {
		return "room";
	}

	// name for logging
	static getLoggingString() {
		return "Room";
	}

	// should some user ACL own each instance?
	static getShouldBeOwned() {
		return true;
	}
	//---------------------------------------------------------------------------


	//---------------------------------------------------------------------------
	static calcSchemaDefinition() {
		const AppModel = jrequire("models/app");
		return {
			...(this.getBaseSchemaDefinition()),
			//
			appid: {
				label: "App Id",
				valueFunction: this.makeModelValueFunctionCrudObjectIdFromList(AppModel, "appid", "appLabel", "applist"),
				// alternative generic way to have crud pages link to this val
				// refModelClass: AppModel,
				mongoose: {
					type: mongoose.Schema.ObjectId,
					required: true,
				},
			},
			shortcode: {
				label: "Shortcode",
				mongoose: {
					type: String,
					unique: true,
					required: true,
				},
			},
			label: {
				label: "Label",
				mongoose: {
					type: String,
				},
			},
			description: {
				label: "Description",
				format: "textarea",
				mongoose: {
					type: String,
				},
			},
			passwordHashed: {
				label: "Password",
				format: "password",
				valueFunction: this.makeModelValueFunctionPasswordAdminEyesOnly(false),
				filterSize: 0,
				mongoose: {
					type: String,
				},
			},
			roles: {
				label: "Roles",
				readOnly: true,
				filterSize: 0,
				valueFunction: this.makeModelValueFunctionRoleOnObjectList(RoomModel),
			},
		};
	}
	//---------------------------------------------------------------------------






	//---------------------------------------------------------------------------
	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
		let reta = [];
		if (operationType === "crudAdd" || operationType === "crudEdit" || operationType === "add") {
			reta = ["appid", "shortcode", "label", "description", "password", "passwordHashed", "disabled", "notes", "extraData"];
		}
		return reta;
	}




	// crud add/edit
	static async doValidateAndSave(jrContext, options, flagSave, user, source, saveFields, preValidatedFields, ignoreFields, obj) {
		// parse form and extrace validated object properies; return if error
		// obj will either be a loaded object if we are editing, or a new as-yet-unsaved model object if adding
		let objdoc;
		const UserModel = jrequire("models/user");

		// set fields from form and validate
		await this.validateMergeAsync(jrContext, "appid", "", source, saveFields, preValidatedFields, obj, true, async (jrr, keyname, inVal, flagRequired) => this.validateModelFieldAppId(jrr, keyname, inVal, user));
		await this.validateMergeAsync(jrContext, "shortcode", "", source, saveFields, preValidatedFields, obj, true, async (jrr, keyname, inVal, flagRequired) => await this.validateRoomShortcodeUnique(jrr, keyname, inVal, obj, source.appid));
		await this.validateMergeAsync(jrContext, "label", "", source, saveFields, preValidatedFields, obj, true, (jrr, keyname, inVal, flagRequired) => jrhValidate.validateString(jrr, keyname, inVal, flagRequired));
		await this.validateMergeAsync(jrContext, "description", "", source, saveFields, preValidatedFields, obj, false, (jrr, keyname, inVal, flagRequired) => jrhValidate.validateString(jrr, keyname, inVal, flagRequired));
		// note that password is not required
		await this.validateMergeAsync(jrContext, "password", "passwordHashed", source, saveFields, preValidatedFields, obj, false, async (jrr, keyname, inVal, flagRequired) => await UserModel.validatePlaintextPasswordConvertToHash(jrr, inVal, flagRequired, true));

		// base fields shared between all? (notes, etc.)
		await this.validateMergeAsyncBaseFields(jrContext, options, flagSave, source, saveFields, preValidatedFields, obj);

		// complain about fields in source that we aren't allowed to save
		await this.validateComplainExtraFields(jrContext, options, source, saveFields, preValidatedFields, ignoreFields);

		// any validation errors?
		if (jrContext.isError()) {
			return null;
		}

		// validated successfully

		if (flagSave) {
			// save it
			objdoc = await obj.dbSave(jrContext);
		}

		// return the saved object
		return objdoc;
	}
	//---------------------------------------------------------------------------


	//---------------------------------------------------------------------------
	// crud add/edit form helper data
	// in case of rooms, this should be the list of APPS that the USER has access to
	static async calcCrudEditHelperData(user, id) {
		// build app list, pairs of id -> nicename
		const AppModel = jrequire("models/app");
		const applist = await AppModel.buildSimpleAppListUserTargetable(user);

		// return it
		return {
			applist,
		};
	}

	// crud helper for view
	static async calcCrudViewHelperData(jrContext, id, obj) {
	// get nice label of the app it's attached to
		let appLabel;
		const appid = obj.appid;
		if (appid) {
			const AppModel = jrequire("models/app");
			const app = await AppModel.mFindOneById(appid);
			if (app) {
				appLabel = app.shortcode + " - " + app.label;
			}
		}
		return {
			appLabel,
		};
	}
	//---------------------------------------------------------------------------


	//---------------------------------------------------------------------------
	static async buildSimpleRoomListUserTargetable(user) {
		// build room list, pairs of id -> nicename, that are targetable to current logged in user
		const roomlist = await this.buildSimpleRoomList(user);
		return roomlist;
	}

	// see http://thecodebarbarian.com/whats-new-in-mongoose-53-async-iterators.html
	static async buildSimpleRoomList(user) {
		const docs = await this.mFindAllAndSelect(null, "_id shortcode label");
		const roomlist = [];
		for (const doc of docs) {
			roomlist[doc._id] = doc.shortcode + " - " + doc.label;
		}

		return roomlist;
	}

	static async buildSimpleRoomIdListUserTargetable(user) {
		const docs = await this.buildSimpleRoomListUserTargetable(user);
		const ids = Object.keys(docs);
		return ids;
	}
	//---------------------------------------------------------------------------







	//---------------------------------------------------------------------------
	static async mFindOneByAppIdAndRoomShortcode(appId, roomShortcode) {
		// first we need to find the app id from the appShortcode
		// now find the room by its appid and shortcode
		const args = {
			appid: appId,
			shortcode: roomShortcode,
		};
		const retv = await this.mFindOne(args);
		return retv;
	}
	//---------------------------------------------------------------------------





	//---------------------------------------------------------------------------
	// delete any ancillary deletions AFTER the normal delete
	static async auxChangeModeById(jrContext, id, mode) {
		// call super callss
		super.auxChangeModeById(jrContext, id, mode);

		// if we are enabling or disabling, then we don't touch rooms
		if (mode === appdef.DefMdbEnable || mode === appdef.DefMdbDisable) {
			// nothing to do
			return;
		}

		// this is a virtual delete or real delete

		// for app model, this means deleting associated rooms
		const roomDataIdList = await this.getAssociatedRoomDatasByRoomId(jrContext, id);
		if (jrContext.isError()) {
			return;
		}

		if (roomDataIdList.length === 0) {
			return;
		}

		// delete them
		const RoomDataModel = jrequire("models/roomdata");
		await RoomDataModel.doChangeModeByIdList(jrContext, roomDataIdList, mode, true);
		if (!jrContext.isError()) {
			const modeLabel = jrhText.capitalizeFirstLetter(appdef.DefStateModeLabels[mode]);
			jrContext.pushSuccess(modeLabel + " " + RoomDataModel.getNiceNamePluralized(roomDataIdList.length) + " attached to " + this.getNiceName() + " #" + id + ".");
		}
	}



	static async getAssociatedRoomDatasByRoomId(jrContext, roomid) {
		// get a list (array) of all room ids that are attached to this app

		const RoomDataModel = jrequire("models/roomdata");
		const roomDataObjs = await RoomDataModel.mFindAllAndSelect({ roomid }, "_id");

		// convert array of objects with _id fields to simple id array
		const roomDataIds = jrhMongo.convertArrayOfObjectIdsToIdArray(roomDataObjs);

		return roomDataIds;
	}
	//---------------------------------------------------------------------------



	//---------------------------------------------------------------------------
	static async validateRoomShortcodeUnique(jrResult, key, val, existingModel, appid) {
		// generic validate of shortcode
		if (val === "$RND") {
			// make a random room shortcode!
			val = await this.makeRandomRoomShortcode(appid);
		}
		return await this.validateShortcodeUnique(jrResult, key, val, existingModel);
	}


	static async makeRandomRoomShortcode(appid) {
		return await this.makeRandomShortcode("shortcode");
	}
	//---------------------------------------------------------------------------








}


// export the class as the sole export
module.exports = RoomModel;