Source: remote-objects/edit.mjs

import RemoteAPI from '../remote-api.mjs';
import RemoteObject from './remote-object.mjs';
import DataSetList from './dataset/list.mjs';
import AttachmentList from './attachment/list.mjs';
import AttachmentObject from './attachment/type.mjs'
import {ParseGentle, ParseHard} from './utils/parsing.mjs';
import DataSetObject from './dataset/dataset-object.mjs';

/**
 * Constructed class Returned by RemoteObjects.openEditObject
 * @extends RemoteObject
 * @property {string} entity - The entity name of the edited record
 * @property {number} key - The key of the edited record
 * @property {number} edithandle - The handle of the edit operation
 * @property {boolean} inserted - True when record is newly inserted in the DB
 * @property {object} data - The master, categories and detail objects available as properties of data
 */
class EditObject extends RemoteObject {
	entity;
	key;
	edithandle;
	data;

	/** @protected */
	commit;
	/** @protected */
	closecontext;
	/** @protected */
	inserted;

	#masterData;
	#categories;
	#details;
	#otherFuncs;
	#dataSetList;
	#attachmentList;

	#isDirty;

	/**
	 * Opens an edit context for the record identified by entity and key.
	 * A context remains memory-resident (on the web server) until it is closed. Always match with a closeContext() call to avoid memory consumption.
	 * @param {RemoteAPI} remoteAPI
	 * @param {number} editHandle - Possibility to pass an existing editHandle
	 * @param {string} entity - The entity name, e.g. "Comp"
	 * @param {number} [key=0] - The key of the record. Use key = 0 to create a new record
	 */
	constructor(remoteAPI, editHandle, entity, key) {
		super(remoteAPI);
		this.entity = entity;
		this.key = ParseGentle.toFloatKey(key)
		this.#dataSetList = new DataSetList(remoteAPI);
		this.#attachmentList = new AttachmentList(remoteAPI);

		this.inserted = (this.key === 0);
		this.edithandle = editHandle > 0 ? editHandle : null;

		this.#resetState();
		this.#setDirty();
	}

	#resetState() {
		this.commit = null;
		this.closecontext = null;
		this.inserted = false;
		this.#masterData = {};
		this.#categories = {};
		this.#details = {};
		this.#otherFuncs = [];

		this.#dataSetList.resetState();
		this.#attachmentList.resetState();
		this.#isDirty = false;
	}
	#setDirty() {
		if (this.#isDirty) return;
		this.api.registerObject(this);
		this.#isDirty = true;
	}

	/**
	 * Retrieves a master [DataSet]{@link Dataset.html} from the edit context.
	 * @returns {DataSetObject}
	 */
	getMasterDataSet() {
		this.#setDirty();
		return this.#dataSetList.getMasterDataSet();
	}

	/**
	 * Retrieves the [DataSet]{@link Dataset.html} for category categoryName. Can be null when the category is not available to the current user.
	 * @param {string} categoryName - name of the category, e.g. "DOCU$INVOICING"
	 * @returns {DataSetObject}
	 */
	getCategoryDataSet(categoryName) {
		this.#setDirty();
		return this.#dataSetList.getCategoryDataSet(categoryName);
	}

	/**
	 * Retrieves a relation [DataSet]{@link Dataset.html} for the specified detail in the edit context.
	 * @param {string} detail - The detail name, e.g. "Comp"
	 * @param {string} [filter=""] - SQL filter expression, e.g. "COMMENT like '%template%'"
	 * @param {boolean} [includeBlobContent=false] - If true, blob fields (e.g. memo, stream) are returned
	 * @returns {DataSetObject}
	 */
	getDetailDataSet(detail, filter = "", includeBlobContent = false) {
		this.#setDirty();
		return this.#dataSetList.getDetailDataSet(detail, filter, includeBlobContent);
	}

	/**
	 * Request attachment from FILES table
	 * @param {number} k_file
	 * @param {number} [version=0]
	 * @returns {AttachmentObject}
	 */
	getAttachment(k_file, version) {
		this.#setDirty();
		return this.#attachmentList.getAttachment(k_file, version);
	}

	/**
	 * Updates the field values of a master data set
	 * @param {string} name
	 * @param {string|number} value
	 */
	updateField(name, value) {
		ParseHard.isFieldValue(value);
		this.#masterData[name] = value;
		this.#setDirty();
	}
	/**
	 * Updates the field values of a master data set.
	 * @param {object} fieldsObj - e.g. {"OPENED": "0"}
	 */
	updateFields(fieldsObj) {
		Object.assign(this.#masterData, ParseGentle.fieldsObj(fieldsObj));
		this.#setDirty();
	}

	/**
	 * Updates the value of a field of any type in a category data set
	 * @param {string} categoryName
	 * @param {string} name
	 * @param {string|number} value
	 */
	updateCategoryField(categoryName, name, value) {
		if (typeof categoryName !== "string") throw new TypeError("EditObject.updateCategoryField::categoryName is not a string");
		ParseHard.isFieldValue(value);

		this.#categories[categoryName] = this.#categories[categoryName] || {};
		this.#categories[categoryName][name] = value;
		this.#setDirty();
	}

	/**
	 * Updates the value of a field of any type in a category data set
	 * @param {string} categoryName
	 * @param {object} fieldsObj - e.g. {"OPENED": "0"}
	 */
	updateCategoryFields(categoryName, fieldsObj) {
		if (typeof categoryName !== "string") throw new TypeError("EditObject.updateCategoryFields::categoryName is not a string");

		this.#categories[categoryName] = this.#categories[categoryName] || {};
		Object.assign(this.#categories[categoryName], ParseGentle.fieldsObj(fieldsObj));
		this.#setDirty();
	}

	/**
	 * Inserts a detail relation
	 * @param {string} detail - The detail name, e.g. "Comp"
	 * @param {number} detailKey - The key of the detail
	 * @param {boolean} [linkMainCompany=false]
	 * @param {boolean} [retrieveName=false]
	 */
	insertDetail(detail, detailKey, linkMainCompany = false, retrieveName = false) {
		if (typeof detail !== "string") throw new TypeError("EditObject.insertDetail::detail is not a string");

		const obj = {
			"@name": "insertDetail",
			"detail": detail,
			"detailkey": detailKey
		}
		if (typeof linkMainCompany === "boolean" && linkMainCompany) obj.maincomp = linkMainCompany;
		if (typeof retrieveName === "boolean" && retrieveName) obj.retrieveName = retrieveName;

		this.#otherFuncs.push(obj);
		this.#setDirty();
	}

	/**
	 * Updates field values of a detail relation. When the detail relation doesn't exist, an exception is thrown.
	 * @param {string} detail - The detail name, e.g. "Comp"
	 * @param {number|string} detailKey - The key of the detail. If detailKey is 0, the current detail record is used
	 * @param {object} fieldsObj, e.g. {"OPENED": "0"}
	 */
	updateDetail(detail, detailKey, fieldsObj) {
		if (typeof detail !== "string") throw new TypeError("EditObject.updateDetail::detail is not a string");

		this.#otherFuncs.push({
			"@name": "updateDetail",
			"detail": detail,
			"detailkey": detailKey,
			"@data": ParseGentle.fieldsObj(fieldsObj)
		});
		this.#setDirty();
	}

	/**
	 * Deletes a detail relation
	 * @param {string} detail - The detail name, e.g. "Comp"
	 * @param {number|string} detailKey - The key of the detail
	 */
	deleteDetail(detail, detailKey) {
		if (typeof detail !== "string") throw new TypeError("EditObject.deleteDetail::detail is not a string");

		this.#otherFuncs.push({
			"@name": "deleteDetail",
			"detail": detail,
			"detailkey": detailKey
		});
		this.#setDirty();
	}

	/**
	 * Clears all relations for the specified detail
	 * @param {string} detail - The detail name, e.g. "Comp"
	 */
	clearDetail(detail) {
		if (typeof detail !== "string") throw new TypeError("EditObject.clearDetail::detail is not a string");

		this.#otherFuncs.push({
			"@name": "clearDetail",
			"detail": detail
		});
		this.#setDirty();
	}

	/**
	 * Activates a category. If the user does not have the appropriate rights on the category, an exception is thrown.
	 * @param {string} categoryName
	 */
	activateCategory(categoryName) {
		if (typeof categoryName !== "string") throw new TypeError("EditObject.activateCategory::categoryName is not a string");

		this.#otherFuncs.push({
			"@name": "activateCategory",
			"category": categoryName
		});
		this.#setDirty();
	}

	/**
	 * Requests that a unique reference number be generated when committing.
	 * @param {number} id - SYS_REFERENCES.K_REFERENCE
	 */
	setReference(id) {
		if (typeof id !== "number") throw new TypeError("EditObject.setReference::id is not a number");

		this.#otherFuncs.push({
			"@name": "reference",
			"id": ParseGentle.toFloatKey(id)
		});
		this.#setDirty();
	}

	/**
	 * Sets the user relations.
	 * @param {array} users - The array of user IDs (keys).
	 * @param {boolean} [clear=false] - If true, clears the current user selection.
	 */
	setUsers(users, clear = false) {
		if (!Array.isArray(users)) throw new TypeError("EditObject.setUsers::users is not an Array");

		const obj = {
			"@name": "setusers",
			"users": users
		}
		if (typeof clear === "boolean") obj.clear = clear;

		this.#otherFuncs.push(obj);
		this.#setDirty();
	}

	/**
	 * Sets the security for a user or group.
	 * @param {number} account - The user or group for which security is added.
	 * @param {number} securityValue - A sum of one or more of the following values: 1 (search), 2 (read), 4 (write), 8 (delete) and 256 (secure). Useful combinations are 7 (read/write), 15 (read/write/delete) and 271 (full control = read/write/delete/secure).
	 */
	setUserSecurity(account, securityValue) {
		if (typeof account !== "number") throw new TypeError("EditObject.setUserSecurity::account is not a number");
		if (typeof securityValue !== "number") throw new TypeError("EditObject.setUserSecurity::securityValue is not a number");

		this.#otherFuncs.push({
			"@name": "setusersecurity",
			"user": account,
			"security": securityValue
		});
		this.#setDirty();
	}

	/**
	 * Inserts an file
	 * @param {number} attachedFileType - 1 = embedded, 2 = linked, 4 = remote, 5 = large
	 * @param {string} path - The path of the file that will be saved in the FILES.PATH field.
	 */
	insertAttachment(attachedFileType, path) {
		if (typeof attachedFileType !== "number") throw new TypeError("EditObject.insertAttachment::attachedFileType is not a number");
		if (typeof path !== "string") throw new TypeError("EditObject.insertAttachment::path is not a string");

		this.#otherFuncs.push({
			"@name": "insertAttachment",
			"type": attachedFileType,
			"path": path
		});
		this.#setDirty();
	}

	/**
	 * Updates an embedded file
	 * @param {number} key - Leave null or 0 to set the stream of the just inserted Attachment
	 * @param {string} base64String
	 */
	updateAttachment(key = 0, base64String) {
		if (typeof key !== "number") throw new TypeError("EditObject.updateAttachment::key is not a number");
		if (typeof base64String !== "string") throw new TypeError("EditObject.updateAttachment::base64String is not a string");

		this.#otherFuncs.push({
			"@name": "updateAttachment",
			"key": key,
			"encodingkind": "MIME64",
			"@data": base64String
		});
		this.#setDirty();
	}

	/**
	 * Copies data from an existing record in the database. The same entity as the current is assumed.
	 * The table views within the index range minIndex to maxIndex are copied. By default, all table views are copied.
	 * To copy a single detail, obtain the table view index using IndexFromDetail and use this value as MinIndex and MaxIndex.
	 * @param {number} key - The key of the source record.
	 * @param {number} [minTableView=0] - The index of first table view to be copied.
	 * @param {number} [maxTableView=999] - The index of last table view to be copied.
	 */
	copyFromExisting(key, minTableView = 0, maxTableView = 999) {
		if (typeof key !== "number") throw new TypeError("EditObject.copyFromExisting::key is not a number");
		if (typeof minTableView !== "number") throw new TypeError("EditObject.copyFromExisting::minTableView is not a number");
		if (typeof maxTableView !== "number") throw new TypeError("EditObject.copyFromExisting::maxTableView is not a number");

		this.#otherFuncs.push({
			"@name": "copyFromExisting",
			"key": key,
			"mintableview": minTableView,
			"maxtableview": maxTableView
		});
		this.#setDirty();
	}

	/**
	 * Commits the changes to the database.
	 */
	commitChanges() {
		this.commit = true;
		this.#setDirty();
	}

	/**
	 * Closes the context and frees the memory on the web server.
	 */
	closeContext() {
		this.closecontext = true;
		this.#setDirty();
	}

	/**
	 * Commits the changes, releases the record and executes closeContext
	 */
	closingCommit() {
		this.commit = true;
		this.closecontext = true;
		this.#setDirty();
	}

	/** @protected */
	asJsonRpc() {
		const requestObject = {
			"#id": this.id,
			"@name": "edit",
			"@func": []
		};

		// lowercase properties are required for the case sensitive JSON RPC
		["entity", "key", "edithandle", "commit", "closecontext"].forEach(property => {
			if (this[property] != null) requestObject[property] = this[property];
		});

		if (typeof this.#details === "object" && Object.keys(this.#details).length > 0) {
			Object.keys(this.#details).forEach(detail => {
				requestObject["@func"].push({
					"@name": "detail",
					"detail": detail
				})
			})
		}

		requestObject["@func"].push(...this.#otherFuncs);

		// Placed after #otherFuncs, because they could have the activateCategory
		if (typeof this.#categories === "object" && Object.keys(this.#categories).length > 0) {
			Object.keys(this.#categories).forEach(categoryName => {
				requestObject["@func"].push({
					"@name": "update",
					"category": categoryName,
					"@data": this.#categories[categoryName]
				})
			})
		}

		requestObject["@func"].push(...this.#dataSetList.funcs);
		requestObject["@func"].push(...this.#attachmentList.funcs);

		if (typeof this.#masterData === "object" && Object.keys(this.#masterData).length > 0) {
			requestObject["@func"].push({
				"@name": "update",
				"@data": this.#masterData
			})
		}

		return requestObject;
	}

	/** @protected */
	afterExecute() {
		super.afterExecute();

		// lowercase properties are required for the case sensitive JSON RPC
		["entity", "key", "edithandle", "commit", "closecontext"].forEach(property => {
			this[property] = this.responseObject[property];
		});

		this.#dataSetList.setResponseObject(this.responseObject);
		this.#dataSetList.afterExecute();
		this.#dataSetList.setData(this);

		this.#attachmentList.setResponseObject(this.responseObject);
		this.#attachmentList.afterExecute();

		this.#resetState();
	}
}

export default EditObject;