Home Reference Source Repository

src/collection/ModelProperty.js


import ObjectProperty from "../ObjectProperty";
import PrimitiveProperty from "../PrimitiveProperty";
import shadow from "../decorators/shadow";
import shadowBound from "../decorators/shadowBound";
import StateType from "../StateType";

import ModelAccess from "./ModelAccess";


/*
	@ignore
*/

export default class ModelProperty extends ObjectProperty {
	constructor(stateType=ModelProperty.type) {
		super(stateType);
	}

	/*
		Used by CollectionProperty to obtain the a model definition suitable for creating a
		ModelProperty instance.
	*/
	static modelDefinitionFor(state, collection) {
		const id = collection.extractId(state);
		const cid = id || collection.makeId();

		return {
			id: id || cid,
			cid: cid,
			data: state,
			dirty: false,
			lastReqId: -1,
			waiting: false,
		}
	}

	get cid() {
		return this._() && this._().cid;
	}

	get collection() {
		return this.parent().parent();
	}

	get data() {
		return this._() && this._().data;
	}

	/*
		Gets the ID by which the collection is tracking the model. This will be the persistent storage
		ID or an ID created by the collection
	*/
	get id() {
		return this._() && this._().id;
	}

	@shadow
	changeId(id) {
		if (this._() && this._().id !== id) {
			this._().id = id;
		}
	}

	clearCheckpoint() {
		const { collection, data } = this;
		const dataProp = data && data.$$();

		if (collection.isAutocheckpoint() && dataProp) {
			dataProp.clearCheckpoint();
		}
	}

	clearDirty() {
		if (this._()) {
			this._().dirty = false;
		}

		this.clearCheckpoint();
	}

	resetDataToCheckpoint() {
		const dataProp = this.data && this.data.$$();

		dataProp && dataProp.resetToCheckpoint();

		if (this._()) {
			this._().dirty = false;
		}
	}

	@shadowBound
	defaults(data) {
		const id = this.collection.extractId(data);
		const state = this._();
		const currDirty = state.$().nextState().dirty;

		// update the ID if it has changed
		if (this._().id !== id) {
			this._().id = id;
		}

		this._().data.__().defaults(data);

		// reset to not dirty if was not dirty before the merge since assuming data being merged
		// is coming from a source of truth, such as data returned from a save
		if (!currDirty) {
			this.clearDirty();
		}
	}

	@shadowBound
	destroy() {
		return this.collection.destroy(this.cid);
	}

	@shadowBound
	isWaiting() {
		return this._() && this._().waiting;
	}

	isDirty() {
		return this._() && this._().dirty;
	}

	/*
		Gets if this model has not yet been saved to persistent storage. Assignment of an ID property
		is used to determine if an object has been saved.
	*/
	@shadowBound
	isNew() {
		const id = this.collection.extractId(this.data);

		return !id;
	}

	@shadowBound
	merge(data) {
		const id = this.collection.extractId(data);
		const state = this._();
		const currDirty = state.$().nextState().dirty;

		// update the ID if it has changed
		if (state.id !== id) {
			state.id = id;
		}

		state.data.__().merge(data);

		// reset to not dirty if was not dirty before the merge since assuming data being merged
		// is coming from a source of truth, such as data returned from a save
		if (!currDirty) {
			this.clearDirty();
		}
	}

	@shadowBound
	save() {
		return this.collection.save(this.cid);
	}

	setBusy(busy) {
		if (busy === this[_busy]) { return }

		this[_busy] = busy;
		this.touch();
	}

	@shadowBound
	setData(data) {
		const id = this.collection.extractId(state);
		const state = this._();

		// set state first since we trap the invalidate() call and set the dirty flag
		state.data = data;

		// now we can explicitly set the dirty flag
		this.clearDirty();

		// update the id if it has changed
		if (state.id !== id) {
			state.id = id;
		}
	}

	//------------------------------------------------------------------------------------------------------
	// Property subclasses may want to override success methods
	//------------------------------------------------------------------------------------------------------

	/*
		Creates a ModelAccess for this property's implementations. ModelAccess will generate a ShadowModelAccess
		for its 'data' child which will in turn generate a ShadowModelAccess for each of its children.
	*/
	create$(impl) {
		return new ModelAccess(impl, this);
	}

	propertyChildInvalidated(childProperty) {
		if (childProperty.__().name() == "data" && !this._().dirty) {
			this._().dirty = true;

			if (this.collection.isAutocheckpoint && !childProperty.hasCheckpoint()) {
				childProperty.checkpoint();
			}
		}
	}
}


StateType.defineType(ModelProperty, spec => {
		spec.initialState({})
			.readonlyOff
			.typeName("ModelProperty")
	});