lib/container.js
/**
* Copyright 2015 Oursky Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint camelcase: 0 */
const request = require('superagent');
const _ = require('lodash');
const store = require('./store');
const ee = require('event-emitter');
import Asset from './asset';
import User from './user';
import Role from './role';
import ACL from './acl';
import Record from './record';
import Reference from './reference';
import Query from './query';
import Database from './database';
import Pubsub from './pubsub';
import {RelationAction} from './relation';
import Geolocation from './geolocation';
import {Sequence} from './type';
import {AccessTokenNotAccepted} from './error';
import {EventHandle} from './util';
var React;
if (typeof window !== 'undefined') {
React = require('react-native');
}
const USER_CHANGED = 'userChanged';
export default class Container {
constructor() {
this.url = '/* @echo API_URL */';
this.apiKey = null;
this.token = null;
this._accessToken = null;
this._user = null;
this._deviceID = null;
this._getAccessToken();
this._getDeviceID();
this._privateDB = null;
this._publicDB = null;
this.request = request;
this._internalPubsub = new Pubsub(this, true);
this._relation = new RelationAction(this);
this._pubsub = new Pubsub(this, false);
this.autoPubsub = true;
this.ee = ee({});
}
config(options) {
if (options.apiKey) {
this.apiKey = options.apiKey;
}
if (options.endPoint) {
this.endPoint = options.endPoint;
}
let promises = [
this._getUser(),
this._getAccessToken(),
this._getDeviceID()
];
let self = this;
return Promise.all(promises).then(function () {
self.reconfigurePubsubIfNeeded();
return self;
}, function () {
return self;
});
}
configApiKey(ApiKey) {
this.apiKey = ApiKey;
}
clearCache() {
let clear = [];
if (this._publicDB) {
clear.push(this._publicDB.clearCache());
}
if (this._privateDB) {
clear.push(this._privateDB.clearCache());
}
return Promise.all(clear);
}
onUserChanged(listener) {
this.ee.on(USER_CHANGED, listener);
return new EventHandle(this.ee, USER_CHANGED, listener);
}
signupWithUsername(username, password) {
return this._signup(username, null, password);
}
signupWithEmail(email, password) {
return this._signup(null, email, password);
}
signupAnonymously() {
return this._signup(null, null, null);
}
_signup(username, email, password) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('auth:signup', {
username: username,
email: email,
password: password
}).then(container._authResolve.bind(container)).then(resolve, reject);
});
}
_authResolve(body) {
let self = this;
return Promise.all([
this._setUser(body.result),
this._setAccessToken(body.result.access_token)
]).then(function () {
self.reconfigurePubsubIfNeeded();
return self.currentUser;
});
}
loginWithUsername(username, password) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('auth:login', {
username: username,
password: password
}).then(container._authResolve.bind(container)).then(resolve, reject);
});
}
loginWithEmail(email, password) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('auth:login', {
email: email,
password: password
}).then(container._authResolve.bind(container)).then(resolve, reject);
});
}
loginWithProvider(provider, authData) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('auth:login', {
provider: provider,
auth_data: authData
}).then(container._authResolve.bind(container)).then(resolve, reject);
});
}
logout() {
let container = this;
this.clearCache();
return container.makeRequest('auth:logout', {}).then(function () {
return Promise.all([
container._setAccessToken(null),
container._setUser(null)
]).then(function () {
return null;
});
}, function (err) {
return container._setAccessToken(null).then(function () {
return Promise.reject(err);
});
});
}
whoami() {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('me', {})
.then(container._authResolve.bind(container))
.then(resolve, reject);
});
}
changePassword(oldPassword, newPassword, invalidate = false) {
if (invalidate) {
throw Error('Invalidate is not yet implemented');
}
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('auth:password', {
old_password: oldPassword,
password: newPassword
})
.then(container._authResolve.bind(container))
.then(resolve, reject);
});
}
saveUser(user) {
let container = this;
return new Promise(function (resolve, reject) {
const payload = {
_id: user.id, // eslint-disable-line camelcase
email: user.email,
username: user.username
};
if (user.roles) {
payload.roles = _.map(user.roles, function (perRole) {
return perRole.name;
});
}
container.makeRequest('user:update', payload).then(function (body) {
const newUser = container.User.fromJSON(body.result);
const currentUser = container.currentUser;
if (newUser && currentUser && newUser.id === currentUser.id) {
return container._setUser(body.result);
} else {
return newUser;
}
})
.then(function (newUser) {
resolve(newUser);
}, function (err) {
reject(err);
});
});
}
_getUsersBy(emails, usernames) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('user:query', {
emails: emails,
usernames: usernames
}).then(function (body) {
const result = body.result;
const length = result.length;
const users = new Array(length);
for (let i = 0; i < length; i++) {
users[i] = new container.User(result[i].data);
}
resolve(users);
}, function (err) {
reject(err);
});
});
}
getUsersByEmail(emails) {
return this._getUsersBy(emails, null);
}
getUsersByUsername(usernames) {
return this._getUsersBy(null, usernames);
}
discoverUserByEmails(emails) {
let container = this;
return new Promise(function (resolve, reject) {
let q = new Query(container.UserRecord);
q.havingEmails(emails);
container.publicDB.query(q)
.then(function (userRecords) {
resolve(userRecords);
}, function (err) {
reject(err);
});
});
}
discoverUserByUsernames(usernames) {
let container = this;
return new Promise(function (resolve, reject) {
let q = new Query(container.UserRecord);
q.havingUsernames(usernames);
container.publicDB.query(q)
.then(function (userRecords) {
resolve(userRecords);
}, function (err) {
reject(err);
});
});
}
setAdminRole(roles) {
let roleNames = _.map(roles, function (perRole) {
return perRole.name;
});
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('role:admin', {
roles: roleNames
}).then(function (body) {
resolve(body.result);
}, function (err) {
reject(err);
});
});
}
setDefaultRole(roles) {
let roleNames = _.map(roles, function (perRole) {
return perRole.name;
});
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('role:default', {
roles: roleNames
}).then(function (body) {
resolve(body.result);
}, function (err) {
reject(err);
});
});
}
get defaultACL() {
return this.Record.defaultACL;
}
setDefaultACL(acl) {
this.Record.defaultACL = acl;
}
setRecordCreateAccess(recordClass, roles) {
let roleNames = _.map(roles, function (perRole) {
return perRole.name;
});
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest('schema:access', {
type: recordClass.recordType,
create_roles: roleNames
}).then(function (body) {
resolve(body.result);
}, function (err) {
reject(err);
});
});
}
registerDevice(token, type) {
if (!token) {
throw new Error('Token cannot be empty');
}
if (!type) {
if (React) {
if (React.Platform.OS === 'ios') {
type = 'ios';
} else {
type = 'android';
}
} else {
// TODO: probably web / node, handle it later
throw new Error('Failed to infer type, please supply a value');
}
}
let deviceID;
if (this.deviceID) {
deviceID = this.deviceID;
}
let self = this;
return self.makeRequest('device:register', {
type: type,
id: deviceID,
device_token: token
}).then(function (body) {
return self._setDeviceID(body.result.id);
}, function (error) {
// Will set the deviceID to null and try again iff deviceID is not null.
// The deviceID can be deleted remotely, by apns feedback.
// If the current deviceID is already null, will regards as server fail.
let skyerr = null;
if (error.error) {
skyerr = error.error.code;
}
if (self.deviceID && skyerr === 110) {
return self._setDeviceID(null).then(function () {
return self.registerDevice(token, type);
});
} else {
return Promise.reject(error);
}
});
}
lambda(name, data) {
let container = this;
return new Promise(function (resolve, reject) {
container.makeRequest(name, {
args: data
}).then(function (resp) {
resolve(resp.result);
}, reject);
});
}
makeUploadAssetRequest(asset) {
let self = this;
return new Promise(function (resolve, reject) {
self.makeRequest('asset:put', {
filename: asset.name,
'content-type': asset.contentType,
'content-size': asset.file.size
})
.then(function (res) {
const newAsset = Asset.fromJSON(res.result.asset);
const postRequest = res.result['post-request'];
let postUrl = postRequest.action;
if (postUrl.indexOf('/') === 0) {
postUrl = postUrl.substring(1);
}
if (postUrl.indexOf('http') !== 0) {
postUrl = self.url + postUrl;
}
let _request = self.request
.post(postUrl)
.set('X-Skygear-API-Key', self.apiKey);
if (postRequest['extra-fields']) {
_.forEach(postRequest['extra-fields'], function (value, key) {
_request = _request.field(key, value);
});
}
_request.attach('file', asset.file).end(function (err) {
if (err) {
reject(err);
return;
}
resolve(newAsset);
});
}, function (err) {
reject(err);
});
});
}
makeRequest(action, data) {
let container = this;
if (this.apiKey === null) {
throw Error('Please config ApiKey');
}
let _data = _.assign({
action: action,
api_key: this.apiKey,
access_token: this.accessToken
}, data);
let _action = action.replace(':', '/');
let _request = this.request
.post(this.url + _action)
.set('X-Skygear-API-Key', this.apiKey)
.set('X-Skygear-Access-Token', this.accessToken)
.set('Accept', 'application/json')
.send(_data);
return new Promise(function (resolve, reject) {
_request.end(function (err, res) {
// Do an application JSON parse beacuse in some condition, the
// content-type header will got strip and it will not deserial
// the json for us.
let body = getRespJSON(res);
if (err) {
let skyErr = body.error || err;
if (skyErr.code === AccessTokenNotAccepted) {
return Promise.all([
container._setAccessToken(null),
container._setUser(null)
]).then(function () {
reject({
status: err.status,
error: skyErr
});
});
}
reject({
status: err.status,
error: skyErr
});
} else {
resolve(body);
}
});
});
}
get Query() {
return Query;
}
get User() {
return User;
}
get Role() {
return Role;
}
get ACL() {
return ACL;
}
get Record() {
return Record;
}
get UserRecord() {
return Record.extend('user');
}
get Sequence() {
return Sequence;
}
get Asset() {
return Asset;
}
get Reference() {
return Reference;
}
get Geolocation() {
return Geolocation;
}
get currentUser() {
return this._user;
}
_getUser() {
let self = this;
return store.getItem('skygear-user').then(function (userJSON) {
let attrs = JSON.parse(userJSON);
self._user = self.User.fromJSON(attrs);
}, function (err) {
console.warn('Failed to get user', err);
self._user = null;
return null;
});
}
_setUser(attrs) {
let container = this;
let value;
if (attrs !== null) {
this._user = new this.User(attrs);
value = JSON.stringify(this._user.toJSON());
} else {
this._user = null;
value = null;
}
const setItem = value === null ? store.removeItem('skygear-user')
: store.setItem('skygear-user', value);
return setItem.then(function () {
container.ee.emit(USER_CHANGED, container._user);
return value;
}, function (err) {
console.warn('Failed to persist user', err);
return value;
});
}
get accessToken() {
return this._accessToken;
}
_getAccessToken() {
let self = this;
return store.getItem('skygear-accesstoken').then(function (token) {
self._accessToken = token;
return token;
}, function (err) {
console.warn('Failed to get access', err);
self._accessToken = null;
return null;
});
}
_setAccessToken(value) {
this._accessToken = value;
const setItem = value === null ? store.removeItem('skygear-accesstoken')
: store.setItem('skygear-accesstoken', value);
return setItem.then(function () {
return value;
}, function (err) {
console.warn('Failed to persist accesstoken', err);
return value;
});
}
get deviceID() {
return this._deviceID;
}
_getDeviceID() {
let self = this;
return store.getItem('skygear-deviceid').then(function (deviceID) {
self._deviceID = deviceID;
return deviceID;
}, function (err) {
console.warn('Failed to get deviceid', err);
self._deviceID = null;
return null;
});
}
_setDeviceID(value) {
let self = this;
this._deviceID = value;
const setItem = value === null ? store.removeItem('skygear-deviceid')
: store.setItem('skygear-deviceid', value);
return setItem.then(function () {
return value;
}, function (err) {
console.warn('Failed to persist deviceid', err);
return value;
}).then(function (deviceID) {
self.reconfigurePubsubIfNeeded();
return deviceID;
});
}
get endPoint() {
return this.url;
}
set endPoint(newEndPoint) {
// TODO: Check the format
if (newEndPoint) {
this.url = newEndPoint;
}
}
get publicDB() {
if (this._publicDB === null) {
this._publicDB = new Database('_public', this);
}
return this._publicDB;
}
get privateDB() {
if (this.accessToken === null) {
throw new Error('You must login before access to privateDB');
}
if (this._privateDB === null) {
this._privateDB = new Database('_private', this);
}
return this._privateDB;
}
get relation() {
return this._relation;
}
get pubsub() {
return this._pubsub;
}
reconfigurePubsubIfNeeded() {
if (!this.autoPubsub) {
return;
}
this._internalPubsub.reset();
if (this.deviceID !== null) {
this._internalPubsub.subscribe('_sub_' + this.deviceID, function (data) {
console.log('Receivied data for subscription: ' + data);
});
}
this._internalPubsub.reconfigure();
this._pubsub.reconfigure();
}
on(channel, callback) {
return this.pubsub.on(channel, callback);
}
off(channel, callback = null) {
this.pubsub.off(channel, callback);
}
}
function getRespJSON(res) {
if (res && res.body) {
return res.body;
}
if (res && res.text) {
try {
return JSON.parse(res.text);
} catch (err) {
console.log('getRespJSON error. error: ', err);
}
}
return {};
}