baqend
Version:
Baqend JavaScript SDK
370 lines (330 loc) • 12.4 kB
text/typescript
import { Managed } from './Managed';
import { EntityPartialUpdateBuilder } from '../partialupdate';
import { enumerable } from '../util/enumerable';
import { PersistentError } from '../error';
import { Filter } from '../query';
import { Json, JsonMap } from '../util';
import { Metadata, ValidationResult } from '../intersection';
import type { EntityManager } from '../EntityManager';
import { Enhancer } from './Enhancer';
export interface Entity {
/**
* Contains the metadata of this managed object
*/
_metadata: Metadata;
}
export class Entity extends Managed {
/**
* Date of the creation of the object
* @name createdAt
* @readonly
* @memberOf Entity.prototype
* @type Date
*/
createdAt?: Date | null;
/**
* Last update date of the object
* @name updatedAt
* @readonly
* @memberOf Entity.prototype
* @type Date
*/
updatedAt?: Date | null;
/**
* The unique id of this object
*
* Sets the unique id of this object, if the id is not formatted as an valid id,
* it will be used as the key component of the id has the same affect as setting the key
*
* @type string
*/
(true)
get id(): string | null {
return this._metadata.id;
}
set id(value: string | null) {
if (this._metadata.id) {
throw new Error(`The id can't be set twice: ${value}`);
}
const val = `${value}`;
if (val.indexOf(`/db/${this._metadata.bucket}/`) === 0) {
this._metadata.id = value;
} else {
this.key = value;
}
}
/**
* The unique key part of the id
* When the key of the unique id is set an error will be thrown if an id is already set.
* @type string
*/
(false)
get key() {
return this._metadata.key;
}
set key(value) {
this._metadata.key = value;
}
/**
* The version of this object
* @type number
* @readonly
*/
(true)
get version() {
return this._metadata.version;
}
/**
* The object read/write permissions
* @type Acl
* @readonly
*/
(true)
get acl() {
this._metadata.throwUnloadedPropertyAccess('acl');
return this._metadata.acl;
}
/**
* Attach this object to the given db
* @param db The db which will be used for future crud operations
* @return
*/
(false)
attach(db: EntityManager): void {
db.attach(this);
}
/**
* Waits on the previously requested operation on this object completes
* operations on this object is completed.
* @return A promise which completes successfully, when the previously requested
* operation completes
*/
ready(): Promise<this>;
/**
* Waits on the previously requested operation on this object completes
* @param doneCallback The callback which will be invoked when the previously
* operations on this object is completed.
* @return A promise which completes successfully, when the previously requested
* operation completes
*/
ready<R>(doneCallback: (entity: this) => R): Promise<R>;
(false)
ready<R>(doneCallback?: (entity: this) => R): Promise<this | R> {
return this._metadata.ready().then(() => this).then(doneCallback);
}
/**
* Saves the object. Inserts the object if it doesn't exists and updates the object if the object exist.
* @param [options] The save options
* @param [options.force=false] Force the save operation, the version will not be validated.
* @param [options.depth=0] The object depth which will be saved. Depth 0 save this object only,
* <code>true</code> saves the objects by reachability.
* @param [options.refresh=false] Refresh the local object state from remote.
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
*/
(false)
save(options?: { force?: boolean, depth?: number | boolean, refresh?: boolean }, doneCallback?: any,
failCallback?: any): Promise<this> {
if (typeof options === 'function') {
return this.save({}, options, doneCallback);
}
return this._metadata.db.save(this, options).then(doneCallback, failCallback);
}
/**
* Inserts a new object. Inserts the object if it doesn't exists and raise an error if the object already exist.
* @param [options] The insertion options
* @param [options.depth=0] The object depth which will be inserted. Depth 0 insert this object only,
* <code>true</code> inserts objects by reachability.
* @param [options.refresh=false] Refresh the local object state from remote.
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
* @method
*/
(false)
insert(options?: { depth?: number | boolean, refresh?: boolean }, doneCallback?: any,
failCallback?: any): Promise<this> {
if (typeof options === 'function') {
return this.insert({}, options, doneCallback);
}
return this._metadata.db.insert(this, options).then(doneCallback, failCallback);
}
/**
* Updates an existing object
*
* Updates the object if it exists and raise an error if the object doesn't exist.
*
* @param [options] The update options
* @param [options.force=false] Force the update operation,
* the version will not be validated, only existence will be checked.
* @param [options.depth=0] The object depth which will be updated. Depth 0 updates this object only,
* <code>true</code> updates objects by reachability.
* @param [options.refresh=false] Refresh the local object state from remote.
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
* @method
*/
(false)
update(options?: { force?: boolean, depth?: number | boolean, refresh?: boolean }, doneCallback?: any,
failCallback?: any): Promise<this> {
if (typeof options === 'function') {
return this.update({}, options, doneCallback);
}
return this._metadata.db.update(this, options).then(doneCallback, failCallback);
}
/**
* Resolves the referenced object in the specified depth
*
* Only unresolved objects will be loaded unless the refresh option is specified.
*
* Removed objects will be marked as removed.
* @param [options] The load options
* @param [options.depth=0] The object depth which will be loaded. Depth set to <code>true</code>
* loads objects by reachability.
* @param [options.refresh=false] Refresh the local object state from remote.
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
* @method
*/
(false)
load(options?: { depth?: number | boolean, refresh?: boolean }, doneCallback?: any,
failCallback?: any): Promise<this> {
if (typeof options === 'function') {
return this.load({}, options, doneCallback);
}
const opt = { local: true, ...options };
if (this.id === null) {
throw new PersistentError("This object can't be loaded, it does have an id.");
}
return this._metadata.db.load(this.id, undefined, opt).then(doneCallback, failCallback);
}
/**
* Deletes an existing object
*
* @param [options] The remove options
* @param [options.force=false] Force the remove operation, the version will not be validated.
* @param [options.depth=0] The object depth which will be removed. Depth 0 removes this object only,
* <code>true</code> removes objects by reachability.
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
* @method
*/
(false)
delete(options?: { force?: boolean, depth?: number | boolean }, doneCallback?: any,
failCallback?: any): Promise<this> {
if (typeof options === 'function') {
return this.delete({}, options, doneCallback);
}
return this._metadata.db.delete(this, options).then(doneCallback, failCallback);
}
/**
* Saves the object and repeats the operation if the object is out of date
*
* In each pass the callback will be called. Ths first parameter of the callback is the entity and the second one
* is a function to abort the process.
*
* @param cb Will be called in each pass
* @param doneCallback Called when the operation succeed.
* @param failCallback Called when the operation failed.
* @return A Promise that will be fulfilled when the asynchronous operation completes.
* @method
*/
(false)
optimisticSave(cb: (entity: this, abort: () => void) => any, doneCallback?: any, failCallback?: any): Promise<this> {
return this._metadata.db.optimisticSave(this, cb).then(doneCallback, failCallback);
}
(false)
attr() {
throw new Error('Attr is not yet implemented.');
}
/**
* Validates the entity by using the validation code of the entity type
*
* @return Contains the result of the Validation
* @method
*/
(false)
validate(): ValidationResult {
return this._metadata.db.validate(this);
}
/**
* Starts a partial update on this entity
*
* @param operations initial operations which should be executed
* @return
*/
(false)
partialUpdate(operations?: Json): EntityPartialUpdateBuilder<this> {
return new EntityPartialUpdateBuilder(this, operations as JsonMap);
}
/**
* Get all objects which refer to this object
*
* @param [options] Some options to pass
* @param [options.classes] An array of class names to filter for, null for no filter
* @return A promise resolving with an array of all referencing objects
* @method
*/
(false)
getReferencing(options?: { classes: string[] }): Promise<Entity[]> {
const { db } = this._metadata;
const references = this._metadata.type.getReferencing(db, options);
// Query all possibly referencing objects
const allResults = Array.from(references).map(([ref, attrs]) => {
// Create query for given entity
const qb = db.createQueryBuilder<Entity>(ref.typeConstructor);
// Add term for each attribute
const terms: Filter<Entity>[] = [];
attrs.forEach((attr) => {
terms.push(qb.equal(attr, this));
});
// If more than one term, put everything in a disjunction
const query = terms.length === 1 ? terms[0] : qb.or(terms);
return query.resultList();
});
return Promise.all(allResults).then((results) => (
// Filter out all objects which did not match
results.filter((result) => !!result.length)
)).then((results) => (
// Flat the array of results
Array.prototype.concat.apply([] as Entity[], results)
));
}
/**
* Returns this object identifier or the baqend type of this object
* @return the object id or type whatever is available
*/
(false)
toString(): string {
const type = Enhancer.getBaqendType(this.constructor);
return this.id || type!.ref;
}
/**
* Converts the object to an JSON-Object
* @param [options=false] to json options by default excludes the metadata
* @param [options.excludeMetadata=false] Excludes the metadata form the serialized json
* @param [options.depth=0] Includes up to depth referenced objects into the serialized json
* @return JSON-Object
* @method
*/
(false)
toJSON(options?: boolean | { excludeMetadata?: boolean, depth?: boolean | number }): Json {
// JSON.stringify calls toJSON with the parent key as the first argument.
// Therefore ignore all unknown option types.
let opt = options;
if (typeof opt === 'boolean') {
opt = {
excludeMetadata: opt,
};
}
if (typeof opt !== 'object') {
opt = {};
}
const state = this._metadata;
return state.type.toJsonValue(state, this, opt);
}
}