@harishreddym/baqend
Version:
Baqend JavaScript SDK
1,518 lines (1,215 loc) • 60.8 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Baqend JavaScript SDK 2.14.1 - Source: lib/EntityManager.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico">
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/bootstrap-baqend.min.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
<link type="text/css" rel="stylesheet" href="styles/font-awesome-4.7.0.css">
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="navbar-inner container">
<!-- Collapsed navigation -->
<div class="navbar-header">
<!-- Expander button -->
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<!-- Main title -->
<a class="navbar-brand" href="/"><img src="img/logo.png"></a>
</div>
<!-- Expanded navigation -->
<div id="nav" class="navbar-collapse collapse">
<!-- Search -->
<form class="navbar-right form-inline search-form">
<div class="form-group search-form-group">
<input type="search" class="form-control search-input" id="search-query" placeholder="Search Guide" name="q" autocomplete="off">
<div id="search-results" class="search-results">
<p class="search-no-results">Please enter a search query ...</p>
</div>
<i class="search-icon fa fa-search"></i>
</div>
</form>
<!-- Main navigation -->
<ul class="nav navbar-nav">
<li>
<a href="https://dashboard.baqend.com/">Dashboard</a>
</li>
<!-- Guide Navigation -->
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Guide <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="https://www.baqend.com/guide/">Home</a></li>
<li><a href="https://www.baqend.com/guide/#speed-kit">Speed Kit</a></li>
<li><a href="https://www.baqend.com/guide/#platform">Platform</a></li>
<li><a href="https://www.baqend.com/guide/roadmap/">Roadmap</a></li>
<li><a href="https://www.baqend.com/guide/topics/faq/">FAQ</a></li>
</ul>
</li>
<!-- Starter Kits -->
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Starter Kits <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="https://www.baqend.com/guide/starter-kits/">Starter Kits Overview</a></li>
<li><a href="https://www.baqend.com/guide/starter-kits/angular2/">Angular 2 Starter Kit</a></li>
<li><a href="https://www.baqend.com/guide/starter-kits/react/">React and Redux Starter Kit</a></li>
<li><a href="https://www.baqend.com/guide/starter-kits/bootstrap/">Bootstrap Starter Kit</a></li>
<li><a href="https://www.baqend.com/guide/starter-kits/ionic2/">Ionic 2 Starter Kit</a></li>
<li><a href="https://www.baqend.com/guide/starter-kits/ionic/">Ionic Starter Kit</a></li>
</ul>
</li>
<li class="active">
<a href="baqend.html">JS API</a>
</li>
<li>
<a href="https://www.baqend.com/tutorial.html">Tutorial</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="box gray pt-32 pb-32">
<div class="container">
<div class="row">
<!-- <div class="col-md-3"></div> -->
<div class="bs-sidebar hidden-print fixed affix" role="complementary">
<input class="filter form-control input-sm" type="text" placeholder="FILTER"/>
<ul class="nav bs-sidenav">
<li class=""><a href="Acl.html" class="nav-name">Acl</a></li>
<li class=""><a href="EntityManager.html" class="nav-name">EntityManager</a></li>
<li class=""><a href="EntityManagerFactory.html" class="nav-name">EntityManagerFactory</a></li>
<li class=""><a href="GeoPoint.html" class="nav-name">GeoPoint</a></li>
<li class=""><a href="RealtimeEvent.html" class="nav-name">RealtimeEvent</a></li>
<li class=""><a href="baqend.html" class="nav-name">baqend</a></li>
<li class="">
<a href="binding.html" class="nav-name">binding</a>
<ul class="nav">
<li class=""><a href="binding.Accessor.html" class="nav-name">Accessor</a></li>
<li class=""><a href="binding.DeviceFactory.html" class="nav-name">DeviceFactory</a></li>
<li class=""><a href="binding.Enhancer.html" class="nav-name">Enhancer</a></li>
<li class=""><a href="binding.Entity.html" class="nav-name">Entity</a></li>
<li class=""><a href="binding.EntityFactory.html" class="nav-name">EntityFactory</a></li>
<li class=""><a href="binding.Factory.html" class="nav-name">Factory</a></li>
<li class=""><a href="binding.File.html" class="nav-name">File</a></li>
<li class=""><a href="binding.FileFactory.html" class="nav-name">FileFactory</a></li>
<li class=""><a href="binding.Managed.html" class="nav-name">Managed</a></li>
<li class=""><a href="binding.ManagedFactory.html" class="nav-name">ManagedFactory</a></li>
<li class=""><a href="binding.Role.html" class="nav-name">Role</a></li>
<li class=""><a href="binding.User.html" class="nav-name">User</a></li>
<li class=""><a href="binding.UserFactory.html" class="nav-name">UserFactory</a></li>
</ul>
</li>
<li class="">
<a href="caching.html" class="nav-name">caching</a>
<ul class="nav">
<li class=""><a href="caching.BloomFilter.html" class="nav-name">BloomFilter</a></li>
</ul>
</li>
<li class="">
<a href="connector.html" class="nav-name">connector</a>
<ul class="nav">
<li class=""><a href="connector.Connector.html" class="nav-name">Connector</a></li>
<li class=""><a href="connector.FetchConnector.html" class="nav-name">FetchConnector</a></li>
<li class=""><a href="connector.IFrameConnector.html" class="nav-name">IFrameConnector</a></li>
<li class=""><a href="connector.Message.html" class="nav-name">Message</a></li>
<li class=""><a href="connector.NodeConnector.html" class="nav-name">NodeConnector</a></li>
<li class=""><a href="connector.ObservableStream.html" class="nav-name">ObservableStream</a></li>
<li class=""><a href="connector.WebSocketConnector.html" class="nav-name">WebSocketConnector</a></li>
<li class=""><a href="connector.XMLHttpConnector.html" class="nav-name">XMLHttpConnector</a></li>
<li class=""><a href="connector.ChannelMessage.html" class="nav-name">ChannelMessage</a></li>
</ul>
</li>
<li class="">
<a href="error.html" class="nav-name">error</a>
<ul class="nav">
<li class=""><a href="error.CommunicationError.html" class="nav-name">CommunicationError</a></li>
<li class=""><a href="error.EntityExistsError.html" class="nav-name">EntityExistsError</a></li>
<li class=""><a href="error.IllegalEntityError.html" class="nav-name">IllegalEntityError</a></li>
<li class=""><a href="error.PersistentError.html" class="nav-name">PersistentError</a></li>
<li class=""><a href="error.RollbackError.html" class="nav-name">RollbackError</a></li>
</ul>
</li>
<li class="">
<a href="metamodel.html" class="nav-name">metamodel</a>
<ul class="nav">
<li class=""><a href="metamodel.Attribute.html" class="nav-name">Attribute</a></li>
<li class=""><a href="metamodel.BasicType.html" class="nav-name">BasicType</a></li>
<li class=""><a href="metamodel.CollectionAttribute.html" class="nav-name">CollectionAttribute</a></li>
<li class=""><a href="metamodel.DbIndex.html" class="nav-name">DbIndex</a></li>
<li class=""><a href="metamodel.EmbeddableType.html" class="nav-name">EmbeddableType</a></li>
<li class=""><a href="metamodel.EntityType.html" class="nav-name">EntityType</a></li>
<li class=""><a href="metamodel.ListAttribute.html" class="nav-name">ListAttribute</a></li>
<li class=""><a href="metamodel.ManagedType.html" class="nav-name">ManagedType</a></li>
<li class=""><a href="metamodel.MapAttribute.html" class="nav-name">MapAttribute</a></li>
<li class=""><a href="metamodel.Metamodel.html" class="nav-name">Metamodel</a></li>
<li class=""><a href="metamodel.ModelBuilder.html" class="nav-name">ModelBuilder</a></li>
<li class=""><a href="metamodel.PluralAttribute.html" class="nav-name">PluralAttribute</a></li>
<li class=""><a href="metamodel.SetAttribute.html" class="nav-name">SetAttribute</a></li>
<li class=""><a href="metamodel.SingularAttribute.html" class="nav-name">SingularAttribute</a></li>
<li class=""><a href="metamodel.Type.html" class="nav-name">Type</a></li>
</ul>
</li>
<li class="">
<a href="model.html" class="nav-name">model</a>
<ul class="nav">
<li class=""><a href="model.Device.html" class="nav-name">Device</a></li>
<li class=""><a href="model.Role.html" class="nav-name">Role</a></li>
<li class=""><a href="model.User.html" class="nav-name">User</a></li>
</ul>
</li>
<li class="">
<a href="partialupdate.html" class="nav-name">partialupdate</a>
<ul class="nav">
<li class=""><a href="partialupdate.EntityPartialUpdateBuilder.html" class="nav-name">EntityPartialUpdateBuilder</a></li>
<li class=""><a href="partialupdate.PartialUpdateBuilder.html" class="nav-name">PartialUpdateBuilder</a></li>
<li class=""><a href="partialupdate.UpdateOperation.html" class="nav-name">UpdateOperation</a></li>
</ul>
</li>
<li class="">
<a href="query.html" class="nav-name">query</a>
<ul class="nav">
<li class=""><a href="query.Builder.html" class="nav-name">Builder</a></li>
<li class=""><a href="query.Filter.html" class="nav-name">Filter</a></li>
<li class=""><a href="query.Node.html" class="nav-name">Node</a></li>
<li class=""><a href="query.Operator.html" class="nav-name">Operator</a></li>
<li class=""><a href="query.Query.html" class="nav-name">Query</a></li>
<li class=""><a href="query.Stream.html" class="nav-name">Stream</a></li>
<li class=""><a href="query.Condition.html" class="nav-name">Condition</a></li>
</ul>
</li>
<li class="">
<a href="util.html" class="nav-name">util</a>
<ul class="nav">
<li class=""><a href="util.Code.html" class="nav-name">Code</a></li>
<li class=""><a href="util.Lockable.html" class="nav-name">Lockable</a></li>
<li class=""><a href="util.Logger.html" class="nav-name">Logger</a></li>
<li class=""><a href="util.Metadata.html" class="nav-name">Metadata</a></li>
<li class=""><a href="util.Modules.html" class="nav-name">Modules</a></li>
<li class=""><a href="util.Permission.html" class="nav-name">Permission</a></li>
<li class=""><a href="util.PushMessage.html" class="nav-name">PushMessage</a></li>
<li class=""><a href="util.TokenStorage.html" class="nav-name">TokenStorage</a></li>
<li class=""><a href="util.ValidationResult.html" class="nav-name">ValidationResult</a></li>
<li class=""><a href="util.Validator.html" class="nav-name">Validator</a></li>
<li class=""><a href="util.TokenStorageFactory.html" class="nav-name">TokenStorageFactory</a></li>
</ul>
</li>
</ul>
</div>
<div class="col-md-12" id="main">
<div class="content">
<h1 class="page-title">Source: lib/EntityManager.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>'use strict';
const messages = require('./message');
const error = require('./error');
const binding = require('./binding');
const util = require('./util');
const query = require('./query');
const UserFactory = require('./binding/UserFactory');
const Metadata = require('./util/Metadata');
const Message = require('./connector/Message');
const BloomFilter = require('./caching/BloomFilter');
const deorecated = require('./util/deprecated');
const StatusCode = Message.StatusCode;
const DB_PREFIX = '/db/';
/**
* @alias EntityManager
* @extends util.Lockable
*/
class EntityManager extends util.Lockable {
/**
* Determine whether the entity manager is open.
* true until the entity manager has been closed
* @type boolean
* @readonly
*/
get isOpen() {
return !!this.connection;
}
/**
* The authentication token if the user is logged in currently
* @type string
*/
get token() {
return this.tokenStorage.token;
}
/**
* Whether caching is disabled
* @type boolean
* @readonly
*/
get isCachingDisabled() {
return !this.bloomFilter;
}
/**
* Returns true if the device token is already registered, otherwise false.
* @type boolean
* @readonly
*/
get isDeviceRegistered() {
return !!this.deviceMe;
}
/**
* The authentication token if the user is logged in currently
* @param {string} value
*/
set token(value) {
this.tokenStorage.update(value);
}
/**
* @param {EntityManagerFactory} entityManagerFactory The factory which of this entityManager instance
*/
constructor(entityManagerFactory) {
super();
/**
* Log messages can created by calling log directly as function, with a specific log level or with the helper
* methods, which a members of the log method.
*
* Logs will be filtered by the client logger and the before they persisted. The default log level is
* 'info' therefore all log messages below the given message aren't persisted.
*
* Examples:
* <pre class="prettyprint">
// default log level ist info
db.log('test message %s', 'my string');
// info: test message my string
// pass a explicit log level as the first argument, one of ('trace', 'debug', 'info', 'warn', 'error')
db.log('warn', 'test message %d', 123);
// warn: test message 123
// debug log level will not be persisted by default, since the default logging level is info
db.log('debug', 'test message %j', {number: 123}, {});
// debug: test message {"number":123}
// data = {}
// One additional json object can be provided, which will be persisted together with the log entry
db.log('info', 'test message %s, %s', 'first', 'second', {number: 123});
// info: test message first, second
// data = {number: 123}
//use the log level helper
db.log.info('test message', 'first', 'second', {number: 123});
// info: test message first second
// data = {number: 123}
//change the default log level to trace, i.e. all log levels will be persisted, note that the log level can be
//additionally configured in the baqend
db.log.level = 'trace';
//trace will be persisted now
db.log.trace('test message', 'first', 'second', {number: 123});
// info: test message first second
// data = {number: 123}
* </pre>
*
* @type util.Logger
* @readonly
*/
this.log = util.Logger.create(this);
/**
* The connector used for requests
* @type connector.Connector
* @private
*/
this.connection = null;
/**
* All managed and cached entity instances
* @type Map<String,binding.Entity>
* @private
*/
this.entities = null;
/**
* @type EntityManagerFactory
* @readonly
*/
this.entityManagerFactory = entityManagerFactory;
/**
* @type metamodel.Metamodel
* @readonly
*/
this.metamodel = entityManagerFactory.metamodel;
/**
* @type util.Code
* @readonly
*/
this.code = entityManagerFactory.code;
/**
* @type util.Modules
* @readonly
*/
this.modules = null;
/**
* The current logged in user object
* @type (model.User|null)
* @readonly
*/
this.me = null;
/**
* The current registered device object
* @type (model.Device|null)
* @readonly
*/
this.deviceMe = null;
/**
* Returns the tokenStorage which will be used to authorize all requests.
* @type {util.TokenStorage}
* @readonly
*/
this.tokenStorage = null;
/**
* @type {caching.BloomFilter}
* @readonly
*/
this.bloomFilter = null;
/**
* Set of object ids that were revalidated after the Bloom filter was loaded.
*/
this.cacheWhiteList = null;
/**
* Set of object ids that were updated but are not yet included in the bloom filter.
* This set essentially implements revalidation by side effect which does not work in Chrome.
*/
this.cacheBlackList = null;
/**
* Bloom filter refresh interval in seconds.
*
* @type {number}
* @readonly
*/
this.bloomFilterRefresh = 60;
/**
* Bloom filter refresh Promise
*
*/
this.bloomFilterLock = new util.Lockable();
}
/**
* Connects this entityManager, used for synchronous and asynchronous initialization
* @param {connector.Connector} connector
* @param {Object} connectData
* @param {util.TokenStorage} tokenStorage The used tokenStorage for token persistence
* @return {void}
*/
connected(connector, connectData, tokenStorage) {
this.connection = connector;
this.tokenStorage = tokenStorage;
this.bloomFilterRefresh = this.entityManagerFactory.staleness;
this.entities = {};
this.File = binding.FileFactory.create(this);
this._createObjectFactory(this.metamodel.embeddables);
this._createObjectFactory(this.metamodel.entities);
this.transaction = {}; // TODO: implement this
this.modules = new util.Modules(this, connector);
if (connectData) {
if (connectData.device) {
this.updateDevice(connectData.device);
}
if (connectData.user && tokenStorage.token) {
this._updateUser(connectData.user, true);
}
if (this.bloomFilterRefresh > 0 && connectData.bloomFilter && util.atob && !util.isNode) {
this.updateBloomFilter(connectData.bloomFilter);
}
}
}
/**
* @param {metamodel.ManagedType[]} types
* @return {binding.ManagedFactory}
* @private
*/
_createObjectFactory(types) {
Object.keys(types).forEach((ref) => {
const type = this.metamodel.managedType(ref);
const name = type.name;
if (this[name]) {
type.typeConstructor = this[name];
Object.defineProperty(this, name, {
value: type.createObjectFactory(this),
});
} else {
Object.defineProperty(this, name, {
get() {
Object.defineProperty(this, name, {
value: type.createObjectFactory(this),
});
return this[name];
},
set(typeConstructor) {
type.typeConstructor = typeConstructor;
},
configurable: true,
});
}
}, this);
}
send(mesage, ignoreCredentialError) {
const msg = mesage;
msg.tokenStorage = this.tokenStorage;
let result = this.connection.send(msg);
if (!ignoreCredentialError) {
result = result.catch((e) => {
if (e.status === StatusCode.BAD_CREDENTIALS) {
this._logout();
}
throw e;
});
}
return result;
}
/**
* Get an instance whose state may be lazily fetched
*
* If the requested instance does not exist in the database, the
* EntityNotFoundError is thrown when the instance state is first accessed.
* The application should not expect that the instance state will be available upon detachment,
* unless it was accessed by the application while the entity manager was open.
*
* @param {(Class<binding.Entity>|string)} entityClass
* @param {string=} key
* @return {binding.Entity}
*/
getReference(entityClass, key) {
let id;
let type;
if (key) {
const keyAsStr = key;
type = this.metamodel.entity(entityClass);
if (keyAsStr.indexOf(DB_PREFIX) === 0) {
id = keyAsStr;
} else {
id = type.ref + '/' + encodeURIComponent(keyAsStr);
}
} else if (typeof entityClass === 'string') {
const keyIndex = entityClass.indexOf('/', DB_PREFIX.length); // skip /db/
if (keyIndex !== -1) {
id = entityClass;
}
type = this.metamodel.entity(keyIndex === -1 ? entityClass : id.substring(0, keyIndex));
} else {
type = this.metamodel.entity(entityClass);
}
let entity = this.entities[id];
if (!entity) {
entity = type.create();
const metadata = Metadata.get(entity);
if (id) {
metadata.id = id;
}
metadata.setUnavailable();
this._attach(entity);
}
return entity;
}
/**
* Creates an instance of {@link query.Builder<T>} for query creation and execution
*
* The query results are instances of the resultClass argument.
*
* @alias EntityManager.prototype.createQueryBuilder<T>
* @param {Class<T>=} resultClass - the type of the query result
* @return {query.Builder<T>} A query builder to create one ore more queries for the specified class
*/
createQueryBuilder(resultClass) {
return new query.Builder(this, resultClass);
}
/**
* Clear the persistence context, causing all managed entities to become detached
*
* Changes made to entities that have not been flushed to the database will not be persisted.
*
* @return {void}
*/
clear() {
this.entities = {};
}
/**
* Close an application-managed entity manager
*
* After the close method has been invoked, all methods on the EntityManager instance
* and any Query and TypedQuery objects obtained from it will throw the IllegalStateError
* except for transaction, and isOpen (which will return false). If this method
* is called when the entity manager is associated with an active transaction,
* the persistence context remains managed until the transaction completes.
*
* @return {void}
*/
close() {
this.connection = null;
return this.clear();
}
/**
* Check if the instance is a managed entity instance belonging to the current persistence context
*
* @param {binding.Entity} entity - entity instance
* @return {boolean} boolean indicating if entity is in persistence context
*/
contains(entity) {
return !!entity && this.entities[entity.id] === entity;
}
/**
* Check if an object with the id from the given entity is already attached
*
* @param {binding.Entity} entity - entity instance
* @return {boolean} boolean indicating if entity with same id is attached
*/
containsById(entity) {
return !!(entity && this.entities[entity.id]);
}
/**
* Remove the given entity from the persistence context, causing a managed entity to become detached
*
* Unflushed changes made to the entity if any (including removal of the entity),
* will not be synchronized to the database. Entities which previously referenced the detached entity will continue
* to reference it.
*
* @param {binding.Entity} entity The entity instance to detach.
* @return {Promise<binding.Entity>}
*/
detach(entity) {
const state = Metadata.get(entity);
return state.withLock(() => {
this.removeReference(entity);
return Promise.resolve(entity);
});
}
/**
* Resolve the depth by loading the referenced objects of the given entity
*
* @param {binding.Entity} entity - entity instance
* @param {Object} [options] The load options
* @return {Promise<binding.Entity>}
*/
resolveDepth(entity, options) {
if (!options || !options.depth) {
return Promise.resolve(entity);
}
options.resolved = options.resolved || [];
const promises = [];
const subOptions = Object.assign({}, options, {
depth: options.depth === true ? true : options.depth - 1,
});
this.getSubEntities(entity, 1).forEach((subEntity) => {
if (subEntity !== null && options.resolved.indexOf(subEntity) === -1) {
options.resolved.push(subEntity);
promises.push(this.load(subEntity.id, null, subOptions));
}
});
return Promise.all(promises).then(() => entity);
}
/**
* Search for an entity of the specified oid
*
* If the entity instance is contained in the persistence context, it is returned from there.
*
* @param {(Class<binding.Entity>|string)} entityClass - entity class
* @param {String} oid - Object ID
* @param {Object} [options] The load options.
* @return {Promise<binding.Entity>} the loaded entity or null
*/
load(entityClass, oid, options) {
const opt = options || {};
const entity = this.getReference(entityClass, oid);
const state = Metadata.get(entity);
if (!opt.refresh && opt.local && state.isAvailable) {
return this.resolveDepth(entity, opt);
}
const msg = new messages.GetObject(state.bucket, state.key);
this.ensureCacheHeader(entity.id, msg, opt.refresh);
return this.send(msg).then((response) => {
// refresh object if loaded older version from cache
// chrome doesn't using cache when ifNoneMatch is set
if (entity.version > response.entity.version) {
opt.refresh = true;
return this.load(entityClass, oid, opt);
}
this.addToWhiteList(response.entity.id);
if (response.status !== StatusCode.NOT_MODIFIED) {
state.setJson(response.entity, { persisting: true });
}
return this.resolveDepth(entity, opt);
}, (e) => {
if (e.status === StatusCode.OBJECT_NOT_FOUND) {
this.removeReference(entity);
state.setRemoved();
return null;
}
throw e;
});
}
/**
* @param {binding.Entity} entity
* @param {Object} options
* @return {Promise<binding.Entity>}
*/
insert(entity, options) {
const opt = options || {};
let isNew;
return this._save(entity, opt, (state, json) => {
if (state.version) {
throw new error.PersistentError('Existing objects can\'t be inserted.');
}
isNew = !state.id;
return new messages.CreateObject(state.bucket, json);
}).then((val) => {
if (isNew) {
this._attach(entity);
}
return val;
});
}
/**
* @param {binding.Entity} entity
* @param {Object} options
* @return {Promise<binding.Entity>}
*/
update(entity, options) {
const opt = options || {};
return this._save(entity, opt, (state, json) => {
if (!state.version) {
throw new error.PersistentError('New objects can\'t be inserted.');
}
if (opt.force) {
delete json.version;
return new messages.ReplaceObject(state.bucket, state.key, json)
.ifMatch('*');
}
return new messages.ReplaceObject(state.bucket, state.key, json)
.ifMatch(state.version);
});
}
/**
* @param {binding.Entity} entity
* @param {Object} options The save options
* @param {boolean=} withoutLock Set true to save the entity without locking
* @return {Promise<binding.Entity>}
*/
save(entity, options, withoutLock) {
const opt = options || {};
const msgFactory = (state, json) => {
if (opt.force) {
if (!state.id) {
throw new error.PersistentError('New special objects can\'t be forcedly saved.');
}
delete json.version;
return new messages.ReplaceObject(state.bucket, state.key, json);
}
if (state.version) {
return new messages.ReplaceObject(state.bucket, state.key, json)
.ifMatch(state.version);
}
return new messages.CreateObject(state.bucket, json);
};
return withoutLock ? this._locklessSave(entity, opt, msgFactory) : this._save(entity, opt, msgFactory);
}
/**
* @param {binding.Entity} entity
* @param {Function} cb pre-safe callback
* @return {Promise<binding.Entity>}
*/
optimisticSave(entity, cb) {
return Metadata.get(entity).withLock(() => this._optimisticSave(entity, cb));
}
/**
* @param {binding.Entity} entity
* @param {Function} cb pre-safe callback
* @return {Promise<binding.Entity>}
* @private
*/
_optimisticSave(entity, cb) {
let abort = false;
const abortFn = () => {
abort = true;
};
const promise = Promise.resolve(cb(entity, abortFn));
if (abort) {
return Promise.resolve(entity);
}
return promise.then(() => (
this.save(entity, {}, true)
.catch((e) => {
if (e.status === 412) {
return this.refresh(entity, {})
.then(() => this._optimisticSave(entity, cb));
}
throw e;
})
));
}
/**
* Save the object state without locking
* @param {binding.Entity} entity
* @param {Object} options
* @param {Function} msgFactory
* @return {Promise.<binding.Entity>}
* @private
*/
_locklessSave(entity, options, msgFactory) {
this.attach(entity);
const state = Metadata.get(entity);
let refPromises;
let json;
if (state.isAvailable) {
// getting json will check all collections changes, therefore we must do it before proofing the dirty state
json = state.getJson({
persisting: true,
});
}
if (state.isDirty) {
if (!options.refresh) {
state.setPersistent();
}
const sendPromise = this.send(msgFactory(state, json)).then((response) => {
if (state.id && state.id !== response.entity.id) {
this.removeReference(entity);
state.id = response.entity.id;
this._attach(entity);
}
state.setJson(response.entity, {
persisting: options.refresh,
onlyMetadata: !options.refresh,
});
return entity;
}, (e) => {
if (e.status === StatusCode.OBJECT_NOT_FOUND) {
this.removeReference(entity);
state.setRemoved();
return null;
}
state.setDirty();
throw e;
});
refPromises = [sendPromise];
} else {
refPromises = [Promise.resolve(entity)];
}
const subOptions = Object.assign({}, options);
subOptions.depth = 0;
this.getSubEntities(entity, options.depth).forEach((sub) => {
refPromises.push(this._save(sub, subOptions, msgFactory));
});
return Promise.all(refPromises).then(() => entity);
}
/**
* Save and lock the object state
* @param {binding.Entity} entity
* @param {Object} options
* @param {Function} msgFactory
* @return {Promise.<binding.Entity>}
* @private
*/
_save(entity, options, msgFactory) {
this.ensureBloomFilterFreshness();
const state = Metadata.get(entity);
if (state.version) {
this.addToBlackList(entity.id);
}
return state.withLock(() => this._locklessSave(entity, options, msgFactory));
}
/**
* Returns all referenced sub entities for the given depth and root entity
* @param {binding.Entity} entity
* @param {boolean|number} depth
* @param {binding.Entity[]} [resolved]
* @param {binding.Entity=} initialEntity
* @return {binding.Entity[]}
*/
getSubEntities(entity, depth, resolved, initialEntity) {
let resolv = resolved || [];
if (!depth) {
return resolv;
}
const obj = initialEntity || entity;
const state = Metadata.get(entity);
const iter = state.type.references();
for (let item = iter.next(); !item.done; item = iter.next()) {
const value = item.value;
const subEntities = this.getSubEntitiesByPath(entity, value.path);
for (let i = 0, len = subEntities.length; i < len; i += 1) {
const subEntity = subEntities[i];
if (resolv.indexOf(subEntity) === -1 && subEntity !== obj) {
resolv.push(subEntity);
resolv = this.getSubEntities(subEntity, depth === true ? depth : depth - 1, resolv, obj);
}
}
}
return resolv;
}
/**
* Returns all referenced one level sub entities for the given path
* @param {binding.Entity} entity
* @param {Array<string>} path
* @return {binding.Entity[]}
*/
getSubEntitiesByPath(entity, path) {
let subEntities = [entity];
path.forEach((attributeName) => {
const tmpSubEntities = [];
subEntities.forEach((subEntity) => {
const curEntity = subEntity[attributeName];
if (!curEntity) {
return;
}
const attribute = this.metamodel.managedType(subEntity.constructor).getAttribute(attributeName);
if (attribute.isCollection) {
const iter = curEntity.entries();
for (let item = iter.next(); !item.done; item = iter.next()) {
const entry = item.value;
tmpSubEntities.push(entry[1]);
if (attribute.keyType && attribute.keyType.isEntity) {
tmpSubEntities.push(entry[0]);
}
}
} else {
tmpSubEntities.push(curEntity);
}
});
subEntities = tmpSubEntities;
});
return subEntities;
}
/**
* Delete the entity instance.
* @param {binding.Entity} entity
* @param {Object} options The delete options
* @return {Promise<binding.Entity>}
*/
'delete'(entity, options) {
const opt = options || {};
this.attach(entity);
const state = Metadata.get(entity);
return state.withLock(() => {
if (!state.version && !opt.force) {
throw new error.IllegalEntityError(entity);
}
const msg = new messages.DeleteObject(state.bucket, state.key);
this.addToBlackList(entity.id);
if (!opt.force) {
msg.ifMatch(state.version);
}
const refPromises = [this.send(msg).then(() => {
this.removeReference(entity);
state.setRemoved();
return entity;
})];
const subOptions = Object.assign({}, opt);
subOptions.depth = 0;
this.getSubEntities(entity, opt.depth).forEach((sub) => {
refPromises.push(this.delete(sub, subOptions));
});
return Promise.all(refPromises).then(() => entity);
});
}
/**
* Synchronize the persistence context to the underlying database.
*
* @return {Promise<*>}
*/
flush() {
// TODO: implement this
}
/**
* Make an instance managed and persistent.
* @param {binding.Entity} entity - entity instance
* @return {void}
*/
persist(entity) {
this.attach(entity);
}
/**
* Refresh the state of the instance from the database, overwriting changes made to the entity, if any.
* @param {binding.Entity} entity - entity instance
* @param {Object} options The refresh options
* @return {Promise<binding.Entity>}
*/
refresh(entity, options) {
const opt = options || {};
opt.refresh = true;
return this.load(entity.id, null, opt);
}
/**
* Attach the instance to this database context, if it is not already attached
* @param {binding.Entity} entity The entity to attach
* @return {void}
*/
attach(entity) {
if (!this.contains(entity)) {
const type = this.metamodel.entity(entity.constructor);
if (!type) {
throw new error.IllegalEntityError(entity);
}
if (this.containsById(entity)) {
throw new error.EntityExistsError(entity);
}
this._attach(entity);
}
}
_attach(entity) {
const metadata = Metadata.get(entity);
if (metadata.isAttached) {
if (metadata.db !== this) {
throw new error.EntityExistsError(entity);
}
} else {
metadata.db = this;
}
if (!metadata.id) {
if (metadata.type.name !== 'User' && metadata.type.name !== 'Role' && metadata.type.name !== 'logs.AppLog') {
metadata.id = DB_PREFIX + metadata.type.name + '/' + util.uuid();
}
}
if (metadata.id) {
this.entities[metadata.id] = entity;
}
}
removeReference(entity) {
const state = Metadata.get(entity);
if (!state) {
throw new error.IllegalEntityError(entity);
}
delete this.entities[state.id];
}
register(user, password, loginOption) {
const login = loginOption > UserFactory.LoginOption.NO_LOGIN;
if (this.me && login) {
throw new error.PersistentError('User is already logged in.');
}
return this.withLock(() => {
const msg = new messages.Register({ user, password, login });
return this._userRequest(msg, loginOption);
});
}
login(username, password, loginOption) {
if (this.me) {
throw new error.PersistentError('User is already logged in.');
}
return this.withLock(() => {
const msg = new messages.Login({ username, password });
return this._userRequest(msg, loginOption);
});
}
logout() {
return this.withLock(() => this.send(new messages.Logout()).then(this._logout.bind(this)));
}
loginWithOAuth(provider, clientID, options) {
if (this.me) {
throw new error.PersistentError('User is already logged in.');
}
const opt = Object.assign({
title: 'Login with ' + provider,
timeout: 5 * 60 * 1000,
state: {},
loginOption: true,
}, options);
if (opt.redirect) {
Object.assign(opt.state, { redirect: opt.redirect, loginOption: opt.loginOption });
}
let msg;
if (Message[provider + 'OAuth']) {
msg = new Message[provider + 'OAuth'](clientID, opt.scope, JSON.stringify(opt.state));
msg.addRedirectOrigin(this.connection.origin + this.connection.basePath);
} else {
throw new Error('OAuth provider ' + provider + ' not supported.');
}
const windowOptions = { width: opt.width, height: opt.height };
if (opt.redirect) {
// use oauth via redirect by opening the login in the same window
// for app wrappers we need to open the system browser
const isBrowser = document.URL.indexOf('http://') !== -1 || document.URL.indexOf('https://') !== -1;
this.openOAuthWindow(msg.request.path, isBrowser ? '_self' : '_system', windowOptions);
return new Promise(() => {});
}
const req = this._userRequest(msg, opt.loginOption);
this.openOAuthWindow(msg.request.path, opt.title, windowOptions);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new error.PersistentError('OAuth login timeout.'));
}, opt.timeout);
req.then(resolve, reject).then(() => {
clearTimeout(timeout);
});
});
}
/**
* Opens a new window use for OAuth logins
* @param {string} url The url to open
* @param {string} targetOrTitle The target of the window, or the title of the popup
* @param {object} options Additional window options
* @return {void}
*/
openOAuthWindow(url, targetOrTitle, options) {
const str = Object.keys(options)
.filter(key => options[key] !== undefined)
.map(key => key + '=' + options[key])
.join(',');
open(url, targetOrTitle, str); // eslint-disable-line no-restricted-globals
}
renew(loginOption) {
return this.withLock(() => {
const msg = new messages.Me();
return this._userRequest(msg, loginOption);
});
}
newPassword(username, password, newPassword) {
return this.withLock(() => {
const msg = new messages.NewPassword({ username, password, newPassword });
return this.send(msg, true).then(response => this._updateUser(response.entity));
});
}
newPasswordWithToken(token, newPassword, loginOption) {
return this.withLock(() => (
this._userRequest(new messages.NewPassword({ token, newPassword }), loginOption)
));
}
resetPassword(username) {
return this.send(new messages.ResetPassword({ username }));
}
changeUsername(username, newUsername, password) {
return this.send(new messages.ChangeUsername({ username, newUsername, password }));
}
_updateUser(obj, updateMe) {
const user = this.getReference(obj.id);
const metadata = Metadata.get(user);
metadata.setJson(obj, { persisting: true });
if (updateMe) {
this.me = user;
}
return user;
}
_logout() {
this.me = null;
this.token = null;
}
_userRequest(msg, loginOption) {
const opt = loginOption === undefined ? true : loginOption;
const login = opt > UserFactory.LoginOption.NO_LOGIN;
if (login) {
this.tokenStorage.temporary = opt < UserFactory.LoginOption.PERSIST_LOGIN;
}
return this.send(msg, !login)
.then(
response => (response.entity ? this._updateUser(response.entity, login) : null),
(e) => {
if (e.status === StatusCode.OBJECT_NOT_FOUND) {
if (login) {
this._logout();
}
return null;
}
throw e;
}
);
}
/**
* @param {string} devicetype The OS of the device (IOS/Android)
* @param {object} subscription WebPush subscription
* @param {model.Device} device
* @return {Promise<model.Device>}
*/
registerDevice(devicetype, subscription, device) {
const msg = new messages.DeviceRegister({ devicetype, subscription, device });
msg.withCredentials = true;
return this.send(msg)
.then(response => this.updateDevice(response.entity));
}
updateDevice(obj) {
const device = this.getReference(obj.id);
const metadata = Metadata.get(device);
metadata.setJson(obj, { persisting: true });
this.deviceMe = device;
return device;
}
checkDeviceRegistration() {
return this.send(new messages.DeviceRegistered())
.then(() => {
this.isDeviceRegistered = true;
return true;
}, (e) => {
if (e.status === StatusCode.OBJECT_NOT_FOUND) {
this.isDeviceRegistered = false;
return false;
}
throw e;
});
}
pushDevice(pushMessage) {
return this.send(new messages.DevicePush(pushMessage));
}
/**
* The given entity will be checked by the validation code of the entity type.
*
* @param {binding.Entity} entity
* @return {util.ValidationResult} result
*/
validate(entity) {
const type = Metadata.get(entity).type;
const result = new util.ValidationResult();
const iter = type.attributes();
for (let item = iter.next(); !item.done; item = iter.next()) {
const validate = new util.Validator(item.value.name, entity);
result.fields[validate.key] = validate;
}
const validationCode = type.validationCode;
if (validationCode) {
validationCode(result.fields);
}
return result;
}
/**
* Adds the given object id to the cacheWhiteList if needed.
* @param {string} objectId The id to add.
* @return {void}
*/
addToWhiteList(objectId) {
if (!this.isCachingDisabled) {
if (this.bloomFilter.contains(objectId)) {
this.cacheWhiteList.add(objectId);
}
this.cacheBlackList.delete(objectId);
}
}
/**
* Adds the given object id to the cacheBlackList if needed.
* @param {string} objectId The id to add.
* @return {void}
*/
addToBlackList(objectId) {
if (!this.isCachingDisabled) {
if (!this.bloomFilter.contains(objectId)) {
this.cacheBlackList.add(objectId);
}
this.cacheWhiteList.delete(objectId);
}
}
refreshBloomFilter() {
if (this.isCachingDisabled) {
return Promise.resolve();
}
const msg = new messages.GetBloomFilter();
msg.noCache();
return this.send(msg).then((response) => {
this.updateBloomFilter(response.entity);
return this.bloomFilter;
});
}
updateBloomFilter(bloomFilter) {
this.bloomFilter = new BloomFilter(bloomFilter);
this.cacheWhiteList = new Set();
this.cacheBlackList = new Set();
}
/**
* Checks the freshness of the bloom filter and does a reload if necessary
* @return {void}
*/
ensureBloomFilterFreshness() {
if (this.isCachingDisabled) {
return;
}
const now = new Date().getTime();
const refreshRate = this.bloom