@nymphjs/nymph
Version:
Nymph.js - Nymph ORM
962 lines • 34.3 kB
JavaScript
import { difference, intersection, isEqual } from 'lodash-es';
import { guid } from '@nymphjs/guid';
import { EntityConflictError, EntityIsSleepingReferenceError, InvalidParametersError, InvalidStateError, } from './errors/index.js';
import { entitiesToReferences, referencesToEntities, uniqueStrings, } from './utils.js';
/**
* Database abstraction object.
*
* Provides a way to access, manipulate, and store data in Nymph.
*
* The GUID is not set until the entity is saved. GUIDs must be unique forever,
* even after deletion. It's the job of the Nymph DB driver to make sure no two
* entities ever have the same GUID. This is generally done by using a large
* randomly generated ID.
*
* Each entity class has an etype that determines which table(s) in the database
* it belongs to. If two entity classes have the same etype, their data will be
* stored in the same table(s). This isn't a good idea, however, because
* references to an entity store a class name, not an etype.
*
* Tags are used to classify entities. Where an etype is used to separate data
* by tables, tags can be used to separate entities within a table. You can
* define specific tags to be protected, meaning they cannot be added/removed
* from the client. It can be useful to allow user defined tags, such as for
* blog posts.
*
* Simply calling $delete() will not unset the entity. It will still take up
* memory. Likewise, simply calling unset will not delete the entity from the
* DB.
*
* Some notes about $equals() and $is(), the replacements for "==":
*
* The == operator will likely not give you the result you want, since two
* instances of the same entity will fail that check, even though they represent
* the same data in the database.
*
* $equals() performs a more strict comparison of the entity to another. Use
* $equals() instead of the == operator when you want to check both the entities
* they represent, and the data inside them. In order to return true for
* $equals(), the entity and object must meet the following criteria:
*
* - They must be entities.
* - They must have equal GUIDs, or both must have no GUID.
* - Their data and tags must be equal.
*
* $is() performs a less strict comparison of the entity to another. Use $is()
* instead of the == operator when the entity's data may have been changed, but
* you only care if they represent the same entity. In order to return true, the
* entity and object must meet the following criteria:
*
* - They must be entities.
* - They must have equal GUIDs, or both must have no GUID.
* - If they have no GUIDs, their data and tags must be equal.
*
* Some notes about saving entities in other entity's properties:
*
* Entities use references in the DB to store an entity in their properties. The
* reference is stored as an array with the values:
*
* - 0 => The string 'nymph_entity_reference'
* - 1 => The referenced entity's GUID.
* - 2 => The referenced entity's class name.
*
* Since the referenced entity's class name (meaning the `class` static
* property, not the name of the class itself) is stored in the reference on the
* parent entity, if you change the class name in an update, you need to
* reassign all referenced entities of that class and resave.
*
* When an entity is loaded, it does not request its referenced entities from
* Nymph. Instead, it creates instances without data called sleeping references.
* When you first access an entity's data, if it is a sleeping reference, it
* will fill its data from the DB. You can call clearCache() to turn all the
* entities back into sleeping references.
*/
export default class Entity {
/**
* The instance of Nymph to use for queries.
*/
static nymph = {};
/**
* A unique name for this type of entity used to separate its data from other
* types of entities in the database.
*/
static ETYPE = 'entity';
/**
* The lookup name for this entity.
*
* This is used for reference arrays (and sleeping references) and client
* requests.
*/
static class = 'Entity';
$nymph;
guid = null;
cdate = null;
mdate = null;
tags = [];
/**
* The data proxy handler.
*/
$dataHandler;
/**
* The actual data store.
*/
$dataStore;
/**
* The actual sdata store.
*/
$sdata;
/**
* The data proxy object.
*/
$data;
/**
* Whether this instance is a sleeping reference.
*/
$isASleepingReference = false;
/**
* The reference to use to wake.
*/
$sleepingReference = null;
/**
* A promise that resolved when the entity's data is wake.
*/
$wakePromise = null;
/**
* Properties that will not be serialized into JSON with toJSON(). This
* can be considered a denylist, because these properties will not be set
* with incoming JSON.
*
* Clients CAN still determine what is in these properties, unless they are
* also listed in searchRestrictedData.
*/
$privateData = [];
/**
* Whether this entity should publish changes to PubSub servers.
*/
static pubSubEnabled = true;
/**
* Whether this entity should be accessible on the frontend through the REST
* server.
*
* If this is false, any request from the client that attempts to use this
* entity will fail.
*/
static restEnabled = true;
/**
* Properties that will not be searchable from the frontend. If the frontend
* includes any of these properties in any of their clauses, they will be
* filtered out before the search is executed.
*/
static searchRestrictedData = [];
/**
* Properties that can only be modified by server side code. They will still
* be visible on the frontend, unlike $privateData, but any changes to them
* that come from the frontend will be ignored.
*
* In addition to what's listed here, all of the access control properties
* will be included when Tilmeld is being used. These are:
*
* - acUser
* - acGroup
* - acOther
* - acRead
* - acWrite
* - acFull
* - user
* - group
*
* You should modify these through client enabled methods or the $save method
* instead, for safety.
*/
$protectedData = [];
/**
* If this is defined, then it lists the only properties that will be
* accepted from incoming JSON. Any other properties will be ignored.
*
* If you use an allowlist, you don't need to use protectedData, since you
* can simply leave those entries out of allowlistData.
*/
$allowlistData;
/**
* Tags that can only be added/removed by server side code. They will still be
* visible on the frontend, but any changes to them that come from the
* frontend will be ignored.
*/
$protectedTags = [];
/**
* If this is defined, then it lists the only tags that will be accepted from
* incoming JSON. Any other tags will be ignored.
*/
$allowlistTags;
/**
* The names of methods allowed to be called by the frontend with serverCall.
*/
$clientEnabledMethods = [];
/**
* The names of static methods allowed to be called by the frontend with
* serverCallStatic.
*/
static clientEnabledStaticMethods = [];
/**
* Whether to use "skipAc" when accessing entity references.
*/
$skipAc = false;
/**
* The AC properties' values when the entity was loaded.
*/
$originalAcValues = null;
/**
* This is used to hold a generated GUID for a new entity.
*/
$guaranteedGUID = null;
/**
* Initialize an entity.
*/
constructor(..._rest) {
this.$nymph = this.constructor.nymph;
this.$dataHandler = {
has: (data, name) => {
this.$check();
return name in data || name in this.$sdata;
},
get: (data, name) => {
this.$check();
if (this.$sdata.hasOwnProperty(name)) {
data[name] = referencesToEntities(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
delete this.$sdata[name];
}
if (data.hasOwnProperty(name)) {
return data[name];
}
return undefined;
},
set: (data, name, value) => {
this.$check();
if (this.$sdata.hasOwnProperty(name)) {
delete this.$sdata[name];
}
data[name] = value;
return true;
},
deleteProperty: (data, name) => {
this.$check();
if (this.$sdata.hasOwnProperty(name)) {
return delete this.$sdata[name];
}
if (data.hasOwnProperty(name)) {
return delete data[name];
}
return true;
},
defineProperty: (data, name, descriptor) => {
this.$check();
if (this.$sdata.hasOwnProperty(name)) {
delete this.$sdata[name];
}
Object.defineProperty(data, name, descriptor);
return true;
},
getOwnPropertyDescriptor: (data, name) => {
this.$check();
if (this.$sdata.hasOwnProperty(name)) {
data[name] = referencesToEntities(JSON.parse(this.$sdata[name]), this.$nymph, this.$skipAc);
delete this.$sdata[name];
}
return Object.getOwnPropertyDescriptor(data, name);
},
ownKeys: (data) => {
this.$check();
return Object.getOwnPropertyNames(data).concat(Object.getOwnPropertyNames(this.$sdata));
},
};
this.$dataStore = {};
this.$sdata = {};
this.$data = new Proxy(this.$dataStore, this.$dataHandler);
return new Proxy(this, {
has: (entity, name) => {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
return name in entity;
}
this.$check();
return name in entity.$data;
},
get: (entity, name) => {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
if (name === 'tags' || name === 'cdate' || name === 'mdate') {
this.$check();
}
return entity[name];
}
this.$check();
if (name in entity.$data) {
return entity.$data[name];
}
return undefined;
},
set: (entity, name, value) => {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
if (name === 'tags' || name === 'cdate' || name === 'mdate') {
this.$check();
}
entity[name] = value;
}
else {
this.$check();
entity.$data[name] = value;
}
return true;
},
deleteProperty: (entity, name) => {
if (name in entity) {
return delete entity[name];
}
else if (name in entity.$data) {
this.$check();
return delete entity.$data[name];
}
return true;
},
getPrototypeOf: (entity) => {
return entity.constructor.prototype;
},
defineProperty: (entity, name, descriptor) => {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
if (name === 'tags' || name === 'cdate' || name === 'mdate') {
this.$check();
}
Object.defineProperty(entity, name, descriptor);
}
else {
this.$check();
Object.defineProperty(entity.$data, name, descriptor);
}
return true;
},
getOwnPropertyDescriptor: (entity, name) => {
if (typeof name !== 'string' ||
name in entity ||
name.substring(0, 1) === '$') {
if (name === 'tags' || name === 'cdate' || name === 'mdate') {
this.$check();
}
return Object.getOwnPropertyDescriptor(entity, name);
}
else {
this.$check();
return Object.getOwnPropertyDescriptor(entity.$data, name);
}
},
ownKeys: (entity) => {
this.$check();
return Object.getOwnPropertyNames(entity).concat(Object.getOwnPropertyNames(entity.$data));
},
});
}
/**
* Create or retrieve a new entity instance.
*
* Note that this will always return an entity, even if the GUID is not found.
*
* @param guid An optional GUID to retrieve.
*/
static async factory(guid) {
const entity = new this();
if (guid != null) {
const entity = await this.nymph.getEntity({
class: this,
}, { type: '&', guid });
if (entity != null) {
return entity;
}
}
return entity;
}
/**
* Create a new entity instance.
*/
static factorySync() {
return new this();
}
/**
* Create a new sleeping reference instance.
*
* Sleeping references won't retrieve their data from the database until they
* are readied with `$wake()` or a parent's `$wakeAll()`.
*
* @param reference The Nymph Entity Reference to use to wake.
* @returns The new instance.
*/
static factoryReference(reference) {
const entity = new this();
entity.$referenceSleep(reference);
return entity;
}
/**
* Get an array of strings that **must** be unique across the current etype.
*
* When you try to save another entity with any of the same unique strings,
* Nymph will throw an error.
*
* The default implementation of this static method instantiates the entity,
* assigns all of the given data, then calls `$getUniques` and returns its
* output. This can have a performance impact if a lot of extra processing
* happens during any of these steps. You can override this method to
* calculate the unique strings faster, but you must return the same strings
* that would be returned by `$getUniques`.
*
* @returns Resolves to an array of entity's unique constraint strings.
*/
static async getUniques({ guid, cdate, mdate, tags, data, sdata, }) {
const entity = new this();
if (guid != null) {
entity.guid = guid;
}
if (cdate != null) {
entity.cdate = cdate;
}
if (mdate != null) {
entity.mdate = mdate;
}
entity.tags = tags;
entity.$putData(data, sdata);
return await entity.$getUniques();
}
toJSON() {
if (this.$isASleepingReference) {
return this.$sleepingReference;
}
const obj = {
class: this.constructor.class,
guid: this.guid,
cdate: this.cdate,
mdate: this.mdate,
tags: [...this.tags],
data: {},
};
for (const key of [
...Object.keys(this.$dataStore),
...Object.keys(this.$sdata),
]) {
if (this.$privateData.indexOf(key) === -1) {
obj.data[key] = entitiesToReferences(this.$data[key], true);
}
}
return obj;
}
$getGuaranteedGUID() {
if (this.guid !== null) {
return this.guid;
}
if (this.$guaranteedGUID == null) {
this.$guaranteedGUID = guid();
}
return this.$guaranteedGUID;
}
$addTag(...tags) {
this.$check();
if (tags.length < 1) {
return;
}
this.tags = uniqueStrings([...this.tags, ...tags]);
}
$arraySearch(array, strict = false) {
this.$check();
if (!Array.isArray(array)) {
return -1;
}
for (let i = 0; i < array.length; i++) {
const curEntity = array[i];
if (strict ? this.$equals(curEntity) : this.$is(curEntity)) {
return i;
}
}
return -1;
}
$clearCache() {
this.$check();
this.$putData(this.$getData(false, true), this.$getSData());
}
$getClientEnabledMethods() {
return this.$clientEnabledMethods;
}
async $delete() {
this.$check();
return await this.$nymph.deleteEntity(this);
}
$equals(object) {
this.$check();
if (!(object instanceof Entity)) {
return false;
}
if (this.guid || object.guid) {
if (this.guid !== object.guid) {
return false;
}
}
if (object.cdate !== this.cdate) {
return false;
}
if (object.mdate !== this.mdate) {
return false;
}
const obTags = [...object.tags].sort();
const myTags = [...this.tags].sort();
if (!isEqual(obTags, myTags)) {
return false;
}
const obData = object.$getData(true, true);
const myData = this.$getData(true, true);
return isEqual(obData, myData);
}
$getData(includeSData = false, referenceOnlyExisting) {
this.$check();
if (includeSData) {
// Access all the serialized properties to initialize them.
for (const key in this.$sdata) {
const _unused = this[key];
}
}
return entitiesToReferences({ ...this.$dataStore }, referenceOnlyExisting);
}
$getSData() {
this.$check();
return this.$sdata;
}
async $getUniques() {
return [];
}
$getOriginalAcValues() {
this.$check();
return this.$originalAcValues ?? this.$getCurrentAcValues();
}
$getCurrentAcValues() {
this.$check();
return {
user: this.$getAcUid(),
group: this.$getAcGid(),
acUser: this.$data.acUser ?? null,
acGroup: this.$data.acGroup ?? null,
acOther: this.$data.acOther ?? null,
acRead: this.$getAcReadIds(),
acWrite: this.$getAcWriteIds(),
acFull: this.$getAcFullIds(),
};
}
$getAcUid() {
if ('user' in this.$sdata) {
const userValue = JSON.parse(this.$sdata.user);
if (Array.isArray(userValue) &&
userValue[0] === 'nymph_entity_reference') {
return userValue[1];
}
}
return (this.$data.user?.guid ?? null);
}
$getAcGid() {
if ('group' in this.$sdata) {
const groupValue = JSON.parse(this.$sdata.group);
if (Array.isArray(groupValue) &&
groupValue[0] === 'nymph_entity_reference') {
return groupValue[1];
}
}
return (this.$data.group?.guid ?? null);
}
$getAcReadIds() {
return (this.$data.acRead?.map((entity) => entity.guid) ??
null);
}
$getAcWriteIds() {
return (this.$data.acWrite?.map((entity) => entity.guid) ??
null);
}
$getAcFullIds() {
return (this.$data.acFull?.map((entity) => entity.guid) ??
null);
}
$getValidatable() {
this.$check();
// Access all the serialized properties to initialize them.
for (const key in this.$sdata) {
const _unused = this[key];
}
return {
guid: this.guid,
cdate: this.cdate,
mdate: this.mdate,
tags: this.tags,
...referencesToEntities(this.$dataStore, this.$nymph, this.$skipAc),
};
}
$getTags() {
this.$check();
return this.tags;
}
$hasTag(...tags) {
this.$check();
if (!tags.length) {
return false;
}
for (const tag of tags) {
if (this.tags.indexOf(tag) === -1) {
return false;
}
}
return true;
}
$inArray(array, strict = false) {
return this.$arraySearch(array, strict) !== -1;
}
$is(object) {
if (!(object instanceof Entity)) {
return false;
}
if (this === object) {
return true;
}
if (this.guid != null || object.guid != null) {
return this.guid === object.guid;
}
this.$check();
if (typeof object.$getData !== 'function') {
return false;
}
else {
const obTags = [...object.tags].sort();
const myTags = [...this.tags].sort();
if (!isEqual(obTags, myTags)) {
return false;
}
const obData = object.$getData(true, true);
const myData = this.$getData(true, true);
return isEqual(obData, myData);
}
}
$jsonAcceptData(input, allowConflict = false) {
this.$check();
if (this.guid != input.guid) {
throw new EntityConflictError('Tried to accept JSON input for the wrong entity.');
}
// Accept the modified date.
const mdate = input.mdate ?? 0;
const thismdate = this.mdate ?? 0;
if (mdate < thismdate && !allowConflict) {
throw new EntityConflictError('This entity is newer than JSON input.');
}
this.mdate = input.mdate;
// Accept the tags.
const currentTags = this.$getTags();
const protectedTags = intersection(this.$protectedTags, currentTags);
let tags = difference(input.tags, this.$protectedTags);
if (this.$allowlistTags != null) {
tags = intersection(tags, this.$allowlistTags);
}
this.$removeTag(...currentTags);
this.$addTag(...protectedTags, ...tags);
// Accept the data.
const data = { ...input.data };
const privateData = {};
for (const name of this.$privateData) {
if (name in this.$data) {
privateData[name] = this.$data[name];
}
if (name in data) {
delete data[name];
}
}
const protectedData = {};
const protectedProps = [...this.$protectedData];
if (this.$nymph.tilmeld) {
protectedProps.push('acUser');
protectedProps.push('acGroup');
protectedProps.push('acOther');
protectedProps.push('acRead');
protectedProps.push('acWrite');
protectedProps.push('acFull');
if ((this.constructor.class !== 'User' &&
this.constructor.class !== 'Group') ||
!this.$nymph.tilmeld.currentUser ||
(!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
!this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
protectedProps.push('user');
protectedProps.push('group');
}
}
for (const name of protectedProps) {
if (name in this.$data) {
protectedData[name] = this.$data[name];
}
if (name in data) {
delete data[name];
}
}
let nonAllowlistData = {};
if (this.$allowlistData != null) {
nonAllowlistData = { ...this.$getData(true, true) };
for (const name of this.$allowlistData) {
delete nonAllowlistData[name];
}
for (const name in data) {
if (this.$allowlistData.indexOf(name) === -1) {
delete data[name];
}
}
}
this.$putData({
...nonAllowlistData,
...data,
...protectedData,
...privateData,
});
}
$jsonAcceptPatch(patch, allowConflict = false) {
this.$check();
if (this.guid != patch.guid) {
throw new EntityConflictError('Tried to accept JSON patch for the wrong entity.');
}
// Accept the modified date.
const mdate = patch.mdate ?? 0;
const thismdate = this.mdate ?? 0;
if (mdate < thismdate && !allowConflict) {
throw new EntityConflictError('This entity is newer than JSON patch.');
}
this.mdate = patch.mdate;
const protectedProps = [...this.$protectedData];
if (this.$nymph.tilmeld) {
protectedProps.push('acUser');
protectedProps.push('acGroup');
protectedProps.push('acOther');
protectedProps.push('acRead');
protectedProps.push('acWrite');
protectedProps.push('acFull');
if ((this.constructor.class !== 'User' &&
this.constructor.class !== 'Group') ||
!this.$nymph.tilmeld.currentUser ||
(!this.$nymph.tilmeld.currentUser.abilities?.includes('tilmeld/admin') &&
!this.$nymph.tilmeld.currentUser.abilities?.includes('system/admin'))) {
protectedProps.push('user');
protectedProps.push('group');
}
}
for (const name in patch.set) {
if ((this.$allowlistData != null &&
this.$allowlistData.indexOf(name) === -1) ||
protectedProps.indexOf(name) !== -1 ||
this.$privateData.indexOf(name) !== -1) {
continue;
}
this.$data[name] = referencesToEntities(patch.set[name], this.$nymph, this.$skipAc);
}
for (const name of patch.unset) {
if ((this.$allowlistData != null &&
this.$allowlistData.indexOf(name) === -1) ||
protectedProps.indexOf(name) !== -1 ||
this.$privateData.indexOf(name) !== -1) {
continue;
}
delete this.$data[name];
}
for (const tag of patch.addTags) {
if ((this.$allowlistTags != null &&
this.$allowlistTags.indexOf(tag) === -1) ||
this.$protectedTags.indexOf(tag) !== -1) {
continue;
}
this.$addTag(tag);
}
for (const tag of patch.removeTags) {
if ((this.$allowlistTags != null &&
this.$allowlistTags.indexOf(tag) === -1) ||
this.$protectedTags.indexOf(tag) !== -1) {
continue;
}
this.$removeTag(tag);
}
}
$putData(data, sdata, _source) {
this.$check();
const mySdata = sdata ?? this.$getSData();
for (const name in data) {
delete mySdata[name];
}
for (const name in this.$dataStore) {
delete this.$dataStore[name];
}
for (const name in data) {
this.$dataStore[name] = referencesToEntities(data[name], this.$nymph, this.$skipAc);
}
this.$sdata = mySdata;
// Set original AC values if not set..
if (this.$originalAcValues == null) {
this.$originalAcValues = this.$getCurrentAcValues();
}
}
/**
* Set up a sleeping reference.
* @param array $reference The reference to use to wake.
*/
$referenceSleep(reference) {
if (reference.length !== 3 ||
reference[0] !== 'nymph_entity_reference' ||
typeof reference[1] !== 'string' ||
typeof reference[2] !== 'string') {
throw new InvalidParametersError('referenceSleep expects parameter 1 to be a valid Nymph entity ' +
'reference.');
}
const thisClass = this.constructor.class;
if (reference[2] !== thisClass) {
throw new InvalidParametersError('referenceSleep can only be called with an entity reference of the ' +
`same class. Given class: ${reference[2]}; this class: ${thisClass}.`);
}
this.$isASleepingReference = true;
this.guid = reference[1];
this.$sleepingReference = reference;
}
/**
* Check if this is a sleeping reference and throw an error if so.
*/
$check() {
if (this.$isASleepingReference || this.$sleepingReference != null) {
throw new EntityIsSleepingReferenceError('This entity is in a sleeping reference state. You must use .$wake() to wake it.');
}
}
/**
* Check if this is a sleeping reference.
*/
$asleep() {
return this.$isASleepingReference || this.$sleepingReference != null;
}
/**
* Wake from a sleeping reference.
*/
$wake() {
if (!this.$isASleepingReference || this.$sleepingReference == null) {
this.$wakePromise = null;
return Promise.resolve(this);
}
if (this.$sleepingReference?.[1] == null) {
throw new InvalidStateError('Tried to wake a sleeping reference with no GUID.');
}
if (!this.$wakePromise) {
const EntityClass = this.$nymph.getEntityClass(this.$sleepingReference[2]);
this.$wakePromise = this.$nymph
.getEntity({
class: EntityClass,
skipAc: this.$skipAc,
}, { type: '&', guid: this.$sleepingReference[1] })
.then((entity) => {
if (entity == null || entity.guid == null) {
return Promise.reject(new InvalidStateError('The sleeping reference could not be retrieved.'));
}
this.$isASleepingReference = false;
this.$sleepingReference = null;
this.guid = entity.guid;
this.tags = entity.tags;
this.cdate = entity.cdate;
this.mdate = entity.mdate;
this.$putData(entity.$getData(false, true), entity.$getSData(), 'server');
return this;
})
.finally(() => {
this.$wakePromise = null;
});
}
return this.$wakePromise;
}
$wakeAll(level) {
return new Promise((resolve, reject) => {
// Run this once this entity is wake.
const wakeProps = () => {
let newLevel;
// If level is undefined, keep going forever, otherwise, stop once we've
// gone deep enough.
if (level !== undefined) {
newLevel = level - 1;
}
if (newLevel !== undefined && newLevel < 0) {
resolve(this);
return;
}
const promises = [];
// Go through data looking for entities to wake.
for (let [key, value] of Object.entries(this.$data)) {
if (value instanceof Entity && value.$isASleepingReference) {
promises.push(value.$wakeAll(newLevel));
}
else if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
if (value[i] instanceof Entity &&
value[i].$isASleepingReference) {
promises.push(value[i].$wakeAll(newLevel));
}
}
}
}
if (promises.length) {
Promise.all(promises).then(() => resolve(this), (errObj) => reject(errObj));
}
else {
resolve(this);
}
};
if (this.$isASleepingReference) {
this.$wake().then(wakeProps, (errObj) => reject(errObj));
}
else {
wakeProps();
}
});
}
async $refresh() {
await this.$wake();
if (this.guid == null) {
return false;
}
const refresh = await this.$nymph.getEntity({
class: this.$nymph.getEntityClass(this.constructor),
skipCache: true,
skipAc: this.$skipAc,
}, { type: '&', guid: this.guid });
if (refresh == null) {
return 0;
}
this.tags = refresh.tags;
this.cdate = refresh.cdate;
this.mdate = refresh.mdate;
this.$putData(refresh.$getData(false, true), refresh.$getSData(), 'server');
return true;
}
$removeTag(...tags) {
this.$check();
this.tags = difference(this.tags, tags);
}
async $save() {
await this.$wake();
return await this.$nymph.saveEntity(this);
}
$toReference(existingOnly) {
if (this.$isASleepingReference && this.$sleepingReference != null) {
return this.$sleepingReference;
}
if (existingOnly && this.guid == null) {
return this;
}
return [
'nymph_entity_reference',
this.$getGuaranteedGUID(),
this.constructor.class,
];
}
$useSkipAc(skipAc) {
this.$skipAc = !!skipAc;
}
}
//# sourceMappingURL=Entity.js.map