struct/Entity.js

const { CMDS } = require("./constants");
const Collection = require("./Collection");
const Berryfi = require("../Berryfi");
const BaseType = require("./BaseType");
const Field = require("./Field");
const App = require("./App");
const Type = require("./Type");
const uuid = require("uuid").v1;

/**
 * Represents an Entity / a collection of pieces of data, like a row in a table view, a person, or a card with information.
 * @prop {Berryfi} berryfi The Berryfi client instance. Circular reference.
 * @prop {Type} parentType The Type this Entity is a child of
 * @prop {object} data The data on this Entity. Object is identical to the API query result.
 * @prop {number} flag The flag value for this type
 * @prop {string} field A proxy to get/set data in the current Entity. E.g. "entity.field['user email']" or "entity.field.Name = 'Hello!'". Setter throws error if field does not exist.
 * @prop {Berryfi} berryfi The Berryfi client instance
 */
class Entity {
	/**
	 * Creates a new entity
	 * @param {Berryfi} berryfi The Berryfi client instance
	 * @param {Type} parentType The Type this Entity is a child of
	 * @param {object} data The data to populate this entity
	 */
	constructor(berryfi, parentType, data) {
		if (!data) throw new Error("Missing data");
		if (!berryfi) throw new Error("Missing berryfi");
		if (!berryfi.constructor.name === "Berryfi") throw new TypeError("'berryfi' parameter must be of the instance of Berryfi");
		if (!parentType) throw new Error("Missing parentType");
		if (!parentType.constructor.name === "Type") throw new TypeError("'parentType' parameter must be of the instance of Type");

		this.berryfi = berryfi;
		this.parentType = parentType;

		this._data = data;

		if (!this._data["fibery/id"]) this._data["fibery/id"] = uuid();

		/**
		 * The flags value for this thing. Is set when loaded by Berryfi.pull()
		 * @type {number}
		 * @public
		 * @abstract
		 */
		this.flag = undefined;
		this.field = new Proxy(this, {
			get: (target, property) => {
				let r = this.findFieldName(property);
				return r && this._data[r] ? this._data[r] : this._data[property] ? this._data[property] : this._data[this.berryfi.stentenceCase(property)] ? this._data[this.berryfi.stentenceCase(property)] : undefined;
			},
			set: (target, property, value) => {
				let r = this.findFieldName(property);
				if (!r) throw new Error(`Invalid property '${this.property}'. Valid ones are ${Object.keys(this.toObject()).join(", ")}`);
				if (this.parentType.readOnly.has(r)) throw new Error(`The field '${r}' is read only`);
				this._data[r] = value;
			},
			deleteProperty: (target, property) => {
				let r = this.findFieldName(property);
				delete this._data[r];
				return true;
			}
		});

		this.validate();
	}

	/**
	 * Checks the Fields associated with this Entity's parent Type to resolve the Fibery name of a field
	 * @param {string} name The field to get Fibery name of
	 * @returns {string}
	 */
	findFieldName(name) {
		if (!name) return undefined;

		if (this.parentType.fieldNames.has(name)) return name;
		for (let n of this.parentType.fieldNames.values()) if (n.toLowerCase().split("/")[1] === name.toLowerCase()) return n;

		return undefined;
	}

	/**
	 * Return the object data reference for this Entity
	 * @public
	 * @type {object}
	 * @returns {object}
	 */
	get fields() {
		return this._data;
	}
	/**
	 * Boolean - Check if this is a user created App
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isApp() {
		return !!(this.berryfi.flags.app & this.flag);
	}
	/**
	 * Boolean - Check if this is a Type inside an App
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isType() {
		return !!(this.berryfi.flags.type & this.flag);
	}
	/**
	 * Boolean - Check if this is a field inside a Type
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isField() {
		return !!(this.berryfi.flags.field & this.flag);
	}
	/**
	 * Boolean - Check if this is an entity
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isEntity() {
		return !!(this.berryfi.flags.entity & this.flag);
	}
	/**
	 * Boolean - Check if this type is directly created by a user
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isUserCreated() {
		return !!(this.berryfi.flags.userCreated & this.flag);
	}
	/**
	 * Boolean - Check if this is a Fibery helper type
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isAuxiliaryType() {
		return !!(this.berryfi.flags.auxiliaryType & this.flag);
	}
	/**
	 * Boolean - Check if this is a Fibery helper field
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isAuxiliaryField() {
		return !!(this.berryfi.flags.auxiliaryField & this.flag);
	}
	/**
	 * Boolean - Check if this is a Fibery helper type or field
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isAuxiliary() {
		return !!(this.berryfi.flags.auxiliaryField | this.berryfi.flags.auxiliaryType & this.flag);
	}
	/**
	 * Boolean - Check if this is a primitive Fibery type
	 * @public
	 * @type {boolean}
	 * @returns {boolean}
	 */
	get isPrimitive() {
		return !!(this.berryfi.flags.primitive & this.flag);
	}
	/**
	 * Number - Returns the Entity["fibery/rank"] value
	 * @public
	 * @type {number}
	 * @returns {number}
	 */
	get rank() {
		return this._data["fibery/rank"];
	}
	/**
	 * String - Returns the Entity["fibery/public-id"] value
	 * @public
	 * @type {string}
	 * @returns {string}
	 */
	get publicId() {
		return this._data["fibery/public-id"];
	}
	/**
	 * Date - Returns the date the entity was modified
	 * @returns {Date}
	 * @type {Date}
	 */
	get modified() {
		return this._data["fiber/modification-date"] ? new Date(this._data["fiber/modification-date"]) : null;
	}
	/**
	 * Returns the UUID of this Entitiy if it has one
	 * @type {string}
	 * @returns {string}
	 */
	get id() {
		return this._data["fibery/id"];
	}
	/**
	 * Returns the creation date of this Entitiy if it has an UUID
	 * @type {Date}
	 * @returns {Date}
	 */
	get created() {
		return this.berryfi.toDate(this.id);
	}
	/**
	 * Returns the name of the App this Entity is a child of. The name and label is the same for apps.
	 * @type {string}
	 * @returns {string}
	 */
	get appName() {
		return this.parentType.parentApp.label;
	}
	/**
	 * Returns the name of the Type this Entity is a child of. The Fibery API name, not UI readable name.
	 * @type {string}
	 * @returns {string}
	 */
	get typeName() {
		return this.parentType.name;
	}
	/**
	 * Returns the label of the Type this Entity is a child of. The UI readable name instead of the Fibery name.
	 * @type {string}
	 * @returns {string}
	 */
	get typeLabel() {
		return this.parentType.label;
	}

	/**
	 * Returns a pure object of the entity and its fields. Basically strips out `Appname/, fibery/, ui/, app/, etc..` to "clean up" the keys.
	 * @param {object} [input] Default: the current entity's data. The data to convert from fibery notion to normal. Max 2 levels deep.
	 * @returns {object}
	 * @example console.log(personEntity); // {"fibery/id": "123", "fibery/name": "John Doe", "Person/age": 24, "Person/workplaces": [{"Companies/name": "Fibery"}], "fibery/public-id": "1"};
	 * console.log(personEntity.toObject()); // {id: "123", name: "John Doe", age: 24, workplaces: [{name: "Fibery"}], "public-id": "1"};
	 */
	toObject(input) {
		if (!input) input = this._data;
		let o = Object();
		for (let k in input) {
			let nk = k.includes("/") ? k.split("/")[1] : k;
			if (Array.isArray(input[k])) {
				o[nk] = Array();
				input[k].forEach(i =>
					i.constructor.name === "Object" ?
						Object.keys(i).forEach(sk => o[nk].push({ [sk.includes("/") ? sk.split("/")[1] : sk]: i[sk] }))
						: o[nk].push(i));
			} else if (input[k].constructor.name === "Object") {
				o[nk] = {};
				for (let sk in input[k]) o[nk][sk.includes("/") ? sk.split("/")[1] : sk] = input[k][sk];
			} else o[nk] = input[k];
		}
		return o;
	}

	/**
	 * Inserts this entity in to Fibery.
	 * @async
	 * @example const mike = people.makeEntity({
	 * 	name: "Mike",
	 * 	age: 24
	 * });
	 * await mike.insert(); // Create new
	 * mike.field.email = "mike@email.com";
	 * await mike.push(); // Update state
	 */
	async insert() {

	}
	

	/**
	 * Pulls the current state of this Entity from Fibery and update itself
	 * @async
	 */
	async pull() {
		let r = await this.berryfi.exec([{
			command: CMDS.getEntity,
			args: {
				query: {
					"q/from": this.parentType.name,
					"q/where": ["==", ["fibery/id"], this.id],
					"q/limit": 1,
					"q/select": Object.keys(this.meta)
				}
			}
		}]);

		this._data = r.result[0];
		this.validate();
	}

	/**
	 * Push the current state of the entity to Fibery.
	 * Undefined/omitted fields will be left out of the update, so they will remain as-is in Fibery.
	 * Nulled values in this Entity will erase the values in Fibery.
	 * @async
	 * @return {Array<object>} The result API response of the update query
	 * @example 
	 * mike.field.email = "mike@email.com";
	 * await mike.push(); // Update state
	 * mike.field.age = 25;
	 * await mike.push() // … another update
	 */
	async push() {
		// Dummy to avoid mutating reference
		let holder = this.validate();

		if (!this.parentType.readOnly.size) {
			console.log("SHIT IS EMPTY AGAIN");
			return;
		}

		// Remove 'undefined' fields
		for (let key in holder) {
			if (key==="fibery/id") continue;
			if (this.parentType.readOnly.has(key)) delete holder[key];			
			if (holder[key] === undefined) delete holder[key];
		}

		let r = await this.berryfi.exec([{
			command: CMDS.updateEntity,
			args: {
				type: this.parentType.name,
				entity: holder
			}
		}]);

		return r;
	}

	/**
	 * Corrects and checks if the schema is valid based on the field schema in cache. 
	 * Updates its own state, but also return a de-referenced object of the object.
	 * Throws on if invalid, with an error stating what is invalid. 
	 * You can use this method in a try...catch before sending to capture errors early. 
	 * When sending, this method will be used automatically as well.
	 * @throws {Error} If invalid, with an error stating what is invalid.
	 * @returns {object}
	 */
	validate() {
		if (!this._data || !Object.keys(this._data).length) return;
		let holder = Object();

		for (let key in this._data) {
			var k = this.findFieldName(key);
			if (!k) throw new Error(`Unable to find field name for the key '${key}'.`);
			holder[k] = this._data[key];
		}
		this._data = holder;
		return JSON.parse(JSON.stringify(holder));
	}
};
module.exports = Entity;