'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.bloomFilterRefresh * 1000;
if (this.bloomFilterLock.isReady && now - this.bloomFilter.creation > refreshRate) {
this.bloomFilterLock.withLock(() => this.refreshBloomFilter());
}
}
/**
* Checks for a given id, if revalidation is required, the resource is stale or caching was disabled
* @param {string} id The object id to check
* @return {boolean} Indicates if the resource must be revalidated
*/
mustRevalidate(id) {
if (util.isNode) {
return false;
}
this.ensureBloomFilterFreshness();
let refresh = this.isCachingDisabled || !this.bloomFilterLock.isReady;
refresh = refresh || (
!this.cacheWhiteList.has(id)
&& (this.cacheBlackList.has(id) || this.bloomFilter.contains(id))
);
return refresh;
}
/**
* @param {string} id To check the bloom filter
* @param {connector.Message} message To attach the headers
* @param {boolean} refresh To force the reload headers
* @return {void}
*/
ensureCacheHeader(id, message, refresh) {
const noCache = refresh || this.mustRevalidate(id);
if (noCache) {
message.noCache();
}
}
/**
* Creates a absolute url for the given relative one
* @param {string} relativePath the relative url
* @param {boolean=} authorize indicates if authorization credentials should be generated and be attached to the url
* @return {string} a absolute url wich is optionaly signed with a resource token which authenticates the currently
* logged in user
*/
createURL(relativePath, authorize) {
let path = this.connection.basePath + relativePath;
let append = false;
if (authorize && this.me) {
path = this.tokenStorage.signPath(path);
append = true;
} else {
path = path.split('/').map(encodeURIComponent).join('/');
}
if (this.mustRevalidate(relativePath)) {
path = path + (append ? '&' : '?') + 'BCB';
}
return this.connection.origin + path;
}
/**
* Requests a perpetual token for the given user
*
* Only users with the admin role are allowed to request an API token.
*
* @param {(Class<binding.Entity>|Class<binding.Managed>)} entityClass
* @param {binding.User|String} user The user object or id of the user object
* @return {Promise<*>}
*/
requestAPIToken(entityClass, user) {
const userObj = this._getUserReference(entityClass, user);
const msg = new messages.UserToken(userObj.key);
return this.send(msg).then(resp => resp.entity);
}
/**
* Revoke all created tokens for the given user
*
* This method will revoke all previously issued tokens and the user must login again.
*
* @param {(Class<binding.Entity>|Class<binding.Managed>)} entityClass
* @param {binding.User|String} user The user object or id of the user object
* @return {Promise<*>}
*/
revokeAllTokens(entityClass, user) {
const userObj = this._getUserReference(entityClass, user);
const msg = new messages.RevokeUserToken(userObj.key);
return this.send(msg);
}
_getUserReference(entityClass, user) {
if (typeof user === 'string') {
return this.getReference(entityClass, user);
}
return user;
}
}
/**
* Constructor for a new List collection
* @function
* @name List<U>
* @memberOf EntityManager.prototype
* @param {...U} args Same arguments can be passed as the Array constructor takes
* @return {Array<U>} The new created List
*/
EntityManager.prototype.List = Array;
/**
* Constructor for a new Set collection
* @function
* @name Set<U>
* @memberOf EntityManager.prototype
* @param {Iterable<U>=} collection The initial array or collection to initialize the new Set
* @return {Set<U>} The new created Set
*/
EntityManager.prototype.Set = Set;
/**
* Constructor for a new Map collection
* @function
* @param {Iterable<*>=} collection The initial array or collection to initialize the new Map
* @return {Map<*, *>} The new created Map
*/
EntityManager.prototype.Map = Map;
/**
* Constructor for a new GeoPoint
* @function
* @param {string|number|Object|Array<number>} [latitude] A coordinate pair (latitude first), a GeoPoint like object or
* the GeoPoint's latitude
* @param {number=} longitude The GeoPoint's longitude
* @return {GeoPoint} The new created GeoPoint
*/
EntityManager.prototype.GeoPoint = require('./GeoPoint');
/**
* An User factory for user objects.
* The User factory can be called to create new instances of users or can be used to register/login/logout users.
* The created instances implements the {@link model.User} interface
* @name User
* @type binding.UserFactory
* @memberOf EntityManager.prototype
*/
/**
* An Role factory for role objects.
* The Role factory can be called to create new instances of roles, later on users can be attached to roles to manage
* the access permissions through this role
* The created instances implements the {@link model.Role} interface
* @name Role
* @memberOf EntityManager.prototype
* @type binding.EntityFactory<model.Role>
*/
/**
* An Device factory for user objects.
* The Device factory can be called to create new instances of devices or can be used to register, push to and
* check registration status of devices.
* @name Device
* @memberOf EntityManager.prototype
* @type binding.DeviceFactory
*/
/**
* An Object factory for entity or embedded objects,
* that can be accessed by the type name of the entity type.
* An object factory can be called to create new instances of the type.
* The created instances implements the {@link binding.Entity} or the {@link binding.Managed} interface
* whenever the class is an entity or embedded object
* @name [YourEntityClass: string]
* @memberOf EntityManager.prototype
* @type {*}
*/
/**
* A File factory for file objects.
* The file factory can be called to create new instances for files.
* The created instances implements the {@link binding.File} interface
* @name File
* @memberOf EntityManager.prototype
* @type binding.FileFactory
*/
deorecated(EntityManager.prototype, '_connector', 'connection');
deorecated(EntityManager.prototype, '_entities', 'entities');
deorecated(EntityManager.prototype, '_bloomFilterLock', 'bloomFilterLock');
module.exports = EntityManager;