struct/Type.js

const Collection = require("./Collection");
const Berryfi = require("../Berryfi");
const BaseType = require("./BaseType");
const Field = require("./Field");
const Entity = require("./Entity");
const App = require("./App");

/**
 * Represents a user-created Type
 * @extends {BaseType}
 * @prop {Berryfi} berryfi The Berryfi client instance. Circular reference.
 * @prop {Collection<UIUID, Entity>} entities A Collection of Entities that are derived from this Type
 * @prop {Collection<UUID, Field>} fields A Collection of Fields that belong to this Type
 * @prop {App} parentApp The App this Type is a child of
 * @prop {string} name The Fibery name of this type. 'some/name'
 * @prop {object} meta The Fibery meta of this type
 * @prop {string} id The ID of this tupe. If App, ID is the app's label.
 * @prop {number} flag The flag value for this type
 * @prop {string} label A readable name, often the one shown in Fibery's UI
 * @prop {Set} fieldNames A set of Fibery field names this type has
 * @prop {Set} readOnly A set of Fibery field names that are 'read only' and cannot be modified
 * @prop {string} [type] The Fibery type. Only present in Fields.
 * @prop {Array<object>} [fields] The Fibery Fields array if this is a non-primitive Type.
 */
class Type extends BaseType {
	/**
	 * Creates a new Type
	 * @param {Berryfi} berryfi The Berryfi client instance
	 * @param {object} data The data to populate this type
	 */
	constructor(berryfi, data) {
		if (!data) throw new Error("Missing data");
		super(berryfi, data);

		this.fieldNames = new Set();
		this.readOnly = new Set();
		
		/**
		 * A Collection of Entities that are derived from this Type
		 * @type {Collection<UIUID, Entity>}
		 */
		this.entities = new Collection();
		//TODO: This conflict with top data
		/**
		 * A Collection of Fields that belong to this Type
		 * @type {Collection<UUID, Field>}
		 */
		this.fields = new Collection();
	}

	/**
	 * Registers a Field or Entity as a child of this Type, and this Type as their parent.
	 * @param {Field|Entity} input A Field or Entity to register in to this Type
	 * @returns {Type}
	 */
	append(input) {
		if (!input.berryfi) input.berryfi = this.berryfi;
		input.parentType = this;
		// console.log(input)
		if (input instanceof Field) {
			// Update field names
			this.fieldNames.add(input.name);
			if (input.isReadOnly) this.readOnly.add(input.name);

			this.fields.set(input.id, input);
		} else if (input instanceof Entity) {
			// Update field names
			if (Object.keys(input.data).length) Object.keys(input.data).forEach(e => this.fieldNames.add(e));

			if (input.isReadOnly) this.readOnly.add(input.name);
			this.entities.set(input.id, input);
		} else throw new TypeError(`Invalid type passed. Must be either a Field or an Entitiy, got '${input.constructor.name}'`);
		
		return this;
	}

	/**
	 * Direct insertion of data, useful if you know the scheme and how to do it.  
	 * Performs a single or batch insert based on if you pass an array of entities or a single object.  
	 * The entities returned from the API are also converted to Entities and stored in Type.entities collection behind the scenes, so you can access the resulted Entities from there as well.
	 * @param {Array<object>|object} data The raw data to pass in to a `fibery.entity/create`. If multiple, will be wrapped around by a batch.
	 * @param {boolean} [returnEntities=false] If set to true, will return an array of created Entities (even if only 1) instead of array of API request results.
	 * @async
	 * @returns {Array<object>} Raw API response
	 * @example let res = await contactType.insert([{
	 * 	"My App/Name": "John Doe",
	 * 	"My App/User email": "john@doe.com"
	 * }, {
	 * 	"My App/Name": "Ola Normann",
	 * 	"My App/User email": "ola@normann.no"
	 * }]);
	 * console.log(res) // [{success:true, result: [Object]}, {success:true, result: [Object]}]
	 * 
	 * // Return array of entity references instead of API result
	 * let rob = await contactType.insert({
	 * 	"My App/Name": "Rob Robinson",
	 * 	"My App/User email": "rob@robinson.net"
	 * }, true);
	 * console.log(rob[0].constructor.name, rob[0].field["User email"]) // "Entity" "rob@robinson.net"
	 */
	async insert(data, returnEntities=false) {
		let r;
		if (Array.isArray(data)) {
			r = await this.berryfi.exec([{
				command: "fibery.command/batch",
				args: {
					commands: data.map(e => {
						return {
							command: "fibery.entity/create",
							args: {
								type: this.name,
								entity: e
							}
						}
					})
				}
			}])
		} else {
			r = await this.berryfi.exec([{
				command: "fibery.entity/create",
				args: {
					type: this.name,
					entity: data
				}
			}]);
		}

		let ents = Array();

		r.forEach(res => {
			const ent = new Entity(this.berryfi, this, res);
			ent.flag = ent.flag | this.berryfi.flags.userCreated | this.berryfi.flags.entity;

			if (returnEntities) ents.push(ent);

			this.entities.set(ent.id, ent);
		});

		return returnEntities ? ents : r;
	}

	/**
	 * Creates a new entity and adds it to cache. By later passing `entity.validate()` you can check if you are using invalid fields.
	 * @param {object} data The data that makes up this new entity
	 * @returns {Entity}
	 */
	makeEntity(data) {
		var ent = new Entity(this.berryfi, this, data);
		ent.flag = ent.flag|this.berryfi.flags.userCreated|this.berryfi.flags.entity;
		this.entities.set(ent.id, ent);
		return ent;
	}
};
module.exports = Type;