@@ -12,11 +12,11 @@ export class AccountLockout {
|
||||
*/
|
||||
_setFailedLoginCount(value) {
|
||||
const query = {
|
||||
username: this._user.username
|
||||
username: this._user.username,
|
||||
};
|
||||
|
||||
const updateFields = {
|
||||
_failed_login_count: value
|
||||
_failed_login_count: value,
|
||||
};
|
||||
|
||||
return this._config.database.update('_User', query, updateFields);
|
||||
@@ -28,17 +28,16 @@ export class AccountLockout {
|
||||
_isFailedLoginCountSet() {
|
||||
const query = {
|
||||
username: this._user.username,
|
||||
_failed_login_count: { $exists: true }
|
||||
_failed_login_count: { $exists: true },
|
||||
};
|
||||
|
||||
return this._config.database.find('_User', query)
|
||||
.then(users => {
|
||||
if (Array.isArray(users) && users.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return this._config.database.find('_User', query).then(users => {
|
||||
if (Array.isArray(users) && users.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,12 +45,11 @@ export class AccountLockout {
|
||||
* else do nothing
|
||||
*/
|
||||
_initFailedLoginCount() {
|
||||
return this._isFailedLoginCountSet()
|
||||
.then(failedLoginCountIsSet => {
|
||||
if (!failedLoginCountIsSet) {
|
||||
return this._setFailedLoginCount(0);
|
||||
}
|
||||
});
|
||||
return this._isFailedLoginCountSet().then(failedLoginCountIsSet => {
|
||||
if (!failedLoginCountIsSet) {
|
||||
return this._setFailedLoginCount(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,10 +57,12 @@ export class AccountLockout {
|
||||
*/
|
||||
_incrementFailedLoginCount() {
|
||||
const query = {
|
||||
username: this._user.username
|
||||
username: this._user.username,
|
||||
};
|
||||
|
||||
const updateFields = {_failed_login_count: {__op: 'Increment', amount: 1}};
|
||||
const updateFields = {
|
||||
_failed_login_count: { __op: 'Increment', amount: 1 },
|
||||
};
|
||||
|
||||
return this._config.database.update('_User', query, updateFields);
|
||||
}
|
||||
@@ -75,18 +75,29 @@ export class AccountLockout {
|
||||
_setLockoutExpiration() {
|
||||
const query = {
|
||||
username: this._user.username,
|
||||
_failed_login_count: { $gte: this._config.accountLockout.threshold }
|
||||
_failed_login_count: { $gte: this._config.accountLockout.threshold },
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const updateFields = {
|
||||
_account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000))
|
||||
_account_lockout_expires_at: Parse._encode(
|
||||
new Date(
|
||||
now.getTime() + this._config.accountLockout.duration * 60 * 1000
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
return this._config.database.update('_User', query, updateFields)
|
||||
return this._config.database
|
||||
.update('_User', query, updateFields)
|
||||
.catch(err => {
|
||||
if (err && err.code && err.message && err.code === 101 && err.message === 'Object not found.') {
|
||||
if (
|
||||
err &&
|
||||
err.code &&
|
||||
err.message &&
|
||||
err.code === 101 &&
|
||||
err.message === 'Object not found.'
|
||||
) {
|
||||
return; // nothing to update so we are good
|
||||
} else {
|
||||
throw err; // unknown error
|
||||
@@ -104,15 +115,19 @@ export class AccountLockout {
|
||||
const query = {
|
||||
username: this._user.username,
|
||||
_account_lockout_expires_at: { $gt: Parse._encode(new Date()) },
|
||||
_failed_login_count: {$gte: this._config.accountLockout.threshold}
|
||||
_failed_login_count: { $gte: this._config.accountLockout.threshold },
|
||||
};
|
||||
|
||||
return this._config.database.find('_User', query)
|
||||
.then(users => {
|
||||
if (Array.isArray(users) && users.length > 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your account is locked due to multiple failed login attempts. Please try again after ' + this._config.accountLockout.duration + ' minute(s)');
|
||||
}
|
||||
});
|
||||
return this._config.database.find('_User', query).then(users => {
|
||||
if (Array.isArray(users) && users.length > 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Your account is locked due to multiple failed login attempts. Please try again after ' +
|
||||
this._config.accountLockout.duration +
|
||||
' minute(s)'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,16 +154,14 @@ export class AccountLockout {
|
||||
if (!this._config.accountLockout) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._notLocked()
|
||||
.then(() => {
|
||||
if (loginSuccessful) {
|
||||
return this._setFailedLoginCount(0);
|
||||
} else {
|
||||
return this._handleFailedLoginAttempt();
|
||||
}
|
||||
});
|
||||
return this._notLocked().then(() => {
|
||||
if (loginSuccessful) {
|
||||
return this._setFailedLoginCount(0);
|
||||
} else {
|
||||
return this._handleFailedLoginAttempt();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AccountLockout;
|
||||
|
||||
@@ -16,10 +16,10 @@ export function loadAdapter<T>(adapter, defaultAdapter, options): T {
|
||||
}
|
||||
// Load from the default adapter when no adapter is set
|
||||
return loadAdapter(defaultAdapter, undefined, options);
|
||||
} else if (typeof adapter === "function") {
|
||||
} else if (typeof adapter === 'function') {
|
||||
try {
|
||||
return adapter(options);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
if (e.name === 'TypeError') {
|
||||
var Adapter = adapter;
|
||||
return new Adapter(options);
|
||||
@@ -27,7 +27,7 @@ export function loadAdapter<T>(adapter, defaultAdapter, options): T {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else if (typeof adapter === "string") {
|
||||
} else if (typeof adapter === 'string') {
|
||||
/* eslint-disable */
|
||||
adapter = require(adapter);
|
||||
// If it's define as a module, get the default
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @interface AnalyticsAdapter
|
||||
*/
|
||||
export class AnalyticsAdapter {
|
||||
|
||||
/**
|
||||
@param {any} parameters: the analytics request body, analytics info will be in the dimensions property
|
||||
@param {Request} req: the original http request
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/*eslint no-unused-vars: "off"*/
|
||||
export class AuthAdapter {
|
||||
|
||||
/*
|
||||
@param appIds: the specified app ids in the configuration
|
||||
@param authData: the client provided authData
|
||||
|
||||
@@ -3,8 +3,11 @@ var https = require('https'),
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var OAuth = function(options) {
|
||||
if(!options) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth');
|
||||
if (!options) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'No options passed to OAuth'
|
||||
);
|
||||
}
|
||||
this.consumer_key = options.consumer_key;
|
||||
this.consumer_secret = options.consumer_secret;
|
||||
@@ -14,23 +17,24 @@ var OAuth = function(options) {
|
||||
this.oauth_params = options.oauth_params || {};
|
||||
};
|
||||
|
||||
OAuth.prototype.send = function(method, path, params, body){
|
||||
|
||||
OAuth.prototype.send = function(method, path, params, body) {
|
||||
var request = this.buildRequest(method, path, params, body);
|
||||
// Encode the body properly, the current Parse Implementation don't do it properly
|
||||
return new Promise(function(resolve, reject) {
|
||||
var httpRequest = https.request(request, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
var httpRequest = https
|
||||
.request(request, function(res) {
|
||||
var data = '';
|
||||
res.on('data', function(chunk) {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', function() {
|
||||
data = JSON.parse(data);
|
||||
resolve(data);
|
||||
});
|
||||
})
|
||||
.on('error', function() {
|
||||
reject('Failed to make an OAuth request');
|
||||
});
|
||||
res.on('end', function() {
|
||||
data = JSON.parse(data);
|
||||
resolve(data);
|
||||
});
|
||||
}).on('error', function() {
|
||||
reject('Failed to make an OAuth request');
|
||||
});
|
||||
if (request.body) {
|
||||
httpRequest.write(request.body);
|
||||
}
|
||||
@@ -39,40 +43,45 @@ OAuth.prototype.send = function(method, path, params, body){
|
||||
};
|
||||
|
||||
OAuth.prototype.buildRequest = function(method, path, params, body) {
|
||||
if (path.indexOf("/") != 0) {
|
||||
path = "/" + path;
|
||||
if (path.indexOf('/') != 0) {
|
||||
path = '/' + path;
|
||||
}
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
path += "?" + OAuth.buildParameterString(params);
|
||||
path += '?' + OAuth.buildParameterString(params);
|
||||
}
|
||||
|
||||
var request = {
|
||||
host: this.host,
|
||||
path: path,
|
||||
method: method.toUpperCase()
|
||||
host: this.host,
|
||||
path: path,
|
||||
method: method.toUpperCase(),
|
||||
};
|
||||
|
||||
var oauth_params = this.oauth_params || {};
|
||||
oauth_params.oauth_consumer_key = this.consumer_key;
|
||||
if(this.auth_token){
|
||||
oauth_params["oauth_token"] = this.auth_token;
|
||||
if (this.auth_token) {
|
||||
oauth_params['oauth_token'] = this.auth_token;
|
||||
}
|
||||
|
||||
request = OAuth.signRequest(request, oauth_params, this.consumer_secret, this.auth_token_secret);
|
||||
request = OAuth.signRequest(
|
||||
request,
|
||||
oauth_params,
|
||||
this.consumer_secret,
|
||||
this.auth_token_secret
|
||||
);
|
||||
|
||||
if (body && Object.keys(body).length > 0) {
|
||||
request.body = OAuth.buildParameterString(body);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
};
|
||||
|
||||
OAuth.prototype.get = function(path, params) {
|
||||
return this.send("GET", path, params);
|
||||
}
|
||||
return this.send('GET', path, params);
|
||||
};
|
||||
|
||||
OAuth.prototype.post = function(path, params, body) {
|
||||
return this.send("POST", path, params, body);
|
||||
}
|
||||
return this.send('POST', path, params, body);
|
||||
};
|
||||
|
||||
/*
|
||||
Proper string %escape encoding
|
||||
@@ -99,8 +108,7 @@ OAuth.encode = function(str) {
|
||||
// example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a');
|
||||
// returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a'
|
||||
|
||||
str = (str + '')
|
||||
.toString();
|
||||
str = (str + '').toString();
|
||||
|
||||
// Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current
|
||||
// PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following.
|
||||
@@ -110,55 +118,72 @@ OAuth.encode = function(str) {
|
||||
.replace(/\(/g, '%28')
|
||||
.replace(/\)/g, '%29')
|
||||
.replace(/\*/g, '%2A');
|
||||
}
|
||||
};
|
||||
|
||||
OAuth.signatureMethod = "HMAC-SHA1";
|
||||
OAuth.version = "1.0";
|
||||
OAuth.signatureMethod = 'HMAC-SHA1';
|
||||
OAuth.version = '1.0';
|
||||
|
||||
/*
|
||||
Generate a nonce
|
||||
*/
|
||||
OAuth.nonce = function(){
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
OAuth.nonce = function() {
|
||||
var text = '';
|
||||
var possible =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
for(var i = 0; i < 30; i++)
|
||||
for (var i = 0; i < 30; i++)
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
OAuth.buildParameterString = function(obj){
|
||||
OAuth.buildParameterString = function(obj) {
|
||||
// Sort keys and encode values
|
||||
if (obj) {
|
||||
var keys = Object.keys(obj).sort();
|
||||
|
||||
// Map key=value, join them by &
|
||||
return keys.map(function(key){
|
||||
return key + "=" + OAuth.encode(obj[key]);
|
||||
}).join("&");
|
||||
return keys
|
||||
.map(function(key) {
|
||||
return key + '=' + OAuth.encode(obj[key]);
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/*
|
||||
Build the signature string from the object
|
||||
*/
|
||||
|
||||
OAuth.buildSignatureString = function(method, url, parameters){
|
||||
return [method.toUpperCase(), OAuth.encode(url), OAuth.encode(parameters)].join("&");
|
||||
}
|
||||
OAuth.buildSignatureString = function(method, url, parameters) {
|
||||
return [
|
||||
method.toUpperCase(),
|
||||
OAuth.encode(url),
|
||||
OAuth.encode(parameters),
|
||||
].join('&');
|
||||
};
|
||||
|
||||
/*
|
||||
Retuns encoded HMAC-SHA1 from key and text
|
||||
*/
|
||||
OAuth.signature = function(text, key){
|
||||
crypto = require("crypto");
|
||||
return OAuth.encode(crypto.createHmac('sha1', key).update(text).digest('base64'));
|
||||
}
|
||||
OAuth.signature = function(text, key) {
|
||||
crypto = require('crypto');
|
||||
return OAuth.encode(
|
||||
crypto
|
||||
.createHmac('sha1', key)
|
||||
.update(text)
|
||||
.digest('base64')
|
||||
);
|
||||
};
|
||||
|
||||
OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_token_secret){
|
||||
OAuth.signRequest = function(
|
||||
request,
|
||||
oauth_parameters,
|
||||
consumer_secret,
|
||||
auth_token_secret
|
||||
) {
|
||||
oauth_parameters = oauth_parameters || {};
|
||||
|
||||
// Set default values
|
||||
@@ -175,20 +200,20 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
oauth_parameters.oauth_version = OAuth.version;
|
||||
}
|
||||
|
||||
if(!auth_token_secret){
|
||||
auth_token_secret = "";
|
||||
if (!auth_token_secret) {
|
||||
auth_token_secret = '';
|
||||
}
|
||||
// Force GET method if unset
|
||||
if (!request.method) {
|
||||
request.method = "GET"
|
||||
request.method = 'GET';
|
||||
}
|
||||
|
||||
// Collect all the parameters in one signatureParameters object
|
||||
var signatureParams = {};
|
||||
var parametersToMerge = [request.params, request.body, oauth_parameters];
|
||||
for(var i in parametersToMerge) {
|
||||
for (var i in parametersToMerge) {
|
||||
var parameters = parametersToMerge[i];
|
||||
for(var k in parameters) {
|
||||
for (var k in parameters) {
|
||||
signatureParams[k] = parameters[k];
|
||||
}
|
||||
}
|
||||
@@ -197,32 +222,41 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to
|
||||
var parameterString = OAuth.buildParameterString(signatureParams);
|
||||
|
||||
// Build the signature string
|
||||
var url = "https://" + request.host + "" + request.path;
|
||||
var url = 'https://' + request.host + '' + request.path;
|
||||
|
||||
var signatureString = OAuth.buildSignatureString(request.method, url, parameterString);
|
||||
var signatureString = OAuth.buildSignatureString(
|
||||
request.method,
|
||||
url,
|
||||
parameterString
|
||||
);
|
||||
// Hash the signature string
|
||||
var signatureKey = [OAuth.encode(consumer_secret), OAuth.encode(auth_token_secret)].join("&");
|
||||
var signatureKey = [
|
||||
OAuth.encode(consumer_secret),
|
||||
OAuth.encode(auth_token_secret),
|
||||
].join('&');
|
||||
|
||||
var signature = OAuth.signature(signatureString, signatureKey);
|
||||
|
||||
// Set the signature in the params
|
||||
oauth_parameters.oauth_signature = signature;
|
||||
if(!request.headers){
|
||||
if (!request.headers) {
|
||||
request.headers = {};
|
||||
}
|
||||
|
||||
// Set the authorization header
|
||||
var authHeader = Object.keys(oauth_parameters).sort().map(function(key){
|
||||
var value = oauth_parameters[key];
|
||||
return key + '="' + value + '"';
|
||||
}).join(", ")
|
||||
var authHeader = Object.keys(oauth_parameters)
|
||||
.sort()
|
||||
.map(function(key) {
|
||||
var value = oauth_parameters[key];
|
||||
return key + '="' + value + '"';
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
request.headers.Authorization = 'OAuth ' + authHeader;
|
||||
|
||||
// Set the content type header
|
||||
request.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
return request;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = OAuth;
|
||||
|
||||
@@ -4,15 +4,17 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest('me?fields=id&access_token=' + authData.access_token)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.');
|
||||
});
|
||||
return graphRequest(
|
||||
'me?fields=id&access_token=' + authData.access_token
|
||||
).then(data => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -21,17 +23,18 @@ function validateAppId(appIds, authData) {
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is not configured.');
|
||||
'Facebook auth is not configured.'
|
||||
);
|
||||
}
|
||||
return graphRequest('app?access_token=' + access_token)
|
||||
.then((data) => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.');
|
||||
});
|
||||
return graphRequest('app?access_token=' + access_token).then(data => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// A promisey wrapper for FB graph requests.
|
||||
@@ -41,5 +44,5 @@ function graphRequest(path) {
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
const crypto = require('crypto');
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
const Parse = require('parse/node').Parse;
|
||||
const Parse = require('parse/node').Parse;
|
||||
|
||||
const graphRequest = (path) => {
|
||||
const graphRequest = path => {
|
||||
return httpsRequest.get(`https://graph.accountkit.com/v1.1/${path}`);
|
||||
};
|
||||
|
||||
function getRequestPath(authData, options) {
|
||||
const access_token = authData.access_token, appSecret = options && options.appSecret;
|
||||
const access_token = authData.access_token,
|
||||
appSecret = options && options.appSecret;
|
||||
if (appSecret) {
|
||||
const appsecret_proof = crypto.createHmac("sha256", appSecret).update(access_token).digest('hex');
|
||||
return `me?access_token=${access_token}&appsecret_proof=${appsecret_proof}`
|
||||
const appsecret_proof = crypto
|
||||
.createHmac('sha256', appSecret)
|
||||
.update(access_token)
|
||||
.digest('hex');
|
||||
return `me?access_token=${access_token}&appsecret_proof=${appsecret_proof}`;
|
||||
}
|
||||
return `me?access_token=${access_token}`;
|
||||
}
|
||||
@@ -20,36 +24,37 @@ function validateAppId(appIds, authData, options) {
|
||||
return Promise.reject(
|
||||
new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook app id for Account Kit is not configured.')
|
||||
)
|
||||
'Facebook app id for Account Kit is not configured.'
|
||||
)
|
||||
);
|
||||
}
|
||||
return graphRequest(getRequestPath(authData, options))
|
||||
.then(data => {
|
||||
if (data && data.application && appIds.indexOf(data.application.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook app id for Account Kit is invalid for this user.');
|
||||
})
|
||||
return graphRequest(getRequestPath(authData, options)).then(data => {
|
||||
if (data && data.application && appIds.indexOf(data.application.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook app id for Account Kit is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validateAuthData(authData, options) {
|
||||
return graphRequest(getRequestPath(authData, options))
|
||||
.then(data => {
|
||||
if (data && data.error) {
|
||||
throw data.error;
|
||||
}
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook Account Kit auth is invalid for this user.');
|
||||
})
|
||||
return graphRequest(getRequestPath(authData, options)).then(data => {
|
||||
if (data && data.error) {
|
||||
throw data.error;
|
||||
}
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Facebook Account Kit auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData
|
||||
validateAuthData,
|
||||
};
|
||||
|
||||
@@ -4,15 +4,15 @@ const httpsRequest = require('./httpsRequest');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request('user', authData.access_token)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Github auth is invalid for this user.');
|
||||
});
|
||||
return request('user', authData.access_token).then(data => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Github auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -26,13 +26,13 @@ function request(path, access_token) {
|
||||
host: 'api.github.com',
|
||||
path: '/' + path,
|
||||
headers: {
|
||||
'Authorization': 'bearer ' + access_token,
|
||||
'User-Agent': 'parse-server'
|
||||
}
|
||||
Authorization: 'bearer ' + access_token,
|
||||
'User-Agent': 'parse-server',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -3,27 +3,27 @@ var Parse = require('parse/node').Parse;
|
||||
const httpsRequest = require('./httpsRequest');
|
||||
|
||||
function validateIdToken(id, token) {
|
||||
return googleRequest("tokeninfo?id_token=" + token)
|
||||
.then((response) => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is invalid for this user.');
|
||||
});
|
||||
return googleRequest('tokeninfo?id_token=' + token).then(response => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validateAuthToken(id, token) {
|
||||
return googleRequest("tokeninfo?access_token=" + token)
|
||||
.then((response) => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is invalid for this user.');
|
||||
});
|
||||
return googleRequest('tokeninfo?access_token=' + token).then(response => {
|
||||
if (response && (response.sub == id || response.user_id == id)) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Google auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this user id is valid.
|
||||
@@ -31,13 +31,16 @@ function validateAuthData(authData) {
|
||||
if (authData.id_token) {
|
||||
return validateIdToken(authData.id, authData.id_token);
|
||||
} else {
|
||||
return validateAuthToken(authData.id, authData.access_token).then(() => {
|
||||
// Validation with auth token worked
|
||||
return;
|
||||
}, () => {
|
||||
// Try with the id_token param
|
||||
return validateIdToken(authData.id, authData.access_token);
|
||||
});
|
||||
return validateAuthToken(authData.id, authData.access_token).then(
|
||||
() => {
|
||||
// Validation with auth token worked
|
||||
return;
|
||||
},
|
||||
() => {
|
||||
// Try with the id_token param
|
||||
return validateIdToken(authData.id, authData.access_token);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +51,10 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function googleRequest(path) {
|
||||
return httpsRequest.get("https://www.googleapis.com/oauth2/v3/" + path);
|
||||
return httpsRequest.get('https://www.googleapis.com/oauth2/v3/' + path);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ const https = require('https');
|
||||
function makeCallback(resolve, reject, noJSON) {
|
||||
return function(res) {
|
||||
let data = '';
|
||||
res.on('data', (chunk) => {
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
@@ -12,7 +12,7 @@ function makeCallback(resolve, reject, noJSON) {
|
||||
}
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return reject(e);
|
||||
}
|
||||
resolve(data);
|
||||
|
||||
@@ -2,20 +2,20 @@ import loadAdapter from '../AdapterLoader';
|
||||
|
||||
const facebook = require('./facebook');
|
||||
const facebookaccountkit = require('./facebookaccountkit');
|
||||
const instagram = require("./instagram");
|
||||
const linkedin = require("./linkedin");
|
||||
const meetup = require("./meetup");
|
||||
const google = require("./google");
|
||||
const github = require("./github");
|
||||
const twitter = require("./twitter");
|
||||
const spotify = require("./spotify");
|
||||
const digits = require("./twitter"); // digits tokens are validated by twitter
|
||||
const janrainengage = require("./janrainengage");
|
||||
const janraincapture = require("./janraincapture");
|
||||
const vkontakte = require("./vkontakte");
|
||||
const qq = require("./qq");
|
||||
const wechat = require("./wechat");
|
||||
const weibo = require("./weibo");
|
||||
const instagram = require('./instagram');
|
||||
const linkedin = require('./linkedin');
|
||||
const meetup = require('./meetup');
|
||||
const google = require('./google');
|
||||
const github = require('./github');
|
||||
const twitter = require('./twitter');
|
||||
const spotify = require('./spotify');
|
||||
const digits = require('./twitter'); // digits tokens are validated by twitter
|
||||
const janrainengage = require('./janrainengage');
|
||||
const janraincapture = require('./janraincapture');
|
||||
const vkontakte = require('./vkontakte');
|
||||
const qq = require('./qq');
|
||||
const wechat = require('./wechat');
|
||||
const weibo = require('./weibo');
|
||||
|
||||
const anonymous = {
|
||||
validateAuthData: () => {
|
||||
@@ -23,8 +23,8 @@ const anonymous = {
|
||||
},
|
||||
validateAppId: () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const providers = {
|
||||
facebook,
|
||||
@@ -43,8 +43,8 @@ const providers = {
|
||||
vkontakte,
|
||||
qq,
|
||||
wechat,
|
||||
weibo
|
||||
}
|
||||
weibo,
|
||||
};
|
||||
function authDataValidator(adapter, appIds, options) {
|
||||
return function(authData) {
|
||||
return adapter.validateAuthData(authData, options).then(() => {
|
||||
@@ -53,7 +53,7 @@ function authDataValidator(adapter, appIds, options) {
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function loadAuthAdapter(provider, authOptions) {
|
||||
@@ -69,9 +69,13 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
|
||||
// Try the configuration methods
|
||||
if (providerOptions) {
|
||||
const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions);
|
||||
const optionalAdapter = loadAdapter(
|
||||
providerOptions,
|
||||
undefined,
|
||||
providerOptions
|
||||
);
|
||||
if (optionalAdapter) {
|
||||
['validateAuthData', 'validateAppId'].forEach((key) => {
|
||||
['validateAuthData', 'validateAppId'].forEach(key => {
|
||||
if (optionalAdapter[key]) {
|
||||
adapter[key] = optionalAdapter[key];
|
||||
}
|
||||
@@ -83,34 +87,32 @@ function loadAuthAdapter(provider, authOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {adapter, appIds, providerOptions};
|
||||
return { adapter, appIds, providerOptions };
|
||||
}
|
||||
|
||||
module.exports = function(authOptions = {}, enableAnonymousUsers = true) {
|
||||
let _enableAnonymousUsers = enableAnonymousUsers;
|
||||
const setEnableAnonymousUsers = function(enable) {
|
||||
_enableAnonymousUsers = enable;
|
||||
}
|
||||
};
|
||||
// To handle the test cases on configuration
|
||||
const getValidatorForProvider = function(provider) {
|
||||
|
||||
if (provider === 'anonymous' && !_enableAnonymousUsers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
adapter,
|
||||
appIds,
|
||||
providerOptions
|
||||
} = loadAuthAdapter(provider, authOptions);
|
||||
const { adapter, appIds, providerOptions } = loadAuthAdapter(
|
||||
provider,
|
||||
authOptions
|
||||
);
|
||||
|
||||
return authDataValidator(adapter, appIds, providerOptions);
|
||||
}
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
getValidatorForProvider,
|
||||
setEnableAnonymousUsers
|
||||
})
|
||||
}
|
||||
setEnableAnonymousUsers,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.loadAuthAdapter = loadAuthAdapter;
|
||||
|
||||
@@ -4,15 +4,17 @@ const httpsRequest = require('./httpsRequest');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request("users/self/?access_token=" + authData.access_token)
|
||||
.then((response) => {
|
||||
return request('users/self/?access_token=' + authData.access_token).then(
|
||||
response => {
|
||||
if (response && response.data && response.data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Instagram auth is invalid for this user.');
|
||||
});
|
||||
'Instagram auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -22,10 +24,10 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function request(path) {
|
||||
return httpsRequest.get("https://api.instagram.com/v1/" + path);
|
||||
return httpsRequest.get('https://api.instagram.com/v1/' + path);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -5,15 +5,19 @@ const httpsRequest = require('./httpsRequest');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
return request(options.janrain_capture_host, authData.access_token)
|
||||
.then((data) => {
|
||||
return request(options.janrain_capture_host, authData.access_token).then(
|
||||
data => {
|
||||
//successful response will have a "stat" (status) of 'ok' and a result node that stores the uuid, because that's all we asked for
|
||||
//see: https://docs.janrain.com/api/registration/entity/#entity
|
||||
if (data && data.stat == 'ok' && data.result == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Janrain capture auth is invalid for this user.');
|
||||
});
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Janrain capture auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -24,10 +28,9 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function request(host, access_token) {
|
||||
|
||||
var query_string_data = querystring.stringify({
|
||||
'access_token': access_token,
|
||||
'attribute_name': 'uuid' // we only need to pull the uuid for this access token to make sure it matches
|
||||
access_token: access_token,
|
||||
attribute_name: 'uuid', // we only need to pull the uuid for this access token to make sure it matches
|
||||
});
|
||||
|
||||
return httpsRequest.get({ host: host, path: '/entity?' + query_string_data });
|
||||
@@ -35,5 +38,5 @@ function request(host, access_token) {
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -5,15 +5,17 @@ var querystring = require('querystring');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
return apiRequest(options.api_key, authData.auth_token)
|
||||
.then((data) => {
|
||||
//successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
|
||||
//see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
|
||||
if (data && data.stat == 'ok' && data.profile.identifier == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Janrain engage auth is invalid for this user.');
|
||||
});
|
||||
return apiRequest(options.api_key, authData.auth_token).then(data => {
|
||||
//successful response will have a "stat" (status) of 'ok' and a profile node with an identifier
|
||||
//see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data
|
||||
if (data && data.stat == 'ok' && data.profile.identifier == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Janrain engage auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -24,11 +26,10 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function apiRequest(api_key, auth_token) {
|
||||
|
||||
var post_data = querystring.stringify({
|
||||
'token': auth_token,
|
||||
'apiKey': api_key,
|
||||
'format': 'json'
|
||||
token: auth_token,
|
||||
apiKey: api_key,
|
||||
format: 'json',
|
||||
});
|
||||
|
||||
var post_options = {
|
||||
@@ -37,8 +38,8 @@ function apiRequest(api_key, auth_token) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': post_data.length
|
||||
}
|
||||
'Content-Length': post_data.length,
|
||||
},
|
||||
};
|
||||
|
||||
return httpsRequest.request(post_options, post_data);
|
||||
@@ -46,5 +47,5 @@ function apiRequest(api_key, auth_token) {
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -4,15 +4,19 @@ const httpsRequest = require('./httpsRequest');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request('people/~:(id)', authData.access_token, authData.is_mobile_sdk)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Linkedin auth is invalid for this user.');
|
||||
});
|
||||
return request(
|
||||
'people/~:(id)',
|
||||
authData.access_token,
|
||||
authData.is_mobile_sdk
|
||||
).then(data => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Linkedin auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -23,21 +27,21 @@ function validateAppId() {
|
||||
// A promisey wrapper for api requests
|
||||
function request(path, access_token, is_mobile_sdk) {
|
||||
var headers = {
|
||||
'Authorization': 'Bearer ' + access_token,
|
||||
Authorization: 'Bearer ' + access_token,
|
||||
'x-li-format': 'json',
|
||||
}
|
||||
};
|
||||
|
||||
if(is_mobile_sdk) {
|
||||
if (is_mobile_sdk) {
|
||||
headers['x-li-src'] = 'msdk';
|
||||
}
|
||||
return httpsRequest.get({
|
||||
host: 'api.linkedin.com',
|
||||
path: '/v1/' + path,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -4,15 +4,15 @@ const httpsRequest = require('./httpsRequest');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request('member/self', authData.access_token)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Meetup auth is invalid for this user.');
|
||||
});
|
||||
return request('member/self', authData.access_token).then(data => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Meetup auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills iff this app id is valid.
|
||||
@@ -26,12 +26,12 @@ function request(path, access_token) {
|
||||
host: 'api.meetup.com',
|
||||
path: '/2/' + path,
|
||||
headers: {
|
||||
'Authorization': 'bearer ' + access_token
|
||||
}
|
||||
Authorization: 'bearer ' + access_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -4,11 +4,16 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest('me?access_token=' + authData.access_token).then(function (data) {
|
||||
return graphRequest('me?access_token=' + authData.access_token).then(function(
|
||||
data
|
||||
) {
|
||||
if (data && data.openid == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,18 +24,23 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for qq graph requests.
|
||||
function graphRequest(path) {
|
||||
return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then((data) => {
|
||||
return parseResponseData(data);
|
||||
});
|
||||
return httpsRequest
|
||||
.get('https://graph.qq.com/oauth2.0/' + path, true)
|
||||
.then(data => {
|
||||
return parseResponseData(data);
|
||||
});
|
||||
}
|
||||
|
||||
function parseResponseData(data) {
|
||||
const starPos = data.indexOf("(");
|
||||
const endPos = data.indexOf(")");
|
||||
if(starPos == -1 || endPos == -1){
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.');
|
||||
const starPos = data.indexOf('(');
|
||||
const endPos = data.indexOf(')');
|
||||
if (starPos == -1 || endPos == -1) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'qq auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
data = data.substring(starPos + 1,endPos - 1);
|
||||
data = data.substring(starPos + 1, endPos - 1);
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return request('me', authData.access_token)
|
||||
.then((data) => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Spotify auth is invalid for this user.');
|
||||
});
|
||||
return request('me', authData.access_token).then(data => {
|
||||
if (data && data.id == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Spotify auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise that fulfills if this app id is valid.
|
||||
@@ -21,17 +21,18 @@ function validateAppId(appIds, authData) {
|
||||
if (!appIds.length) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Spotify auth is not configured.');
|
||||
'Spotify auth is not configured.'
|
||||
);
|
||||
}
|
||||
return request('me', access_token)
|
||||
.then((data) => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Spotify auth is invalid for this user.');
|
||||
});
|
||||
return request('me', access_token).then(data => {
|
||||
if (data && appIds.indexOf(data.id) != -1) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Spotify auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// A promisey wrapper for Spotify API requests.
|
||||
@@ -40,12 +41,12 @@ function request(path, access_token) {
|
||||
host: 'api.spotify.com',
|
||||
path: '/v1/' + path,
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
}
|
||||
Authorization: 'Bearer ' + access_token,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -5,22 +5,26 @@ var logger = require('../../logger').default;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, options) {
|
||||
if(!options) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing');
|
||||
if (!options) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'Twitter auth configuration missing'
|
||||
);
|
||||
}
|
||||
options = handleMultipleConfigurations(authData, options);
|
||||
var client = new OAuth(options);
|
||||
client.host = "api.twitter.com";
|
||||
client.host = 'api.twitter.com';
|
||||
client.auth_token = authData.auth_token;
|
||||
client.auth_token_secret = authData.auth_token_secret;
|
||||
|
||||
return client.get("/1.1/account/verify_credentials.json").then((data) => {
|
||||
return client.get('/1.1/account/verify_credentials.json').then(data => {
|
||||
if (data && data.id_str == '' + authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.');
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,16 +37,28 @@ function handleMultipleConfigurations(authData, options) {
|
||||
if (Array.isArray(options)) {
|
||||
const consumer_key = authData.consumer_key;
|
||||
if (!consumer_key) {
|
||||
logger.error('Twitter Auth', 'Multiple twitter configurations are available, by no consumer_key was sent by the client.');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
logger.error(
|
||||
'Twitter Auth',
|
||||
'Multiple twitter configurations are available, by no consumer_key was sent by the client.'
|
||||
);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
options = options.filter((option) => {
|
||||
options = options.filter(option => {
|
||||
return option.consumer_key == consumer_key;
|
||||
});
|
||||
|
||||
if (options.length == 0) {
|
||||
logger.error('Twitter Auth','Cannot find a configuration for the provided consumer_key');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.');
|
||||
logger.error(
|
||||
'Twitter Auth',
|
||||
'Cannot find a configuration for the provided consumer_key'
|
||||
);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Twitter auth is invalid for this user.'
|
||||
);
|
||||
}
|
||||
options = options[0];
|
||||
}
|
||||
@@ -52,5 +68,5 @@ function handleMultipleConfigurations(authData, options) {
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData,
|
||||
handleMultipleConfigurations
|
||||
handleMultipleConfigurations,
|
||||
};
|
||||
|
||||
@@ -8,29 +8,62 @@ var logger = require('../../logger').default;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData, params) {
|
||||
return vkOAuth2Request(params).then(function (response) {
|
||||
return vkOAuth2Request(params).then(function(response) {
|
||||
if (response && response.access_token) {
|
||||
return request("api.vk.com", "method/users.get?access_token=" + authData.access_token + "&v=5.8").then(function (response) {
|
||||
if (response && response.response && response.response.length && response.response[0].id == authData.id) {
|
||||
return request(
|
||||
'api.vk.com',
|
||||
'method/users.get?access_token=' + authData.access_token + '&v=5.8'
|
||||
).then(function(response) {
|
||||
if (
|
||||
response &&
|
||||
response.response &&
|
||||
response.response.length &&
|
||||
response.response[0].id == authData.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Vk auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
logger.error('Vk Auth', 'Vk appIds or appSecret is incorrect.');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Vk appIds or appSecret is incorrect.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function vkOAuth2Request(params) {
|
||||
return new Promise(function (resolve) {
|
||||
if (!params || !params.appIds || !params.appIds.length || !params.appSecret || !params.appSecret.length) {
|
||||
logger.error('Vk Auth', 'Vk auth is not configured. Missing appIds or appSecret.');
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is not configured. Missing appIds or appSecret.');
|
||||
return new Promise(function(resolve) {
|
||||
if (
|
||||
!params ||
|
||||
!params.appIds ||
|
||||
!params.appIds.length ||
|
||||
!params.appSecret ||
|
||||
!params.appSecret.length
|
||||
) {
|
||||
logger.error(
|
||||
'Vk Auth',
|
||||
'Vk auth is not configured. Missing appIds or appSecret.'
|
||||
);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Vk auth is not configured. Missing appIds or appSecret.'
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
}).then(function () {
|
||||
return request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials");
|
||||
}).then(function() {
|
||||
return request(
|
||||
'oauth.vk.com',
|
||||
'access_token?client_id=' +
|
||||
params.appIds +
|
||||
'&client_secret=' +
|
||||
params.appSecret +
|
||||
'&v=5.59&grant_type=client_credentials'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,10 +74,10 @@ function validateAppId() {
|
||||
|
||||
// A promisey wrapper for api requests
|
||||
function request(host, path) {
|
||||
return httpsRequest.get("https://" + host + "/" + path);
|
||||
return httpsRequest.get('https://' + host + '/' + path);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId: validateAppId,
|
||||
validateAuthData: validateAuthData
|
||||
validateAuthData: validateAuthData,
|
||||
};
|
||||
|
||||
@@ -4,11 +4,16 @@ var Parse = require('parse/node').Parse;
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(function (data) {
|
||||
return graphRequest(
|
||||
'auth?access_token=' + authData.access_token + '&openid=' + authData.id
|
||||
).then(function(data) {
|
||||
if (data.errcode == 0) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'wechat auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,5 +29,5 @@ function graphRequest(path) {
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData
|
||||
validateAuthData,
|
||||
};
|
||||
|
||||
@@ -5,11 +5,14 @@ var querystring = require('querystring');
|
||||
|
||||
// Returns a promise that fulfills iff this user id is valid.
|
||||
function validateAuthData(authData) {
|
||||
return graphRequest(authData.access_token).then(function (data) {
|
||||
return graphRequest(authData.access_token).then(function(data) {
|
||||
if (data && data.uid == authData.id) {
|
||||
return;
|
||||
}
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'weibo auth is invalid for this user.'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +24,7 @@ function validateAppId() {
|
||||
// A promisey wrapper for weibo graph requests.
|
||||
function graphRequest(access_token) {
|
||||
var postData = querystring.stringify({
|
||||
"access_token": access_token
|
||||
access_token: access_token,
|
||||
});
|
||||
var options = {
|
||||
hostname: 'api.weibo.com',
|
||||
@@ -29,13 +32,13 @@ function graphRequest(access_token) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Content-Length': Buffer.byteLength(postData)
|
||||
}
|
||||
'Content-Length': Buffer.byteLength(postData),
|
||||
},
|
||||
};
|
||||
return httpsRequest.request(options, postData);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateAppId,
|
||||
validateAuthData
|
||||
validateAuthData,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
const DEFAULT_CACHE_TTL = 5 * 1000;
|
||||
|
||||
|
||||
export class InMemoryCache {
|
||||
constructor({
|
||||
ttl = DEFAULT_CACHE_TTL
|
||||
}) {
|
||||
constructor({ ttl = DEFAULT_CACHE_TTL }) {
|
||||
this.ttl = ttl;
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
@@ -32,8 +29,8 @@ export class InMemoryCache {
|
||||
|
||||
var record = {
|
||||
value: value,
|
||||
expire: ttl + Date.now()
|
||||
}
|
||||
expire: ttl + Date.now(),
|
||||
};
|
||||
|
||||
if (!isNaN(record.expire)) {
|
||||
record.timeout = setTimeout(() => {
|
||||
@@ -59,7 +56,6 @@ export class InMemoryCache {
|
||||
clear() {
|
||||
this.cache = Object.create(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default InMemoryCache;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {LRUCache} from './LRUCache';
|
||||
import { LRUCache } from './LRUCache';
|
||||
|
||||
export class InMemoryCacheAdapter {
|
||||
|
||||
constructor(ctx) {
|
||||
this.cache = new LRUCache(ctx)
|
||||
this.cache = new LRUCache(ctx);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import LRU from 'lru-cache';
|
||||
import defaults from '../../defaults';
|
||||
import defaults from '../../defaults';
|
||||
|
||||
export class LRUCache {
|
||||
constructor({
|
||||
ttl = defaults.cacheTTL,
|
||||
maxSize = defaults.cacheMaxSize,
|
||||
}) {
|
||||
constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: ttl
|
||||
maxAge: ttl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,7 +24,6 @@ export class LRUCache {
|
||||
clear() {
|
||||
this.cache.reset();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default LRUCache;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
export class NullCacheAdapter {
|
||||
|
||||
constructor() {}
|
||||
|
||||
get() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
return resolve(null);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
put() {
|
||||
|
||||
@@ -8,7 +8,6 @@ function debug() {
|
||||
}
|
||||
|
||||
export class RedisCacheAdapter {
|
||||
|
||||
constructor(redisCtx, ttl = DEFAULT_REDIS_TTL) {
|
||||
this.client = redis.createClient(redisCtx);
|
||||
this.p = Promise.resolve();
|
||||
@@ -18,10 +17,10 @@ export class RedisCacheAdapter {
|
||||
get(key) {
|
||||
debug('get', key);
|
||||
this.p = this.p.then(() => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.client.get(key, function(err, res) {
|
||||
debug('-> get', key, res);
|
||||
if(!res) {
|
||||
if (!res) {
|
||||
return resolve(null);
|
||||
}
|
||||
resolve(JSON.parse(res));
|
||||
@@ -41,7 +40,7 @@ export class RedisCacheAdapter {
|
||||
ttl = DEFAULT_REDIS_TTL;
|
||||
}
|
||||
this.p = this.p.then(() => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
if (ttl === Infinity) {
|
||||
this.client.set(key, value, function() {
|
||||
resolve();
|
||||
@@ -59,7 +58,7 @@ export class RedisCacheAdapter {
|
||||
del(key) {
|
||||
debug('del', key);
|
||||
this.p = this.p.then(() => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.client.del(key, function() {
|
||||
resolve();
|
||||
});
|
||||
@@ -71,7 +70,7 @@ export class RedisCacheAdapter {
|
||||
clear() {
|
||||
debug('clear');
|
||||
this.p = this.p.then(() => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
this.client.flushdb(function() {
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// and for the API server to be using the DatabaseController with Mongo
|
||||
// database adapter.
|
||||
|
||||
import type { Config } from '../../Config'
|
||||
import type { Config } from '../../Config';
|
||||
/**
|
||||
* @module Adapters
|
||||
*/
|
||||
@@ -21,7 +21,6 @@ import type { Config } from '../../Config'
|
||||
* @interface FilesAdapter
|
||||
*/
|
||||
export class FilesAdapter {
|
||||
|
||||
/** Responsible for storing the file in order to be retrieved later by its filename
|
||||
*
|
||||
* @param {string} filename - the filename to save
|
||||
@@ -31,7 +30,7 @@ export class FilesAdapter {
|
||||
*
|
||||
* @return {Promise} a promise that should fail if the storage didn't succeed
|
||||
*/
|
||||
createFile(filename: string, data, contentType: string): Promise { }
|
||||
createFile(filename: string, data, contentType: string): Promise {}
|
||||
|
||||
/** Responsible for deleting the specified file
|
||||
*
|
||||
@@ -39,7 +38,7 @@ export class FilesAdapter {
|
||||
*
|
||||
* @return {Promise} a promise that should fail if the deletion didn't succeed
|
||||
*/
|
||||
deleteFile(filename: string): Promise { }
|
||||
deleteFile(filename: string): Promise {}
|
||||
|
||||
/** Responsible for retrieving the data of the specified file
|
||||
*
|
||||
@@ -47,7 +46,7 @@ export class FilesAdapter {
|
||||
*
|
||||
* @return {Promise} a promise that should pass with the file data or fail on error
|
||||
*/
|
||||
getFileData(filename: string): Promise<any> { }
|
||||
getFileData(filename: string): Promise<any> {}
|
||||
|
||||
/** Returns an absolute URL where the file can be accessed
|
||||
*
|
||||
@@ -56,7 +55,7 @@ export class FilesAdapter {
|
||||
*
|
||||
* @return {string} Absolute URL
|
||||
*/
|
||||
getFileLocation(config: Config, filename: string): string { }
|
||||
getFileLocation(config: Config, filename: string): string {}
|
||||
}
|
||||
|
||||
export default FilesAdapter;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
*/
|
||||
|
||||
// @flow-disable-next
|
||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import defaults from '../../defaults';
|
||||
import { MongoClient, GridStore, Db } from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import defaults from '../../defaults';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
@@ -22,8 +22,9 @@ export class GridStoreAdapter extends FilesAdapter {
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI)
|
||||
.then((client) => client.db(client.s.options.dbName));
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI).then(
|
||||
client => client.db(client.s.options.dbName)
|
||||
);
|
||||
}
|
||||
return this._connectionPromise;
|
||||
}
|
||||
@@ -31,41 +32,54 @@ export class GridStoreAdapter extends FilesAdapter {
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(filename: string, data) {
|
||||
return this._connect().then((database) => {
|
||||
const gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
}).then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
const gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(filename: string) {
|
||||
return this._connect().then(database => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.unlink();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.close();
|
||||
});
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.unlink();
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(filename: string) {
|
||||
return this._connect().then(database => {
|
||||
return GridStore.exist(database, filename)
|
||||
.then(() => {
|
||||
return this._connect()
|
||||
.then(database => {
|
||||
return GridStore.exist(database, filename).then(() => {
|
||||
const gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
}).then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
})
|
||||
.then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
getFileLocation(config, filename) {
|
||||
return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename));
|
||||
return (
|
||||
config.mount +
|
||||
'/files/' +
|
||||
config.applicationId +
|
||||
'/' +
|
||||
encodeURIComponent(filename)
|
||||
);
|
||||
}
|
||||
|
||||
getFileStream(filename: string) {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class LoggerAdapter {
|
||||
* @param {String} message
|
||||
* @param {Object} metadata
|
||||
*/
|
||||
log(level, message, /* meta */) {}
|
||||
log(level, message /* meta */) {}
|
||||
}
|
||||
|
||||
export default LoggerAdapter;
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import _ from 'lodash';
|
||||
import defaults from '../../defaults';
|
||||
import defaults from '../../defaults';
|
||||
|
||||
const logger = new winston.Logger();
|
||||
const additionalTransports = [];
|
||||
@@ -17,31 +17,47 @@ function updateTransports(options) {
|
||||
delete transports['parse-server'];
|
||||
delete transports['parse-server-error'];
|
||||
} else if (!_.isUndefined(options.dirname)) {
|
||||
transports['parse-server'] = new (DailyRotateFile)(
|
||||
Object.assign({}, {
|
||||
filename: 'parse-server.info',
|
||||
name: 'parse-server',
|
||||
}, options, { timestamp: true }));
|
||||
transports['parse-server-error'] = new (DailyRotateFile)(
|
||||
Object.assign({}, {
|
||||
filename: 'parse-server.err',
|
||||
name: 'parse-server-error',
|
||||
}, options, { level: 'error', timestamp: true }));
|
||||
transports['parse-server'] = new DailyRotateFile(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
filename: 'parse-server.info',
|
||||
name: 'parse-server',
|
||||
},
|
||||
options,
|
||||
{ timestamp: true }
|
||||
)
|
||||
);
|
||||
transports['parse-server-error'] = new DailyRotateFile(
|
||||
Object.assign(
|
||||
{},
|
||||
{
|
||||
filename: 'parse-server.err',
|
||||
name: 'parse-server-error',
|
||||
},
|
||||
options,
|
||||
{ level: 'error', timestamp: true }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
transports.console = new (winston.transports.Console)(
|
||||
Object.assign({
|
||||
colorize: true,
|
||||
name: 'console',
|
||||
silent
|
||||
}, options));
|
||||
transports.console = new winston.transports.Console(
|
||||
Object.assign(
|
||||
{
|
||||
colorize: true,
|
||||
name: 'console',
|
||||
silent,
|
||||
},
|
||||
options
|
||||
)
|
||||
);
|
||||
}
|
||||
// Mount the additional transports
|
||||
additionalTransports.forEach((transport) => {
|
||||
additionalTransports.forEach(transport => {
|
||||
transports[transport.name] = transport;
|
||||
});
|
||||
logger.configure({
|
||||
transports: _.values(transports)
|
||||
transports: _.values(transports),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,8 +66,8 @@ export function configureLogger({
|
||||
jsonLogs = defaults.jsonLogs,
|
||||
logLevel = winston.level,
|
||||
verbose = defaults.verbose,
|
||||
silent = defaults.silent } = {}) {
|
||||
|
||||
silent = defaults.silent,
|
||||
} = {}) {
|
||||
if (verbose) {
|
||||
logLevel = 'verbose';
|
||||
}
|
||||
@@ -65,7 +81,9 @@ export function configureLogger({
|
||||
}
|
||||
try {
|
||||
fs.mkdirSync(logsFolder);
|
||||
} catch (e) { /* */ }
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
}
|
||||
options.dirname = logsFolder;
|
||||
options.level = logLevel;
|
||||
@@ -84,13 +102,14 @@ export function addTransport(transport) {
|
||||
}
|
||||
|
||||
export function removeTransport(transport) {
|
||||
const transportName = typeof transport == 'string' ? transport : transport.name;
|
||||
const transportName =
|
||||
typeof transport == 'string' ? transport : transport.name;
|
||||
const transports = Object.assign({}, logger.transports);
|
||||
delete transports[transportName];
|
||||
logger.configure({
|
||||
transports: _.values(transports)
|
||||
transports: _.values(transports),
|
||||
});
|
||||
_.remove(additionalTransports, (transport) => {
|
||||
_.remove(additionalTransports, transport => {
|
||||
return transport.name === transportName;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ export class WinstonLoggerAdapter extends LoggerAdapter {
|
||||
options = {};
|
||||
}
|
||||
// defaults to 7 days prior
|
||||
const from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
|
||||
const from =
|
||||
options.from || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
|
||||
const until = options.until || new Date();
|
||||
const limit = options.size || 10;
|
||||
const order = options.order || 'desc';
|
||||
@@ -38,7 +39,7 @@ export class WinstonLoggerAdapter extends LoggerAdapter {
|
||||
from,
|
||||
until,
|
||||
limit,
|
||||
order
|
||||
order,
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -54,7 +55,7 @@ export class WinstonLoggerAdapter extends LoggerAdapter {
|
||||
callback(res['parse-server']);
|
||||
resolve(res['parse-server']);
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,9 @@ class Consumer extends events.EventEmitter {
|
||||
|
||||
subscribe(channel: string): void {
|
||||
unsubscribe(channel);
|
||||
const handler = (message) => {
|
||||
const handler = message => {
|
||||
this.emit('message', channel, message);
|
||||
}
|
||||
};
|
||||
subscriptions.set(channel, handler);
|
||||
this.emitter.on(channel, handler);
|
||||
}
|
||||
@@ -57,9 +57,7 @@ function createSubscriber(): any {
|
||||
|
||||
const EventEmitterMQ = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
createSubscriber,
|
||||
};
|
||||
|
||||
export {
|
||||
EventEmitterMQ
|
||||
}
|
||||
export { EventEmitterMQ };
|
||||
|
||||
@@ -25,9 +25,9 @@ class Subscriber extends events.EventEmitter {
|
||||
}
|
||||
|
||||
subscribe(channel: string): void {
|
||||
const handler = (message) => {
|
||||
const handler = message => {
|
||||
this.emit('message', channel, message);
|
||||
}
|
||||
};
|
||||
this.subscriptions.set(channel, handler);
|
||||
this.emitter.on(channel, handler);
|
||||
}
|
||||
@@ -51,9 +51,7 @@ function createSubscriber(): any {
|
||||
|
||||
const EventEmitterPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
createSubscriber,
|
||||
};
|
||||
|
||||
export {
|
||||
EventEmitterPubSub
|
||||
}
|
||||
export { EventEmitterPubSub };
|
||||
|
||||
@@ -25,7 +25,7 @@ interface Publisher {
|
||||
* @param {String} channel the channel in which to publish
|
||||
* @param {String} message the message to publish
|
||||
*/
|
||||
publish(channel: string, message: string):void;
|
||||
publish(channel: string, message: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import redis from 'redis';
|
||||
|
||||
function createPublisher({redisURL}): any {
|
||||
function createPublisher({ redisURL }): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
function createSubscriber({redisURL}): any {
|
||||
function createSubscriber({ redisURL }): any {
|
||||
return redis.createClient(redisURL, { no_ready_check: true });
|
||||
}
|
||||
|
||||
const RedisPubSub = {
|
||||
createPublisher,
|
||||
createSubscriber
|
||||
}
|
||||
createSubscriber,
|
||||
};
|
||||
|
||||
export {
|
||||
RedisPubSub
|
||||
}
|
||||
export { RedisPubSub };
|
||||
|
||||
@@ -31,7 +31,7 @@ export class PushAdapter {
|
||||
* @returns {Array} An array of valid push types
|
||||
*/
|
||||
getValidPushTypes(): string[] {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ const mongodb = require('mongodb');
|
||||
const Collection = mongodb.Collection;
|
||||
|
||||
export default class MongoCollection {
|
||||
_mongoCollection:Collection;
|
||||
_mongoCollection: Collection;
|
||||
|
||||
constructor(mongoCollection:Collection) {
|
||||
constructor(mongoCollection: Collection) {
|
||||
this._mongoCollection = mongoCollection;
|
||||
}
|
||||
|
||||
@@ -15,33 +15,58 @@ export default class MongoCollection {
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
// Support for Full Text Search - $text
|
||||
if(keys && keys.$score) {
|
||||
if (keys && keys.$score) {
|
||||
delete keys.$score;
|
||||
keys.score = {$meta: 'textScore'};
|
||||
keys.score = { $meta: 'textScore' };
|
||||
}
|
||||
return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference })
|
||||
.catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
return this._rawFind(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
}).catch(error => {
|
||||
// Check for "no geoindex" error
|
||||
if (
|
||||
error.code != 17007 &&
|
||||
!error.message.match(/unable to find index for .geoNear/)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
// Figure out what key needs an index
|
||||
const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
return this._mongoCollection.createIndex(index)
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
return (
|
||||
this._mongoCollection
|
||||
.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference }));
|
||||
});
|
||||
.then(() =>
|
||||
this._rawFind(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
keys,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) {
|
||||
let findOperation = this._mongoCollection
|
||||
.find(query, { skip, limit, sort, readPreference })
|
||||
let findOperation = this._mongoCollection.find(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
readPreference,
|
||||
});
|
||||
|
||||
if (keys) {
|
||||
findOperation = findOperation.project(keys);
|
||||
@@ -55,7 +80,13 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) {
|
||||
const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference });
|
||||
const countOperation = this._mongoCollection.count(query, {
|
||||
skip,
|
||||
limit,
|
||||
sort,
|
||||
maxTimeMS,
|
||||
readPreference,
|
||||
});
|
||||
|
||||
return countOperation;
|
||||
}
|
||||
@@ -65,7 +96,9 @@ export default class MongoCollection {
|
||||
}
|
||||
|
||||
aggregate(pipeline, { maxTimeMS, readPreference } = {}) {
|
||||
return this._mongoCollection.aggregate(pipeline, { maxTimeMS, readPreference }).toArray();
|
||||
return this._mongoCollection
|
||||
.aggregate(pipeline, { maxTimeMS, readPreference })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
insertOne(object) {
|
||||
@@ -76,7 +109,7 @@ export default class MongoCollection {
|
||||
// If there is nothing that matches the query - does insert
|
||||
// Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5.
|
||||
upsertOne(query, update) {
|
||||
return this._mongoCollection.update(query, update, { upsert: true })
|
||||
return this._mongoCollection.update(query, update, { upsert: true });
|
||||
}
|
||||
|
||||
updateOne(query, update) {
|
||||
@@ -93,13 +126,17 @@ export default class MongoCollection {
|
||||
|
||||
_ensureSparseUniqueIndexInBackground(indexRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
this._mongoCollection.ensureIndex(
|
||||
indexRequest,
|
||||
{ unique: true, background: true, sparse: true },
|
||||
error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import MongoCollection from './MongoCollection';
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
function mongoFieldToParseSchemaField(type) {
|
||||
if (type[0] === '*') {
|
||||
@@ -15,31 +15,43 @@ function mongoFieldToParseSchemaField(type) {
|
||||
};
|
||||
}
|
||||
switch (type) {
|
||||
case 'number': return {type: 'Number'};
|
||||
case 'string': return {type: 'String'};
|
||||
case 'boolean': return {type: 'Boolean'};
|
||||
case 'date': return {type: 'Date'};
|
||||
case 'map':
|
||||
case 'object': return {type: 'Object'};
|
||||
case 'array': return {type: 'Array'};
|
||||
case 'geopoint': return {type: 'GeoPoint'};
|
||||
case 'file': return {type: 'File'};
|
||||
case 'bytes': return {type: 'Bytes'};
|
||||
case 'polygon': return {type: 'Polygon'};
|
||||
case 'number':
|
||||
return { type: 'Number' };
|
||||
case 'string':
|
||||
return { type: 'String' };
|
||||
case 'boolean':
|
||||
return { type: 'Boolean' };
|
||||
case 'date':
|
||||
return { type: 'Date' };
|
||||
case 'map':
|
||||
case 'object':
|
||||
return { type: 'Object' };
|
||||
case 'array':
|
||||
return { type: 'Array' };
|
||||
case 'geopoint':
|
||||
return { type: 'GeoPoint' };
|
||||
case 'file':
|
||||
return { type: 'File' };
|
||||
case 'bytes':
|
||||
return { type: 'Bytes' };
|
||||
case 'polygon':
|
||||
return { type: 'Polygon' };
|
||||
}
|
||||
}
|
||||
|
||||
const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions'];
|
||||
function mongoSchemaFieldsToParseSchemaFields(schema) {
|
||||
var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1);
|
||||
var fieldNames = Object.keys(schema).filter(
|
||||
key => nonFieldSchemaKeys.indexOf(key) === -1
|
||||
);
|
||||
var response = fieldNames.reduce((obj, fieldName) => {
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName])
|
||||
obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]);
|
||||
return obj;
|
||||
}, {});
|
||||
response.ACL = {type: 'ACL'};
|
||||
response.createdAt = {type: 'Date'};
|
||||
response.updatedAt = {type: 'Date'};
|
||||
response.objectId = {type: 'String'};
|
||||
response.ACL = { type: 'ACL' };
|
||||
response.createdAt = { type: 'Date' };
|
||||
response.updatedAt = { type: 'Date' };
|
||||
response.objectId = { type: 'String' };
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -53,23 +65,23 @@ const emptyCLPS = Object.freeze({
|
||||
});
|
||||
|
||||
const defaultCLPS = Object.freeze({
|
||||
find: {'*': true},
|
||||
get: {'*': true},
|
||||
create: {'*': true},
|
||||
update: {'*': true},
|
||||
delete: {'*': true},
|
||||
addField: {'*': true},
|
||||
find: { '*': true },
|
||||
get: { '*': true },
|
||||
create: { '*': true },
|
||||
update: { '*': true },
|
||||
delete: { '*': true },
|
||||
addField: { '*': true },
|
||||
});
|
||||
|
||||
function mongoSchemaToParseSchema(mongoSchema) {
|
||||
let clps = defaultCLPS;
|
||||
let indexes = {}
|
||||
let indexes = {};
|
||||
if (mongoSchema._metadata) {
|
||||
if (mongoSchema._metadata.class_permissions) {
|
||||
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
||||
clps = { ...emptyCLPS, ...mongoSchema._metadata.class_permissions };
|
||||
}
|
||||
if (mongoSchema._metadata.indexes) {
|
||||
indexes = {...mongoSchema._metadata.indexes};
|
||||
indexes = { ...mongoSchema._metadata.indexes };
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -90,23 +102,34 @@ function _mongoSchemaQueryFromNameQuery(name: string, query) {
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
// Returns a type suitable for inserting into mongo _SCHEMA collection.
|
||||
// Does no validation. That is expected to be done in Parse Server.
|
||||
function parseFieldTypeToMongoFieldType({ type, targetClass }) {
|
||||
switch (type) {
|
||||
case 'Pointer': return `*${targetClass}`;
|
||||
case 'Relation': return `relation<${targetClass}>`;
|
||||
case 'Number': return 'number';
|
||||
case 'String': return 'string';
|
||||
case 'Boolean': return 'boolean';
|
||||
case 'Date': return 'date';
|
||||
case 'Object': return 'object';
|
||||
case 'Array': return 'array';
|
||||
case 'GeoPoint': return 'geopoint';
|
||||
case 'File': return 'file';
|
||||
case 'Bytes': return 'bytes';
|
||||
case 'Polygon': return 'polygon';
|
||||
case 'Pointer':
|
||||
return `*${targetClass}`;
|
||||
case 'Relation':
|
||||
return `relation<${targetClass}>`;
|
||||
case 'Number':
|
||||
return 'number';
|
||||
case 'String':
|
||||
return 'string';
|
||||
case 'Boolean':
|
||||
return 'boolean';
|
||||
case 'Date':
|
||||
return 'date';
|
||||
case 'Object':
|
||||
return 'object';
|
||||
case 'Array':
|
||||
return 'array';
|
||||
case 'GeoPoint':
|
||||
return 'geopoint';
|
||||
case 'File':
|
||||
return 'file';
|
||||
case 'Bytes':
|
||||
return 'bytes';
|
||||
case 'Polygon':
|
||||
return 'polygon';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,43 +141,60 @@ class MongoSchemaCollection {
|
||||
}
|
||||
|
||||
_fetchAllSchemasFrom_SCHEMA() {
|
||||
return this._collection._rawFind({})
|
||||
return this._collection
|
||||
._rawFind({})
|
||||
.then(schemas => schemas.map(mongoSchemaToParseSchema));
|
||||
}
|
||||
|
||||
_fetchOneSchemaFrom_SCHEMA(name: string) {
|
||||
return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => {
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
return this._collection
|
||||
._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 })
|
||||
.then(results => {
|
||||
if (results.length === 1) {
|
||||
return mongoSchemaToParseSchema(results[0]);
|
||||
} else {
|
||||
throw undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
findAndDeleteSchema(name: string) {
|
||||
return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []);
|
||||
return this._collection._mongoCollection.findAndRemove(
|
||||
_mongoSchemaQueryFromNameQuery(name),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
insertSchema(schema: any) {
|
||||
return this._collection.insertOne(schema)
|
||||
return this._collection
|
||||
.insertOne(schema)
|
||||
.then(result => mongoSchemaToParseSchema(result.ops[0]))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Class already exists.');
|
||||
if (error.code === 11000) {
|
||||
//Mongo's duplicate key error
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'Class already exists.'
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
updateSchema(name: string, update) {
|
||||
return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update);
|
||||
return this._collection.updateOne(
|
||||
_mongoSchemaQueryFromNameQuery(name),
|
||||
update
|
||||
);
|
||||
}
|
||||
|
||||
upsertSchema(name: string, query: string, update) {
|
||||
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
|
||||
return this._collection.upsertOne(
|
||||
_mongoSchemaQueryFromNameQuery(name, query),
|
||||
update
|
||||
);
|
||||
}
|
||||
|
||||
// Add a field to the schema. If database does not support the field
|
||||
@@ -170,34 +210,45 @@ class MongoSchemaCollection {
|
||||
// TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint.
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: string) {
|
||||
return this._fetchOneSchemaFrom_SCHEMA(className)
|
||||
.then(schema => {
|
||||
// If a field with this name already exists, it will be handled elsewhere.
|
||||
if (schema.fields[fieldName] != undefined) {
|
||||
return;
|
||||
}
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (Object.keys(schema.fields).some(existingField => schema.fields[existingField].type === 'GeoPoint')) {
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.');
|
||||
.then(
|
||||
schema => {
|
||||
// If a field with this name already exists, it will be handled elsewhere.
|
||||
if (schema.fields[fieldName] != undefined) {
|
||||
return;
|
||||
}
|
||||
// The schema exists. Check for existing GeoPoints.
|
||||
if (type.type === 'GeoPoint') {
|
||||
// Make sure there are not other geopoint fields
|
||||
if (
|
||||
Object.keys(schema.fields).some(
|
||||
existingField =>
|
||||
schema.fields[existingField].type === 'GeoPoint'
|
||||
)
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
'MongoDB only supports one GeoPoint field in a class.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}, error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return;
|
||||
},
|
||||
error => {
|
||||
// If error is undefined, the schema doesn't exist, and we can create the schema with the field.
|
||||
// If some other error, reject with it.
|
||||
if (error === undefined) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
// We use $exists and $set to avoid overwriting the field type if it
|
||||
// already exists. (it could have added inbetween the last query and the update)
|
||||
return this.upsertSchema(
|
||||
className,
|
||||
{ [fieldName]: { '$exists': false } },
|
||||
{ '$set' : { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
{ [fieldName]: { $exists: false } },
|
||||
{ $set: { [fieldName]: parseFieldTypeToMongoFieldType(type) } }
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -205,7 +256,7 @@ class MongoSchemaCollection {
|
||||
|
||||
// Exported for testing reasons and because we haven't moved all mongo schema format
|
||||
// related logic into the database adapter yet.
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
|
||||
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType
|
||||
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema;
|
||||
MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType;
|
||||
|
||||
export default MongoSchemaCollection
|
||||
export default MongoSchemaCollection;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoCollection from './MongoCollection';
|
||||
import MongoSchemaCollection from './MongoSchemaCollection';
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type { SchemaType,
|
||||
import { StorageAdapter } from '../StorageAdapter';
|
||||
import type {
|
||||
SchemaType,
|
||||
QueryType,
|
||||
StorageClass,
|
||||
QueryOptions } from '../StorageAdapter';
|
||||
QueryOptions,
|
||||
} from '../StorageAdapter';
|
||||
import {
|
||||
parse as parseUrl,
|
||||
format as formatUrl,
|
||||
@@ -19,11 +21,11 @@ import {
|
||||
transformPointerString,
|
||||
} from './MongoTransform';
|
||||
// @flow-disable-next
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
// @flow-disable-next
|
||||
import _ from 'lodash';
|
||||
import defaults from '../../../defaults';
|
||||
import logger from '../../../logger';
|
||||
import _ from 'lodash';
|
||||
import defaults from '../../../defaults';
|
||||
import logger from '../../../logger';
|
||||
|
||||
// @flow-disable-next
|
||||
const mongodb = require('mongodb');
|
||||
@@ -33,7 +35,8 @@ const ReadPreference = mongodb.ReadPreference;
|
||||
const MongoSchemaCollectionName = '_SCHEMA';
|
||||
|
||||
const storageAdapterAllCollections = mongoAdapter => {
|
||||
return mongoAdapter.connect()
|
||||
return mongoAdapter
|
||||
.connect()
|
||||
.then(() => mongoAdapter.database.collections())
|
||||
.then(collections => {
|
||||
return collections.filter(collection => {
|
||||
@@ -42,12 +45,14 @@ const storageAdapterAllCollections = mongoAdapter => {
|
||||
}
|
||||
// TODO: If you have one app with a collection prefix that happens to be a prefix of another
|
||||
// apps prefix, this will go very very badly. We should fix that somehow.
|
||||
return (collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0);
|
||||
return (
|
||||
collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const convertParseSchemaToMongoSchema = ({...schema}) => {
|
||||
const convertParseSchemaToMongoSchema = ({ ...schema }) => {
|
||||
delete schema.fields._rperm;
|
||||
delete schema.fields._wperm;
|
||||
|
||||
@@ -60,11 +65,16 @@ const convertParseSchemaToMongoSchema = ({...schema}) => {
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns { code, error } if invalid, or { result }, an object
|
||||
// suitable for inserting into _SCHEMA collection, otherwise.
|
||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions, indexes) => {
|
||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (
|
||||
fields,
|
||||
className,
|
||||
classLevelPermissions,
|
||||
indexes
|
||||
) => {
|
||||
const mongoObject = {
|
||||
_id: className,
|
||||
objectId: 'string',
|
||||
@@ -74,7 +84,9 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
||||
};
|
||||
|
||||
for (const fieldName in fields) {
|
||||
mongoObject[fieldName] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
mongoObject[
|
||||
fieldName
|
||||
] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]);
|
||||
}
|
||||
|
||||
if (typeof classLevelPermissions !== 'undefined') {
|
||||
@@ -86,18 +98,22 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
||||
}
|
||||
}
|
||||
|
||||
if (indexes && typeof indexes === 'object' && Object.keys(indexes).length > 0) {
|
||||
if (
|
||||
indexes &&
|
||||
typeof indexes === 'object' &&
|
||||
Object.keys(indexes).length > 0
|
||||
) {
|
||||
mongoObject._metadata = mongoObject._metadata || {};
|
||||
mongoObject._metadata.indexes = indexes;
|
||||
}
|
||||
|
||||
if (!mongoObject._metadata) { // cleanup the unused _metadata
|
||||
if (!mongoObject._metadata) {
|
||||
// cleanup the unused _metadata
|
||||
delete mongoObject._metadata;
|
||||
}
|
||||
|
||||
return mongoObject;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Private
|
||||
@@ -135,34 +151,40 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// encoded
|
||||
const encodedUri = formatUrl(parseUrl(this._uri));
|
||||
|
||||
this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
// Fortunately, we can get back the options and use them to select the proper DB.
|
||||
// https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885
|
||||
const options = client.s.options;
|
||||
const database = client.db(options.dbName);
|
||||
if (!database) {
|
||||
delete this.connectionPromise;
|
||||
return;
|
||||
}
|
||||
database.on('error', () => {
|
||||
this.connectionPromise = MongoClient.connect(
|
||||
encodedUri,
|
||||
this._mongoOptions
|
||||
)
|
||||
.then(client => {
|
||||
// Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client
|
||||
// Fortunately, we can get back the options and use them to select the proper DB.
|
||||
// https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885
|
||||
const options = client.s.options;
|
||||
const database = client.db(options.dbName);
|
||||
if (!database) {
|
||||
delete this.connectionPromise;
|
||||
return;
|
||||
}
|
||||
database.on('error', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
database.on('close', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
this.client = client;
|
||||
this.database = database;
|
||||
})
|
||||
.catch(err => {
|
||||
delete this.connectionPromise;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
database.on('close', () => {
|
||||
delete this.connectionPromise;
|
||||
});
|
||||
this.client = client;
|
||||
this.database = database;
|
||||
}).catch((err) => {
|
||||
delete this.connectionPromise;
|
||||
return Promise.reject(err);
|
||||
});
|
||||
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
handleError<T>(error: ?(Error | Parse.Error)): Promise<T> {
|
||||
if (error && error.code === 13) { // Unauthorized error
|
||||
if (error && error.code === 13) {
|
||||
// Unauthorized error
|
||||
delete this.client;
|
||||
delete this.database;
|
||||
delete this.connectionPromise;
|
||||
@@ -192,36 +214,55 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
classExists(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: this._collectionPrefix + name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
}).catch(err => this.handleError(err));
|
||||
return this.connect()
|
||||
.then(() => {
|
||||
return this.database
|
||||
.listCollections({ name: this._collectionPrefix + name })
|
||||
.toArray();
|
||||
})
|
||||
.then(collections => {
|
||||
return collections.length > 0;
|
||||
})
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setClassLevelPermissions(className: string, CLPs: any): Promise<void> {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.class_permissions': CLPs }
|
||||
})).catch(err => this.handleError(err));
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.class_permissions': CLPs },
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any): Promise<void> {
|
||||
setIndexesWithSchemaFormat(
|
||||
className: string,
|
||||
submittedIndexes: any,
|
||||
existingIndexes: any = {},
|
||||
fields: any
|
||||
): Promise<void> {
|
||||
if (submittedIndexes === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (Object.keys(existingIndexes).length === 0) {
|
||||
existingIndexes = { _id_: { _id: 1} };
|
||||
existingIndexes = { _id_: { _id: 1 } };
|
||||
}
|
||||
const deletePromises = [];
|
||||
const insertedIndexes = [];
|
||||
Object.keys(submittedIndexes).forEach(name => {
|
||||
const field = submittedIndexes[name];
|
||||
if (existingIndexes[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Index ${name} exists, cannot update.`
|
||||
);
|
||||
}
|
||||
if (!existingIndexes[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Index ${name} does not exist, cannot delete.`
|
||||
);
|
||||
}
|
||||
if (field.__op === 'Delete') {
|
||||
const promise = this.dropIndex(className, name);
|
||||
@@ -230,7 +271,10 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
} else {
|
||||
Object.keys(field).forEach(key => {
|
||||
if (!fields.hasOwnProperty(key)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Field ${key} does not exist, cannot add index.`
|
||||
);
|
||||
}
|
||||
});
|
||||
existingIndexes[name] = field;
|
||||
@@ -247,30 +291,34 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return Promise.all(deletePromises)
|
||||
.then(() => insertPromise)
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': existingIndexes }
|
||||
}))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': existingIndexes },
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
setIndexesFromMongo(className: string) {
|
||||
return this.getIndexes(className).then((indexes) => {
|
||||
indexes = indexes.reduce((obj, index) => {
|
||||
if (index.key._fts) {
|
||||
delete index.key._fts;
|
||||
delete index.key._ftsx;
|
||||
for (const field in index.weights) {
|
||||
index.key[field] = 'text';
|
||||
return this.getIndexes(className)
|
||||
.then(indexes => {
|
||||
indexes = indexes.reduce((obj, index) => {
|
||||
if (index.key._fts) {
|
||||
delete index.key._fts;
|
||||
delete index.key._ftsx;
|
||||
for (const field in index.weights) {
|
||||
index.key[field] = 'text';
|
||||
}
|
||||
}
|
||||
}
|
||||
obj[index.name] = index.key;
|
||||
return obj;
|
||||
}, {});
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': indexes }
|
||||
}));
|
||||
})
|
||||
obj[index.name] = index.key;
|
||||
return obj;
|
||||
}, {});
|
||||
return this._schemaCollection().then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, {
|
||||
$set: { '_metadata.indexes': indexes },
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch(err => this.handleError(err))
|
||||
.catch(() => {
|
||||
// Ignore if collection not found
|
||||
@@ -280,17 +328,33 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
createClass(className: string, schema: SchemaType): Promise<void> {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
|
||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(
|
||||
schema.fields,
|
||||
className,
|
||||
schema.classLevelPermissions,
|
||||
schema.indexes
|
||||
);
|
||||
mongoObject._id = className;
|
||||
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
|
||||
return this.setIndexesWithSchemaFormat(
|
||||
className,
|
||||
schema.indexes,
|
||||
{},
|
||||
schema.fields
|
||||
)
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.insertSchema(mongoObject))
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void> {
|
||||
addFieldIfNotExists(
|
||||
className: string,
|
||||
fieldName: string,
|
||||
type: any
|
||||
): Promise<void> {
|
||||
return this._schemaCollection()
|
||||
.then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.addFieldIfNotExists(className, fieldName, type)
|
||||
)
|
||||
.then(() => this.createIndexesIfNeeded(className, fieldName, type))
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
@@ -298,24 +362,33 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
|
||||
// and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
|
||||
deleteClass(className: string) {
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
// We've dropped the collection, now remove the _SCHEMA document
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.findAndDeleteSchema(className))
|
||||
.catch(err => this.handleError(err));
|
||||
return (
|
||||
this._adaptiveCollection(className)
|
||||
.then(collection => collection.drop())
|
||||
.catch(error => {
|
||||
// 'ns not found' means collection was already gone. Ignore deletion attempt.
|
||||
if (error.message == 'ns not found') {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
// We've dropped the collection, now remove the _SCHEMA document
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.findAndDeleteSchema(className)
|
||||
)
|
||||
.catch(err => this.handleError(err))
|
||||
);
|
||||
}
|
||||
|
||||
deleteAllClasses(fast: boolean) {
|
||||
return storageAdapterAllCollections(this)
|
||||
.then(collections => Promise.all(collections.map(collection => fast ? collection.remove({}) : collection.drop())));
|
||||
return storageAdapterAllCollections(this).then(collections =>
|
||||
Promise.all(
|
||||
collections.map(
|
||||
collection => (fast ? collection.remove({}) : collection.drop())
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove the column and all the data. For Relations, the _Join collection is handled
|
||||
@@ -341,17 +414,17 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
deleteFields(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||
const mongoFormatNames = fieldNames.map(fieldName => {
|
||||
if (schema.fields[fieldName].type === 'Pointer') {
|
||||
return `_p_${fieldName}`
|
||||
return `_p_${fieldName}`;
|
||||
} else {
|
||||
return fieldName;
|
||||
}
|
||||
});
|
||||
const collectionUpdate = { '$unset' : {} };
|
||||
const collectionUpdate = { $unset: {} };
|
||||
mongoFormatNames.forEach(name => {
|
||||
collectionUpdate['$unset'][name] = null;
|
||||
});
|
||||
|
||||
const schemaUpdate = { '$unset' : {} };
|
||||
const schemaUpdate = { $unset: {} };
|
||||
fieldNames.forEach(name => {
|
||||
schemaUpdate['$unset'][name] = null;
|
||||
});
|
||||
@@ -359,7 +432,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.updateMany({}, collectionUpdate))
|
||||
.then(() => this._schemaCollection())
|
||||
.then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate))
|
||||
.then(schemaCollection =>
|
||||
schemaCollection.updateSchema(className, schemaUpdate)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -367,7 +442,10 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// schemas cannot be retrieved, returns a promise that rejects. Requirements for the
|
||||
// rejection reason are TBD.
|
||||
getAllClasses(): Promise<StorageClass[]> {
|
||||
return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA())
|
||||
return this._schemaCollection()
|
||||
.then(schemasCollection =>
|
||||
schemasCollection._fetchAllSchemasFrom_SCHEMA()
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -376,7 +454,9 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// undefined as the reason.
|
||||
getClass(className: string): Promise<StorageClass> {
|
||||
return this._schemaCollection()
|
||||
.then(schemasCollection => schemasCollection._fetchOneSchemaFrom_SCHEMA(className))
|
||||
.then(schemasCollection =>
|
||||
schemasCollection._fetchOneSchemaFrom_SCHEMA(className)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -385,15 +465,25 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// the schema only for the legacy mongo format. We'll figure that out later.
|
||||
createObject(className: string, schema: SchemaType, object: any) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema);
|
||||
const mongoObject = parseObjectToMongoObjectForCreate(
|
||||
className,
|
||||
object,
|
||||
schema
|
||||
);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.insertOne(mongoObject))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { // Duplicate value
|
||||
const err = new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
if (error.code === 11000) {
|
||||
// Duplicate value
|
||||
const err = new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'A duplicate value for a field with unique values was provided'
|
||||
);
|
||||
err.underlyingError = error;
|
||||
if (error.message) {
|
||||
const matches = error.message.match(/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/);
|
||||
const matches = error.message.match(
|
||||
/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/
|
||||
);
|
||||
if (matches && Array.isArray(matches)) {
|
||||
err.userInfo = { duplicated_field: matches[1] };
|
||||
}
|
||||
@@ -408,26 +498,44 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Remove all objects that match the given Parse Query.
|
||||
// If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
|
||||
// If there is some other error, reject with INTERNAL_SERVER_ERROR.
|
||||
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType) {
|
||||
deleteObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => {
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return collection.deleteMany(mongoWhere)
|
||||
return collection.deleteMany(mongoWhere);
|
||||
})
|
||||
.catch(err => this.handleError(err))
|
||||
.then(({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
.then(
|
||||
({ result }) => {
|
||||
if (result.n === 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
() => {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'Database adapter error'
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}, () => {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error');
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// Apply the update to all objects that match the given Parse Query.
|
||||
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
updateObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
@@ -438,16 +546,28 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// Return value not currently well specified.
|
||||
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
findOneAndUpdate(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }))
|
||||
.then(collection =>
|
||||
collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, {
|
||||
new: true,
|
||||
})
|
||||
)
|
||||
.then(result => mongoObjectToParseObject(className, result.value, schema))
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'A duplicate value for a field with unique values was provided'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
@@ -455,7 +575,12 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
// Hopefully we can get rid of this. It's only used for config and hooks.
|
||||
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) {
|
||||
upsertOneObject(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoUpdate = transformUpdate(className, update, schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
@@ -465,32 +590,49 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
|
||||
// Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }.
|
||||
find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys, readPreference }: QueryOptions): Promise<any> {
|
||||
find(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
{ skip, limit, sort, keys, readPreference }: QueryOptions
|
||||
): Promise<any> {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const mongoWhere = transformWhere(className, query, schema);
|
||||
const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema));
|
||||
const mongoKeys = _.reduce(keys, (memo, key) => {
|
||||
if (key === 'ACL') {
|
||||
memo['_rperm'] = 1;
|
||||
memo['_wperm'] = 1;
|
||||
} else {
|
||||
memo[transformKey(className, key, schema)] = 1;
|
||||
}
|
||||
return memo;
|
||||
}, {});
|
||||
const mongoSort = _.mapKeys(sort, (value, fieldName) =>
|
||||
transformKey(className, fieldName, schema)
|
||||
);
|
||||
const mongoKeys = _.reduce(
|
||||
keys,
|
||||
(memo, key) => {
|
||||
if (key === 'ACL') {
|
||||
memo['_rperm'] = 1;
|
||||
memo['_wperm'] = 1;
|
||||
} else {
|
||||
memo[transformKey(className, key, schema)] = 1;
|
||||
}
|
||||
return memo;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this.createTextIndexesIfNeeded(className, query, schema)
|
||||
.then(() => this._adaptiveCollection(className))
|
||||
.then(collection => collection.find(mongoWhere, {
|
||||
skip,
|
||||
limit,
|
||||
sort: mongoSort,
|
||||
keys: mongoKeys,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
}))
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
.then(collection =>
|
||||
collection.find(mongoWhere, {
|
||||
skip,
|
||||
limit,
|
||||
sort: mongoSort,
|
||||
keys: mongoKeys,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
.then(objects =>
|
||||
objects.map(object =>
|
||||
mongoObjectToParseObject(className, object, schema)
|
||||
)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -499,18 +641,29 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// As such, we shouldn't expose this function to users of parse until we have an out-of-band
|
||||
// Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
|
||||
// which is why we use sparse indexes.
|
||||
ensureUniqueness(className: string, schema: SchemaType, fieldNames: string[]) {
|
||||
ensureUniqueness(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
fieldNames: string[]
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const indexCreationRequest = {};
|
||||
const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema));
|
||||
const mongoFieldNames = fieldNames.map(fieldName =>
|
||||
transformKey(className, fieldName, schema)
|
||||
);
|
||||
mongoFieldNames.forEach(fieldName => {
|
||||
indexCreationRequest[fieldName] = 1;
|
||||
});
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest))
|
||||
.then(collection =>
|
||||
collection._ensureSparseUniqueIndexInBackground(indexCreationRequest)
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.code === 11000) {
|
||||
throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.DUPLICATE_VALUE,
|
||||
'Tried to ensure field uniqueness for a class that already has duplicates.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
@@ -519,33 +672,52 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
// Used in tests
|
||||
_rawFind(className: string, query: QueryType) {
|
||||
return this._adaptiveCollection(className).then(collection => collection.find(query, {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})).catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
// Executes a count.
|
||||
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.count(transformWhere(className, query, schema), {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
}))
|
||||
.then(collection =>
|
||||
collection.find(query, {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) {
|
||||
// Executes a count.
|
||||
count(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection =>
|
||||
collection.count(transformWhere(className, query, schema), {
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
readPreference,
|
||||
})
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
distinct(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
fieldName: string
|
||||
) {
|
||||
schema = convertParseSchemaToMongoSchema(schema);
|
||||
const isPointerField =
|
||||
schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer';
|
||||
if (isPointerField) {
|
||||
fieldName = `_p_${fieldName}`
|
||||
fieldName = `_p_${fieldName}`;
|
||||
}
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.distinct(fieldName, transformWhere(className, query, schema)))
|
||||
.then(collection =>
|
||||
collection.distinct(fieldName, transformWhere(className, query, schema))
|
||||
)
|
||||
.then(objects => {
|
||||
objects = objects.filter((obj) => obj != null);
|
||||
objects = objects.filter(obj => obj != null);
|
||||
return objects.map(object => {
|
||||
if (isPointerField) {
|
||||
const field = fieldName.substring(3);
|
||||
@@ -557,12 +729,21 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) {
|
||||
aggregate(
|
||||
className: string,
|
||||
schema: any,
|
||||
pipeline: any,
|
||||
readPreference: ?string
|
||||
) {
|
||||
let isPointerField = false;
|
||||
pipeline = pipeline.map((stage) => {
|
||||
pipeline = pipeline.map(stage => {
|
||||
if (stage.$group) {
|
||||
stage.$group = this._parseAggregateGroupArgs(schema, stage.$group);
|
||||
if (stage.$group._id && (typeof stage.$group._id === 'string') && stage.$group._id.indexOf('$_p_') >= 0) {
|
||||
if (
|
||||
stage.$group._id &&
|
||||
typeof stage.$group._id === 'string' &&
|
||||
stage.$group._id.indexOf('$_p_') >= 0
|
||||
) {
|
||||
isPointerField = true;
|
||||
}
|
||||
}
|
||||
@@ -570,13 +751,21 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
stage.$match = this._parseAggregateArgs(schema, stage.$match);
|
||||
}
|
||||
if (stage.$project) {
|
||||
stage.$project = this._parseAggregateProjectArgs(schema, stage.$project);
|
||||
stage.$project = this._parseAggregateProjectArgs(
|
||||
schema,
|
||||
stage.$project
|
||||
);
|
||||
}
|
||||
return stage;
|
||||
});
|
||||
readPreference = this._parseReadPreference(readPreference);
|
||||
return this._adaptiveCollection(className)
|
||||
.then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS }))
|
||||
.then(collection =>
|
||||
collection.aggregate(pipeline, {
|
||||
readPreference,
|
||||
maxTimeMS: this._maxTimeMS,
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.code === 16006) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, error.message);
|
||||
@@ -598,7 +787,11 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
});
|
||||
return results;
|
||||
})
|
||||
.then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema)))
|
||||
.then(objects =>
|
||||
objects.map(object =>
|
||||
mongoObjectToParseObject(className, object, schema)
|
||||
)
|
||||
)
|
||||
.catch(err => this.handleError(err));
|
||||
}
|
||||
|
||||
@@ -623,7 +816,7 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// down a tree to find a "leaf node" and checking to see if it needs to be converted.
|
||||
_parseAggregateArgs(schema: any, pipeline: any): any {
|
||||
if (Array.isArray(pipeline)) {
|
||||
return pipeline.map((value) => this._parseAggregateArgs(schema, value));
|
||||
return pipeline.map(value => this._parseAggregateArgs(schema, value));
|
||||
} else if (typeof pipeline === 'object') {
|
||||
const returnValue = {};
|
||||
for (const field in pipeline) {
|
||||
@@ -632,12 +825,20 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// Pass objects down to MongoDB...this is more than likely an $exists operator.
|
||||
returnValue[`_p_${field}`] = pipeline[field];
|
||||
} else {
|
||||
returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${pipeline[field]}`;
|
||||
returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${
|
||||
pipeline[field]
|
||||
}`;
|
||||
}
|
||||
} else if (schema.fields[field] && schema.fields[field].type === 'Date') {
|
||||
} else if (
|
||||
schema.fields[field] &&
|
||||
schema.fields[field].type === 'Date'
|
||||
) {
|
||||
returnValue[field] = this._convertToDate(pipeline[field]);
|
||||
} else {
|
||||
returnValue[field] = this._parseAggregateArgs(schema, pipeline[field]);
|
||||
returnValue[field] = this._parseAggregateArgs(
|
||||
schema,
|
||||
pipeline[field]
|
||||
);
|
||||
}
|
||||
|
||||
if (field === 'objectId') {
|
||||
@@ -690,11 +891,16 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
// updatedAt or objectId and change it accordingly.
|
||||
_parseAggregateGroupArgs(schema: any, pipeline: any): any {
|
||||
if (Array.isArray(pipeline)) {
|
||||
return pipeline.map((value) => this._parseAggregateGroupArgs(schema, value));
|
||||
return pipeline.map(value =>
|
||||
this._parseAggregateGroupArgs(schema, value)
|
||||
);
|
||||
} else if (typeof pipeline === 'object') {
|
||||
const returnValue = {};
|
||||
for (const field in pipeline) {
|
||||
returnValue[field] = this._parseAggregateGroupArgs(schema, pipeline[field]);
|
||||
returnValue[field] = this._parseAggregateGroupArgs(
|
||||
schema,
|
||||
pipeline[field]
|
||||
);
|
||||
}
|
||||
return returnValue;
|
||||
} else if (typeof pipeline === 'string') {
|
||||
@@ -719,34 +925,37 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
const returnValue = {}
|
||||
const returnValue = {};
|
||||
for (const field in value) {
|
||||
returnValue[field] = this._convertToDate(value[field])
|
||||
returnValue[field] = this._convertToDate(value[field]);
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
_parseReadPreference(readPreference: ?string): ?string {
|
||||
switch (readPreference) {
|
||||
case 'PRIMARY':
|
||||
readPreference = ReadPreference.PRIMARY;
|
||||
break;
|
||||
case 'PRIMARY_PREFERRED':
|
||||
readPreference = ReadPreference.PRIMARY_PREFERRED;
|
||||
break;
|
||||
case 'SECONDARY':
|
||||
readPreference = ReadPreference.SECONDARY;
|
||||
break;
|
||||
case 'SECONDARY_PREFERRED':
|
||||
readPreference = ReadPreference.SECONDARY_PREFERRED;
|
||||
break;
|
||||
case 'NEAREST':
|
||||
readPreference = ReadPreference.NEAREST;
|
||||
break;
|
||||
case undefined:
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Not supported read preference.');
|
||||
case 'PRIMARY':
|
||||
readPreference = ReadPreference.PRIMARY;
|
||||
break;
|
||||
case 'PRIMARY_PREFERRED':
|
||||
readPreference = ReadPreference.PRIMARY_PREFERRED;
|
||||
break;
|
||||
case 'SECONDARY':
|
||||
readPreference = ReadPreference.SECONDARY;
|
||||
break;
|
||||
case 'SECONDARY_PREFERRED':
|
||||
readPreference = ReadPreference.SECONDARY_PREFERRED;
|
||||
break;
|
||||
case 'NEAREST':
|
||||
readPreference = ReadPreference.NEAREST;
|
||||
break;
|
||||
case undefined:
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Not supported read preference.'
|
||||
);
|
||||
}
|
||||
return readPreference;
|
||||
}
|
||||
@@ -770,15 +979,19 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
createIndexesIfNeeded(className: string, fieldName: string, type: any) {
|
||||
if (type && type.type === 'Polygon') {
|
||||
const index = {
|
||||
[fieldName]: '2dsphere'
|
||||
[fieldName]: '2dsphere',
|
||||
};
|
||||
return this.createIndex(className, index);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
createTextIndexesIfNeeded(className: string, query: QueryType, schema: any): Promise<void> {
|
||||
for(const fieldName in query) {
|
||||
createTextIndexesIfNeeded(
|
||||
className: string,
|
||||
query: QueryType,
|
||||
schema: any
|
||||
): Promise<void> {
|
||||
for (const fieldName in query) {
|
||||
if (!query[fieldName] || !query[fieldName].$text) {
|
||||
continue;
|
||||
}
|
||||
@@ -791,15 +1004,20 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
}
|
||||
const indexName = `${fieldName}_text`;
|
||||
const textIndex = {
|
||||
[indexName]: { [fieldName]: 'text' }
|
||||
[indexName]: { [fieldName]: 'text' },
|
||||
};
|
||||
return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields)
|
||||
.catch((error) => {
|
||||
if (error.code === 85) { // Index exist with different options
|
||||
return this.setIndexesFromMongo(className);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
return this.setIndexesWithSchemaFormat(
|
||||
className,
|
||||
textIndex,
|
||||
existingIndexes,
|
||||
schema.fields
|
||||
).catch(error => {
|
||||
if (error.code === 85) {
|
||||
// Index exist with different options
|
||||
return this.setIndexesFromMongo(className);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -824,8 +1042,8 @@ export class MongoStorageAdapter implements StorageAdapter {
|
||||
|
||||
updateSchemaWithIndexes(): Promise<any> {
|
||||
return this.getAllClasses()
|
||||
.then((classes) => {
|
||||
const promises = classes.map((schema) => {
|
||||
.then(classes => {
|
||||
const promises = classes.map(schema => {
|
||||
return this.setIndexesFromMongo(schema.className);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
|
||||
const parser = require('./PostgresConfigParser');
|
||||
|
||||
export function createClient(uri, databaseOptions) {
|
||||
|
||||
@@ -19,11 +19,14 @@ function getDatabaseOptionsFromURI(uri) {
|
||||
databaseOptions.ssl =
|
||||
queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false;
|
||||
databaseOptions.binary =
|
||||
queryParams.binary && queryParams.binary.toLowerCase() === 'true' ? true : false;
|
||||
queryParams.binary && queryParams.binary.toLowerCase() === 'true'
|
||||
? true
|
||||
: false;
|
||||
|
||||
databaseOptions.client_encoding = queryParams.client_encoding;
|
||||
databaseOptions.application_name = queryParams.application_name;
|
||||
databaseOptions.fallback_application_name = queryParams.fallback_application_name;
|
||||
databaseOptions.fallback_application_name =
|
||||
queryParams.fallback_application_name;
|
||||
|
||||
if (queryParams.poolSize) {
|
||||
databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10;
|
||||
@@ -35,19 +38,15 @@ function getDatabaseOptionsFromURI(uri) {
|
||||
function parseQueryParams(queryString) {
|
||||
queryString = queryString || '';
|
||||
|
||||
return queryString
|
||||
.split('&')
|
||||
.reduce((p, c) => {
|
||||
const parts = c.split('=');
|
||||
p[decodeURIComponent(parts[0])] =
|
||||
parts.length > 1
|
||||
? decodeURIComponent(parts.slice(1).join('='))
|
||||
: '';
|
||||
return p;
|
||||
}, {});
|
||||
return queryString.split('&').reduce((p, c) => {
|
||||
const parts = c.split('=');
|
||||
p[decodeURIComponent(parts[0])] =
|
||||
parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : '';
|
||||
return p;
|
||||
}, {});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseQueryParams: parseQueryParams,
|
||||
getDatabaseOptionsFromURI: getDatabaseOptionsFromURI
|
||||
getDatabaseOptionsFromURI: getDatabaseOptionsFromURI,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,20 +10,19 @@ module.exports = {
|
||||
contains: sql('array/contains.sql'),
|
||||
containsAll: sql('array/contains-all.sql'),
|
||||
containsAllRegex: sql('array/contains-all-regex.sql'),
|
||||
remove: sql('array/remove.sql')
|
||||
remove: sql('array/remove.sql'),
|
||||
},
|
||||
misc: {
|
||||
jsonObjectSetKeys: sql('misc/json-object-set-keys.sql')
|
||||
}
|
||||
jsonObjectSetKeys: sql('misc/json-object-set-keys.sql'),
|
||||
},
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// Helper for linking to external query files;
|
||||
function sql(file) {
|
||||
|
||||
var fullPath = path.join(__dirname, file); // generating full path;
|
||||
|
||||
var qf = new QueryFile(fullPath, {minify: true});
|
||||
var qf = new QueryFile(fullPath, { minify: true });
|
||||
|
||||
if (qf.error) {
|
||||
throw qf.error;
|
||||
|
||||
@@ -7,7 +7,7 @@ export type QueryOptions = {
|
||||
skip?: number,
|
||||
limit?: number,
|
||||
acl?: string[],
|
||||
sort?: {[string]: number},
|
||||
sort?: { [string]: number },
|
||||
count?: boolean | number,
|
||||
keys?: string[],
|
||||
op?: string,
|
||||
@@ -18,8 +18,8 @@ export type QueryOptions = {
|
||||
|
||||
export type UpdateQueryOptions = {
|
||||
many?: boolean,
|
||||
upsert?: boolean
|
||||
}
|
||||
upsert?: boolean,
|
||||
};
|
||||
|
||||
export type FullQueryOptions = QueryOptions & UpdateQueryOptions;
|
||||
|
||||
@@ -29,27 +29,88 @@ export interface StorageAdapter {
|
||||
classExists(className: string): Promise<boolean>;
|
||||
setClassLevelPermissions(className: string, clps: any): Promise<void>;
|
||||
createClass(className: string, schema: SchemaType): Promise<void>;
|
||||
addFieldIfNotExists(className: string, fieldName: string, type: any): Promise<void>;
|
||||
addFieldIfNotExists(
|
||||
className: string,
|
||||
fieldName: string,
|
||||
type: any
|
||||
): Promise<void>;
|
||||
deleteClass(className: string): Promise<void>;
|
||||
deleteAllClasses(fast: boolean): Promise<void>;
|
||||
deleteFields(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>;
|
||||
deleteFields(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
fieldNames: Array<string>
|
||||
): Promise<void>;
|
||||
getAllClasses(): Promise<StorageClass[]>;
|
||||
getClass(className: string): Promise<StorageClass>;
|
||||
createObject(className: string, schema: SchemaType, object: any): Promise<any>;
|
||||
deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType): Promise<void>;
|
||||
updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any): Promise<[any]>;
|
||||
findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any): Promise<any>;
|
||||
upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any): Promise<any>;
|
||||
find(className: string, schema: SchemaType, query: QueryType, options: QueryOptions): Promise<[any]>;
|
||||
ensureUniqueness(className: string, schema: SchemaType, fieldNames: Array<string>): Promise<void>;
|
||||
count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string): Promise<number>;
|
||||
distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string): Promise<any>;
|
||||
aggregate(className: string, schema: any, pipeline: any, readPreference: ?string): Promise<any>;
|
||||
createObject(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
object: any
|
||||
): Promise<any>;
|
||||
deleteObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType
|
||||
): Promise<void>;
|
||||
updateObjectsByQuery(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
): Promise<[any]>;
|
||||
findOneAndUpdate(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
): Promise<any>;
|
||||
upsertOneObject(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
update: any
|
||||
): Promise<any>;
|
||||
find(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
options: QueryOptions
|
||||
): Promise<[any]>;
|
||||
ensureUniqueness(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
fieldNames: Array<string>
|
||||
): Promise<void>;
|
||||
count(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
readPreference: ?string
|
||||
): Promise<number>;
|
||||
distinct(
|
||||
className: string,
|
||||
schema: SchemaType,
|
||||
query: QueryType,
|
||||
fieldName: string
|
||||
): Promise<any>;
|
||||
aggregate(
|
||||
className: string,
|
||||
schema: any,
|
||||
pipeline: any,
|
||||
readPreference: ?string
|
||||
): Promise<any>;
|
||||
performInitialization(options: ?any): Promise<void>;
|
||||
|
||||
// Indexing
|
||||
createIndexes(className: string, indexes: any, conn: ?any): Promise<void>;
|
||||
getIndexes(className: string, connection: ?any): Promise<void>;
|
||||
updateSchemaWithIndexes(): Promise<void>;
|
||||
setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any, fields: any, conn: ?any): Promise<void>;
|
||||
setIndexesWithSchemaFormat(
|
||||
className: string,
|
||||
submittedIndexes: any,
|
||||
existingIndexes: any,
|
||||
fields: any,
|
||||
conn: ?any
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
243
src/Auth.js
243
src/Auth.js
@@ -5,7 +5,14 @@ const Parse = require('parse/node');
|
||||
// An Auth object tells you who is requesting something and whether
|
||||
// the master key was used.
|
||||
// userObject is a Parse.User and can be null if there's no user.
|
||||
function Auth({ config, cacheController = undefined, isMaster = false, isReadOnly = false, user, installationId }) {
|
||||
function Auth({
|
||||
config,
|
||||
cacheController = undefined,
|
||||
isMaster = false,
|
||||
isReadOnly = false,
|
||||
user,
|
||||
installationId,
|
||||
}) {
|
||||
this.config = config;
|
||||
this.cacheController = cacheController || (config && config.cacheController);
|
||||
this.installationId = installationId;
|
||||
@@ -47,15 +54,27 @@ function nobody(config) {
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
const getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) {
|
||||
const getAuthForSessionToken = async function({
|
||||
config,
|
||||
cacheController,
|
||||
sessionToken,
|
||||
installationId,
|
||||
}) {
|
||||
cacheController = cacheController || (config && config.cacheController);
|
||||
if (cacheController) {
|
||||
const userJSON = await cacheController.user.get(sessionToken);
|
||||
if (userJSON) {
|
||||
const cachedUser = Parse.Object.fromJSON(userJSON);
|
||||
return Promise.resolve(new Auth({config, cacheController, isMaster: false, installationId, user: cachedUser}));
|
||||
return Promise.resolve(
|
||||
new Auth({
|
||||
config,
|
||||
cacheController,
|
||||
isMaster: false,
|
||||
installationId,
|
||||
user: cachedUser,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,27 +82,40 @@ const getAuthForSessionToken = async function({ config, cacheController, session
|
||||
if (config) {
|
||||
const restOptions = {
|
||||
limit: 1,
|
||||
include: 'user'
|
||||
include: 'user',
|
||||
};
|
||||
|
||||
const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions);
|
||||
const query = new RestQuery(
|
||||
config,
|
||||
master(config),
|
||||
'_Session',
|
||||
{ sessionToken },
|
||||
restOptions
|
||||
);
|
||||
results = (await query.execute()).results;
|
||||
} else {
|
||||
results = (await new Parse.Query(Parse.Session)
|
||||
.limit(1)
|
||||
.include('user')
|
||||
.equalTo('sessionToken', sessionToken)
|
||||
.find({ useMasterKey: true })).map((obj) => obj.toJSON())
|
||||
.find({ useMasterKey: true })).map(obj => obj.toJSON());
|
||||
}
|
||||
|
||||
if (results.length !== 1 || !results[0]['user']) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Invalid session token'
|
||||
);
|
||||
}
|
||||
const now = new Date(),
|
||||
expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined;
|
||||
expiresAt = results[0].expiresAt
|
||||
? new Date(results[0].expiresAt.iso)
|
||||
: undefined;
|
||||
if (expiresAt < now) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token is expired.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Session token is expired.'
|
||||
);
|
||||
}
|
||||
const obj = results[0]['user'];
|
||||
delete obj.password;
|
||||
@@ -93,25 +125,49 @@ const getAuthForSessionToken = async function({ config, cacheController, session
|
||||
cacheController.user.put(sessionToken, obj);
|
||||
}
|
||||
const userObject = Parse.Object.fromJSON(obj);
|
||||
return new Auth({ config, cacheController, isMaster: false, installationId, user: userObject });
|
||||
return new Auth({
|
||||
config,
|
||||
cacheController,
|
||||
isMaster: false,
|
||||
installationId,
|
||||
user: userObject,
|
||||
});
|
||||
};
|
||||
|
||||
var getAuthForLegacySessionToken = function({ config, sessionToken, installationId }) {
|
||||
var getAuthForLegacySessionToken = function({
|
||||
config,
|
||||
sessionToken,
|
||||
installationId,
|
||||
}) {
|
||||
var restOptions = {
|
||||
limit: 1
|
||||
limit: 1,
|
||||
};
|
||||
var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions);
|
||||
return query.execute().then((response) => {
|
||||
var query = new RestQuery(
|
||||
config,
|
||||
master(config),
|
||||
'_User',
|
||||
{ sessionToken },
|
||||
restOptions
|
||||
);
|
||||
return query.execute().then(response => {
|
||||
var results = response.results;
|
||||
if (results.length !== 1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'invalid legacy session token'
|
||||
);
|
||||
}
|
||||
const obj = results[0];
|
||||
obj.className = '_User';
|
||||
const userObject = Parse.Object.fromJSON(obj);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
return new Auth({
|
||||
config,
|
||||
isMaster: false,
|
||||
installationId,
|
||||
user: userObject,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a promise that resolves to an array of role names
|
||||
Auth.prototype.getUserRoles = function() {
|
||||
@@ -131,21 +187,27 @@ Auth.prototype.getUserRoles = function() {
|
||||
Auth.prototype.getRolesForUser = function() {
|
||||
if (this.config) {
|
||||
const restWhere = {
|
||||
'users': {
|
||||
users: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.user.id
|
||||
}
|
||||
objectId: this.user.id,
|
||||
},
|
||||
};
|
||||
const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {});
|
||||
const query = new RestQuery(
|
||||
this.config,
|
||||
master(this.config),
|
||||
'_Role',
|
||||
restWhere,
|
||||
{}
|
||||
);
|
||||
return query.execute().then(({ results }) => results);
|
||||
}
|
||||
|
||||
return new Parse.Query(Parse.Role)
|
||||
.equalTo('users', this.user)
|
||||
.find({ useMasterKey: true })
|
||||
.then((results) => results.map((obj) => obj.toJSON()));
|
||||
}
|
||||
.then(results => results.map(obj => obj.toJSON()));
|
||||
};
|
||||
|
||||
// Iterates through the role tree and compiles a user's roles
|
||||
Auth.prototype._loadRoles = async function() {
|
||||
@@ -169,15 +231,21 @@ Auth.prototype._loadRoles = async function() {
|
||||
return this.userRoles;
|
||||
}
|
||||
|
||||
const rolesMap = results.reduce((m, r) => {
|
||||
m.names.push(r.name);
|
||||
m.ids.push(r.objectId);
|
||||
return m;
|
||||
}, {ids: [], names: []});
|
||||
const rolesMap = results.reduce(
|
||||
(m, r) => {
|
||||
m.names.push(r.name);
|
||||
m.ids.push(r.objectId);
|
||||
return m;
|
||||
},
|
||||
{ ids: [], names: [] }
|
||||
);
|
||||
|
||||
// run the recursive finding
|
||||
const roleNames = await this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names);
|
||||
this.userRoles = roleNames.map((r) => {
|
||||
const roleNames = await this._getAllRolesNamesForRoleIds(
|
||||
rolesMap.ids,
|
||||
rolesMap.names
|
||||
);
|
||||
this.userRoles = roleNames.map(r => {
|
||||
return 'role:' + r;
|
||||
});
|
||||
this.fetchedRoles = true;
|
||||
@@ -192,38 +260,45 @@ Auth.prototype.cacheRoles = function() {
|
||||
}
|
||||
this.cacheController.role.put(this.user.id, Array(...this.userRoles));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
Auth.prototype.getRolesByIds = function(ins) {
|
||||
const roles = ins.map((id) => {
|
||||
const roles = ins.map(id => {
|
||||
return {
|
||||
__type: 'Pointer',
|
||||
className: '_Role',
|
||||
objectId: id
|
||||
}
|
||||
objectId: id,
|
||||
};
|
||||
});
|
||||
const restWhere = { 'roles': { '$in': roles }};
|
||||
const restWhere = { roles: { $in: roles } };
|
||||
|
||||
// Build an OR query across all parentRoles
|
||||
if (!this.config) {
|
||||
return new Parse.Query(Parse.Role)
|
||||
.containedIn('roles', ins.map((id) => {
|
||||
const role = new Parse.Object(Parse.Role);
|
||||
role.id = id;
|
||||
return role;
|
||||
}))
|
||||
.containedIn(
|
||||
'roles',
|
||||
ins.map(id => {
|
||||
const role = new Parse.Object(Parse.Role);
|
||||
role.id = id;
|
||||
return role;
|
||||
})
|
||||
)
|
||||
.find({ useMasterKey: true })
|
||||
.then((results) => results.map((obj) => obj.toJSON()));
|
||||
.then(results => results.map(obj => obj.toJSON()));
|
||||
}
|
||||
|
||||
return new RestQuery(this.config, master(this.config), '_Role', restWhere, {})
|
||||
.execute()
|
||||
.then(({ results }) => results);
|
||||
}
|
||||
};
|
||||
|
||||
// Given a list of roleIds, find all the parent roles, returns a promise with all names
|
||||
Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) {
|
||||
const ins = roleIDs.filter((roleID) => {
|
||||
Auth.prototype._getAllRolesNamesForRoleIds = function(
|
||||
roleIDs,
|
||||
names = [],
|
||||
queriedRoles = {}
|
||||
) {
|
||||
const ins = roleIDs.filter(roleID => {
|
||||
const wasQueried = queriedRoles[roleID] !== true;
|
||||
queriedRoles[roleID] = true;
|
||||
return wasQueried;
|
||||
@@ -234,32 +309,39 @@ Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queri
|
||||
return Promise.resolve([...new Set(names)]);
|
||||
}
|
||||
|
||||
return this.getRolesByIds(ins).then((results) => {
|
||||
// Nothing found
|
||||
if (!results.length) {
|
||||
return Promise.resolve(names);
|
||||
}
|
||||
// Map the results with all Ids and names
|
||||
const resultMap = results.reduce((memo, role) => {
|
||||
memo.names.push(role.name);
|
||||
memo.ids.push(role.objectId);
|
||||
return memo;
|
||||
}, {ids: [], names: []});
|
||||
// store the new found names
|
||||
names = names.concat(resultMap.names);
|
||||
// find the next ones, circular roles will be cut
|
||||
return this._getAllRolesNamesForRoleIds(resultMap.ids, names, queriedRoles)
|
||||
}).then((names) => {
|
||||
return Promise.resolve([...new Set(names)])
|
||||
})
|
||||
}
|
||||
return this.getRolesByIds(ins)
|
||||
.then(results => {
|
||||
// Nothing found
|
||||
if (!results.length) {
|
||||
return Promise.resolve(names);
|
||||
}
|
||||
// Map the results with all Ids and names
|
||||
const resultMap = results.reduce(
|
||||
(memo, role) => {
|
||||
memo.names.push(role.name);
|
||||
memo.ids.push(role.objectId);
|
||||
return memo;
|
||||
},
|
||||
{ ids: [], names: [] }
|
||||
);
|
||||
// store the new found names
|
||||
names = names.concat(resultMap.names);
|
||||
// find the next ones, circular roles will be cut
|
||||
return this._getAllRolesNamesForRoleIds(
|
||||
resultMap.ids,
|
||||
names,
|
||||
queriedRoles
|
||||
);
|
||||
})
|
||||
.then(names => {
|
||||
return Promise.resolve([...new Set(names)]);
|
||||
});
|
||||
};
|
||||
|
||||
const createSession = function(config, {
|
||||
userId,
|
||||
createdWith,
|
||||
installationId,
|
||||
additionalSessionData,
|
||||
}) {
|
||||
const createSession = function(
|
||||
config,
|
||||
{ userId, createdWith, installationId, additionalSessionData }
|
||||
) {
|
||||
const token = 'r:' + cryptoUtils.newToken();
|
||||
const expiresAt = config.generateSessionExpiresAt();
|
||||
const sessionData = {
|
||||
@@ -267,15 +349,15 @@ const createSession = function(config, {
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: userId
|
||||
objectId: userId,
|
||||
},
|
||||
createdWith,
|
||||
restricted: false,
|
||||
expiresAt: Parse._encode(expiresAt)
|
||||
expiresAt: Parse._encode(expiresAt),
|
||||
};
|
||||
|
||||
if (installationId) {
|
||||
sessionData.installationId = installationId
|
||||
sessionData.installationId = installationId;
|
||||
}
|
||||
|
||||
Object.assign(sessionData, additionalSessionData);
|
||||
@@ -284,9 +366,16 @@ const createSession = function(config, {
|
||||
|
||||
return {
|
||||
sessionData,
|
||||
createSession: () => new RestWrite(config, master(config), '_Session', null, sessionData).execute()
|
||||
}
|
||||
}
|
||||
createSession: () =>
|
||||
new RestWrite(
|
||||
config,
|
||||
master(config),
|
||||
'_Session',
|
||||
null,
|
||||
sessionData
|
||||
).execute(),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
Auth,
|
||||
|
||||
@@ -12,12 +12,12 @@ function compatible(compatibleSDK) {
|
||||
const clientVersion = clientSDK.version;
|
||||
const compatiblityVersion = compatibleSDK[clientSDK.sdk];
|
||||
return semver.satisfies(clientVersion, compatiblityVersion);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function supportsForwardDelete(clientSDK) {
|
||||
return compatible({
|
||||
js: '>=1.9.0'
|
||||
js: '>=1.9.0',
|
||||
})(clientSDK);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ function fromString(version) {
|
||||
if (match && match.length === 3) {
|
||||
return {
|
||||
sdk: match[1],
|
||||
version: match[2]
|
||||
}
|
||||
version: match[2],
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -36,5 +36,5 @@ function fromString(version) {
|
||||
module.exports = {
|
||||
compatible,
|
||||
supportsForwardDelete,
|
||||
fromString
|
||||
}
|
||||
fromString,
|
||||
};
|
||||
|
||||
167
src/Config.js
167
src/Config.js
@@ -11,7 +11,7 @@ function removeTrailingSlash(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
if (str.endsWith("/")) {
|
||||
if (str.endsWith('/')) {
|
||||
str = str.substr(0, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
@@ -25,19 +25,28 @@ export class Config {
|
||||
}
|
||||
const config = new Config();
|
||||
config.applicationId = applicationId;
|
||||
Object.keys(cacheInfo).forEach((key) => {
|
||||
Object.keys(cacheInfo).forEach(key => {
|
||||
if (key == 'databaseController') {
|
||||
const schemaCache = new SchemaCache(cacheInfo.cacheController,
|
||||
const schemaCache = new SchemaCache(
|
||||
cacheInfo.cacheController,
|
||||
cacheInfo.schemaCacheTTL,
|
||||
cacheInfo.enableSingleSchemaCache);
|
||||
config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache);
|
||||
cacheInfo.enableSingleSchemaCache
|
||||
);
|
||||
config.database = new DatabaseController(
|
||||
cacheInfo.databaseController.adapter,
|
||||
schemaCache
|
||||
);
|
||||
} else {
|
||||
config[key] = cacheInfo[key];
|
||||
}
|
||||
});
|
||||
config.mount = removeTrailingSlash(mount);
|
||||
config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(config);
|
||||
config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind(config);
|
||||
config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(
|
||||
config
|
||||
);
|
||||
config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind(
|
||||
config
|
||||
);
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -64,14 +73,18 @@ export class Config {
|
||||
masterKey,
|
||||
readOnlyMasterKey,
|
||||
}) {
|
||||
|
||||
if (masterKey === readOnlyMasterKey) {
|
||||
throw new Error('masterKey and readOnlyMasterKey should be different');
|
||||
}
|
||||
|
||||
const emailAdapter = userController.adapter;
|
||||
if (verifyUserEmails) {
|
||||
this.validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration});
|
||||
this.validateEmailConfiguration({
|
||||
emailAdapter,
|
||||
appName,
|
||||
publicServerURL,
|
||||
emailVerifyTokenValidityDuration,
|
||||
});
|
||||
}
|
||||
|
||||
this.validateAccountLockoutPolicy(accountLockout);
|
||||
@@ -83,8 +96,11 @@ export class Config {
|
||||
}
|
||||
|
||||
if (publicServerURL) {
|
||||
if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) {
|
||||
throw "publicServerURL should be a valid HTTPS URL starting with https://"
|
||||
if (
|
||||
!publicServerURL.startsWith('http://') &&
|
||||
!publicServerURL.startsWith('https://')
|
||||
) {
|
||||
throw 'publicServerURL should be a valid HTTPS URL starting with https://';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,11 +113,19 @@ export class Config {
|
||||
|
||||
static validateAccountLockoutPolicy(accountLockout) {
|
||||
if (accountLockout) {
|
||||
if (typeof accountLockout.duration !== 'number' || accountLockout.duration <= 0 || accountLockout.duration > 99999) {
|
||||
if (
|
||||
typeof accountLockout.duration !== 'number' ||
|
||||
accountLockout.duration <= 0 ||
|
||||
accountLockout.duration > 99999
|
||||
) {
|
||||
throw 'Account lockout duration should be greater than 0 and less than 100000';
|
||||
}
|
||||
|
||||
if (!Number.isInteger(accountLockout.threshold) || accountLockout.threshold < 1 || accountLockout.threshold > 999) {
|
||||
if (
|
||||
!Number.isInteger(accountLockout.threshold) ||
|
||||
accountLockout.threshold < 1 ||
|
||||
accountLockout.threshold > 999
|
||||
) {
|
||||
throw 'Account lockout threshold should be an integer greater than 0 and less than 1000';
|
||||
}
|
||||
}
|
||||
@@ -109,33 +133,52 @@ export class Config {
|
||||
|
||||
static validatePasswordPolicy(passwordPolicy) {
|
||||
if (passwordPolicy) {
|
||||
if (passwordPolicy.maxPasswordAge !== undefined && (typeof passwordPolicy.maxPasswordAge !== 'number' || passwordPolicy.maxPasswordAge < 0)) {
|
||||
if (
|
||||
passwordPolicy.maxPasswordAge !== undefined &&
|
||||
(typeof passwordPolicy.maxPasswordAge !== 'number' ||
|
||||
passwordPolicy.maxPasswordAge < 0)
|
||||
) {
|
||||
throw 'passwordPolicy.maxPasswordAge must be a positive number';
|
||||
}
|
||||
|
||||
if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) {
|
||||
if (
|
||||
passwordPolicy.resetTokenValidityDuration !== undefined &&
|
||||
(typeof passwordPolicy.resetTokenValidityDuration !== 'number' ||
|
||||
passwordPolicy.resetTokenValidityDuration <= 0)
|
||||
) {
|
||||
throw 'passwordPolicy.resetTokenValidityDuration must be a positive number';
|
||||
}
|
||||
|
||||
if(passwordPolicy.validatorPattern){
|
||||
if(typeof(passwordPolicy.validatorPattern) === 'string') {
|
||||
passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern);
|
||||
}
|
||||
else if(!(passwordPolicy.validatorPattern instanceof RegExp)){
|
||||
if (passwordPolicy.validatorPattern) {
|
||||
if (typeof passwordPolicy.validatorPattern === 'string') {
|
||||
passwordPolicy.validatorPattern = new RegExp(
|
||||
passwordPolicy.validatorPattern
|
||||
);
|
||||
} else if (!(passwordPolicy.validatorPattern instanceof RegExp)) {
|
||||
throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') {
|
||||
if (
|
||||
passwordPolicy.validatorCallback &&
|
||||
typeof passwordPolicy.validatorCallback !== 'function'
|
||||
) {
|
||||
throw 'passwordPolicy.validatorCallback must be a function.';
|
||||
}
|
||||
|
||||
if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') {
|
||||
if (
|
||||
passwordPolicy.doNotAllowUsername &&
|
||||
typeof passwordPolicy.doNotAllowUsername !== 'boolean'
|
||||
) {
|
||||
throw 'passwordPolicy.doNotAllowUsername must be a boolean value.';
|
||||
}
|
||||
|
||||
if (passwordPolicy.maxPasswordHistory && (!Number.isInteger(passwordPolicy.maxPasswordHistory) || passwordPolicy.maxPasswordHistory <= 0 || passwordPolicy.maxPasswordHistory > 20)) {
|
||||
if (
|
||||
passwordPolicy.maxPasswordHistory &&
|
||||
(!Number.isInteger(passwordPolicy.maxPasswordHistory) ||
|
||||
passwordPolicy.maxPasswordHistory <= 0 ||
|
||||
passwordPolicy.maxPasswordHistory > 20)
|
||||
) {
|
||||
throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20';
|
||||
}
|
||||
}
|
||||
@@ -144,13 +187,18 @@ export class Config {
|
||||
// if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern
|
||||
static setupPasswordValidator(passwordPolicy) {
|
||||
if (passwordPolicy && passwordPolicy.validatorPattern) {
|
||||
passwordPolicy.patternValidator = (value) => {
|
||||
passwordPolicy.patternValidator = value => {
|
||||
return passwordPolicy.validatorPattern.test(value);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) {
|
||||
static validateEmailConfiguration({
|
||||
emailAdapter,
|
||||
appName,
|
||||
publicServerURL,
|
||||
emailVerifyTokenValidityDuration,
|
||||
}) {
|
||||
if (!emailAdapter) {
|
||||
throw 'An emailAdapter is required for e-mail verification and password resets.';
|
||||
}
|
||||
@@ -164,14 +212,14 @@ export class Config {
|
||||
if (isNaN(emailVerifyTokenValidityDuration)) {
|
||||
throw 'Email verify token validity duration must be a valid number.';
|
||||
} else if (emailVerifyTokenValidityDuration <= 0) {
|
||||
throw 'Email verify token validity duration must be a value greater than 0.'
|
||||
throw 'Email verify token validity duration must be a value greater than 0.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static validateMasterKeyIps(masterKeyIps) {
|
||||
for (const ip of masterKeyIps) {
|
||||
if(!net.isIP(ip)){
|
||||
if (!net.isIP(ip)) {
|
||||
throw `Invalid ip in masterKeyIps: ${ip}`;
|
||||
}
|
||||
}
|
||||
@@ -193,16 +241,15 @@ export class Config {
|
||||
if (expireInactiveSessions) {
|
||||
if (isNaN(sessionLength)) {
|
||||
throw 'Session length must be a valid number.';
|
||||
}
|
||||
else if (sessionLength <= 0) {
|
||||
throw 'Session length must be a value greater than 0.'
|
||||
} else if (sessionLength <= 0) {
|
||||
throw 'Session length must be a value greater than 0.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static validateMaxLimit(maxLimit) {
|
||||
if (maxLimit <= 0) {
|
||||
throw 'Max limit must be a value greater than 0.'
|
||||
throw 'Max limit must be a value greater than 0.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,15 +258,22 @@ export class Config {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration * 1000));
|
||||
return new Date(
|
||||
now.getTime() + this.emailVerifyTokenValidityDuration * 1000
|
||||
);
|
||||
}
|
||||
|
||||
generatePasswordResetTokenExpiresAt() {
|
||||
if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) {
|
||||
if (
|
||||
!this.passwordPolicy ||
|
||||
!this.passwordPolicy.resetTokenValidityDuration
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const now = new Date();
|
||||
return new Date(now.getTime() + (this.passwordPolicy.resetTokenValidityDuration * 1000));
|
||||
return new Date(
|
||||
now.getTime() + this.passwordPolicy.resetTokenValidityDuration * 1000
|
||||
);
|
||||
}
|
||||
|
||||
generateSessionExpiresAt() {
|
||||
@@ -227,39 +281,62 @@ export class Config {
|
||||
return undefined;
|
||||
}
|
||||
var now = new Date();
|
||||
return new Date(now.getTime() + (this.sessionLength * 1000));
|
||||
return new Date(now.getTime() + this.sessionLength * 1000);
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
return (
|
||||
this.customPages.invalidLink ||
|
||||
`${this.publicServerURL}/apps/invalid_link.html`
|
||||
);
|
||||
}
|
||||
|
||||
get invalidVerificationLinkURL() {
|
||||
return this.customPages.invalidVerificationLink || `${this.publicServerURL}/apps/invalid_verification_link.html`;
|
||||
return (
|
||||
this.customPages.invalidVerificationLink ||
|
||||
`${this.publicServerURL}/apps/invalid_verification_link.html`
|
||||
);
|
||||
}
|
||||
|
||||
get linkSendSuccessURL() {
|
||||
return this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html`
|
||||
return (
|
||||
this.customPages.linkSendSuccess ||
|
||||
`${this.publicServerURL}/apps/link_send_success.html`
|
||||
);
|
||||
}
|
||||
|
||||
get linkSendFailURL() {
|
||||
return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html`
|
||||
return (
|
||||
this.customPages.linkSendFail ||
|
||||
`${this.publicServerURL}/apps/link_send_fail.html`
|
||||
);
|
||||
}
|
||||
|
||||
get verifyEmailSuccessURL() {
|
||||
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
|
||||
return (
|
||||
this.customPages.verifyEmailSuccess ||
|
||||
`${this.publicServerURL}/apps/verify_email_success.html`
|
||||
);
|
||||
}
|
||||
|
||||
get choosePasswordURL() {
|
||||
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
|
||||
return (
|
||||
this.customPages.choosePassword ||
|
||||
`${this.publicServerURL}/apps/choose_password`
|
||||
);
|
||||
}
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
return `${this.publicServerURL}/apps/${
|
||||
this.applicationId
|
||||
}/request_password_reset`;
|
||||
}
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
return (
|
||||
this.customPages.passwordResetSuccess ||
|
||||
`${this.publicServerURL}/apps/password_reset_success.html`
|
||||
);
|
||||
}
|
||||
|
||||
get parseFrameURL() {
|
||||
|
||||
@@ -13,7 +13,6 @@ var _adapter = Symbol();
|
||||
import Config from '../Config';
|
||||
|
||||
export class AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options) {
|
||||
this.options = options;
|
||||
this.appId = appId;
|
||||
@@ -34,7 +33,7 @@ export class AdaptableController {
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
throw new Error("Subclasses should implement expectedAdapterType()");
|
||||
throw new Error('Subclasses should implement expectedAdapterType()');
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
@@ -43,7 +42,7 @@ export class AdaptableController {
|
||||
|
||||
static validateAdapter(adapter, self, ExpectedType) {
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name + " requires an adapter");
|
||||
throw new Error(this.constructor.name + ' requires an adapter');
|
||||
}
|
||||
|
||||
const Type = ExpectedType || self.expectedAdapterType();
|
||||
@@ -53,20 +52,27 @@ export class AdaptableController {
|
||||
}
|
||||
|
||||
// Makes sure the prototype matches
|
||||
const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce((obj, key) => {
|
||||
const adapterType = typeof adapter[key];
|
||||
const expectedType = typeof Type.prototype[key];
|
||||
if (adapterType !== expectedType) {
|
||||
obj[key] = {
|
||||
expected: expectedType,
|
||||
actual: adapterType
|
||||
const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce(
|
||||
(obj, key) => {
|
||||
const adapterType = typeof adapter[key];
|
||||
const expectedType = typeof Type.prototype[key];
|
||||
if (adapterType !== expectedType) {
|
||||
obj[key] = {
|
||||
expected: expectedType,
|
||||
actual: adapterType,
|
||||
};
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
if (Object.keys(mismatches).length > 0) {
|
||||
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
|
||||
throw new Error(
|
||||
"Adapter prototype don't match expected prototype",
|
||||
adapter,
|
||||
mismatches
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,29 @@ import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
|
||||
export class AnalyticsController extends AdaptableController {
|
||||
appOpened(req) {
|
||||
return Promise.resolve().then(() => {
|
||||
return this.adapter.appOpened(req.body, req);
|
||||
}).then((response) => {
|
||||
return { response: response || {} };
|
||||
}).catch(() => {
|
||||
return { response: {} };
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.adapter.appOpened(req.body, req);
|
||||
})
|
||||
.then(response => {
|
||||
return { response: response || {} };
|
||||
})
|
||||
.catch(() => {
|
||||
return { response: {} };
|
||||
});
|
||||
}
|
||||
|
||||
trackEvent(req) {
|
||||
return Promise.resolve().then(() => {
|
||||
return this.adapter.trackEvent(req.params.eventName, req.body, req);
|
||||
}).then((response) => {
|
||||
return { response: response || {} };
|
||||
}).catch(() => {
|
||||
return { response: {} };
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.adapter.trackEvent(req.params.eventName, req.body, req);
|
||||
})
|
||||
.then(response => {
|
||||
return { response: response || {} };
|
||||
})
|
||||
.catch(() => {
|
||||
return { response: {} };
|
||||
});
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AdaptableController from './AdaptableController';
|
||||
import CacheAdapter from '../Adapters/Cache/CacheAdapter';
|
||||
import CacheAdapter from '../Adapters/Cache/CacheAdapter';
|
||||
|
||||
const KEY_SEPARATOR_CHAR = ':';
|
||||
|
||||
@@ -39,9 +39,7 @@ export class SubCache {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class CacheController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,16 +5,16 @@ import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
const legacyFilesRegex = new RegExp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*");
|
||||
const legacyFilesRegex = new RegExp(
|
||||
'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}-.*'
|
||||
);
|
||||
|
||||
export class FilesController extends AdaptableController {
|
||||
|
||||
getFileData(config, filename) {
|
||||
return this.adapter.getFileData(filename);
|
||||
}
|
||||
|
||||
createFile(config, filename, data, contentType) {
|
||||
|
||||
const extname = path.extname(filename);
|
||||
|
||||
const hasExtension = extname.length > 0;
|
||||
@@ -33,7 +33,7 @@ export class FilesController extends AdaptableController {
|
||||
return this.adapter.createFile(filename, data, contentType).then(() => {
|
||||
return Promise.resolve({
|
||||
url: location,
|
||||
name: filename
|
||||
name: filename,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -49,7 +49,7 @@ export class FilesController extends AdaptableController {
|
||||
*/
|
||||
expandFilesInObject(config, object) {
|
||||
if (object instanceof Array) {
|
||||
object.map((obj) => this.expandFilesInObject(config, obj));
|
||||
object.map(obj => this.expandFilesInObject(config, obj));
|
||||
return;
|
||||
}
|
||||
if (typeof object !== 'object') {
|
||||
@@ -69,9 +69,17 @@ export class FilesController extends AdaptableController {
|
||||
fileObject['url'] = this.adapter.getFileLocation(config, filename);
|
||||
} else {
|
||||
if (filename.indexOf('tfss-') === 0) {
|
||||
fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
fileObject['url'] =
|
||||
'http://files.parsetfss.com/' +
|
||||
config.fileKey +
|
||||
'/' +
|
||||
encodeURIComponent(filename);
|
||||
} else if (legacyFilesRegex.test(filename)) {
|
||||
fileObject['url'] = 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename);
|
||||
fileObject['url'] =
|
||||
'http://files.parse.com/' +
|
||||
config.fileKey +
|
||||
'/' +
|
||||
encodeURIComponent(filename);
|
||||
} else {
|
||||
fileObject['url'] = this.adapter.getFileLocation(config, filename);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
/** @flow weak */
|
||||
|
||||
import * as triggers from "../triggers";
|
||||
import * as triggers from '../triggers';
|
||||
// @flow-disable-next
|
||||
import * as Parse from "parse/node";
|
||||
import * as Parse from 'parse/node';
|
||||
// @flow-disable-next
|
||||
import * as request from "request";
|
||||
import { logger } from '../logger';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
import * as request from 'request';
|
||||
import { logger } from '../logger';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
const DefaultHooksCollectionName = "_Hooks";
|
||||
const DefaultHooksCollectionName = '_Hooks';
|
||||
const HTTPAgents = {
|
||||
http: new http.Agent({ keepAlive: true }),
|
||||
https: new https.Agent({ keepAlive: true }),
|
||||
}
|
||||
};
|
||||
|
||||
export class HooksController {
|
||||
_applicationId:string;
|
||||
_webhookKey:string;
|
||||
_applicationId: string;
|
||||
_webhookKey: string;
|
||||
database: any;
|
||||
|
||||
constructor(applicationId:string, databaseController, webhookKey) {
|
||||
constructor(applicationId: string, databaseController, webhookKey) {
|
||||
this._applicationId = applicationId;
|
||||
this._webhookKey = webhookKey;
|
||||
this.database = databaseController;
|
||||
@@ -29,14 +29,16 @@ export class HooksController {
|
||||
load() {
|
||||
return this._getHooks().then(hooks => {
|
||||
hooks = hooks || [];
|
||||
hooks.forEach((hook) => {
|
||||
hooks.forEach(hook => {
|
||||
this.addHookToTriggers(hook);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getFunction(functionName) {
|
||||
return this._getHooks({ functionName: functionName }).then(results => results[0]);
|
||||
return this._getHooks({ functionName: functionName }).then(
|
||||
results => results[0]
|
||||
);
|
||||
}
|
||||
|
||||
getFunctions() {
|
||||
@@ -44,11 +46,17 @@ export class HooksController {
|
||||
}
|
||||
|
||||
getTrigger(className, triggerName) {
|
||||
return this._getHooks({ className: className, triggerName: triggerName }).then(results => results[0]);
|
||||
return this._getHooks({
|
||||
className: className,
|
||||
triggerName: triggerName,
|
||||
}).then(results => results[0]);
|
||||
}
|
||||
|
||||
getTriggers() {
|
||||
return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } });
|
||||
return this._getHooks({
|
||||
className: { $exists: true },
|
||||
triggerName: { $exists: true },
|
||||
});
|
||||
}
|
||||
|
||||
deleteFunction(functionName) {
|
||||
@@ -58,16 +66,21 @@ export class HooksController {
|
||||
|
||||
deleteTrigger(className, triggerName) {
|
||||
triggers.removeTrigger(triggerName, className, this._applicationId);
|
||||
return this._removeHooks({ className: className, triggerName: triggerName });
|
||||
return this._removeHooks({
|
||||
className: className,
|
||||
triggerName: triggerName,
|
||||
});
|
||||
}
|
||||
|
||||
_getHooks(query = {}) {
|
||||
return this.database.find(DefaultHooksCollectionName, query).then((results) => {
|
||||
return results.map((result) => {
|
||||
delete result.objectId;
|
||||
return result;
|
||||
return this.database
|
||||
.find(DefaultHooksCollectionName, query)
|
||||
.then(results => {
|
||||
return results.map(result => {
|
||||
delete result.objectId;
|
||||
return result;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_removeHooks(query) {
|
||||
@@ -79,24 +92,36 @@ export class HooksController {
|
||||
saveHook(hook) {
|
||||
var query;
|
||||
if (hook.functionName && hook.url) {
|
||||
query = { functionName: hook.functionName }
|
||||
query = { functionName: hook.functionName };
|
||||
} else if (hook.triggerName && hook.className && hook.url) {
|
||||
query = { className: hook.className, triggerName: hook.triggerName }
|
||||
query = { className: hook.className, triggerName: hook.triggerName };
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
throw new Parse.Error(143, 'invalid hook declaration');
|
||||
}
|
||||
return this.database.update(DefaultHooksCollectionName, query, hook, {upsert: true}).then(() => {
|
||||
return Promise.resolve(hook);
|
||||
})
|
||||
return this.database
|
||||
.update(DefaultHooksCollectionName, query, hook, { upsert: true })
|
||||
.then(() => {
|
||||
return Promise.resolve(hook);
|
||||
});
|
||||
}
|
||||
|
||||
addHookToTriggers(hook) {
|
||||
var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey);
|
||||
wrappedFunction.url = hook.url;
|
||||
if (hook.className) {
|
||||
triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId)
|
||||
triggers.addTrigger(
|
||||
hook.triggerName,
|
||||
hook.className,
|
||||
wrappedFunction,
|
||||
this._applicationId
|
||||
);
|
||||
} else {
|
||||
triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId);
|
||||
triggers.addFunction(
|
||||
hook.functionName,
|
||||
wrappedFunction,
|
||||
null,
|
||||
this._applicationId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,14 +136,19 @@ export class HooksController {
|
||||
hook = {};
|
||||
hook.functionName = aHook.functionName;
|
||||
hook.url = aHook.url;
|
||||
} else if (aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName]) {
|
||||
} else if (
|
||||
aHook &&
|
||||
aHook.className &&
|
||||
aHook.url &&
|
||||
aHook.triggerName &&
|
||||
triggers.Types[aHook.triggerName]
|
||||
) {
|
||||
hook = {};
|
||||
hook.className = aHook.className;
|
||||
hook.url = aHook.url;
|
||||
hook.triggerName = aHook.triggerName;
|
||||
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
throw new Parse.Error(143, 'invalid hook declaration');
|
||||
}
|
||||
|
||||
return this.addHook(hook);
|
||||
@@ -126,47 +156,62 @@ export class HooksController {
|
||||
|
||||
createHook(aHook) {
|
||||
if (aHook.functionName) {
|
||||
return this.getFunction(aHook.functionName).then((result) => {
|
||||
return this.getFunction(aHook.functionName).then(result => {
|
||||
if (result) {
|
||||
throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`);
|
||||
throw new Parse.Error(
|
||||
143,
|
||||
`function name: ${aHook.functionName} already exits`
|
||||
);
|
||||
} else {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
});
|
||||
} else if (aHook.className && aHook.triggerName) {
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
|
||||
if (result) {
|
||||
throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`);
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then(
|
||||
result => {
|
||||
if (result) {
|
||||
throw new Parse.Error(
|
||||
143,
|
||||
`class ${aHook.className} already has trigger ${
|
||||
aHook.triggerName
|
||||
}`
|
||||
);
|
||||
}
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
return this.createOrUpdateHook(aHook);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
throw new Parse.Error(143, 'invalid hook declaration');
|
||||
}
|
||||
|
||||
updateHook(aHook) {
|
||||
if (aHook.functionName) {
|
||||
return this.getFunction(aHook.functionName).then((result) => {
|
||||
return this.getFunction(aHook.functionName).then(result => {
|
||||
if (result) {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`);
|
||||
throw new Parse.Error(
|
||||
143,
|
||||
`no function named: ${aHook.functionName} is defined`
|
||||
);
|
||||
});
|
||||
} else if (aHook.className && aHook.triggerName) {
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then((result) => {
|
||||
if (result) {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
return this.getTrigger(aHook.className, aHook.triggerName).then(
|
||||
result => {
|
||||
if (result) {
|
||||
return this.createOrUpdateHook(aHook);
|
||||
}
|
||||
throw new Parse.Error(143, `class ${aHook.className} does not exist`);
|
||||
}
|
||||
throw new Parse.Error(143, `class ${aHook.className} does not exist`);
|
||||
});
|
||||
);
|
||||
}
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
throw new Parse.Error(143, 'invalid hook declaration');
|
||||
}
|
||||
}
|
||||
|
||||
function wrapToHTTPRequest(hook, key) {
|
||||
return (req) => {
|
||||
return req => {
|
||||
const jsonBody = {};
|
||||
for (var i in req) {
|
||||
jsonBody[i] = req[i];
|
||||
@@ -181,32 +226,36 @@ function wrapToHTTPRequest(hook, key) {
|
||||
}
|
||||
const jsonRequest: any = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(jsonBody),
|
||||
};
|
||||
|
||||
const agent = hook.url.startsWith('https') ? HTTPAgents['https'] : HTTPAgents['http'];
|
||||
const agent = hook.url.startsWith('https')
|
||||
? HTTPAgents['https']
|
||||
: HTTPAgents['http'];
|
||||
jsonRequest.agent = agent;
|
||||
|
||||
if (key) {
|
||||
jsonRequest.headers['X-Parse-Webhook-Key'] = key;
|
||||
} else {
|
||||
logger.warn('Making outgoing webhook request without webhookKey being set!');
|
||||
logger.warn(
|
||||
'Making outgoing webhook request without webhookKey being set!'
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
request.post(hook.url, jsonRequest, function (err, httpResponse, body) {
|
||||
request.post(hook.url, jsonRequest, function(err, httpResponse, body) {
|
||||
var result;
|
||||
if (body) {
|
||||
if (typeof body === "string") {
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (e) {
|
||||
err = {
|
||||
error: "Malformed response",
|
||||
error: 'Malformed response',
|
||||
code: -1,
|
||||
partialResponse: body.substring(0, 100)
|
||||
partialResponse: body.substring(0, 100),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -222,13 +271,13 @@ function wrapToHTTPRequest(hook, key) {
|
||||
delete result.createdAt;
|
||||
delete result.updatedAt;
|
||||
}
|
||||
return resolve({object: result});
|
||||
return resolve({ object: result });
|
||||
} else {
|
||||
return resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default HooksController;
|
||||
|
||||
@@ -11,7 +11,7 @@ export class LiveQueryController {
|
||||
} else if (config.classNames instanceof Array) {
|
||||
this.classNames = new Set(config.classNames);
|
||||
} else {
|
||||
throw 'liveQuery.classes should be an array of string'
|
||||
throw 'liveQuery.classes should be an array of string';
|
||||
}
|
||||
this.liveQueryPublisher = new ParseCloudCodePublisher(config);
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class LiveQueryController {
|
||||
|
||||
_makePublisherRequest(currentObject: any, originalObject: any): any {
|
||||
const req = {
|
||||
object: currentObject
|
||||
object: currentObject,
|
||||
};
|
||||
if (currentObject) {
|
||||
req.original = originalObject;
|
||||
|
||||
@@ -9,26 +9,18 @@ const truncationMarker = '... (truncated)';
|
||||
|
||||
export const LogLevel = {
|
||||
INFO: 'info',
|
||||
ERROR: 'error'
|
||||
}
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
export const LogOrder = {
|
||||
DESCENDING: 'desc',
|
||||
ASCENDING: 'asc'
|
||||
}
|
||||
ASCENDING: 'asc',
|
||||
};
|
||||
|
||||
const logLevels = [
|
||||
'error',
|
||||
'warn',
|
||||
'info',
|
||||
'debug',
|
||||
'verbose',
|
||||
'silly',
|
||||
]
|
||||
const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
|
||||
|
||||
export class LoggerController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {logLevel: 'info'}) {
|
||||
constructor(adapter, appId, options = { logLevel: 'info' }) {
|
||||
super(adapter, appId, options);
|
||||
let level = 'info';
|
||||
if (options.verbose) {
|
||||
@@ -39,7 +31,8 @@ export class LoggerController extends AdaptableController {
|
||||
}
|
||||
const index = logLevels.indexOf(level); // info by default
|
||||
logLevels.forEach((level, levelIndex) => {
|
||||
if (levelIndex > index) { // silence the levels that are > maxIndex
|
||||
if (levelIndex > index) {
|
||||
// silence the levels that are > maxIndex
|
||||
this[level] = () => {};
|
||||
}
|
||||
});
|
||||
@@ -50,8 +43,8 @@ export class LoggerController extends AdaptableController {
|
||||
const query = urlObj.query;
|
||||
let sanitizedQuery = '?';
|
||||
|
||||
for(const key in query) {
|
||||
if(key !== 'password') {
|
||||
for (const key in query) {
|
||||
if (key !== 'password') {
|
||||
// normal value
|
||||
sanitizedQuery += key + '=' + query[key] + '&';
|
||||
} else {
|
||||
@@ -83,7 +76,8 @@ export class LoggerController extends AdaptableController {
|
||||
// for strings
|
||||
if (typeof e.url === 'string') {
|
||||
e.url = this.maskSensitiveUrl(e.url);
|
||||
} else if (Array.isArray(e.url)) { // for strings in array
|
||||
} else if (Array.isArray(e.url)) {
|
||||
// for strings in array
|
||||
e.url = e.url.map(item => {
|
||||
if (typeof item === 'string') {
|
||||
return this.maskSensitiveUrl(item);
|
||||
@@ -119,10 +113,15 @@ export class LoggerController extends AdaptableController {
|
||||
log(level, args) {
|
||||
// make the passed in arguments object an array with the spread operator
|
||||
args = this.maskSensitive([...args]);
|
||||
args = [].concat(level, args.map((arg) => {
|
||||
if (typeof arg === 'function') { return arg(); }
|
||||
return arg;
|
||||
}));
|
||||
args = [].concat(
|
||||
level,
|
||||
args.map(arg => {
|
||||
if (typeof arg === 'function') {
|
||||
return arg();
|
||||
}
|
||||
return arg;
|
||||
})
|
||||
);
|
||||
this.adapter.log.apply(this.adapter, args);
|
||||
}
|
||||
|
||||
@@ -150,33 +149,28 @@ export class LoggerController extends AdaptableController {
|
||||
return this.log('silly', arguments);
|
||||
}
|
||||
|
||||
logRequest({
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
body
|
||||
}) {
|
||||
this.verbose(() => {
|
||||
const stringifiedBody = JSON.stringify(body, null, 2);
|
||||
return `REQUEST for [${method}] ${url}: ${stringifiedBody}`;
|
||||
}, {
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
body
|
||||
});
|
||||
logRequest({ method, url, headers, body }) {
|
||||
this.verbose(
|
||||
() => {
|
||||
const stringifiedBody = JSON.stringify(body, null, 2);
|
||||
return `REQUEST for [${method}] ${url}: ${stringifiedBody}`;
|
||||
},
|
||||
{
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
body,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
logResponse({
|
||||
method,
|
||||
url,
|
||||
result
|
||||
}) {
|
||||
logResponse({ method, url, result }) {
|
||||
this.verbose(
|
||||
() => { const stringifiedResponse = JSON.stringify(result, null, 2);
|
||||
() => {
|
||||
const stringifiedResponse = JSON.stringify(result, null, 2);
|
||||
return `RESPONSE from [${method}] ${url}: ${stringifiedResponse}`;
|
||||
},
|
||||
{result: result}
|
||||
{ result: result }
|
||||
);
|
||||
}
|
||||
// check that date input is valid
|
||||
@@ -195,7 +189,8 @@ export class LoggerController extends AdaptableController {
|
||||
|
||||
truncateLogMessage(string) {
|
||||
if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) {
|
||||
const truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker;
|
||||
const truncated =
|
||||
string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker;
|
||||
return truncated;
|
||||
}
|
||||
|
||||
@@ -203,7 +198,8 @@ export class LoggerController extends AdaptableController {
|
||||
}
|
||||
|
||||
static parseOptions(options = {}) {
|
||||
const from = LoggerController.validDateTime(options.from) ||
|
||||
const from =
|
||||
LoggerController.validDateTime(options.from) ||
|
||||
new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
|
||||
const until = LoggerController.validDateTime(options.until) || new Date();
|
||||
const size = Number(options.size) || 10;
|
||||
@@ -228,12 +224,16 @@ export class LoggerController extends AdaptableController {
|
||||
// size (optional) Number of rows returned by search. Defaults to 10
|
||||
getLogs(options = {}) {
|
||||
if (!this.adapter) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not available');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not available'
|
||||
);
|
||||
}
|
||||
if (typeof this.adapter.query !== 'function') {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Querying logs is not supported with this adapter');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Querying logs is not supported with this adapter'
|
||||
);
|
||||
}
|
||||
options = LoggerController.parseOptions(options);
|
||||
return this.adapter.query(options);
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import RestQuery from '../RestQuery';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { master } from '../Auth';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
import { Parse } from 'parse/node';
|
||||
import RestQuery from '../RestQuery';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { master } from '../Auth';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
import { applyDeviceTokenExists } from '../Push/utils';
|
||||
|
||||
export class PushController {
|
||||
|
||||
sendPush(body = {}, where = {}, config, auth, onPushStatusSaved = () => {}, now = new Date()) {
|
||||
sendPush(
|
||||
body = {},
|
||||
where = {},
|
||||
config,
|
||||
auth,
|
||||
onPushStatusSaved = () => {},
|
||||
now = new Date()
|
||||
) {
|
||||
if (!config.hasPushSupport) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Missing push configuration');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Missing push configuration'
|
||||
);
|
||||
}
|
||||
|
||||
// Replace the expiration_time and push_time with a valid Unix epoch milliseconds time
|
||||
@@ -19,13 +27,14 @@ export class PushController {
|
||||
if (body.expiration_time && body.expiration_interval) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Both expiration_time and expiration_interval cannot be set');
|
||||
'Both expiration_time and expiration_interval cannot be set'
|
||||
);
|
||||
}
|
||||
|
||||
// Immediate push
|
||||
if (body.expiration_interval && !body.hasOwnProperty('push_time')) {
|
||||
const ttlMs = body.expiration_interval * 1000;
|
||||
body.expiration_time = (new Date(now.valueOf() + ttlMs)).valueOf();
|
||||
body.expiration_time = new Date(now.valueOf() + ttlMs).valueOf();
|
||||
}
|
||||
|
||||
const pushTime = PushController.getPushTime(body);
|
||||
@@ -37,18 +46,22 @@ export class PushController {
|
||||
// pushes to be sent. We probably change this behaviour in the future.
|
||||
let badgeUpdate = () => {
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
if (body.data && body.data.badge) {
|
||||
const badge = body.data.badge;
|
||||
let restUpdate = {};
|
||||
if (typeof badge == 'string' && badge.toLowerCase() === 'increment') {
|
||||
restUpdate = { badge: { __op: 'Increment', amount: 1 } }
|
||||
} else if (typeof badge == 'object' && typeof badge.__op == 'string' &&
|
||||
badge.__op.toLowerCase() == 'increment' && Number(badge.amount)) {
|
||||
restUpdate = { badge: { __op: 'Increment', amount: badge.amount } }
|
||||
restUpdate = { badge: { __op: 'Increment', amount: 1 } };
|
||||
} else if (
|
||||
typeof badge == 'object' &&
|
||||
typeof badge.__op == 'string' &&
|
||||
badge.__op.toLowerCase() == 'increment' &&
|
||||
Number(badge.amount)
|
||||
) {
|
||||
restUpdate = { badge: { __op: 'Increment', amount: badge.amount } };
|
||||
} else if (Number(badge)) {
|
||||
restUpdate = { badge: badge }
|
||||
restUpdate = { badge: badge };
|
||||
} else {
|
||||
throw "Invalid value for badge, expected number or 'Increment' or {increment: number}";
|
||||
}
|
||||
@@ -57,44 +70,75 @@ export class PushController {
|
||||
const updateWhere = applyDeviceTokenExists(where);
|
||||
badgeUpdate = () => {
|
||||
// Build a real RestQuery so we can use it in RestWrite
|
||||
const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere);
|
||||
const restQuery = new RestQuery(
|
||||
config,
|
||||
master(config),
|
||||
'_Installation',
|
||||
updateWhere
|
||||
);
|
||||
return restQuery.buildRestWhere().then(() => {
|
||||
const write = new RestWrite(config, master(config), '_Installation', restQuery.restWhere, restUpdate);
|
||||
const write = new RestWrite(
|
||||
config,
|
||||
master(config),
|
||||
'_Installation',
|
||||
restQuery.restWhere,
|
||||
restUpdate
|
||||
);
|
||||
write.runOptions.many = true;
|
||||
return write.execute();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
const pushStatus = pushStatusHandler(config);
|
||||
return Promise.resolve().then(() => {
|
||||
return pushStatus.setInitial(body, where);
|
||||
}).then(() => {
|
||||
onPushStatusSaved(pushStatus.objectId);
|
||||
return badgeUpdate();
|
||||
}).then(() => {
|
||||
// Update audience lastUsed and timesUsed
|
||||
if (body.audience_id) {
|
||||
const audienceId = body.audience_id;
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return pushStatus.setInitial(body, where);
|
||||
})
|
||||
.then(() => {
|
||||
onPushStatusSaved(pushStatus.objectId);
|
||||
return badgeUpdate();
|
||||
})
|
||||
.then(() => {
|
||||
// Update audience lastUsed and timesUsed
|
||||
if (body.audience_id) {
|
||||
const audienceId = body.audience_id;
|
||||
|
||||
var updateAudience = {
|
||||
lastUsed: { __type: "Date", iso: new Date().toISOString() },
|
||||
timesUsed: { __op: "Increment", "amount": 1 }
|
||||
};
|
||||
const write = new RestWrite(config, master(config), '_Audience', {objectId: audienceId}, updateAudience);
|
||||
write.execute();
|
||||
}
|
||||
// Don't wait for the audience update promise to resolve.
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
if (body.hasOwnProperty('push_time') && config.hasPushScheduledSupport) {
|
||||
var updateAudience = {
|
||||
lastUsed: { __type: 'Date', iso: new Date().toISOString() },
|
||||
timesUsed: { __op: 'Increment', amount: 1 },
|
||||
};
|
||||
const write = new RestWrite(
|
||||
config,
|
||||
master(config),
|
||||
'_Audience',
|
||||
{ objectId: audienceId },
|
||||
updateAudience
|
||||
);
|
||||
write.execute();
|
||||
}
|
||||
// Don't wait for the audience update promise to resolve.
|
||||
return Promise.resolve();
|
||||
}
|
||||
return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus);
|
||||
}).catch((err) => {
|
||||
return pushStatus.fail(err).then(() => {
|
||||
throw err;
|
||||
})
|
||||
.then(() => {
|
||||
if (
|
||||
body.hasOwnProperty('push_time') &&
|
||||
config.hasPushScheduledSupport
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return config.pushControllerQueue.enqueue(
|
||||
body,
|
||||
where,
|
||||
config,
|
||||
auth,
|
||||
pushStatus
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
return pushStatus.fail(err).then(() => {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -114,13 +158,17 @@ export class PushController {
|
||||
} else if (typeof expirationTimeParam === 'string') {
|
||||
expirationTime = new Date(expirationTimeParam);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['expiration_time'] + ' is not valid time.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['expiration_time'] + ' is not valid time.'
|
||||
);
|
||||
}
|
||||
// Check expirationTime is valid or not, if it is not valid, expirationTime is NaN
|
||||
if (!isFinite(expirationTime)) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['expiration_time'] + ' is not valid time.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['expiration_time'] + ' is not valid time.'
|
||||
);
|
||||
}
|
||||
return expirationTime.valueOf();
|
||||
}
|
||||
@@ -132,9 +180,14 @@ export class PushController {
|
||||
}
|
||||
|
||||
var expirationIntervalParam = body['expiration_interval'];
|
||||
if (typeof expirationIntervalParam !== 'number' || expirationIntervalParam <= 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
`expiration_interval must be a number greater than 0`);
|
||||
if (
|
||||
typeof expirationIntervalParam !== 'number' ||
|
||||
expirationIntervalParam <= 0
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
`expiration_interval must be a number greater than 0`
|
||||
);
|
||||
}
|
||||
return expirationIntervalParam;
|
||||
}
|
||||
@@ -159,13 +212,17 @@ export class PushController {
|
||||
isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam);
|
||||
date = new Date(pushTimeParam);
|
||||
} else {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['push_time'] + ' is not valid time.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['push_time'] + ' is not valid time.'
|
||||
);
|
||||
}
|
||||
// Check pushTime is valid or not, if it is not valid, pushTime is NaN
|
||||
if (!isFinite(date)) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['push_time'] + ' is not valid time.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
body['push_time'] + ' is not valid time.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -181,8 +238,10 @@ export class PushController {
|
||||
*/
|
||||
static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean {
|
||||
const offsetPattern = /(.+)([+-])\d\d:\d\d$/;
|
||||
return pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 // 2007-04-05T12:30Z
|
||||
|| offsetPattern.test(pushTimeParam); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00
|
||||
return (
|
||||
pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 || // 2007-04-05T12:30Z
|
||||
offsetPattern.test(pushTimeParam)
|
||||
); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,8 +250,15 @@ export class PushController {
|
||||
* @param isLocalTime {boolean}
|
||||
* @returns {string}
|
||||
*/
|
||||
static formatPushTime({ date, isLocalTime }: { date: Date, isLocalTime: boolean }) {
|
||||
if (isLocalTime) { // Strip 'Z'
|
||||
static formatPushTime({
|
||||
date,
|
||||
isLocalTime,
|
||||
}: {
|
||||
date: Date,
|
||||
isLocalTime: boolean,
|
||||
}) {
|
||||
if (isLocalTime) {
|
||||
// Strip 'Z'
|
||||
const isoString = date.toISOString();
|
||||
return isoString.substring(0, isoString.indexOf('Z'));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const MAIN_SCHEMA = "__MAIN_SCHEMA";
|
||||
const SCHEMA_CACHE_PREFIX = "__SCHEMA";
|
||||
const ALL_KEYS = "__ALL_KEYS";
|
||||
const MAIN_SCHEMA = '__MAIN_SCHEMA';
|
||||
const SCHEMA_CACHE_PREFIX = '__SCHEMA';
|
||||
const ALL_KEYS = '__ALL_KEYS';
|
||||
|
||||
import { randomString } from '../cryptoUtils';
|
||||
import defaults from '../defaults';
|
||||
@@ -8,7 +8,11 @@ import defaults from '../defaults';
|
||||
export default class SchemaCache {
|
||||
cache: Object;
|
||||
|
||||
constructor(cacheController, ttl = defaults.schemaCacheTTL, singleCache = false) {
|
||||
constructor(
|
||||
cacheController,
|
||||
ttl = defaults.schemaCacheTTL,
|
||||
singleCache = false
|
||||
) {
|
||||
this.ttl = ttl;
|
||||
if (typeof ttl == 'string') {
|
||||
this.ttl = parseInt(ttl);
|
||||
@@ -21,10 +25,13 @@ export default class SchemaCache {
|
||||
}
|
||||
|
||||
put(key, value) {
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then(allKeys => {
|
||||
allKeys = allKeys || {};
|
||||
allKeys[key] = true;
|
||||
return Promise.all([this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]);
|
||||
return Promise.all([
|
||||
this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl),
|
||||
this.cache.put(key, value, this.ttl),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,13 +60,13 @@ export default class SchemaCache {
|
||||
if (!this.ttl) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return this.cache.get(this.prefix + className).then((schema) => {
|
||||
return this.cache.get(this.prefix + className).then(schema => {
|
||||
if (schema) {
|
||||
return Promise.resolve(schema);
|
||||
}
|
||||
return this.cache.get(this.prefix + MAIN_SCHEMA).then((cachedSchemas) => {
|
||||
return this.cache.get(this.prefix + MAIN_SCHEMA).then(cachedSchemas => {
|
||||
cachedSchemas = cachedSchemas || [];
|
||||
schema = cachedSchemas.find((cachedSchema) => {
|
||||
schema = cachedSchemas.find(cachedSchema => {
|
||||
return cachedSchema.className === className;
|
||||
});
|
||||
if (schema) {
|
||||
@@ -72,11 +79,11 @@ export default class SchemaCache {
|
||||
|
||||
clear() {
|
||||
// That clears all caches...
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => {
|
||||
return this.cache.get(this.prefix + ALL_KEYS).then(allKeys => {
|
||||
if (!allKeys) {
|
||||
return;
|
||||
}
|
||||
const promises = Object.keys(allKeys).map((key) => {
|
||||
const promises = Object.keys(allKeys).map(key => {
|
||||
return this.cache.del(key);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,14 @@
|
||||
import { randomString } from '../cryptoUtils';
|
||||
import { inflate } from '../triggers';
|
||||
import { randomString } from '../cryptoUtils';
|
||||
import { inflate } from '../triggers';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import MailAdapter from '../Adapters/Email/MailAdapter';
|
||||
import rest from '../rest';
|
||||
import Parse from 'parse/node';
|
||||
import MailAdapter from '../Adapters/Email/MailAdapter';
|
||||
import rest from '../rest';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
var RestQuery = require('../RestQuery');
|
||||
var Auth = require('../Auth');
|
||||
|
||||
export class UserController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
}
|
||||
@@ -36,7 +35,9 @@ export class UserController extends AdaptableController {
|
||||
user.emailVerified = false;
|
||||
|
||||
if (this.config.emailVerifyTokenValidityDuration) {
|
||||
user._email_verify_token_expires_at = Parse._encode(this.config.generateEmailVerifyTokenExpiresAt());
|
||||
user._email_verify_token_expires_at = Parse._encode(
|
||||
this.config.generateEmailVerifyTokenExpiresAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,8 +49,11 @@ export class UserController extends AdaptableController {
|
||||
throw undefined;
|
||||
}
|
||||
|
||||
const query = {username: username, _email_verify_token: token};
|
||||
const updateFields = { emailVerified: true, _email_verify_token: {__op: 'Delete'}};
|
||||
const query = { username: username, _email_verify_token: token };
|
||||
const updateFields = {
|
||||
emailVerified: true,
|
||||
_email_verify_token: { __op: 'Delete' },
|
||||
};
|
||||
|
||||
// if the email verify token needs to be validated then
|
||||
// add additional query params and additional fields that need to be updated
|
||||
@@ -57,10 +61,15 @@ export class UserController extends AdaptableController {
|
||||
query.emailVerified = false;
|
||||
query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) };
|
||||
|
||||
updateFields._email_verify_token_expires_at = {__op: 'Delete'};
|
||||
updateFields._email_verify_token_expires_at = { __op: 'Delete' };
|
||||
}
|
||||
const masterAuth = Auth.master(this.config);
|
||||
var checkIfAlreadyVerified = new RestQuery(this.config, Auth.master(this.config), '_User', {username: username, emailVerified: true});
|
||||
var checkIfAlreadyVerified = new RestQuery(
|
||||
this.config,
|
||||
Auth.master(this.config),
|
||||
'_User',
|
||||
{ username: username, emailVerified: true }
|
||||
);
|
||||
return checkIfAlreadyVerified.execute().then(result => {
|
||||
if (result.results.length) {
|
||||
return Promise.resolve(result.results.length[0]);
|
||||
@@ -70,25 +79,34 @@ export class UserController extends AdaptableController {
|
||||
}
|
||||
|
||||
checkResetTokenValidity(username, token) {
|
||||
return this.config.database.find('_User', {
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, {limit: 1}).then(results => {
|
||||
if (results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
|
||||
if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
|
||||
let expiresDate = results[0]._perishable_token_expires_at;
|
||||
if (expiresDate && expiresDate.__type == 'Date') {
|
||||
expiresDate = new Date(expiresDate.iso);
|
||||
return this.config.database
|
||||
.find(
|
||||
'_User',
|
||||
{
|
||||
username: username,
|
||||
_perishable_token: token,
|
||||
},
|
||||
{ limit: 1 }
|
||||
)
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
if (expiresDate < new Date())
|
||||
throw 'The password reset link has expired';
|
||||
}
|
||||
|
||||
return results[0];
|
||||
});
|
||||
if (
|
||||
this.config.passwordPolicy &&
|
||||
this.config.passwordPolicy.resetTokenValidityDuration
|
||||
) {
|
||||
let expiresDate = results[0]._perishable_token_expires_at;
|
||||
if (expiresDate && expiresDate.__type == 'Date') {
|
||||
expiresDate = new Date(expiresDate.iso);
|
||||
}
|
||||
if (expiresDate < new Date())
|
||||
throw 'The password reset link has expired';
|
||||
}
|
||||
|
||||
return results[0];
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
@@ -103,13 +121,18 @@ export class UserController extends AdaptableController {
|
||||
where.email = user.email;
|
||||
}
|
||||
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
return query.execute().then(function(result){
|
||||
var query = new RestQuery(
|
||||
this.config,
|
||||
Auth.master(this.config),
|
||||
'_User',
|
||||
where
|
||||
);
|
||||
return query.execute().then(function(result) {
|
||||
if (result.results.length != 1) {
|
||||
throw undefined;
|
||||
}
|
||||
return result.results[0];
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
sendVerificationEmail(user) {
|
||||
@@ -118,10 +141,15 @@ export class UserController extends AdaptableController {
|
||||
}
|
||||
const token = encodeURIComponent(user._email_verify_token);
|
||||
// We may need to fetch the user in case of update email
|
||||
this.getUserIfNeeded(user).then((user) => {
|
||||
this.getUserIfNeeded(user).then(user => {
|
||||
const username = encodeURIComponent(user.username);
|
||||
|
||||
const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config);
|
||||
const link = buildEmailLink(
|
||||
this.config.verifyEmailURL,
|
||||
username,
|
||||
token,
|
||||
this.config
|
||||
);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
@@ -143,11 +171,15 @@ export class UserController extends AdaptableController {
|
||||
*/
|
||||
regenerateEmailVerifyToken(user) {
|
||||
this.setEmailVerifyToken(user);
|
||||
return this.config.database.update('_User', { username: user.username }, user);
|
||||
return this.config.database.update(
|
||||
'_User',
|
||||
{ username: user.username },
|
||||
user
|
||||
);
|
||||
}
|
||||
|
||||
resendVerificationEmail(username) {
|
||||
return this.getUserIfNeeded({username: username}).then((aUser) => {
|
||||
return this.getUserIfNeeded({ username: username }).then(aUser => {
|
||||
if (!aUser || aUser.emailVerified) {
|
||||
throw undefined;
|
||||
}
|
||||
@@ -160,93 +192,141 @@ export class UserController extends AdaptableController {
|
||||
setPasswordResetToken(email) {
|
||||
const token = { _perishable_token: randomString(25) };
|
||||
|
||||
if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) {
|
||||
token._perishable_token_expires_at = Parse._encode(this.config.generatePasswordResetTokenExpiresAt());
|
||||
if (
|
||||
this.config.passwordPolicy &&
|
||||
this.config.passwordPolicy.resetTokenValidityDuration
|
||||
) {
|
||||
token._perishable_token_expires_at = Parse._encode(
|
||||
this.config.generatePasswordResetTokenExpiresAt()
|
||||
);
|
||||
}
|
||||
|
||||
return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, token, {}, true)
|
||||
return this.config.database.update(
|
||||
'_User',
|
||||
{ $or: [{ email }, { username: email, email: { $exists: false } }] },
|
||||
token,
|
||||
{},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(email) {
|
||||
if (!this.adapter) {
|
||||
throw "Trying to send a reset password but no adapter is set";
|
||||
throw 'Trying to send a reset password but no adapter is set';
|
||||
// TODO: No adapter?
|
||||
}
|
||||
|
||||
return this.setPasswordResetToken(email)
|
||||
.then(user => {
|
||||
const token = encodeURIComponent(user._perishable_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
return this.setPasswordResetToken(email).then(user => {
|
||||
const token = encodeURIComponent(user._perishable_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
|
||||
const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', user),
|
||||
};
|
||||
const link = buildEmailLink(
|
||||
this.config.requestResetPasswordURL,
|
||||
username,
|
||||
token,
|
||||
this.config
|
||||
);
|
||||
const options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', user),
|
||||
};
|
||||
|
||||
if (this.adapter.sendPasswordResetEmail) {
|
||||
this.adapter.sendPasswordResetEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
||||
}
|
||||
if (this.adapter.sendPasswordResetEmail) {
|
||||
this.adapter.sendPasswordResetEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultResetPasswordEmail(options));
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
});
|
||||
return Promise.resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
updatePassword(username, token, password) {
|
||||
return this.checkResetTokenValidity(username, token)
|
||||
.then(user => updateUserPassword(user.objectId, password, this.config))
|
||||
// clear reset password token
|
||||
.then(() => this.config.database.update('_User', {username}, {
|
||||
_perishable_token: {__op: 'Delete'},
|
||||
_perishable_token_expires_at: {__op: 'Delete'}
|
||||
})).catch((error) => {
|
||||
if (error.message) { // in case of Parse.Error, fail with the error message only
|
||||
return Promise.reject(error.message);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
return (
|
||||
this.checkResetTokenValidity(username, token)
|
||||
.then(user => updateUserPassword(user.objectId, password, this.config))
|
||||
// clear reset password token
|
||||
.then(() =>
|
||||
this.config.database.update(
|
||||
'_User',
|
||||
{ username },
|
||||
{
|
||||
_perishable_token: { __op: 'Delete' },
|
||||
_perishable_token_expires_at: { __op: 'Delete' },
|
||||
}
|
||||
)
|
||||
)
|
||||
.catch(error => {
|
||||
if (error.message) {
|
||||
// in case of Parse.Error, fail with the error message only
|
||||
return Promise.reject(error.message);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
defaultVerificationEmail({link, user, appName, }) {
|
||||
const text = "Hi,\n\n" +
|
||||
"You are being asked to confirm the e-mail address " + user.get("email") + " with " + appName + "\n\n" +
|
||||
"" +
|
||||
"Click here to confirm it:\n" + link;
|
||||
const to = user.get("email");
|
||||
defaultVerificationEmail({ link, user, appName }) {
|
||||
const text =
|
||||
'Hi,\n\n' +
|
||||
'You are being asked to confirm the e-mail address ' +
|
||||
user.get('email') +
|
||||
' with ' +
|
||||
appName +
|
||||
'\n\n' +
|
||||
'' +
|
||||
'Click here to confirm it:\n' +
|
||||
link;
|
||||
const to = user.get('email');
|
||||
const subject = 'Please verify your e-mail for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
|
||||
defaultResetPasswordEmail({link, user, appName, }) {
|
||||
const text = "Hi,\n\n" +
|
||||
"You requested to reset your password for " + appName +
|
||||
(user.get('username') ? (" (your username is '" + user.get('username') + "')") : "") + ".\n\n" +
|
||||
"" +
|
||||
"Click here to reset it:\n" + link;
|
||||
const to = user.get("email") || user.get('username');
|
||||
const subject = 'Password Reset for ' + appName;
|
||||
defaultResetPasswordEmail({ link, user, appName }) {
|
||||
const text =
|
||||
'Hi,\n\n' +
|
||||
'You requested to reset your password for ' +
|
||||
appName +
|
||||
(user.get('username')
|
||||
? " (your username is '" + user.get('username') + "')"
|
||||
: '') +
|
||||
'.\n\n' +
|
||||
'' +
|
||||
'Click here to reset it:\n' +
|
||||
link;
|
||||
const to = user.get('email') || user.get('username');
|
||||
const subject = 'Password Reset for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this private
|
||||
function updateUserPassword(userId, password, config) {
|
||||
return rest.update(config, Auth.master(config), '_User', { objectId: userId }, {
|
||||
password: password
|
||||
});
|
||||
return rest.update(
|
||||
config,
|
||||
Auth.master(config),
|
||||
'_User',
|
||||
{ objectId: userId },
|
||||
{
|
||||
password: password,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildEmailLink(destination, username, token, config) {
|
||||
const usernameAndToken = `token=${token}&username=${username}`
|
||||
const usernameAndToken = `token=${token}&username=${username}`;
|
||||
|
||||
if (config.parseFrameURL) {
|
||||
const destinationWithoutHost = destination.replace(config.publicServerURL, '');
|
||||
const destinationWithoutHost = destination.replace(
|
||||
config.publicServerURL,
|
||||
''
|
||||
);
|
||||
|
||||
return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`;
|
||||
return `${config.parseFrameURL}?link=${encodeURIComponent(
|
||||
destinationWithoutHost
|
||||
)}&${usernameAndToken}`;
|
||||
} else {
|
||||
return `${destination}?${usernameAndToken}`;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import authDataManager from '../Adapters/Auth';
|
||||
import { ParseServerOptions } from '../Options';
|
||||
import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import defaults from '../defaults';
|
||||
import url from 'url';
|
||||
import authDataManager from '../Adapters/Auth';
|
||||
import { ParseServerOptions } from '../Options';
|
||||
import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import defaults from '../defaults';
|
||||
import url from 'url';
|
||||
// Controllers
|
||||
import { LoggerController } from './LoggerController';
|
||||
import { FilesController } from './FilesController';
|
||||
import { HooksController } from './HooksController';
|
||||
import { UserController } from './UserController';
|
||||
import { CacheController } from './CacheController';
|
||||
import { LiveQueryController } from './LiveQueryController';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { PushController } from './PushController';
|
||||
import { PushQueue } from '../Push/PushQueue';
|
||||
import { PushWorker } from '../Push/PushWorker';
|
||||
import DatabaseController from './DatabaseController';
|
||||
import SchemaCache from './SchemaCache';
|
||||
import { LoggerController } from './LoggerController';
|
||||
import { FilesController } from './FilesController';
|
||||
import { HooksController } from './HooksController';
|
||||
import { UserController } from './UserController';
|
||||
import { CacheController } from './CacheController';
|
||||
import { LiveQueryController } from './LiveQueryController';
|
||||
import { AnalyticsController } from './AnalyticsController';
|
||||
import { PushController } from './PushController';
|
||||
import { PushQueue } from '../Push/PushQueue';
|
||||
import { PushWorker } from '../Push/PushWorker';
|
||||
import DatabaseController from './DatabaseController';
|
||||
import SchemaCache from './SchemaCache';
|
||||
|
||||
// Adapters
|
||||
import { GridStoreAdapter } from '../Adapters/Files/GridStoreAdapter';
|
||||
import { GridStoreAdapter } from '../Adapters/Files/GridStoreAdapter';
|
||||
import { WinstonLoggerAdapter } from '../Adapters/Logger/WinstonLoggerAdapter';
|
||||
import { InMemoryCacheAdapter } from '../Adapters/Cache/InMemoryCacheAdapter';
|
||||
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
import ParsePushAdapter from '@parse/push-adapter';
|
||||
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter';
|
||||
import ParsePushAdapter from '@parse/push-adapter';
|
||||
|
||||
export function getControllers(options: ParseServerOptions) {
|
||||
const loggerController = getLoggerController(options);
|
||||
@@ -35,7 +35,7 @@ export function getControllers(options: ParseServerOptions) {
|
||||
hasPushScheduledSupport,
|
||||
hasPushSupport,
|
||||
pushControllerQueue,
|
||||
pushWorker
|
||||
pushWorker,
|
||||
} = getPushController(options);
|
||||
const cacheController = getCacheController(options);
|
||||
const analyticsController = getAnalyticsController(options);
|
||||
@@ -61,7 +61,9 @@ export function getControllers(options: ParseServerOptions) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLoggerController(options: ParseServerOptions): LoggerController {
|
||||
export function getLoggerController(
|
||||
options: ParseServerOptions
|
||||
): LoggerController {
|
||||
const {
|
||||
appId,
|
||||
jsonLogs,
|
||||
@@ -72,11 +74,17 @@ export function getLoggerController(options: ParseServerOptions): LoggerControll
|
||||
loggerAdapter,
|
||||
} = options;
|
||||
const loggerOptions = { jsonLogs, logsFolder, verbose, logLevel, silent };
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, loggerOptions);
|
||||
const loggerControllerAdapter = loadAdapter(
|
||||
loggerAdapter,
|
||||
WinstonLoggerAdapter,
|
||||
loggerOptions
|
||||
);
|
||||
return new LoggerController(loggerControllerAdapter, appId, loggerOptions);
|
||||
}
|
||||
|
||||
export function getFilesController(options: ParseServerOptions): FilesController {
|
||||
export function getFilesController(
|
||||
options: ParseServerOptions
|
||||
): FilesController {
|
||||
const {
|
||||
appId,
|
||||
databaseURI,
|
||||
@@ -90,43 +98,52 @@ export function getFilesController(options: ParseServerOptions): FilesController
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridStoreAdapter(databaseURI);
|
||||
});
|
||||
return new FilesController(filesControllerAdapter, appId, { preserveFileName });
|
||||
return new FilesController(filesControllerAdapter, appId, {
|
||||
preserveFileName,
|
||||
});
|
||||
}
|
||||
|
||||
export function getUserController(options: ParseServerOptions): UserController {
|
||||
const {
|
||||
appId,
|
||||
emailAdapter,
|
||||
verifyUserEmails,
|
||||
} = options;
|
||||
const { appId, emailAdapter, verifyUserEmails } = options;
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
return new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
return new UserController(emailControllerAdapter, appId, {
|
||||
verifyUserEmails,
|
||||
});
|
||||
}
|
||||
|
||||
export function getCacheController(options: ParseServerOptions): CacheController {
|
||||
const {
|
||||
appId,
|
||||
export function getCacheController(
|
||||
options: ParseServerOptions
|
||||
): CacheController {
|
||||
const { appId, cacheAdapter, cacheTTL, cacheMaxSize } = options;
|
||||
const cacheControllerAdapter = loadAdapter(
|
||||
cacheAdapter,
|
||||
cacheTTL,
|
||||
cacheMaxSize,
|
||||
} = options;
|
||||
const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId, ttl: cacheTTL, maxSize: cacheMaxSize });
|
||||
InMemoryCacheAdapter,
|
||||
{ appId: appId, ttl: cacheTTL, maxSize: cacheMaxSize }
|
||||
);
|
||||
return new CacheController(cacheControllerAdapter, appId);
|
||||
}
|
||||
|
||||
export function getAnalyticsController(options: ParseServerOptions): AnalyticsController {
|
||||
const {
|
||||
export function getAnalyticsController(
|
||||
options: ParseServerOptions
|
||||
): AnalyticsController {
|
||||
const { analyticsAdapter } = options;
|
||||
const analyticsControllerAdapter = loadAdapter(
|
||||
analyticsAdapter,
|
||||
} = options;
|
||||
const analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter);
|
||||
AnalyticsAdapter
|
||||
);
|
||||
return new AnalyticsController(analyticsControllerAdapter);
|
||||
}
|
||||
|
||||
export function getLiveQueryController(options: ParseServerOptions): LiveQueryController {
|
||||
export function getLiveQueryController(
|
||||
options: ParseServerOptions
|
||||
): LiveQueryController {
|
||||
return new LiveQueryController(options.liveQuery);
|
||||
}
|
||||
|
||||
export function getDatabaseController(options: ParseServerOptions, cacheController: CacheController): DatabaseController {
|
||||
export function getDatabaseController(
|
||||
options: ParseServerOptions,
|
||||
cacheController: CacheController
|
||||
): DatabaseController {
|
||||
const {
|
||||
databaseURI,
|
||||
databaseOptions,
|
||||
@@ -134,39 +151,48 @@ export function getDatabaseController(options: ParseServerOptions, cacheControll
|
||||
schemaCacheTTL,
|
||||
enableSingleSchemaCache,
|
||||
} = options;
|
||||
let {
|
||||
let { databaseAdapter } = options;
|
||||
if (
|
||||
(databaseOptions ||
|
||||
(databaseURI && databaseURI !== defaults.databaseURI) ||
|
||||
collectionPrefix !== defaults.collectionPrefix) &&
|
||||
databaseAdapter
|
||||
} = options;
|
||||
if ((databaseOptions || (databaseURI && databaseURI !== defaults.databaseURI) || collectionPrefix !== defaults.collectionPrefix) && databaseAdapter) {
|
||||
) {
|
||||
throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.';
|
||||
} else if (!databaseAdapter) {
|
||||
databaseAdapter = getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions)
|
||||
databaseAdapter = getDatabaseAdapter(
|
||||
databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions
|
||||
);
|
||||
} else {
|
||||
databaseAdapter = loadAdapter(databaseAdapter)
|
||||
databaseAdapter = loadAdapter(databaseAdapter);
|
||||
}
|
||||
return new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache));
|
||||
return new DatabaseController(
|
||||
databaseAdapter,
|
||||
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)
|
||||
);
|
||||
}
|
||||
|
||||
export function getHooksController(options: ParseServerOptions, databaseController: DatabaseController): HooksController {
|
||||
const {
|
||||
appId,
|
||||
webhookKey,
|
||||
} = options;
|
||||
export function getHooksController(
|
||||
options: ParseServerOptions,
|
||||
databaseController: DatabaseController
|
||||
): HooksController {
|
||||
const { appId, webhookKey } = options;
|
||||
return new HooksController(appId, databaseController, webhookKey);
|
||||
}
|
||||
|
||||
interface PushControlling {
|
||||
pushController: PushController,
|
||||
hasPushScheduledSupport: boolean,
|
||||
pushControllerQueue: PushQueue,
|
||||
pushWorker: PushWorker
|
||||
pushController: PushController;
|
||||
hasPushScheduledSupport: boolean;
|
||||
pushControllerQueue: PushQueue;
|
||||
pushWorker: PushWorker;
|
||||
}
|
||||
|
||||
export function getPushController(options: ParseServerOptions): PushControlling {
|
||||
const {
|
||||
scheduledPush,
|
||||
push,
|
||||
} = options;
|
||||
export function getPushController(
|
||||
options: ParseServerOptions
|
||||
): PushControlling {
|
||||
const { scheduledPush, push } = options;
|
||||
|
||||
const pushOptions = Object.assign({}, push);
|
||||
const pushQueueOptions = pushOptions.queueOptions || {};
|
||||
@@ -175,16 +201,18 @@ export function getPushController(options: ParseServerOptions): PushControlling
|
||||
}
|
||||
|
||||
// Pass the push options too as it works with the default
|
||||
const pushAdapter = loadAdapter(pushOptions && pushOptions.adapter, ParsePushAdapter, pushOptions);
|
||||
const pushAdapter = loadAdapter(
|
||||
pushOptions && pushOptions.adapter,
|
||||
ParsePushAdapter,
|
||||
pushOptions
|
||||
);
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const pushController = new PushController();
|
||||
const hasPushSupport = !!(pushAdapter && push);
|
||||
const hasPushScheduledSupport = hasPushSupport && (scheduledPush === true);
|
||||
const hasPushScheduledSupport = hasPushSupport && scheduledPush === true;
|
||||
|
||||
const {
|
||||
disablePushWorker
|
||||
} = pushQueueOptions;
|
||||
const { disablePushWorker } = pushQueueOptions;
|
||||
|
||||
const pushControllerQueue = new PushQueue(pushQueueOptions);
|
||||
let pushWorker;
|
||||
@@ -196,36 +224,39 @@ export function getPushController(options: ParseServerOptions): PushControlling
|
||||
hasPushSupport,
|
||||
hasPushScheduledSupport,
|
||||
pushControllerQueue,
|
||||
pushWorker
|
||||
}
|
||||
pushWorker,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAuthDataManager(options: ParseServerOptions) {
|
||||
const {
|
||||
auth,
|
||||
enableAnonymousUsers
|
||||
} = options;
|
||||
return authDataManager(auth, enableAnonymousUsers)
|
||||
const { auth, enableAnonymousUsers } = options;
|
||||
return authDataManager(auth, enableAnonymousUsers);
|
||||
}
|
||||
|
||||
export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) {
|
||||
export function getDatabaseAdapter(
|
||||
databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions
|
||||
) {
|
||||
let protocol;
|
||||
try {
|
||||
const parsedURI = url.parse(databaseURI);
|
||||
protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null;
|
||||
} catch(e) { /* */ }
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
switch (protocol) {
|
||||
case 'postgres:':
|
||||
return new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions
|
||||
});
|
||||
default:
|
||||
return new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
case 'postgres:':
|
||||
return new PostgresStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
databaseOptions,
|
||||
});
|
||||
default:
|
||||
return new MongoStorageAdapter({
|
||||
uri: databaseURI,
|
||||
collectionPrefix,
|
||||
mongoOptions: databaseOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
export type LoadSchemaOptions = {
|
||||
clearCache: boolean
|
||||
clearCache: boolean,
|
||||
};
|
||||
|
||||
export type SchemaField = {
|
||||
type: string;
|
||||
targetClass?: ?string;
|
||||
}
|
||||
type: string,
|
||||
targetClass?: ?string,
|
||||
};
|
||||
|
||||
export type SchemaFields = { [string]: SchemaField }
|
||||
export type SchemaFields = { [string]: SchemaField };
|
||||
|
||||
export type Schema = {
|
||||
className: string,
|
||||
fields: SchemaFields,
|
||||
classLevelPermissions: ClassLevelPermissions,
|
||||
indexes?: ?any
|
||||
indexes?: ?any,
|
||||
};
|
||||
|
||||
export type ClassLevelPermissions = {
|
||||
find?: {[string]: boolean};
|
||||
count?: {[string]: boolean};
|
||||
get?: {[string]: boolean};
|
||||
create?: {[string]: boolean};
|
||||
update?: {[string]: boolean};
|
||||
delete?: {[string]: boolean};
|
||||
addField?: {[string]: boolean};
|
||||
readUserFields?: string[];
|
||||
writeUserFields?: string[];
|
||||
find?: { [string]: boolean },
|
||||
count?: { [string]: boolean },
|
||||
get?: { [string]: boolean },
|
||||
create?: { [string]: boolean },
|
||||
update?: { [string]: boolean },
|
||||
delete?: { [string]: boolean },
|
||||
addField?: { [string]: boolean },
|
||||
readUserFields?: string[],
|
||||
writeUserFields?: string[],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,13 @@ import logger from '../logger';
|
||||
import type { FlattenedObjectData } from './Subscription';
|
||||
export type Message = { [attr: string]: any };
|
||||
|
||||
const dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL'];
|
||||
const dafaultFields = [
|
||||
'className',
|
||||
'objectId',
|
||||
'updatedAt',
|
||||
'createdAt',
|
||||
'ACL',
|
||||
];
|
||||
|
||||
class Client {
|
||||
id: number;
|
||||
@@ -42,13 +48,21 @@ class Client {
|
||||
parseWebSocket.send(message);
|
||||
}
|
||||
|
||||
static pushError(parseWebSocket: any, code: number, error: string, reconnect: boolean = true): void {
|
||||
Client.pushResponse(parseWebSocket, JSON.stringify({
|
||||
'op': 'error',
|
||||
'error': error,
|
||||
'code': code,
|
||||
'reconnect': reconnect
|
||||
}));
|
||||
static pushError(
|
||||
parseWebSocket: any,
|
||||
code: number,
|
||||
error: string,
|
||||
reconnect: boolean = true
|
||||
): void {
|
||||
Client.pushResponse(
|
||||
parseWebSocket,
|
||||
JSON.stringify({
|
||||
op: 'error',
|
||||
error: error,
|
||||
code: code,
|
||||
reconnect: reconnect,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addSubscriptionInfo(requestId: number, subscriptionInfo: any): void {
|
||||
@@ -66,8 +80,8 @@ class Client {
|
||||
_pushEvent(type: string): Function {
|
||||
return function(subscriptionId: number, parseObjectJSON: any): void {
|
||||
const response: Message = {
|
||||
'op' : type,
|
||||
'clientId' : this.id
|
||||
op: type,
|
||||
clientId: this.id,
|
||||
};
|
||||
if (typeof subscriptionId !== 'undefined') {
|
||||
response['requestId'] = subscriptionId;
|
||||
@@ -80,7 +94,7 @@ class Client {
|
||||
response['object'] = this._toJSONWithFields(parseObjectJSON, fields);
|
||||
}
|
||||
Client.pushResponse(this.parseWebSocket, JSON.stringify(response));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_toJSONWithFields(parseObjectJSON: any, fields: any): FlattenedObjectData {
|
||||
@@ -100,6 +114,4 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Client
|
||||
}
|
||||
export { Client };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ParsePubSub } from './ParsePubSub';
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
import logger from '../logger';
|
||||
|
||||
class ParseCloudCodePublisher {
|
||||
@@ -21,11 +21,15 @@ class ParseCloudCodePublisher {
|
||||
|
||||
// Request is the request object from cloud code functions. request.object is a ParseObject.
|
||||
_onCloudCodeMessage(type: string, request: any): void {
|
||||
logger.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original);
|
||||
logger.verbose(
|
||||
'Raw request from cloud code current : %j | original : %j',
|
||||
request.object,
|
||||
request.original
|
||||
);
|
||||
// We need the full JSON which includes className
|
||||
const message = {
|
||||
currentParseObject: request.object._toFullJSON()
|
||||
}
|
||||
currentParseObject: request.object._toFullJSON(),
|
||||
};
|
||||
if (request.original) {
|
||||
message.originalParseObject = request.original._toFullJSON();
|
||||
}
|
||||
@@ -33,6 +37,4 @@ class ParseCloudCodePublisher {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParseCloudCodePublisher
|
||||
}
|
||||
export { ParseCloudCodePublisher };
|
||||
|
||||
@@ -17,7 +17,7 @@ class ParseLiveQueryServer {
|
||||
// className -> (queryHash -> subscription)
|
||||
subscriptions: Object;
|
||||
parseWebSocketServer: Object;
|
||||
keyPairs : any;
|
||||
keyPairs: any;
|
||||
// The subscriber we use to get object update from publisher
|
||||
subscriber: Object;
|
||||
|
||||
@@ -49,7 +49,7 @@ class ParseLiveQueryServer {
|
||||
// Initialize websocket server
|
||||
this.parseWebSocketServer = new ParseWebSocketServer(
|
||||
server,
|
||||
(parseWebsocket) => this._onConnect(parseWebsocket),
|
||||
parseWebsocket => this._onConnect(parseWebsocket),
|
||||
config.websocketTimeout
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ class ParseLiveQueryServer {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(messageStr);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
logger.error('unable to parse message', messageStr, e);
|
||||
return;
|
||||
}
|
||||
@@ -74,7 +74,11 @@ class ParseLiveQueryServer {
|
||||
} else if (channel === Parse.applicationId + 'afterDelete') {
|
||||
this._onAfterDelete(message);
|
||||
} else {
|
||||
logger.error('Get message %s from unknown channel %j', message, channel);
|
||||
logger.error(
|
||||
'Get message %s from unknown channel %j',
|
||||
message,
|
||||
channel
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,7 +112,11 @@ class ParseLiveQueryServer {
|
||||
|
||||
const deletedParseObject = message.currentParseObject.toJSON();
|
||||
const className = deletedParseObject.className;
|
||||
logger.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id);
|
||||
logger.verbose(
|
||||
'ClassName: %j | ObjectId: %s',
|
||||
className,
|
||||
deletedParseObject.id
|
||||
);
|
||||
logger.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
const classSubscriptions = this.subscriptions.get(className);
|
||||
@@ -117,11 +125,16 @@ class ParseLiveQueryServer {
|
||||
return;
|
||||
}
|
||||
for (const subscription of classSubscriptions.values()) {
|
||||
const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
|
||||
const isSubscriptionMatched = this._matchesSubscription(
|
||||
deletedParseObject,
|
||||
subscription
|
||||
);
|
||||
if (!isSubscriptionMatched) {
|
||||
continue;
|
||||
}
|
||||
for (const [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
|
||||
for (const [clientId, requestIds] of _.entries(
|
||||
subscription.clientRequestIds
|
||||
)) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
@@ -129,14 +142,17 @@ class ParseLiveQueryServer {
|
||||
for (const requestId of requestIds) {
|
||||
const acl = message.currentParseObject.getACL();
|
||||
// Check ACL
|
||||
this._matchesACL(acl, client, requestId).then((isMatched) => {
|
||||
if (!isMatched) {
|
||||
return null;
|
||||
this._matchesACL(acl, client, requestId).then(
|
||||
isMatched => {
|
||||
if (!isMatched) {
|
||||
return null;
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
},
|
||||
error => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
}
|
||||
client.pushDelete(requestId, deletedParseObject);
|
||||
}, (error) => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +169,11 @@ class ParseLiveQueryServer {
|
||||
}
|
||||
const currentParseObject = message.currentParseObject.toJSON();
|
||||
const className = currentParseObject.className;
|
||||
logger.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id);
|
||||
logger.verbose(
|
||||
'ClassName: %s | ObjectId: %s',
|
||||
className,
|
||||
currentParseObject.id
|
||||
);
|
||||
logger.verbose('Current client number : %d', this.clients.size);
|
||||
|
||||
const classSubscriptions = this.subscriptions.get(className);
|
||||
@@ -162,9 +182,17 @@ class ParseLiveQueryServer {
|
||||
return;
|
||||
}
|
||||
for (const subscription of classSubscriptions.values()) {
|
||||
const isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription);
|
||||
const isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription);
|
||||
for (const [clientId, requestIds] of _.entries(subscription.clientRequestIds)) {
|
||||
const isOriginalSubscriptionMatched = this._matchesSubscription(
|
||||
originalParseObject,
|
||||
subscription
|
||||
);
|
||||
const isCurrentSubscriptionMatched = this._matchesSubscription(
|
||||
currentParseObject,
|
||||
subscription
|
||||
);
|
||||
for (const [clientId, requestIds] of _.entries(
|
||||
subscription.clientRequestIds
|
||||
)) {
|
||||
const client = this.clients.get(clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
continue;
|
||||
@@ -180,7 +208,11 @@ class ParseLiveQueryServer {
|
||||
if (message.originalParseObject) {
|
||||
originalACL = message.originalParseObject.getACL();
|
||||
}
|
||||
originalACLCheckingPromise = this._matchesACL(originalACL, client, requestId);
|
||||
originalACLCheckingPromise = this._matchesACL(
|
||||
originalACL,
|
||||
client,
|
||||
requestId
|
||||
);
|
||||
}
|
||||
// Set current ParseObject ACL checking promise, if the object does not match
|
||||
// subscription, we do not need to check ACL
|
||||
@@ -189,56 +221,62 @@ class ParseLiveQueryServer {
|
||||
currentACLCheckingPromise = Promise.resolve(false);
|
||||
} else {
|
||||
const currentACL = message.currentParseObject.getACL();
|
||||
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
|
||||
currentACLCheckingPromise = this._matchesACL(
|
||||
currentACL,
|
||||
client,
|
||||
requestId
|
||||
);
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
[
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise
|
||||
]
|
||||
).then(([isOriginalMatched, isCurrentMatched]) => {
|
||||
logger.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
Promise.all([
|
||||
originalACLCheckingPromise,
|
||||
currentACLCheckingPromise,
|
||||
]).then(
|
||||
([isOriginalMatched, isCurrentMatched]) => {
|
||||
logger.verbose(
|
||||
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
|
||||
originalParseObject,
|
||||
currentParseObject,
|
||||
isOriginalSubscriptionMatched,
|
||||
isCurrentSubscriptionMatched,
|
||||
isOriginalMatched,
|
||||
isCurrentMatched,
|
||||
subscription.hash
|
||||
);
|
||||
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
// Decide event type
|
||||
let type;
|
||||
if (isOriginalMatched && isCurrentMatched) {
|
||||
type = 'Update';
|
||||
} else if (isOriginalMatched && !isCurrentMatched) {
|
||||
type = 'Leave';
|
||||
} else if (!isOriginalMatched && isCurrentMatched) {
|
||||
if (originalParseObject) {
|
||||
type = 'Enter';
|
||||
} else {
|
||||
type = 'Create';
|
||||
}
|
||||
} else {
|
||||
type = 'Create';
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
const functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
},
|
||||
error => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
}
|
||||
const functionName = 'push' + type;
|
||||
client[functionName](requestId, currentParseObject);
|
||||
}, (error) => {
|
||||
logger.error('Matching ACL error : ', error);
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onConnect(parseWebsocket: any): void {
|
||||
parseWebsocket.on('message', (request) => {
|
||||
parseWebsocket.on('message', request => {
|
||||
if (typeof request === 'string') {
|
||||
try {
|
||||
request = JSON.parse(request);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
logger.error('unable to parse request', request, e);
|
||||
return;
|
||||
}
|
||||
@@ -246,28 +284,31 @@ class ParseLiveQueryServer {
|
||||
logger.verbose('Request: %j', request);
|
||||
|
||||
// Check whether this request is a valid request, return error directly if not
|
||||
if (!tv4.validate(request, RequestSchema['general']) || !tv4.validate(request, RequestSchema[request.op])) {
|
||||
if (
|
||||
!tv4.validate(request, RequestSchema['general']) ||
|
||||
!tv4.validate(request, RequestSchema[request.op])
|
||||
) {
|
||||
Client.pushError(parseWebsocket, 1, tv4.error.message);
|
||||
logger.error('Connect message error %s', tv4.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch(request.op) {
|
||||
case 'connect':
|
||||
this._handleConnect(parseWebsocket, request);
|
||||
break;
|
||||
case 'subscribe':
|
||||
this._handleSubscribe(parseWebsocket, request);
|
||||
break;
|
||||
case 'update':
|
||||
this._handleUpdateSubscription(parseWebsocket, request);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
this._handleUnsubscribe(parseWebsocket, request);
|
||||
break;
|
||||
default:
|
||||
Client.pushError(parseWebsocket, 3, 'Get unknown operation');
|
||||
logger.error('Get unknown operation', request.op);
|
||||
switch (request.op) {
|
||||
case 'connect':
|
||||
this._handleConnect(parseWebsocket, request);
|
||||
break;
|
||||
case 'subscribe':
|
||||
this._handleSubscribe(parseWebsocket, request);
|
||||
break;
|
||||
case 'update':
|
||||
this._handleUpdateSubscription(parseWebsocket, request);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
this._handleUnsubscribe(parseWebsocket, request);
|
||||
break;
|
||||
default:
|
||||
Client.pushError(parseWebsocket, 3, 'Get unknown operation');
|
||||
logger.error('Get unknown operation', request.op);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -279,7 +320,7 @@ class ParseLiveQueryServer {
|
||||
event: 'ws_disconnect_error',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size,
|
||||
error: `Unable to find client ${clientId}`
|
||||
error: `Unable to find client ${clientId}`,
|
||||
});
|
||||
logger.error(`Can not find client ${clientId} on disconnect`);
|
||||
return;
|
||||
@@ -290,12 +331,16 @@ class ParseLiveQueryServer {
|
||||
this.clients.delete(clientId);
|
||||
|
||||
// Delete client from subscriptions
|
||||
for (const [requestId, subscriptionInfo] of _.entries(client.subscriptionInfos)) {
|
||||
for (const [requestId, subscriptionInfo] of _.entries(
|
||||
client.subscriptionInfos
|
||||
)) {
|
||||
const subscription = subscriptionInfo.subscription;
|
||||
subscription.deleteClientSubscription(clientId, requestId);
|
||||
|
||||
// If there is no client which is subscribing this subscription, remove it from subscriptions
|
||||
const classSubscriptions = this.subscriptions.get(subscription.className);
|
||||
const classSubscriptions = this.subscriptions.get(
|
||||
subscription.className
|
||||
);
|
||||
if (!subscription.hasSubscribingClient()) {
|
||||
classSubscriptions.delete(subscription.hash);
|
||||
}
|
||||
@@ -310,14 +355,14 @@ class ParseLiveQueryServer {
|
||||
runLiveQueryEventHandlers({
|
||||
event: 'ws_disconnect',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size
|
||||
subscriptions: this.subscriptions.size,
|
||||
});
|
||||
});
|
||||
|
||||
runLiveQueryEventHandlers({
|
||||
event: 'ws_connect',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size
|
||||
subscriptions: this.subscriptions.size,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -341,80 +386,86 @@ class ParseLiveQueryServer {
|
||||
}
|
||||
|
||||
const subscriptionSessionToken = subscriptionInfo.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
}).then((isSubscriptionSessionTokenMatched) => {
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// Check if the user has any roles that match the ACL
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
// Resolve false right away if the acl doesn't have any roles
|
||||
const acl_has_roles = Object.keys(acl.permissionsById).some(key => key.startsWith("role:"));
|
||||
if (!acl_has_roles) {
|
||||
return resolve(false);
|
||||
return this.sessionTokenCache
|
||||
.getUserId(subscriptionSessionToken)
|
||||
.then(userId => {
|
||||
return acl.getReadAccess(userId);
|
||||
})
|
||||
.then(isSubscriptionSessionTokenMatched => {
|
||||
if (isSubscriptionSessionTokenMatched) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
this.sessionTokenCache.getUserId(subscriptionSessionToken)
|
||||
.then((userId) => {
|
||||
// Check if the user has any roles that match the ACL
|
||||
return new Promise((resolve, reject) => {
|
||||
// Resolve false right away if the acl doesn't have any roles
|
||||
const acl_has_roles = Object.keys(acl.permissionsById).some(key =>
|
||||
key.startsWith('role:')
|
||||
);
|
||||
if (!acl_has_roles) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
// Pass along a null if there is no user id
|
||||
if (!userId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Prepare a user object to query for roles
|
||||
// To eliminate a query for the user, create one locally with the id
|
||||
var user = new Parse.User();
|
||||
user.id = userId;
|
||||
return user;
|
||||
|
||||
})
|
||||
.then((user) => {
|
||||
|
||||
// Pass along an empty array (of roles) if no user
|
||||
if (!user) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// Then get the user's roles
|
||||
var rolesQuery = new Parse.Query(Parse.Role);
|
||||
rolesQuery.equalTo("users", user);
|
||||
return rolesQuery.find({useMasterKey:true});
|
||||
}).
|
||||
then((roles) => {
|
||||
|
||||
// Finally, see if any of the user's roles allow them read access
|
||||
for (const role of roles) {
|
||||
if (acl.getRoleReadAccess(role)) {
|
||||
return resolve(true);
|
||||
this.sessionTokenCache
|
||||
.getUserId(subscriptionSessionToken)
|
||||
.then(userId => {
|
||||
// Pass along a null if there is no user id
|
||||
if (!userId) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
|
||||
// Prepare a user object to query for roles
|
||||
// To eliminate a query for the user, create one locally with the id
|
||||
var user = new Parse.User();
|
||||
user.id = userId;
|
||||
return user;
|
||||
})
|
||||
.then(user => {
|
||||
// Pass along an empty array (of roles) if no user
|
||||
if (!user) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
// Then get the user's roles
|
||||
var rolesQuery = new Parse.Query(Parse.Role);
|
||||
rolesQuery.equalTo('users', user);
|
||||
return rolesQuery.find({ useMasterKey: true });
|
||||
})
|
||||
.then(roles => {
|
||||
// Finally, see if any of the user's roles allow them read access
|
||||
for (const role of roles) {
|
||||
if (acl.getRoleReadAccess(role)) {
|
||||
return resolve(true);
|
||||
}
|
||||
}
|
||||
resolve(false);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(isRoleMatched => {
|
||||
if (isRoleMatched) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// Check client sessionToken matches ACL
|
||||
const clientSessionToken = client.sessionToken;
|
||||
return this.sessionTokenCache
|
||||
.getUserId(clientSessionToken)
|
||||
.then(userId => {
|
||||
return acl.getReadAccess(userId);
|
||||
});
|
||||
|
||||
});
|
||||
}).then((isRoleMatched) => {
|
||||
|
||||
if(isRoleMatched) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
// Check client sessionToken matches ACL
|
||||
const clientSessionToken = client.sessionToken;
|
||||
return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => {
|
||||
return acl.getReadAccess(userId);
|
||||
});
|
||||
}).then((isMatched) => {
|
||||
return Promise.resolve(isMatched);
|
||||
}, () => {
|
||||
return Promise.resolve(false);
|
||||
});
|
||||
})
|
||||
.then(
|
||||
isMatched => {
|
||||
return Promise.resolve(isMatched);
|
||||
},
|
||||
() => {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_handleConnect(parseWebsocket: any, request: any): any {
|
||||
@@ -433,19 +484,22 @@ class ParseLiveQueryServer {
|
||||
runLiveQueryEventHandlers({
|
||||
event: 'connect',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size
|
||||
subscriptions: this.subscriptions.size,
|
||||
});
|
||||
}
|
||||
|
||||
_hasMasterKey(request: any, validKeyPairs: any): boolean {
|
||||
if(!validKeyPairs || validKeyPairs.size == 0 ||
|
||||
!validKeyPairs.has("masterKey")) {
|
||||
if (
|
||||
!validKeyPairs ||
|
||||
validKeyPairs.size == 0 ||
|
||||
!validKeyPairs.has('masterKey')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if(!request || !request.hasOwnProperty("masterKey")) {
|
||||
if (!request || !request.hasOwnProperty('masterKey')) {
|
||||
return false;
|
||||
}
|
||||
return request.masterKey === validKeyPairs.get("masterKey");
|
||||
return request.masterKey === validKeyPairs.get('masterKey');
|
||||
}
|
||||
|
||||
_validateKeys(request: any, validKeyPairs: any): boolean {
|
||||
@@ -466,8 +520,14 @@ class ParseLiveQueryServer {
|
||||
_handleSubscribe(parseWebsocket: any, request: any): any {
|
||||
// If we can not find this client, return error to client
|
||||
if (!parseWebsocket.hasOwnProperty('clientId')) {
|
||||
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing');
|
||||
logger.error('Can not find this client, make sure you connect to server before subscribing');
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
2,
|
||||
'Can not find this client, make sure you connect to server before subscribing'
|
||||
);
|
||||
logger.error(
|
||||
'Can not find this client, make sure you connect to server before subscribing'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const client = this.clients.get(parseWebsocket.clientId);
|
||||
@@ -484,13 +544,17 @@ class ParseLiveQueryServer {
|
||||
if (classSubscriptions.has(subscriptionHash)) {
|
||||
subscription = classSubscriptions.get(subscriptionHash);
|
||||
} else {
|
||||
subscription = new Subscription(className, request.query.where, subscriptionHash);
|
||||
subscription = new Subscription(
|
||||
className,
|
||||
request.query.where,
|
||||
subscriptionHash
|
||||
);
|
||||
classSubscriptions.set(subscriptionHash, subscription);
|
||||
}
|
||||
|
||||
// Add subscriptionInfo to client
|
||||
const subscriptionInfo = {
|
||||
subscription: subscription
|
||||
subscription: subscription,
|
||||
};
|
||||
// Add selected fields and sessionToken for this subscription if necessary
|
||||
if (request.query.fields) {
|
||||
@@ -502,16 +566,23 @@ class ParseLiveQueryServer {
|
||||
client.addSubscriptionInfo(request.requestId, subscriptionInfo);
|
||||
|
||||
// Add clientId to subscription
|
||||
subscription.addClientSubscription(parseWebsocket.clientId, request.requestId);
|
||||
subscription.addClientSubscription(
|
||||
parseWebsocket.clientId,
|
||||
request.requestId
|
||||
);
|
||||
|
||||
client.pushSubscribe(request.requestId);
|
||||
|
||||
logger.verbose(`Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}`);
|
||||
logger.verbose(
|
||||
`Create client ${parseWebsocket.clientId} new subscription: ${
|
||||
request.requestId
|
||||
}`
|
||||
);
|
||||
logger.verbose('Current client number: %d', this.clients.size);
|
||||
runLiveQueryEventHandlers({
|
||||
event: 'subscribe',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size
|
||||
subscriptions: this.subscriptions.size,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,27 +591,54 @@ class ParseLiveQueryServer {
|
||||
this._handleSubscribe(parseWebsocket, request);
|
||||
}
|
||||
|
||||
_handleUnsubscribe(parseWebsocket: any, request: any, notifyClient: bool = true): any {
|
||||
_handleUnsubscribe(
|
||||
parseWebsocket: any,
|
||||
request: any,
|
||||
notifyClient: boolean = true
|
||||
): any {
|
||||
// If we can not find this client, return error to client
|
||||
if (!parseWebsocket.hasOwnProperty('clientId')) {
|
||||
Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing');
|
||||
logger.error('Can not find this client, make sure you connect to server before unsubscribing');
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
2,
|
||||
'Can not find this client, make sure you connect to server before unsubscribing'
|
||||
);
|
||||
logger.error(
|
||||
'Can not find this client, make sure you connect to server before unsubscribing'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const requestId = request.requestId;
|
||||
const client = this.clients.get(parseWebsocket.clientId);
|
||||
if (typeof client === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId +
|
||||
'. Make sure you connect to live query server before unsubscribing.');
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
2,
|
||||
'Cannot find client with clientId ' +
|
||||
parseWebsocket.clientId +
|
||||
'. Make sure you connect to live query server before unsubscribing.'
|
||||
);
|
||||
logger.error('Can not find this client ' + parseWebsocket.clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
const subscriptionInfo = client.getSubscriptionInfo(requestId);
|
||||
if (typeof subscriptionInfo === 'undefined') {
|
||||
Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId +
|
||||
' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.');
|
||||
logger.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId);
|
||||
Client.pushError(
|
||||
parseWebsocket,
|
||||
2,
|
||||
'Cannot find subscription with clientId ' +
|
||||
parseWebsocket.clientId +
|
||||
' subscriptionId ' +
|
||||
requestId +
|
||||
'. Make sure you subscribe to live query server before unsubscribing.'
|
||||
);
|
||||
logger.error(
|
||||
'Can not find subscription with clientId ' +
|
||||
parseWebsocket.clientId +
|
||||
' subscriptionId ' +
|
||||
requestId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -562,7 +660,7 @@ class ParseLiveQueryServer {
|
||||
runLiveQueryEventHandlers({
|
||||
event: 'unsubscribe',
|
||||
clients: this.clients.size,
|
||||
subscriptions: this.subscriptions.size
|
||||
subscriptions: this.subscriptions.size,
|
||||
});
|
||||
|
||||
if (!notifyClient) {
|
||||
@@ -571,10 +669,12 @@ class ParseLiveQueryServer {
|
||||
|
||||
client.pushUnsubscribe(request.requestId);
|
||||
|
||||
logger.verbose(`Delete client: ${parseWebsocket.clientId} | subscription: ${request.requestId}`);
|
||||
logger.verbose(
|
||||
`Delete client: ${parseWebsocket.clientId} | subscription: ${
|
||||
request.requestId
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
ParseLiveQueryServer
|
||||
}
|
||||
export { ParseLiveQueryServer };
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { loadAdapter } from '../Adapters/AdapterLoader';
|
||||
import {
|
||||
EventEmitterPubSub
|
||||
} from '../Adapters/PubSub/EventEmitterPubSub';
|
||||
import { EventEmitterPubSub } from '../Adapters/PubSub/EventEmitterPubSub';
|
||||
|
||||
import {
|
||||
RedisPubSub
|
||||
} from '../Adapters/PubSub/RedisPubSub';
|
||||
import { RedisPubSub } from '../Adapters/PubSub/RedisPubSub';
|
||||
|
||||
const ParsePubSub = {};
|
||||
|
||||
@@ -18,26 +14,32 @@ ParsePubSub.createPublisher = function(config: any): any {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createPublisher(config);
|
||||
} else {
|
||||
const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config)
|
||||
const adapter = loadAdapter(
|
||||
config.pubSubAdapter,
|
||||
EventEmitterPubSub,
|
||||
config
|
||||
);
|
||||
if (typeof adapter.createPublisher !== 'function') {
|
||||
throw 'pubSubAdapter should have createPublisher()';
|
||||
}
|
||||
return adapter.createPublisher(config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ParsePubSub.createSubscriber = function(config: any): void {
|
||||
if (useRedis(config)) {
|
||||
return RedisPubSub.createSubscriber(config);
|
||||
} else {
|
||||
const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config)
|
||||
const adapter = loadAdapter(
|
||||
config.pubSubAdapter,
|
||||
EventEmitterPubSub,
|
||||
config
|
||||
);
|
||||
if (typeof adapter.createSubscriber !== 'function') {
|
||||
throw 'pubSubAdapter should have createSubscriber()';
|
||||
}
|
||||
return adapter.createSubscriber(config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
ParsePubSub
|
||||
}
|
||||
export { ParsePubSub };
|
||||
|
||||
@@ -4,21 +4,25 @@ const typeMap = new Map([['disconnect', 'close']]);
|
||||
const getWS = function() {
|
||||
try {
|
||||
return require('uws');
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return require('ws');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class ParseWebSocketServer {
|
||||
server: Object;
|
||||
|
||||
constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) {
|
||||
constructor(
|
||||
server: any,
|
||||
onConnect: Function,
|
||||
websocketTimeout: number = 10 * 1000
|
||||
) {
|
||||
const WebSocketServer = getWS().Server;
|
||||
const wss = new WebSocketServer({ server: server });
|
||||
wss.on('listening', () => {
|
||||
logger.info('Parse LiveQuery Server starts running');
|
||||
});
|
||||
wss.on('connection', (ws) => {
|
||||
wss.on('connection', ws => {
|
||||
onConnect(new ParseWebSocket(ws));
|
||||
// Send ping to client periodically
|
||||
const pingIntervalId = setInterval(() => {
|
||||
|
||||
@@ -55,8 +55,8 @@ function queryHash(query) {
|
||||
if (query instanceof Parse.Query) {
|
||||
query = {
|
||||
className: query.className,
|
||||
where: query._where
|
||||
}
|
||||
where: query._where,
|
||||
};
|
||||
}
|
||||
var where = flattenOrQueries(query.where || {});
|
||||
var columns = [];
|
||||
@@ -99,8 +99,10 @@ function contains(haystack: Array, needle: any): boolean {
|
||||
if (typeof ptr === 'string' && ptr === needle.objectId) {
|
||||
return true;
|
||||
}
|
||||
if (ptr.className === needle.className &&
|
||||
ptr.objectId === needle.objectId) {
|
||||
if (
|
||||
ptr.className === needle.className &&
|
||||
ptr.objectId === needle.objectId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +119,7 @@ function contains(haystack: Array, needle: any): boolean {
|
||||
function matchesQuery(object: any, query: any): boolean {
|
||||
if (query instanceof Parse.Query) {
|
||||
var className =
|
||||
(object.id instanceof Id) ? object.id.className : object.className;
|
||||
object.id instanceof Id ? object.id.className : object.className;
|
||||
if (className !== query.className) {
|
||||
return false;
|
||||
}
|
||||
@@ -144,7 +146,6 @@ function equalObjectsGeneric(obj, compareTo, eqlFn) {
|
||||
return eqlFn(obj, compareTo);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether an object matches a single key's constraints
|
||||
*/
|
||||
@@ -152,12 +153,16 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
if (constraints === null) {
|
||||
return false;
|
||||
}
|
||||
if(key.indexOf(".") >= 0){
|
||||
if (key.indexOf('.') >= 0) {
|
||||
// Key references a subobject
|
||||
var keyComponents = key.split(".");
|
||||
var keyComponents = key.split('.');
|
||||
var subObjectKey = keyComponents[0];
|
||||
var keyRemainder = keyComponents.slice(1).join(".");
|
||||
return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints);
|
||||
var keyRemainder = keyComponents.slice(1).join('.');
|
||||
return matchesKeyConstraints(
|
||||
object[subObjectKey] || {},
|
||||
keyRemainder,
|
||||
constraints
|
||||
);
|
||||
}
|
||||
var i;
|
||||
if (key === '$or') {
|
||||
@@ -191,7 +196,11 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
});
|
||||
}
|
||||
|
||||
return equalObjectsGeneric(object[key], Parse._decode(key, constraints), equalObjects);
|
||||
return equalObjectsGeneric(
|
||||
object[key],
|
||||
Parse._decode(key, constraints),
|
||||
equalObjects
|
||||
);
|
||||
}
|
||||
// More complex cases
|
||||
for (var condition in constraints) {
|
||||
@@ -200,124 +209,131 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
compareTo = Parse._decode(key, compareTo);
|
||||
}
|
||||
switch (condition) {
|
||||
case '$lt':
|
||||
if (object[key] >= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$lte':
|
||||
if (object[key] > compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gt':
|
||||
if (object[key] <= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gte':
|
||||
if (object[key] < compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$ne':
|
||||
if (equalObjects(object[key], compareTo)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$in':
|
||||
if (!contains(compareTo, object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nin':
|
||||
if (contains(compareTo, object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$all':
|
||||
for (i = 0; i < compareTo.length; i++) {
|
||||
if (object[key].indexOf(compareTo[i]) < 0) {
|
||||
case '$lt':
|
||||
if (object[key] >= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$lte':
|
||||
if (object[key] > compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gt':
|
||||
if (object[key] <= compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$gte':
|
||||
if (object[key] < compareTo) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$ne':
|
||||
if (equalObjects(object[key], compareTo)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$in':
|
||||
if (!contains(compareTo, object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nin':
|
||||
if (contains(compareTo, object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$all':
|
||||
for (i = 0; i < compareTo.length; i++) {
|
||||
if (object[key].indexOf(compareTo[i]) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '$exists': {
|
||||
const propertyExists = typeof object[key] !== 'undefined';
|
||||
const existenceIsRequired = constraints['$exists'];
|
||||
if (typeof constraints['$exists'] !== 'boolean') {
|
||||
// The SDK will never submit a non-boolean for $exists, but if someone
|
||||
// tries to submit a non-boolean for $exits outside the SDKs, just ignore it.
|
||||
break;
|
||||
}
|
||||
if (
|
||||
(!propertyExists && existenceIsRequired) ||
|
||||
(propertyExists && !existenceIsRequired)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '$exists': {
|
||||
const propertyExists = typeof object[key] !== 'undefined';
|
||||
const existenceIsRequired = constraints['$exists'];
|
||||
if (typeof constraints['$exists'] !== 'boolean') {
|
||||
// The SDK will never submit a non-boolean for $exists, but if someone
|
||||
// tries to submit a non-boolean for $exits outside the SDKs, just ignore it.
|
||||
break;
|
||||
}
|
||||
if ((!propertyExists && existenceIsRequired) || (propertyExists && !existenceIsRequired)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '$regex':
|
||||
if (typeof compareTo === 'object') {
|
||||
return compareTo.test(object[key]);
|
||||
}
|
||||
// JS doesn't support perl-style escaping
|
||||
var expString = '';
|
||||
var escapeEnd = -2;
|
||||
var escapeStart = compareTo.indexOf('\\Q');
|
||||
while (escapeStart > -1) {
|
||||
// Add the unescaped portion
|
||||
expString += compareTo.substring(escapeEnd + 2, escapeStart);
|
||||
escapeEnd = compareTo.indexOf('\\E', escapeStart);
|
||||
if (escapeEnd > -1) {
|
||||
expString += compareTo.substring(escapeStart + 2, escapeEnd)
|
||||
.replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&');
|
||||
case '$regex':
|
||||
if (typeof compareTo === 'object') {
|
||||
return compareTo.test(object[key]);
|
||||
}
|
||||
// JS doesn't support perl-style escaping
|
||||
var expString = '';
|
||||
var escapeEnd = -2;
|
||||
var escapeStart = compareTo.indexOf('\\Q');
|
||||
while (escapeStart > -1) {
|
||||
// Add the unescaped portion
|
||||
expString += compareTo.substring(escapeEnd + 2, escapeStart);
|
||||
escapeEnd = compareTo.indexOf('\\E', escapeStart);
|
||||
if (escapeEnd > -1) {
|
||||
expString += compareTo
|
||||
.substring(escapeStart + 2, escapeEnd)
|
||||
.replace(/\\\\\\\\E/g, '\\E')
|
||||
.replace(/\W/g, '\\$&');
|
||||
}
|
||||
|
||||
escapeStart = compareTo.indexOf('\\Q', escapeEnd);
|
||||
}
|
||||
expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2));
|
||||
var exp = new RegExp(expString, constraints.$options || '');
|
||||
if (!exp.test(object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nearSphere':
|
||||
if (!compareTo || !object[key]) {
|
||||
return false;
|
||||
}
|
||||
var distance = compareTo.radiansTo(object[key]);
|
||||
var max = constraints.$maxDistance || Infinity;
|
||||
return distance <= max;
|
||||
case '$within':
|
||||
if (!compareTo || !object[key]) {
|
||||
return false;
|
||||
}
|
||||
var southWest = compareTo.$box[0];
|
||||
var northEast = compareTo.$box[1];
|
||||
if (southWest.latitude > northEast.latitude ||
|
||||
southWest.longitude > northEast.longitude) {
|
||||
// Invalid box, crosses the date line
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
object[key].latitude > southWest.latitude &&
|
||||
escapeStart = compareTo.indexOf('\\Q', escapeEnd);
|
||||
}
|
||||
expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2));
|
||||
var exp = new RegExp(expString, constraints.$options || '');
|
||||
if (!exp.test(object[key])) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case '$nearSphere':
|
||||
if (!compareTo || !object[key]) {
|
||||
return false;
|
||||
}
|
||||
var distance = compareTo.radiansTo(object[key]);
|
||||
var max = constraints.$maxDistance || Infinity;
|
||||
return distance <= max;
|
||||
case '$within':
|
||||
if (!compareTo || !object[key]) {
|
||||
return false;
|
||||
}
|
||||
var southWest = compareTo.$box[0];
|
||||
var northEast = compareTo.$box[1];
|
||||
if (
|
||||
southWest.latitude > northEast.latitude ||
|
||||
southWest.longitude > northEast.longitude
|
||||
) {
|
||||
// Invalid box, crosses the date line
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
object[key].latitude > southWest.latitude &&
|
||||
object[key].latitude < northEast.latitude &&
|
||||
object[key].longitude > southWest.longitude &&
|
||||
object[key].longitude < northEast.longitude
|
||||
);
|
||||
case '$options':
|
||||
// Not a query type, but a way to add options to $regex. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$maxDistance':
|
||||
// Not a query type, but a way to add a cap to $nearSphere. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$select':
|
||||
return false;
|
||||
case '$dontSelect':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
);
|
||||
case '$options':
|
||||
// Not a query type, but a way to add options to $regex. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$maxDistance':
|
||||
// Not a query type, but a way to add a cap to $nearSphere. Ignore and
|
||||
// avoid the default
|
||||
break;
|
||||
case '$select':
|
||||
return false;
|
||||
case '$dontSelect':
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -325,7 +341,7 @@ function matchesKeyConstraints(object, key, constraints) {
|
||||
|
||||
var QueryTools = {
|
||||
queryHash: queryHash,
|
||||
matchesQuery: matchesQuery
|
||||
matchesQuery: matchesQuery,
|
||||
};
|
||||
|
||||
module.exports = QueryTools;
|
||||
|
||||
@@ -1,141 +1,141 @@
|
||||
const general = {
|
||||
'title': 'General request schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': {
|
||||
'type': 'string',
|
||||
'enum': ['connect', 'subscribe', 'unsubscribe', 'update']
|
||||
title: 'General request schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: {
|
||||
type: 'string',
|
||||
enum: ['connect', 'subscribe', 'unsubscribe', 'update'],
|
||||
},
|
||||
},
|
||||
'required': ['op']
|
||||
required: ['op'],
|
||||
};
|
||||
|
||||
const connect = {
|
||||
'title': 'Connect operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'connect',
|
||||
'applicationId': {
|
||||
'type': 'string'
|
||||
const connect = {
|
||||
title: 'Connect operation schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: 'connect',
|
||||
applicationId: {
|
||||
type: 'string',
|
||||
},
|
||||
'javascriptKey': {
|
||||
type: 'string'
|
||||
javascriptKey: {
|
||||
type: 'string',
|
||||
},
|
||||
'masterKey': {
|
||||
type: 'string'
|
||||
masterKey: {
|
||||
type: 'string',
|
||||
},
|
||||
'clientKey': {
|
||||
type: 'string'
|
||||
clientKey: {
|
||||
type: 'string',
|
||||
},
|
||||
'windowsKey': {
|
||||
type: 'string'
|
||||
windowsKey: {
|
||||
type: 'string',
|
||||
},
|
||||
'restAPIKey': {
|
||||
'type': 'string'
|
||||
restAPIKey: {
|
||||
type: 'string',
|
||||
},
|
||||
sessionToken: {
|
||||
type: 'string',
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'applicationId'],
|
||||
"additionalProperties": false
|
||||
required: ['op', 'applicationId'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const subscribe = {
|
||||
'title': 'Subscribe operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'subscribe',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
title: 'Subscribe operation schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: 'subscribe',
|
||||
requestId: {
|
||||
type: 'number',
|
||||
},
|
||||
'query': {
|
||||
'title': 'Query field schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'className': {
|
||||
'type': 'string'
|
||||
query: {
|
||||
title: 'Query field schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
className: {
|
||||
type: 'string',
|
||||
},
|
||||
'where': {
|
||||
'type': 'object'
|
||||
where: {
|
||||
type: 'object',
|
||||
},
|
||||
'fields': {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
fields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
'required': ['where', 'className'],
|
||||
'additionalProperties': false
|
||||
required: ['where', 'className'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
sessionToken: {
|
||||
type: 'string',
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'requestId', 'query'],
|
||||
'additionalProperties': false
|
||||
required: ['op', 'requestId', 'query'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const update = {
|
||||
'title': 'Update operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'update',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
title: 'Update operation schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: 'update',
|
||||
requestId: {
|
||||
type: 'number',
|
||||
},
|
||||
'query': {
|
||||
'title': 'Query field schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'className': {
|
||||
'type': 'string'
|
||||
query: {
|
||||
title: 'Query field schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
className: {
|
||||
type: 'string',
|
||||
},
|
||||
'where': {
|
||||
'type': 'object'
|
||||
where: {
|
||||
type: 'object',
|
||||
},
|
||||
'fields': {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
fields: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
minItems: 1,
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
'required': ['where', 'className'],
|
||||
'additionalProperties': false
|
||||
required: ['where', 'className'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
sessionToken: {
|
||||
type: 'string',
|
||||
},
|
||||
'sessionToken': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
'required': ['op', 'requestId', 'query'],
|
||||
'additionalProperties': false
|
||||
required: ['op', 'requestId', 'query'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const unsubscribe = {
|
||||
'title': 'Unsubscribe operation schema',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'op': 'unsubscribe',
|
||||
'requestId': {
|
||||
'type': 'number'
|
||||
}
|
||||
title: 'Unsubscribe operation schema',
|
||||
type: 'object',
|
||||
properties: {
|
||||
op: 'unsubscribe',
|
||||
requestId: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
'required': ['op', 'requestId'],
|
||||
"additionalProperties": false
|
||||
}
|
||||
required: ['op', 'requestId'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
const RequestSchema = {
|
||||
'general': general,
|
||||
'connect': connect,
|
||||
'subscribe': subscribe,
|
||||
'update': update,
|
||||
'unsubscribe': unsubscribe
|
||||
}
|
||||
general: general,
|
||||
connect: connect,
|
||||
subscribe: subscribe,
|
||||
update: update,
|
||||
unsubscribe: unsubscribe,
|
||||
};
|
||||
|
||||
export default RequestSchema;
|
||||
|
||||
@@ -2,24 +2,27 @@ import Parse from 'parse/node';
|
||||
import LRU from 'lru-cache';
|
||||
import logger from '../logger';
|
||||
|
||||
function userForSessionToken(sessionToken){
|
||||
var q = new Parse.Query("_Session");
|
||||
q.equalTo("sessionToken", sessionToken);
|
||||
return q.first({useMasterKey:true}).then(function(session){
|
||||
if(!session){
|
||||
return Promise.reject("No session found for session token");
|
||||
function userForSessionToken(sessionToken) {
|
||||
var q = new Parse.Query('_Session');
|
||||
q.equalTo('sessionToken', sessionToken);
|
||||
return q.first({ useMasterKey: true }).then(function(session) {
|
||||
if (!session) {
|
||||
return Promise.reject('No session found for session token');
|
||||
}
|
||||
return session.get("user");
|
||||
return session.get('user');
|
||||
});
|
||||
}
|
||||
|
||||
class SessionTokenCache {
|
||||
cache: Object;
|
||||
|
||||
constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) {
|
||||
constructor(
|
||||
timeout: number = 30 * 24 * 60 * 60 * 1000,
|
||||
maxSize: number = 10000
|
||||
) {
|
||||
this.cache = new LRU({
|
||||
max: maxSize,
|
||||
maxAge: timeout
|
||||
maxAge: timeout,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,21 +32,34 @@ class SessionTokenCache {
|
||||
}
|
||||
const userId = this.cache.get(sessionToken);
|
||||
if (userId) {
|
||||
logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken);
|
||||
logger.verbose(
|
||||
'Fetch userId %s of sessionToken %s from Cache',
|
||||
userId,
|
||||
sessionToken
|
||||
);
|
||||
return Promise.resolve(userId);
|
||||
}
|
||||
return userForSessionToken(sessionToken).then((user) => {
|
||||
logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken);
|
||||
const userId = user.id;
|
||||
this.cache.set(sessionToken, userId);
|
||||
return Promise.resolve(userId);
|
||||
}, (error) => {
|
||||
logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error);
|
||||
return Promise.reject(error);
|
||||
});
|
||||
return userForSessionToken(sessionToken).then(
|
||||
user => {
|
||||
logger.verbose(
|
||||
'Fetch userId %s of sessionToken %s from Parse',
|
||||
user.id,
|
||||
sessionToken
|
||||
);
|
||||
const userId = user.id;
|
||||
this.cache.set(sessionToken, userId);
|
||||
return Promise.resolve(userId);
|
||||
},
|
||||
error => {
|
||||
logger.error(
|
||||
'Can not fetch userId for sessionToken %j, error %j',
|
||||
sessionToken,
|
||||
error
|
||||
);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
SessionTokenCache
|
||||
}
|
||||
export { SessionTokenCache };
|
||||
|
||||
@@ -34,7 +34,11 @@ class Subscription {
|
||||
|
||||
const index = requestIds.indexOf(requestId);
|
||||
if (index < 0) {
|
||||
logger.error('Can not find client %d subscription %d to delete', clientId, requestId);
|
||||
logger.error(
|
||||
'Can not find client %d subscription %d to delete',
|
||||
clientId,
|
||||
requestId
|
||||
);
|
||||
return;
|
||||
}
|
||||
requestIds.splice(index, 1);
|
||||
@@ -49,6 +53,4 @@ class Subscription {
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Subscription
|
||||
}
|
||||
export { Subscription };
|
||||
|
||||
@@ -9,14 +9,14 @@ function equalObjects(a, b) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a !== 'object') {
|
||||
return (a === b);
|
||||
return a === b;
|
||||
}
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (toString.call(a) === '[object Date]') {
|
||||
if (toString.call(b) === '[object Date]') {
|
||||
return (+a === +b);
|
||||
return +a === +b;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,399 +2,421 @@
|
||||
**** GENERATED CODE ****
|
||||
This code has been generated by resources/buildConfigDefinitions.js
|
||||
Do not edit manually, but update Options/index.js
|
||||
*/"use strict";
|
||||
*/ 'use strict';
|
||||
|
||||
var parsers = require("./parsers");
|
||||
var parsers = require('./parsers');
|
||||
|
||||
module.exports.ParseServerOptions = {
|
||||
"appId": {
|
||||
"env": "PARSE_SERVER_APPLICATION_ID",
|
||||
"help": "Your Parse Application ID",
|
||||
"required": true
|
||||
appId: {
|
||||
env: 'PARSE_SERVER_APPLICATION_ID',
|
||||
help: 'Your Parse Application ID',
|
||||
required: true,
|
||||
},
|
||||
"masterKey": {
|
||||
"env": "PARSE_SERVER_MASTER_KEY",
|
||||
"help": "Your Parse Master Key",
|
||||
"required": true
|
||||
masterKey: {
|
||||
env: 'PARSE_SERVER_MASTER_KEY',
|
||||
help: 'Your Parse Master Key',
|
||||
required: true,
|
||||
},
|
||||
"serverURL": {
|
||||
"env": "PARSE_SERVER_URL",
|
||||
"help": "URL to your parse server with http:// or https://.",
|
||||
"required": true
|
||||
serverURL: {
|
||||
env: 'PARSE_SERVER_URL',
|
||||
help: 'URL to your parse server with http:// or https://.',
|
||||
required: true,
|
||||
},
|
||||
"masterKeyIps": {
|
||||
"env": "PARSE_SERVER_MASTER_KEY_IPS",
|
||||
"help": "Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)",
|
||||
"action": parsers.arrayParser,
|
||||
"default": []
|
||||
masterKeyIps: {
|
||||
env: 'PARSE_SERVER_MASTER_KEY_IPS',
|
||||
help:
|
||||
'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)',
|
||||
action: parsers.arrayParser,
|
||||
default: [],
|
||||
},
|
||||
"appName": {
|
||||
"env": "PARSE_SERVER_APP_NAME",
|
||||
"help": "Sets the app name"
|
||||
appName: {
|
||||
env: 'PARSE_SERVER_APP_NAME',
|
||||
help: 'Sets the app name',
|
||||
},
|
||||
"analyticsAdapter": {
|
||||
"env": "PARSE_SERVER_ANALYTICS_ADAPTER",
|
||||
"help": "Adapter module for the analytics",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
analyticsAdapter: {
|
||||
env: 'PARSE_SERVER_ANALYTICS_ADAPTER',
|
||||
help: 'Adapter module for the analytics',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"filesAdapter": {
|
||||
"env": "PARSE_SERVER_FILES_ADAPTER",
|
||||
"help": "Adapter module for the files sub-system",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
filesAdapter: {
|
||||
env: 'PARSE_SERVER_FILES_ADAPTER',
|
||||
help: 'Adapter module for the files sub-system',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"push": {
|
||||
"env": "PARSE_SERVER_PUSH",
|
||||
"help": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications",
|
||||
"action": parsers.objectParser
|
||||
push: {
|
||||
env: 'PARSE_SERVER_PUSH',
|
||||
help:
|
||||
'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"scheduledPush": {
|
||||
"env": "PARSE_SERVER_SCHEDULED_PUSH",
|
||||
"help": "Configuration for push scheduling, defaults to false.",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
scheduledPush: {
|
||||
env: 'PARSE_SERVER_SCHEDULED_PUSH',
|
||||
help: 'Configuration for push scheduling, defaults to false.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"loggerAdapter": {
|
||||
"env": "PARSE_SERVER_LOGGER_ADAPTER",
|
||||
"help": "Adapter module for the logging sub-system",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
loggerAdapter: {
|
||||
env: 'PARSE_SERVER_LOGGER_ADAPTER',
|
||||
help: 'Adapter module for the logging sub-system',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"jsonLogs": {
|
||||
"env": "JSON_LOGS",
|
||||
"help": "Log as structured JSON objects",
|
||||
"action": parsers.booleanParser
|
||||
jsonLogs: {
|
||||
env: 'JSON_LOGS',
|
||||
help: 'Log as structured JSON objects',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
"logsFolder": {
|
||||
"env": "PARSE_SERVER_LOGS_FOLDER",
|
||||
"help": "Folder for the logs (defaults to './logs'); set to null to disable file based logging",
|
||||
"default": "./logs"
|
||||
logsFolder: {
|
||||
env: 'PARSE_SERVER_LOGS_FOLDER',
|
||||
help:
|
||||
"Folder for the logs (defaults to './logs'); set to null to disable file based logging",
|
||||
default: './logs',
|
||||
},
|
||||
"verbose": {
|
||||
"env": "VERBOSE",
|
||||
"help": "Set the logging to verbose",
|
||||
"action": parsers.booleanParser
|
||||
verbose: {
|
||||
env: 'VERBOSE',
|
||||
help: 'Set the logging to verbose',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
"logLevel": {
|
||||
"env": "PARSE_SERVER_LOG_LEVEL",
|
||||
"help": "Sets the level for logs"
|
||||
logLevel: {
|
||||
env: 'PARSE_SERVER_LOG_LEVEL',
|
||||
help: 'Sets the level for logs',
|
||||
},
|
||||
"silent": {
|
||||
"env": "SILENT",
|
||||
"help": "Disables console output",
|
||||
"action": parsers.booleanParser
|
||||
silent: {
|
||||
env: 'SILENT',
|
||||
help: 'Disables console output',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
"databaseURI": {
|
||||
"env": "PARSE_SERVER_DATABASE_URI",
|
||||
"help": "The full URI to your database. Supported databases are mongodb or postgres.",
|
||||
"required": true,
|
||||
"default": "mongodb://localhost:27017/parse"
|
||||
databaseURI: {
|
||||
env: 'PARSE_SERVER_DATABASE_URI',
|
||||
help:
|
||||
'The full URI to your database. Supported databases are mongodb or postgres.',
|
||||
required: true,
|
||||
default: 'mongodb://localhost:27017/parse',
|
||||
},
|
||||
"databaseOptions": {
|
||||
"env": "PARSE_SERVER_DATABASE_OPTIONS",
|
||||
"help": "Options to pass to the mongodb client",
|
||||
"action": parsers.objectParser
|
||||
databaseOptions: {
|
||||
env: 'PARSE_SERVER_DATABASE_OPTIONS',
|
||||
help: 'Options to pass to the mongodb client',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"databaseAdapter": {
|
||||
"env": "PARSE_SERVER_DATABASE_ADAPTER",
|
||||
"help": "Adapter module for the database",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
databaseAdapter: {
|
||||
env: 'PARSE_SERVER_DATABASE_ADAPTER',
|
||||
help: 'Adapter module for the database',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"cloud": {
|
||||
"env": "PARSE_SERVER_CLOUD",
|
||||
"help": "Full path to your cloud code main.js"
|
||||
cloud: {
|
||||
env: 'PARSE_SERVER_CLOUD',
|
||||
help: 'Full path to your cloud code main.js',
|
||||
},
|
||||
"collectionPrefix": {
|
||||
"env": "PARSE_SERVER_COLLECTION_PREFIX",
|
||||
"help": "A collection prefix for the classes",
|
||||
"default": ""
|
||||
collectionPrefix: {
|
||||
env: 'PARSE_SERVER_COLLECTION_PREFIX',
|
||||
help: 'A collection prefix for the classes',
|
||||
default: '',
|
||||
},
|
||||
"clientKey": {
|
||||
"env": "PARSE_SERVER_CLIENT_KEY",
|
||||
"help": "Key for iOS, MacOS, tvOS clients"
|
||||
clientKey: {
|
||||
env: 'PARSE_SERVER_CLIENT_KEY',
|
||||
help: 'Key for iOS, MacOS, tvOS clients',
|
||||
},
|
||||
"javascriptKey": {
|
||||
"env": "PARSE_SERVER_JAVASCRIPT_KEY",
|
||||
"help": "Key for the Javascript SDK"
|
||||
javascriptKey: {
|
||||
env: 'PARSE_SERVER_JAVASCRIPT_KEY',
|
||||
help: 'Key for the Javascript SDK',
|
||||
},
|
||||
"dotNetKey": {
|
||||
"env": "PARSE_SERVER_DOT_NET_KEY",
|
||||
"help": "Key for Unity and .Net SDK"
|
||||
dotNetKey: {
|
||||
env: 'PARSE_SERVER_DOT_NET_KEY',
|
||||
help: 'Key for Unity and .Net SDK',
|
||||
},
|
||||
"restAPIKey": {
|
||||
"env": "PARSE_SERVER_REST_API_KEY",
|
||||
"help": "Key for REST calls"
|
||||
restAPIKey: {
|
||||
env: 'PARSE_SERVER_REST_API_KEY',
|
||||
help: 'Key for REST calls',
|
||||
},
|
||||
"readOnlyMasterKey": {
|
||||
"env": "PARSE_SERVER_READ_ONLY_MASTER_KEY",
|
||||
"help": "Read-only key, which has the same capabilities as MasterKey without writes"
|
||||
readOnlyMasterKey: {
|
||||
env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY',
|
||||
help:
|
||||
'Read-only key, which has the same capabilities as MasterKey without writes',
|
||||
},
|
||||
"webhookKey": {
|
||||
"env": "PARSE_SERVER_WEBHOOK_KEY",
|
||||
"help": "Key sent with outgoing webhook calls"
|
||||
webhookKey: {
|
||||
env: 'PARSE_SERVER_WEBHOOK_KEY',
|
||||
help: 'Key sent with outgoing webhook calls',
|
||||
},
|
||||
"fileKey": {
|
||||
"env": "PARSE_SERVER_FILE_KEY",
|
||||
"help": "Key for your files"
|
||||
fileKey: {
|
||||
env: 'PARSE_SERVER_FILE_KEY',
|
||||
help: 'Key for your files',
|
||||
},
|
||||
"preserveFileName": {
|
||||
"env": "PARSE_SERVER_PRESERVE_FILE_NAME",
|
||||
"help": "Enable (or disable) the addition of a unique hash to the file names",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
preserveFileName: {
|
||||
env: 'PARSE_SERVER_PRESERVE_FILE_NAME',
|
||||
help: 'Enable (or disable) the addition of a unique hash to the file names',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"userSensitiveFields": {
|
||||
"env": "PARSE_SERVER_USER_SENSITIVE_FIELDS",
|
||||
"help": "Personally identifiable information fields in the user table the should be removed for non-authorized users.",
|
||||
"action": parsers.arrayParser,
|
||||
"default": ["email"]
|
||||
userSensitiveFields: {
|
||||
env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS',
|
||||
help:
|
||||
'Personally identifiable information fields in the user table the should be removed for non-authorized users.',
|
||||
action: parsers.arrayParser,
|
||||
default: ['email'],
|
||||
},
|
||||
"enableAnonymousUsers": {
|
||||
"env": "PARSE_SERVER_ENABLE_ANON_USERS",
|
||||
"help": "Enable (or disable) anon users, defaults to true",
|
||||
"action": parsers.booleanParser,
|
||||
"default": true
|
||||
enableAnonymousUsers: {
|
||||
env: 'PARSE_SERVER_ENABLE_ANON_USERS',
|
||||
help: 'Enable (or disable) anon users, defaults to true',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
"allowClientClassCreation": {
|
||||
"env": "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION",
|
||||
"help": "Enable (or disable) client class creation, defaults to true",
|
||||
"action": parsers.booleanParser,
|
||||
"default": true
|
||||
allowClientClassCreation: {
|
||||
env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION',
|
||||
help: 'Enable (or disable) client class creation, defaults to true',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
"auth": {
|
||||
"env": "PARSE_SERVER_AUTH_PROVIDERS",
|
||||
"help": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication",
|
||||
"action": parsers.objectParser
|
||||
auth: {
|
||||
env: 'PARSE_SERVER_AUTH_PROVIDERS',
|
||||
help:
|
||||
'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"maxUploadSize": {
|
||||
"env": "PARSE_SERVER_MAX_UPLOAD_SIZE",
|
||||
"help": "Max file size for uploads, defaults to 20mb",
|
||||
"default": "20mb"
|
||||
maxUploadSize: {
|
||||
env: 'PARSE_SERVER_MAX_UPLOAD_SIZE',
|
||||
help: 'Max file size for uploads, defaults to 20mb',
|
||||
default: '20mb',
|
||||
},
|
||||
"verifyUserEmails": {
|
||||
"env": "PARSE_SERVER_VERIFY_USER_EMAILS",
|
||||
"help": "Enable (or disable) user email validation, defaults to false",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
verifyUserEmails: {
|
||||
env: 'PARSE_SERVER_VERIFY_USER_EMAILS',
|
||||
help: 'Enable (or disable) user email validation, defaults to false',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"preventLoginWithUnverifiedEmail": {
|
||||
"env": "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL",
|
||||
"help": "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
preventLoginWithUnverifiedEmail: {
|
||||
env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL',
|
||||
help:
|
||||
'Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"emailVerifyTokenValidityDuration": {
|
||||
"env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION",
|
||||
"help": "Email verification token validity duration, in seconds",
|
||||
"action": parsers.numberParser("emailVerifyTokenValidityDuration")
|
||||
emailVerifyTokenValidityDuration: {
|
||||
env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION',
|
||||
help: 'Email verification token validity duration, in seconds',
|
||||
action: parsers.numberParser('emailVerifyTokenValidityDuration'),
|
||||
},
|
||||
"accountLockout": {
|
||||
"env": "PARSE_SERVER_ACCOUNT_LOCKOUT",
|
||||
"help": "account lockout policy for failed login attempts",
|
||||
"action": parsers.objectParser
|
||||
accountLockout: {
|
||||
env: 'PARSE_SERVER_ACCOUNT_LOCKOUT',
|
||||
help: 'account lockout policy for failed login attempts',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"passwordPolicy": {
|
||||
"env": "PARSE_SERVER_PASSWORD_POLICY",
|
||||
"help": "Password policy for enforcing password related rules",
|
||||
"action": parsers.objectParser
|
||||
passwordPolicy: {
|
||||
env: 'PARSE_SERVER_PASSWORD_POLICY',
|
||||
help: 'Password policy for enforcing password related rules',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"cacheAdapter": {
|
||||
"env": "PARSE_SERVER_CACHE_ADAPTER",
|
||||
"help": "Adapter module for the cache",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
cacheAdapter: {
|
||||
env: 'PARSE_SERVER_CACHE_ADAPTER',
|
||||
help: 'Adapter module for the cache',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"emailAdapter": {
|
||||
"env": "PARSE_SERVER_EMAIL_ADAPTER",
|
||||
"help": "Adapter module for email sending",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
emailAdapter: {
|
||||
env: 'PARSE_SERVER_EMAIL_ADAPTER',
|
||||
help: 'Adapter module for email sending',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"publicServerURL": {
|
||||
"env": "PARSE_PUBLIC_SERVER_URL",
|
||||
"help": "Public URL to your parse server with http:// or https://."
|
||||
publicServerURL: {
|
||||
env: 'PARSE_PUBLIC_SERVER_URL',
|
||||
help: 'Public URL to your parse server with http:// or https://.',
|
||||
},
|
||||
"customPages": {
|
||||
"env": "PARSE_SERVER_CUSTOM_PAGES",
|
||||
"help": "custom pages for password validation and reset",
|
||||
"action": parsers.objectParser,
|
||||
"default": {}
|
||||
customPages: {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES',
|
||||
help: 'custom pages for password validation and reset',
|
||||
action: parsers.objectParser,
|
||||
default: {},
|
||||
},
|
||||
"liveQuery": {
|
||||
"env": "PARSE_SERVER_LIVE_QUERY",
|
||||
"help": "parse-server's LiveQuery configuration object",
|
||||
"action": parsers.objectParser
|
||||
liveQuery: {
|
||||
env: 'PARSE_SERVER_LIVE_QUERY',
|
||||
help: "parse-server's LiveQuery configuration object",
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"sessionLength": {
|
||||
"env": "PARSE_SERVER_SESSION_LENGTH",
|
||||
"help": "Session duration, in seconds, defaults to 1 year",
|
||||
"action": parsers.numberParser("sessionLength"),
|
||||
"default": 31536000
|
||||
sessionLength: {
|
||||
env: 'PARSE_SERVER_SESSION_LENGTH',
|
||||
help: 'Session duration, in seconds, defaults to 1 year',
|
||||
action: parsers.numberParser('sessionLength'),
|
||||
default: 31536000,
|
||||
},
|
||||
"maxLimit": {
|
||||
"env": "PARSE_SERVER_MAX_LIMIT",
|
||||
"help": "Max value for limit option on queries, defaults to unlimited",
|
||||
"action": parsers.numberParser("maxLimit")
|
||||
maxLimit: {
|
||||
env: 'PARSE_SERVER_MAX_LIMIT',
|
||||
help: 'Max value for limit option on queries, defaults to unlimited',
|
||||
action: parsers.numberParser('maxLimit'),
|
||||
},
|
||||
"expireInactiveSessions": {
|
||||
"env": "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS",
|
||||
"help": "Sets wether we should expire the inactive sessions, defaults to true",
|
||||
"action": parsers.booleanParser,
|
||||
"default": true
|
||||
expireInactiveSessions: {
|
||||
env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS',
|
||||
help:
|
||||
'Sets wether we should expire the inactive sessions, defaults to true',
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
"revokeSessionOnPasswordReset": {
|
||||
"env": "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET",
|
||||
"help": "When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",
|
||||
"action": parsers.booleanParser,
|
||||
"default": true
|
||||
revokeSessionOnPasswordReset: {
|
||||
env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET',
|
||||
help:
|
||||
"When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions.",
|
||||
action: parsers.booleanParser,
|
||||
default: true,
|
||||
},
|
||||
"schemaCacheTTL": {
|
||||
"env": "PARSE_SERVER_SCHEMA_CACHE_TTL",
|
||||
"help": "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.",
|
||||
"action": parsers.numberParser("schemaCacheTTL"),
|
||||
"default": 5000
|
||||
schemaCacheTTL: {
|
||||
env: 'PARSE_SERVER_SCHEMA_CACHE_TTL',
|
||||
help:
|
||||
'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.',
|
||||
action: parsers.numberParser('schemaCacheTTL'),
|
||||
default: 5000,
|
||||
},
|
||||
"cacheTTL": {
|
||||
"env": "PARSE_SERVER_CACHE_TTL",
|
||||
"help": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)",
|
||||
"action": parsers.numberParser("cacheTTL"),
|
||||
"default": 5000
|
||||
cacheTTL: {
|
||||
env: 'PARSE_SERVER_CACHE_TTL',
|
||||
help:
|
||||
'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)',
|
||||
action: parsers.numberParser('cacheTTL'),
|
||||
default: 5000,
|
||||
},
|
||||
"cacheMaxSize": {
|
||||
"env": "PARSE_SERVER_CACHE_MAX_SIZE",
|
||||
"help": "Sets the maximum size for the in memory cache, defaults to 10000",
|
||||
"action": parsers.numberParser("cacheMaxSize"),
|
||||
"default": 10000
|
||||
cacheMaxSize: {
|
||||
env: 'PARSE_SERVER_CACHE_MAX_SIZE',
|
||||
help: 'Sets the maximum size for the in memory cache, defaults to 10000',
|
||||
action: parsers.numberParser('cacheMaxSize'),
|
||||
default: 10000,
|
||||
},
|
||||
"enableSingleSchemaCache": {
|
||||
"env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE",
|
||||
"help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
enableSingleSchemaCache: {
|
||||
env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE',
|
||||
help:
|
||||
'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"enableExpressErrorHandler": {
|
||||
"env": "PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER",
|
||||
"help": "Enables the default express error handler for all errors",
|
||||
"action": parsers.booleanParser,
|
||||
"default": false
|
||||
enableExpressErrorHandler: {
|
||||
env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER',
|
||||
help: 'Enables the default express error handler for all errors',
|
||||
action: parsers.booleanParser,
|
||||
default: false,
|
||||
},
|
||||
"objectIdSize": {
|
||||
"env": "PARSE_SERVER_OBJECT_ID_SIZE",
|
||||
"help": "Sets the number of characters in generated object id's, default 10",
|
||||
"action": parsers.numberParser("objectIdSize"),
|
||||
"default": 10
|
||||
objectIdSize: {
|
||||
env: 'PARSE_SERVER_OBJECT_ID_SIZE',
|
||||
help: "Sets the number of characters in generated object id's, default 10",
|
||||
action: parsers.numberParser('objectIdSize'),
|
||||
default: 10,
|
||||
},
|
||||
"port": {
|
||||
"env": "PORT",
|
||||
"help": "The port to run the ParseServer, defaults to 1337.",
|
||||
"action": parsers.numberParser("port"),
|
||||
"default": 1337
|
||||
port: {
|
||||
env: 'PORT',
|
||||
help: 'The port to run the ParseServer, defaults to 1337.',
|
||||
action: parsers.numberParser('port'),
|
||||
default: 1337,
|
||||
},
|
||||
"host": {
|
||||
"env": "PARSE_SERVER_HOST",
|
||||
"help": "The host to serve ParseServer on, defaults to 0.0.0.0",
|
||||
"default": "0.0.0.0"
|
||||
host: {
|
||||
env: 'PARSE_SERVER_HOST',
|
||||
help: 'The host to serve ParseServer on, defaults to 0.0.0.0',
|
||||
default: '0.0.0.0',
|
||||
},
|
||||
"mountPath": {
|
||||
"env": "PARSE_SERVER_MOUNT_PATH",
|
||||
"help": "Mount path for the server, defaults to /parse",
|
||||
"default": "/parse"
|
||||
mountPath: {
|
||||
env: 'PARSE_SERVER_MOUNT_PATH',
|
||||
help: 'Mount path for the server, defaults to /parse',
|
||||
default: '/parse',
|
||||
},
|
||||
"cluster": {
|
||||
"env": "PARSE_SERVER_CLUSTER",
|
||||
"help": "Run with cluster, optionally set the number of processes default to os.cpus().length",
|
||||
"action": parsers.numberOrBooleanParser
|
||||
cluster: {
|
||||
env: 'PARSE_SERVER_CLUSTER',
|
||||
help:
|
||||
'Run with cluster, optionally set the number of processes default to os.cpus().length',
|
||||
action: parsers.numberOrBooleanParser,
|
||||
},
|
||||
"middleware": {
|
||||
"env": "PARSE_SERVER_MIDDLEWARE",
|
||||
"help": "middleware for express server, can be string or function"
|
||||
middleware: {
|
||||
env: 'PARSE_SERVER_MIDDLEWARE',
|
||||
help: 'middleware for express server, can be string or function',
|
||||
},
|
||||
"startLiveQueryServer": {
|
||||
"env": "PARSE_SERVER_START_LIVE_QUERY_SERVER",
|
||||
"help": "Starts the liveQuery server",
|
||||
"action": parsers.booleanParser
|
||||
startLiveQueryServer: {
|
||||
env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER',
|
||||
help: 'Starts the liveQuery server',
|
||||
action: parsers.booleanParser,
|
||||
},
|
||||
liveQueryServerOptions: {
|
||||
env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS',
|
||||
help:
|
||||
'Live query server configuration options (will start the liveQuery server)',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"liveQueryServerOptions": {
|
||||
"env": "PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS",
|
||||
"help": "Live query server configuration options (will start the liveQuery server)",
|
||||
"action": parsers.objectParser
|
||||
}
|
||||
};
|
||||
module.exports.CustomPagesOptions = {
|
||||
"invalidLink": {
|
||||
"env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK",
|
||||
"help": "invalid link page path"
|
||||
invalidLink: {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK',
|
||||
help: 'invalid link page path',
|
||||
},
|
||||
"verifyEmailSuccess": {
|
||||
"env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS",
|
||||
"help": "verify email success page path"
|
||||
verifyEmailSuccess: {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS',
|
||||
help: 'verify email success page path',
|
||||
},
|
||||
"choosePassword": {
|
||||
"env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD",
|
||||
"help": "choose password page path"
|
||||
choosePassword: {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD',
|
||||
help: 'choose password page path',
|
||||
},
|
||||
passwordResetSuccess: {
|
||||
env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS',
|
||||
help: 'password reset success page path',
|
||||
},
|
||||
"passwordResetSuccess": {
|
||||
"env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS",
|
||||
"help": "password reset success page path"
|
||||
}
|
||||
};
|
||||
module.exports.LiveQueryOptions = {
|
||||
"classNames": {
|
||||
"env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES",
|
||||
"help": "parse-server's LiveQuery classNames",
|
||||
"action": parsers.arrayParser
|
||||
classNames: {
|
||||
env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES',
|
||||
help: "parse-server's LiveQuery classNames",
|
||||
action: parsers.arrayParser,
|
||||
},
|
||||
"redisURL": {
|
||||
"env": "PARSE_SERVER_LIVEQUERY_REDIS_URL",
|
||||
"help": "parse-server's LiveQuery redisURL"
|
||||
redisURL: {
|
||||
env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL',
|
||||
help: "parse-server's LiveQuery redisURL",
|
||||
},
|
||||
pubSubAdapter: {
|
||||
env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER',
|
||||
help: 'LiveQuery pubsub adapter',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"pubSubAdapter": {
|
||||
"env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER",
|
||||
"help": "LiveQuery pubsub adapter",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
}
|
||||
};
|
||||
module.exports.LiveQueryServerOptions = {
|
||||
"appId": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_APP_ID",
|
||||
"help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId."
|
||||
appId: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_APP_ID',
|
||||
help:
|
||||
'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.',
|
||||
},
|
||||
"masterKey": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY",
|
||||
"help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey."
|
||||
masterKey: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY',
|
||||
help:
|
||||
'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.',
|
||||
},
|
||||
"serverURL": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL",
|
||||
"help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL."
|
||||
serverURL: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL',
|
||||
help:
|
||||
'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.',
|
||||
},
|
||||
"keyPairs": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS",
|
||||
"help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.",
|
||||
"action": parsers.objectParser
|
||||
keyPairs: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS',
|
||||
help:
|
||||
'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.',
|
||||
action: parsers.objectParser,
|
||||
},
|
||||
"websocketTimeout": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT",
|
||||
"help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).",
|
||||
"action": parsers.numberParser("websocketTimeout")
|
||||
websocketTimeout: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT',
|
||||
help:
|
||||
'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).',
|
||||
action: parsers.numberParser('websocketTimeout'),
|
||||
},
|
||||
"cacheTimeout": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT",
|
||||
"help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
|
||||
"action": parsers.numberParser("cacheTimeout")
|
||||
cacheTimeout: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT',
|
||||
help:
|
||||
"Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).",
|
||||
action: parsers.numberParser('cacheTimeout'),
|
||||
},
|
||||
"logLevel": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL",
|
||||
"help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO."
|
||||
logLevel: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL',
|
||||
help:
|
||||
'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.',
|
||||
},
|
||||
"port": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_PORT",
|
||||
"help": "The port to run the LiveQuery server, defaults to 1337.",
|
||||
"action": parsers.numberParser("port"),
|
||||
"default": 1337
|
||||
port: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_PORT',
|
||||
help: 'The port to run the LiveQuery server, defaults to 1337.',
|
||||
action: parsers.numberParser('port'),
|
||||
default: 1337,
|
||||
},
|
||||
"redisURL": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL",
|
||||
"help": "parse-server's LiveQuery redisURL"
|
||||
redisURL: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL',
|
||||
help: "parse-server's LiveQuery redisURL",
|
||||
},
|
||||
pubSubAdapter: {
|
||||
env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER',
|
||||
help: 'LiveQuery pubsub adapter',
|
||||
action: parsers.moduleOrObjectParser,
|
||||
},
|
||||
"pubSubAdapter": {
|
||||
"env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER",
|
||||
"help": "LiveQuery pubsub adapter",
|
||||
"action": parsers.moduleOrObjectParser
|
||||
}
|
||||
};
|
||||
|
||||
@@ -90,4 +90,3 @@
|
||||
* @property {String} redisURL parse-server's LiveQuery redisURL
|
||||
* @property {Adapter<PubSubAdapter>} pubSubAdapter LiveQuery pubsub adapter
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { AnalyticsAdapter } from "../Adapters/Analytics/AnalyticsAdapter";
|
||||
import { FilesAdapter } from "../Adapters/Files/FilesAdapter";
|
||||
import { LoggerAdapter } from "../Adapters/Logger/LoggerAdapter";
|
||||
import { StorageAdapter } from "../Adapters/Storage/StorageAdapter";
|
||||
import { CacheAdapter } from "../Adapters/Cache/CacheAdapter";
|
||||
import { MailAdapter } from "../Adapters/Email/MailAdapter";
|
||||
import { PubSubAdapter } from "../Adapters/PubSub/PubSubAdapter";
|
||||
import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter';
|
||||
import { StorageAdapter } from '../Adapters/Storage/StorageAdapter';
|
||||
import { CacheAdapter } from '../Adapters/Cache/CacheAdapter';
|
||||
import { MailAdapter } from '../Adapters/Email/MailAdapter';
|
||||
import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter';
|
||||
|
||||
// @flow
|
||||
type Adapter<T> = string|any|T;
|
||||
type NumberOrBoolean = number|boolean;
|
||||
type Adapter<T> = string | any | T;
|
||||
type NumberOrBoolean = number | boolean;
|
||||
|
||||
export interface ParseServerOptions {
|
||||
/* Your Parse Application ID
|
||||
@@ -20,7 +20,7 @@ export interface ParseServerOptions {
|
||||
:ENV: PARSE_SERVER_URL */
|
||||
serverURL: string;
|
||||
/* Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) */
|
||||
masterKeyIps: ?string[]; // = []
|
||||
masterKeyIps: ?(string[]); // = []
|
||||
/* Sets the app name */
|
||||
appName: ?string;
|
||||
/* Adapter module for the analytics */
|
||||
@@ -76,7 +76,7 @@ export interface ParseServerOptions {
|
||||
:ENV: PARSE_SERVER_PRESERVE_FILE_NAME */
|
||||
preserveFileName: ?boolean; // = false
|
||||
/* Personally identifiable information fields in the user table the should be removed for non-authorized users. */
|
||||
userSensitiveFields: ?string[]; // = ["email"]
|
||||
userSensitiveFields: ?(string[]); // = ["email"]
|
||||
/* Enable (or disable) anon users, defaults to true
|
||||
:ENV: PARSE_SERVER_ENABLE_ANON_USERS */
|
||||
enableAnonymousUsers: ?boolean; // = true
|
||||
@@ -122,7 +122,7 @@ export interface ParseServerOptions {
|
||||
/* Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) */
|
||||
cacheTTL: ?number; // = 5000
|
||||
/* Sets the maximum size for the in memory cache, defaults to 10000 */
|
||||
cacheMaxSize : ?number; // = 10000
|
||||
cacheMaxSize: ?number; // = 10000
|
||||
/* Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request. */
|
||||
enableSingleSchemaCache: ?boolean; // = false
|
||||
/* Enables the default express error handler for all errors */
|
||||
@@ -139,13 +139,13 @@ export interface ParseServerOptions {
|
||||
/* Run with cluster, optionally set the number of processes default to os.cpus().length */
|
||||
cluster: ?NumberOrBoolean;
|
||||
/* middleware for express server, can be string or function */
|
||||
middleware: ?((()=>void)|string);
|
||||
middleware: ?((() => void) | string);
|
||||
/* Starts the liveQuery server */
|
||||
startLiveQueryServer: ?boolean;
|
||||
/* Live query server configuration options (will start the liveQuery server) */
|
||||
liveQueryServerOptions: ?LiveQueryServerOptions;
|
||||
|
||||
__indexBuildCompletionCallbackForTests: ?()=>void;
|
||||
__indexBuildCompletionCallbackForTests: ?() => void;
|
||||
}
|
||||
|
||||
export interface CustomPagesOptions {
|
||||
@@ -162,32 +162,32 @@ export interface CustomPagesOptions {
|
||||
export interface LiveQueryOptions {
|
||||
/* parse-server's LiveQuery classNames
|
||||
:ENV: PARSE_SERVER_LIVEQUERY_CLASSNAMES */
|
||||
classNames: ?string[],
|
||||
classNames: ?(string[]);
|
||||
/* parse-server's LiveQuery redisURL */
|
||||
redisURL: ?string,
|
||||
redisURL: ?string;
|
||||
/* LiveQuery pubsub adapter */
|
||||
pubSubAdapter: ?Adapter<PubSubAdapter>,
|
||||
pubSubAdapter: ?Adapter<PubSubAdapter>;
|
||||
}
|
||||
|
||||
export interface LiveQueryServerOptions {
|
||||
/* This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.*/
|
||||
appId: ?string,
|
||||
appId: ?string;
|
||||
/* This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.*/
|
||||
masterKey: ?string,
|
||||
masterKey: ?string;
|
||||
/* This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.*/
|
||||
serverURL: ?string,
|
||||
serverURL: ?string;
|
||||
/* A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.*/
|
||||
keyPairs: ?any,
|
||||
keyPairs: ?any;
|
||||
/* Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).*/
|
||||
websocketTimeout: ?number,
|
||||
websocketTimeout: ?number;
|
||||
/* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).*/
|
||||
cacheTimeout: ?number,
|
||||
cacheTimeout: ?number;
|
||||
/* This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.*/
|
||||
logLevel: ?string,
|
||||
logLevel: ?string;
|
||||
/* The port to run the LiveQuery server, defaults to 1337.*/
|
||||
port: ?number, // = 1337
|
||||
port: ?number; // = 1337
|
||||
/* parse-server's LiveQuery redisURL */
|
||||
redisURL: ?string,
|
||||
redisURL: ?string;
|
||||
/* LiveQuery pubsub adapter */
|
||||
pubSubAdapter: ?Adapter<PubSubAdapter>,
|
||||
pubSubAdapter: ?Adapter<PubSubAdapter>;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ function numberParser(key) {
|
||||
throw new Error(`Key ${key} has invalid value ${opt}`);
|
||||
}
|
||||
return intOpt;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function numberOrBoolParser(key) {
|
||||
@@ -20,14 +20,14 @@ function numberOrBoolParser(key) {
|
||||
return false;
|
||||
}
|
||||
return numberParser(key)(opt);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function objectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
return JSON.parse(opt)
|
||||
return JSON.parse(opt);
|
||||
}
|
||||
|
||||
function arrayParser(opt) {
|
||||
@@ -41,12 +41,14 @@ function arrayParser(opt) {
|
||||
}
|
||||
|
||||
function moduleOrObjectParser(opt) {
|
||||
if (typeof opt == 'object') {
|
||||
if (typeof opt == 'object') {
|
||||
return opt;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(opt);
|
||||
} catch(e) { /* */ }
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
@@ -71,5 +73,5 @@ module.exports = {
|
||||
booleanParser,
|
||||
moduleOrObjectParser,
|
||||
arrayParser,
|
||||
objectParser
|
||||
objectParser,
|
||||
};
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import {
|
||||
EventEmitterMQ
|
||||
} from './Adapters/MessageQueue/EventEmitterMQ';
|
||||
import { EventEmitterMQ } from './Adapters/MessageQueue/EventEmitterMQ';
|
||||
|
||||
const ParseMessageQueue = {};
|
||||
|
||||
ParseMessageQueue.createPublisher = function(config: any): any {
|
||||
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config);
|
||||
const adapter = loadAdapter(
|
||||
config.messageQueueAdapter,
|
||||
EventEmitterMQ,
|
||||
config
|
||||
);
|
||||
if (typeof adapter.createPublisher !== 'function') {
|
||||
throw 'pubSubAdapter should have createPublisher()';
|
||||
}
|
||||
return adapter.createPublisher(config);
|
||||
}
|
||||
};
|
||||
|
||||
ParseMessageQueue.createSubscriber = function(config: any): void {
|
||||
const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config)
|
||||
const adapter = loadAdapter(
|
||||
config.messageQueueAdapter,
|
||||
EventEmitterMQ,
|
||||
config
|
||||
);
|
||||
if (typeof adapter.createSubscriber !== 'function') {
|
||||
throw 'messageQueueAdapter should have createSubscriber()';
|
||||
}
|
||||
return adapter.createSubscriber(config);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
ParseMessageQueue
|
||||
}
|
||||
export { ParseMessageQueue };
|
||||
|
||||
@@ -7,34 +7,33 @@ var batch = require('./batch'),
|
||||
Parse = require('parse/node').Parse,
|
||||
path = require('path');
|
||||
|
||||
import { ParseServerOptions,
|
||||
LiveQueryServerOptions } from './Options';
|
||||
import defaults from './defaults';
|
||||
import * as logging from './logger';
|
||||
import Config from './Config';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { ParseServerOptions, LiveQueryServerOptions } from './Options';
|
||||
import defaults from './defaults';
|
||||
import * as logging from './logger';
|
||||
import Config from './Config';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { PurgeRouter } from './Routers/PurgeRouter';
|
||||
import { AudiencesRouter } from './Routers/AudiencesRouter';
|
||||
import { AggregateRouter } from './Routers/AggregateRouter';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { CloudCodeRouter } from './Routers/CloudCodeRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { PurgeRouter } from './Routers/PurgeRouter';
|
||||
import { AudiencesRouter } from './Routers/AudiencesRouter';
|
||||
import { AggregateRouter } from './Routers/AggregateRouter';
|
||||
|
||||
import { ParseServerRESTController } from './ParseServerRESTController';
|
||||
import * as controllers from './Controllers';
|
||||
@@ -72,7 +71,7 @@ class ParseServer {
|
||||
/**
|
||||
* @constructor
|
||||
* @param {ParseServerOptions} options the parse server initialization options
|
||||
*/
|
||||
*/
|
||||
constructor(options: ParseServerOptions) {
|
||||
injectDefaults(options);
|
||||
const {
|
||||
@@ -108,7 +107,7 @@ class ParseServer {
|
||||
if (cloud) {
|
||||
addParseCloud();
|
||||
if (typeof cloud === 'function') {
|
||||
cloud(Parse)
|
||||
cloud(Parse);
|
||||
} else if (typeof cloud === 'string') {
|
||||
require(path.resolve(process.cwd(), cloud));
|
||||
} else {
|
||||
@@ -135,25 +134,33 @@ class ParseServer {
|
||||
* @static
|
||||
* Create an express app for the parse server
|
||||
* @param {Object} options let you specify the maxUploadSize when creating the express app */
|
||||
static app({maxUploadSize = '20mb', appId}) {
|
||||
static app({ maxUploadSize = '20mb', appId }) {
|
||||
// This app serves the Parse API directly.
|
||||
// It's the equivalent of https://api.parse.com/1 in the hosted Parse API.
|
||||
var api = express();
|
||||
//api.use("/apps", express.static(__dirname + "/public"));
|
||||
// File handling needs to be before default middlewares are applied
|
||||
api.use('/', middlewares.allowCrossDomain, new FilesRouter().expressRouter({
|
||||
maxUploadSize: maxUploadSize
|
||||
}));
|
||||
api.use(
|
||||
'/',
|
||||
middlewares.allowCrossDomain,
|
||||
new FilesRouter().expressRouter({
|
||||
maxUploadSize: maxUploadSize,
|
||||
})
|
||||
);
|
||||
|
||||
api.use('/health', (function(req, res) {
|
||||
api.use('/health', function(req, res) {
|
||||
res.json({
|
||||
status: 'ok'
|
||||
status: 'ok',
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressRouter());
|
||||
api.use(
|
||||
'/',
|
||||
bodyParser.urlencoded({ extended: false }),
|
||||
new PublicAPIRouter().expressRouter()
|
||||
);
|
||||
|
||||
api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize }));
|
||||
api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize }));
|
||||
api.use(middlewares.allowCrossDomain);
|
||||
api.use(middlewares.allowMethodOverride);
|
||||
api.use(middlewares.handleParseHeaders);
|
||||
@@ -167,9 +174,12 @@ class ParseServer {
|
||||
if (!process.env.TESTING) {
|
||||
//This causes tests to spew some useless warnings, so disable in test
|
||||
/* istanbul ignore next */
|
||||
process.on('uncaughtException', (err) => {
|
||||
if (err.code === "EADDRINUSE") { // user-friendly message for this common error
|
||||
process.stderr.write(`Unable to listen on port ${err.port}. The port is already in use.`);
|
||||
process.on('uncaughtException', err => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
// user-friendly message for this common error
|
||||
process.stderr.write(
|
||||
`Unable to listen on port ${err.port}. The port is already in use.`
|
||||
);
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw err;
|
||||
@@ -182,12 +192,14 @@ class ParseServer {
|
||||
});
|
||||
}
|
||||
if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') {
|
||||
Parse.CoreManager.setRESTController(ParseServerRESTController(appId, appRouter));
|
||||
Parse.CoreManager.setRESTController(
|
||||
ParseServerRESTController(appId, appRouter)
|
||||
);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
static promiseRouter({appId}) {
|
||||
static promiseRouter({ appId }) {
|
||||
const routers = [
|
||||
new ClassesRouter(),
|
||||
new UsersRouter(),
|
||||
@@ -206,7 +218,7 @@ class ParseServer {
|
||||
new HooksRouter(),
|
||||
new CloudCodeRouter(),
|
||||
new AudiencesRouter(),
|
||||
new AggregateRouter()
|
||||
new AggregateRouter(),
|
||||
];
|
||||
|
||||
const routes = routers.reduce((memo, router) => {
|
||||
@@ -225,7 +237,7 @@ class ParseServer {
|
||||
* @param {Function} callback called when the server has started
|
||||
* @returns {ParseServer} the parse server instance
|
||||
*/
|
||||
start(options: ParseServerOptions, callback: ?()=>void) {
|
||||
start(options: ParseServerOptions, callback: ?() => void) {
|
||||
const app = express();
|
||||
if (options.middleware) {
|
||||
let middleware;
|
||||
@@ -242,7 +254,10 @@ class ParseServer {
|
||||
this.server = server;
|
||||
|
||||
if (options.startLiveQueryServer || options.liveQueryServerOptions) {
|
||||
this.liveQueryServer = ParseServer.createLiveQueryServer(server, options.liveQueryServerOptions);
|
||||
this.liveQueryServer = ParseServer.createLiveQueryServer(
|
||||
server,
|
||||
options.liveQueryServerOptions
|
||||
);
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
if (!process.env.TESTING) {
|
||||
@@ -258,7 +273,7 @@ class ParseServer {
|
||||
* @param {Function} callback called when the server has started
|
||||
* @returns {ParseServer} the parse server instance
|
||||
*/
|
||||
static start(options: ParseServerOptions, callback: ?()=>void) {
|
||||
static start(options: ParseServerOptions, callback: ?() => void) {
|
||||
const parseServer = new ParseServer(options);
|
||||
return parseServer.start(options, callback);
|
||||
}
|
||||
@@ -281,25 +296,36 @@ class ParseServer {
|
||||
|
||||
static verifyServerUrl(callback) {
|
||||
// perform a health check on the serverURL value
|
||||
if(Parse.serverURL) {
|
||||
if (Parse.serverURL) {
|
||||
const request = require('request');
|
||||
request(Parse.serverURL.replace(/\/$/, "") + "/health", function (error, response, body) {
|
||||
request(Parse.serverURL.replace(/\/$/, '') + '/health', function(
|
||||
error,
|
||||
response,
|
||||
body
|
||||
) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
json = null;
|
||||
}
|
||||
if (error || response.statusCode !== 200 || !json || json && json.status !== 'ok') {
|
||||
if (
|
||||
error ||
|
||||
response.statusCode !== 200 ||
|
||||
!json ||
|
||||
(json && json.status !== 'ok')
|
||||
) {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(`\nWARNING, Unable to connect to '${Parse.serverURL}'.` +
|
||||
` Cloud code and push notifications may be unavailable!\n`);
|
||||
console.warn(
|
||||
`\nWARNING, Unable to connect to '${Parse.serverURL}'.` +
|
||||
` Cloud code and push notifications may be unavailable!\n`
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
if(callback) {
|
||||
if (callback) {
|
||||
callback(false);
|
||||
}
|
||||
} else {
|
||||
if(callback) {
|
||||
if (callback) {
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
@@ -309,13 +335,13 @@ class ParseServer {
|
||||
}
|
||||
|
||||
function addParseCloud() {
|
||||
const ParseCloud = require("./cloud-code/Parse.Cloud");
|
||||
const ParseCloud = require('./cloud-code/Parse.Cloud');
|
||||
Object.assign(Parse.Cloud, ParseCloud);
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
function injectDefaults(options: ParseServerOptions) {
|
||||
Object.keys(defaults).forEach((key) => {
|
||||
Object.keys(defaults).forEach(key => {
|
||||
if (!options.hasOwnProperty(key)) {
|
||||
options[key] = defaults[key];
|
||||
}
|
||||
@@ -325,15 +351,20 @@ function injectDefaults(options: ParseServerOptions) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
options.userSensitiveFields = Array.from(new Set(options.userSensitiveFields.concat(
|
||||
defaults.userSensitiveFields,
|
||||
options.userSensitiveFields
|
||||
)));
|
||||
options.userSensitiveFields = Array.from(
|
||||
new Set(
|
||||
options.userSensitiveFields.concat(
|
||||
defaults.userSensitiveFields,
|
||||
options.userSensitiveFields
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
options.masterKeyIps = Array.from(new Set(options.masterKeyIps.concat(
|
||||
defaults.masterKeyIps,
|
||||
options.masterKeyIps
|
||||
)));
|
||||
options.masterKeyIps = Array.from(
|
||||
new Set(
|
||||
options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Those can't be tested as it requires a subprocess
|
||||
@@ -343,7 +374,7 @@ function configureListeners(parseServer) {
|
||||
const sockets = {};
|
||||
/* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642)
|
||||
This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */
|
||||
server.on('connection', (socket) => {
|
||||
server.on('connection', socket => {
|
||||
const socketId = socket.remoteAddress + ':' + socket.remotePort;
|
||||
sockets[socketId] = socket;
|
||||
socket.on('close', () => {
|
||||
@@ -355,9 +386,11 @@ function configureListeners(parseServer) {
|
||||
for (const socketId in sockets) {
|
||||
try {
|
||||
sockets[socketId].destroy();
|
||||
} catch (e) { /* */ }
|
||||
} catch (e) {
|
||||
/* */
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShutdown = function() {
|
||||
process.stdout.write('Termination signal received. Shutting down.');
|
||||
|
||||
@@ -14,20 +14,22 @@ function getSessionToken(options) {
|
||||
function getAuth(options = {}, config) {
|
||||
const installationId = options.installationId || 'cloud';
|
||||
if (options.useMasterKey) {
|
||||
return Promise.resolve(new Auth.Auth({config, isMaster: true, installationId }));
|
||||
return Promise.resolve(
|
||||
new Auth.Auth({ config, isMaster: true, installationId })
|
||||
);
|
||||
}
|
||||
return getSessionToken(options).then((sessionToken) => {
|
||||
return getSessionToken(options).then(sessionToken => {
|
||||
if (sessionToken) {
|
||||
options.sessionToken = sessionToken;
|
||||
return Auth.getAuthForSessionToken({
|
||||
config,
|
||||
sessionToken: sessionToken,
|
||||
installationId
|
||||
installationId,
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve(new Auth.Auth({ config, installationId }));
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
function ParseServerRESTController(applicationId, router) {
|
||||
@@ -41,17 +43,27 @@ function ParseServerRESTController(applicationId, router) {
|
||||
path = path.slice(serverURL.path.length, path.length);
|
||||
}
|
||||
|
||||
if (path[0] !== "/") {
|
||||
path = "/" + path;
|
||||
if (path[0] !== '/') {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
if (path === '/batch') {
|
||||
const promises = data.requests.map((request) => {
|
||||
return handleRequest(request.method, request.path, request.body, options).then((response) => {
|
||||
return Promise.resolve({success: response});
|
||||
}, (error) => {
|
||||
return Promise.resolve({error: {code: error.code, error: error.message}});
|
||||
});
|
||||
const promises = data.requests.map(request => {
|
||||
return handleRequest(
|
||||
request.method,
|
||||
request.path,
|
||||
request.body,
|
||||
options
|
||||
).then(
|
||||
response => {
|
||||
return Promise.resolve({ success: response });
|
||||
},
|
||||
error => {
|
||||
return Promise.resolve({
|
||||
error: { code: error.code, error: error.message },
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
@@ -62,37 +74,44 @@ function ParseServerRESTController(applicationId, router) {
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
getAuth(options, config).then((auth) => {
|
||||
getAuth(options, config).then(auth => {
|
||||
const request = {
|
||||
body: data,
|
||||
config,
|
||||
auth,
|
||||
info: {
|
||||
applicationId: applicationId,
|
||||
sessionToken: options.sessionToken
|
||||
sessionToken: options.sessionToken,
|
||||
},
|
||||
query
|
||||
query,
|
||||
};
|
||||
return Promise.resolve().then(() => {
|
||||
return router.tryRouteRequest(method, path, request);
|
||||
}).then((response) => {
|
||||
resolve(response.response, response.status, response);
|
||||
}, (err) => {
|
||||
if (err instanceof Parse.Error &&
|
||||
err.code == Parse.Error.INVALID_JSON &&
|
||||
err.message == `cannot route ${method} ${path}`) {
|
||||
RESTController.request.apply(null, args).then(resolve, reject);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return router.tryRouteRequest(method, path, request);
|
||||
})
|
||||
.then(
|
||||
response => {
|
||||
resolve(response.response, response.status, response);
|
||||
},
|
||||
err => {
|
||||
if (
|
||||
err instanceof Parse.Error &&
|
||||
err.code == Parse.Error.INVALID_JSON &&
|
||||
err.message == `cannot route ${method} ${path}`
|
||||
) {
|
||||
RESTController.request.apply(null, args).then(resolve, reject);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
return {
|
||||
request: handleRequest,
|
||||
ajax: RESTController.ajax
|
||||
ajax: RESTController.ajax,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
// themselves use our routing information, without disturbing express
|
||||
// components that external developers may be modifying.
|
||||
|
||||
import Parse from 'parse/node';
|
||||
import express from 'express';
|
||||
import log from './logger';
|
||||
import {inspect} from 'util';
|
||||
import Parse from 'parse/node';
|
||||
import express from 'express';
|
||||
import log from './logger';
|
||||
import { inspect } from 'util';
|
||||
const Layer = require('express/lib/router/layer');
|
||||
|
||||
function validateParameter(key, value) {
|
||||
@@ -25,7 +25,6 @@ function validateParameter(key, value) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class PromiseRouter {
|
||||
// Each entry should be an object with:
|
||||
// path: the path to route, in express format
|
||||
@@ -54,14 +53,14 @@ export default class PromiseRouter {
|
||||
}
|
||||
|
||||
route(method, path, ...handlers) {
|
||||
switch(method) {
|
||||
case 'POST':
|
||||
case 'GET':
|
||||
case 'PUT':
|
||||
case 'DELETE':
|
||||
break;
|
||||
default:
|
||||
throw 'cannot route method: ' + method;
|
||||
switch (method) {
|
||||
case 'POST':
|
||||
case 'GET':
|
||||
case 'PUT':
|
||||
case 'DELETE':
|
||||
break;
|
||||
default:
|
||||
throw 'cannot route method: ' + method;
|
||||
}
|
||||
|
||||
let handler = handlers[0];
|
||||
@@ -73,14 +72,14 @@ export default class PromiseRouter {
|
||||
return handler(req);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.routes.push({
|
||||
path: path,
|
||||
method: method,
|
||||
handler: handler,
|
||||
layer: new Layer(path, null, handler)
|
||||
layer: new Layer(path, null, handler),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,17 +96,17 @@ export default class PromiseRouter {
|
||||
const match = layer.match(path);
|
||||
if (match) {
|
||||
const params = layer.params;
|
||||
Object.keys(params).forEach((key) => {
|
||||
Object.keys(params).forEach(key => {
|
||||
params[key] = validateParameter(key, params[key]);
|
||||
});
|
||||
return {params: params, handler: route.handler};
|
||||
return { params: params, handler: route.handler };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mount the routes on this router onto an express app (or express router)
|
||||
mountOnto(expressApp) {
|
||||
this.routes.forEach((route) => {
|
||||
this.routes.forEach(route => {
|
||||
const method = route.method.toLowerCase();
|
||||
const handler = makeExpressHandler(this.appId, route.handler);
|
||||
expressApp[method].call(expressApp, route.path, handler);
|
||||
@@ -124,7 +123,8 @@ export default class PromiseRouter {
|
||||
if (!match) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'cannot route ' + method + ' ' + path);
|
||||
'cannot route ' + method + ' ' + path
|
||||
);
|
||||
}
|
||||
request.params = match.params;
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -148,55 +148,63 @@ function makeExpressHandler(appId, promiseHandler) {
|
||||
method,
|
||||
url,
|
||||
headers,
|
||||
body
|
||||
body,
|
||||
});
|
||||
promiseHandler(req).then((result) => {
|
||||
if (!result.response && !result.location && !result.text) {
|
||||
log.error('the handler did not include a "response" or a "location" field');
|
||||
throw 'control should not get here';
|
||||
}
|
||||
promiseHandler(req)
|
||||
.then(
|
||||
result => {
|
||||
if (!result.response && !result.location && !result.text) {
|
||||
log.error(
|
||||
'the handler did not include a "response" or a "location" field'
|
||||
);
|
||||
throw 'control should not get here';
|
||||
}
|
||||
|
||||
log.logResponse({ method, url, result });
|
||||
log.logResponse({ method, url, result });
|
||||
|
||||
var status = result.status || 200;
|
||||
res.status(status);
|
||||
var status = result.status || 200;
|
||||
res.status(status);
|
||||
|
||||
if (result.text) {
|
||||
res.send(result.text);
|
||||
return;
|
||||
}
|
||||
if (result.text) {
|
||||
res.send(result.text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.location) {
|
||||
res.set('Location', result.location);
|
||||
// Override the default expressjs response
|
||||
// as it double encodes %encoded chars in URL
|
||||
if (!result.response) {
|
||||
res.send('Found. Redirecting to ' + result.location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (result.headers) {
|
||||
Object.keys(result.headers).forEach((header) => {
|
||||
res.set(header, result.headers[header]);
|
||||
})
|
||||
}
|
||||
res.json(result.response);
|
||||
}, (error) => next(error)).catch((e) => {
|
||||
log.error(`Error generating response. ${inspect(e)}`, {error: e});
|
||||
next(e);
|
||||
});
|
||||
if (result.location) {
|
||||
res.set('Location', result.location);
|
||||
// Override the default expressjs response
|
||||
// as it double encodes %encoded chars in URL
|
||||
if (!result.response) {
|
||||
res.send('Found. Redirecting to ' + result.location);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (result.headers) {
|
||||
Object.keys(result.headers).forEach(header => {
|
||||
res.set(header, result.headers[header]);
|
||||
});
|
||||
}
|
||||
res.json(result.response);
|
||||
},
|
||||
error => next(error)
|
||||
)
|
||||
.catch(e => {
|
||||
log.error(`Error generating response. ${inspect(e)}`, { error: e });
|
||||
next(e);
|
||||
});
|
||||
} catch (e) {
|
||||
log.error(`Error handling request: ${inspect(e)}`, {error: e});
|
||||
log.error(`Error handling request: ${inspect(e)}`, { error: e });
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function maskSensitiveUrl(req) {
|
||||
let maskUrl = req.originalUrl.toString();
|
||||
const shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login')
|
||||
&& !req.originalUrl.includes('classes');
|
||||
const shouldMaskUrl =
|
||||
req.method === 'GET' &&
|
||||
req.originalUrl.includes('/login') &&
|
||||
!req.originalUrl.includes('classes');
|
||||
if (shouldMaskUrl) {
|
||||
maskUrl = log.maskSensitiveUrl(maskUrl);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import rest from '../rest';
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import rest from '../rest';
|
||||
import { applyDeviceTokenExists } from './utils';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
@@ -30,33 +30,39 @@ export class PushQueue {
|
||||
|
||||
// Order by objectId so no impact on the DB
|
||||
const order = 'objectId';
|
||||
return Promise.resolve().then(() => {
|
||||
return rest.find(config,
|
||||
auth,
|
||||
'_Installation',
|
||||
where,
|
||||
{limit: 0, count: true});
|
||||
}).then(({results, count}) => {
|
||||
if (!results || count == 0) {
|
||||
return pushStatus.complete();
|
||||
}
|
||||
pushStatus.setRunning(Math.ceil(count / limit));
|
||||
let skip = 0;
|
||||
while (skip < count) {
|
||||
const query = { where,
|
||||
limit,
|
||||
skip,
|
||||
order };
|
||||
|
||||
const pushWorkItem = {
|
||||
body,
|
||||
query,
|
||||
pushStatus: { objectId: pushStatus.objectId },
|
||||
applicationId: config.applicationId
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return rest.find(config, auth, '_Installation', where, {
|
||||
limit: 0,
|
||||
count: true,
|
||||
});
|
||||
})
|
||||
.then(({ results, count }) => {
|
||||
if (!results || count == 0) {
|
||||
return pushStatus.complete();
|
||||
}
|
||||
this.parsePublisher.publish(this.channel, JSON.stringify(pushWorkItem));
|
||||
skip += limit;
|
||||
}
|
||||
});
|
||||
pushStatus.setRunning(Math.ceil(count / limit));
|
||||
let skip = 0;
|
||||
while (skip < count) {
|
||||
const query = {
|
||||
where,
|
||||
limit,
|
||||
skip,
|
||||
order,
|
||||
};
|
||||
|
||||
const pushWorkItem = {
|
||||
body,
|
||||
query,
|
||||
pushStatus: { objectId: pushStatus.objectId },
|
||||
applicationId: config.applicationId,
|
||||
};
|
||||
this.parsePublisher.publish(
|
||||
this.channel,
|
||||
JSON.stringify(pushWorkItem)
|
||||
);
|
||||
skip += limit;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// @flow
|
||||
// @flow-disable-next
|
||||
import deepcopy from 'deepcopy';
|
||||
import AdaptableController from '../Controllers/AdaptableController';
|
||||
import { master } from '../Auth';
|
||||
import Config from '../Config';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import rest from '../rest';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
import * as utils from './utils';
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import { PushQueue } from './PushQueue';
|
||||
import logger from '../logger';
|
||||
import deepcopy from 'deepcopy';
|
||||
import AdaptableController from '../Controllers/AdaptableController';
|
||||
import { master } from '../Auth';
|
||||
import Config from '../Config';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import rest from '../rest';
|
||||
import { pushStatusHandler } from '../StatusHandler';
|
||||
import * as utils from './utils';
|
||||
import { ParseMessageQueue } from '../ParseMessageQueue';
|
||||
import { PushQueue } from './PushQueue';
|
||||
import logger from '../logger';
|
||||
|
||||
function groupByBadge(installations) {
|
||||
return installations.reduce((map, installation) => {
|
||||
@@ -48,15 +48,23 @@ export class PushWorker {
|
||||
const where = utils.applyDeviceTokenExists(query.where);
|
||||
delete query.where;
|
||||
pushStatus = pushStatusHandler(config, pushStatus.objectId);
|
||||
return rest.find(config, auth, '_Installation', where, query).then(({results}) => {
|
||||
if (results.length == 0) {
|
||||
return;
|
||||
}
|
||||
return this.sendToAdapter(body, results, pushStatus, config, UTCOffset);
|
||||
});
|
||||
return rest
|
||||
.find(config, auth, '_Installation', where, query)
|
||||
.then(({ results }) => {
|
||||
if (results.length == 0) {
|
||||
return;
|
||||
}
|
||||
return this.sendToAdapter(body, results, pushStatus, config, UTCOffset);
|
||||
});
|
||||
}
|
||||
|
||||
sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config, UTCOffset: ?any): Promise<*> {
|
||||
sendToAdapter(
|
||||
body: any,
|
||||
installations: any[],
|
||||
pushStatus: any,
|
||||
config: Config,
|
||||
UTCOffset: ?any
|
||||
): Promise<*> {
|
||||
// Check if we have locales in the push body
|
||||
const locales = utils.getLocalesFromPush(body);
|
||||
if (locales.length > 0) {
|
||||
@@ -64,31 +72,48 @@ export class PushWorker {
|
||||
const bodiesPerLocales = utils.bodiesPerLocales(body, locales);
|
||||
|
||||
// Group installations on the specified locales (en, fr, default etc...)
|
||||
const grouppedInstallations = utils.groupByLocaleIdentifier(installations, locales);
|
||||
const promises = Object.keys(grouppedInstallations).map((locale) => {
|
||||
const grouppedInstallations = utils.groupByLocaleIdentifier(
|
||||
installations,
|
||||
locales
|
||||
);
|
||||
const promises = Object.keys(grouppedInstallations).map(locale => {
|
||||
const installations = grouppedInstallations[locale];
|
||||
const body = bodiesPerLocales[locale];
|
||||
return this.sendToAdapter(body, installations, pushStatus, config, UTCOffset);
|
||||
return this.sendToAdapter(
|
||||
body,
|
||||
installations,
|
||||
pushStatus,
|
||||
config,
|
||||
UTCOffset
|
||||
);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
if (!utils.isPushIncrementing(body)) {
|
||||
logger.verbose(`Sending push to ${installations.length}`);
|
||||
return this.adapter.send(body, installations, pushStatus.objectId).then((results) => {
|
||||
return pushStatus.trackSent(results, UTCOffset).then(() => results);
|
||||
});
|
||||
return this.adapter
|
||||
.send(body, installations, pushStatus.objectId)
|
||||
.then(results => {
|
||||
return pushStatus.trackSent(results, UTCOffset).then(() => results);
|
||||
});
|
||||
}
|
||||
|
||||
// Collect the badges to reduce the # of calls
|
||||
const badgeInstallationsMap = groupByBadge(installations);
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
const promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
const promises = Object.keys(badgeInstallationsMap).map(badge => {
|
||||
const payload = deepcopy(body);
|
||||
payload.data.badge = parseInt(badge);
|
||||
const installations = badgeInstallationsMap[badge];
|
||||
return this.sendToAdapter(payload, installations, pushStatus, config, UTCOffset);
|
||||
return this.sendToAdapter(
|
||||
payload,
|
||||
installations,
|
||||
pushStatus,
|
||||
config,
|
||||
UTCOffset
|
||||
);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Parse from 'parse/node';
|
||||
import Parse from 'parse/node';
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
export function isPushIncrementing(body) {
|
||||
@@ -7,12 +7,16 @@ export function isPushIncrementing(body) {
|
||||
}
|
||||
|
||||
const badge = body.data.badge;
|
||||
if (typeof badge == 'string' && badge.toLowerCase() == "increment") {
|
||||
if (typeof badge == 'string' && badge.toLowerCase() == 'increment') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return typeof badge == 'object' && typeof badge.__op == 'string' &&
|
||||
badge.__op.toLowerCase() == "increment" && Number(badge.amount);
|
||||
return (
|
||||
typeof badge == 'object' &&
|
||||
typeof badge.__op == 'string' &&
|
||||
badge.__op.toLowerCase() == 'increment' &&
|
||||
Number(badge.amount)
|
||||
);
|
||||
}
|
||||
|
||||
const localizableKeys = ['alert', 'title'];
|
||||
@@ -22,14 +26,18 @@ export function getLocalesFromPush(body) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
return [...new Set(Object.keys(data).reduce((memo, key) => {
|
||||
localizableKeys.forEach((localizableKey) => {
|
||||
if (key.indexOf(`${localizableKey}-`) == 0) {
|
||||
memo.push(key.slice(localizableKey.length + 1));
|
||||
}
|
||||
});
|
||||
return memo;
|
||||
}, []))];
|
||||
return [
|
||||
...new Set(
|
||||
Object.keys(data).reduce((memo, key) => {
|
||||
localizableKeys.forEach(localizableKey => {
|
||||
if (key.indexOf(`${localizableKey}-`) == 0) {
|
||||
memo.push(key.slice(localizableKey.length + 1));
|
||||
}
|
||||
});
|
||||
return memo;
|
||||
}, [])
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
export function transformPushBodyForLocale(body, locale) {
|
||||
@@ -38,7 +46,7 @@ export function transformPushBodyForLocale(body, locale) {
|
||||
return body;
|
||||
}
|
||||
body = deepcopy(body);
|
||||
localizableKeys.forEach((key) => {
|
||||
localizableKeys.forEach(key => {
|
||||
const localeValue = body.data[`${key}-${locale}`];
|
||||
if (localeValue) {
|
||||
body.data[key] = localeValue;
|
||||
@@ -48,9 +56,11 @@ export function transformPushBodyForLocale(body, locale) {
|
||||
}
|
||||
|
||||
export function stripLocalesFromBody(body) {
|
||||
if (!body.data) { return body; }
|
||||
Object.keys(body.data).forEach((key) => {
|
||||
localizableKeys.forEach((localizableKey) => {
|
||||
if (!body.data) {
|
||||
return body;
|
||||
}
|
||||
Object.keys(body.data).forEach(key => {
|
||||
localizableKeys.forEach(localizableKey => {
|
||||
if (key.indexOf(`${localizableKey}-`) == 0) {
|
||||
delete body.data[key];
|
||||
}
|
||||
@@ -71,23 +81,29 @@ export function bodiesPerLocales(body, locales = []) {
|
||||
}
|
||||
|
||||
export function groupByLocaleIdentifier(installations, locales = []) {
|
||||
return installations.reduce((map, installation) => {
|
||||
let added = false;
|
||||
locales.forEach((locale) => {
|
||||
if (added) {
|
||||
return;
|
||||
return installations.reduce(
|
||||
(map, installation) => {
|
||||
let added = false;
|
||||
locales.forEach(locale => {
|
||||
if (added) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
installation.localeIdentifier &&
|
||||
installation.localeIdentifier.indexOf(locale) === 0
|
||||
) {
|
||||
added = true;
|
||||
map[locale] = map[locale] || [];
|
||||
map[locale].push(installation);
|
||||
}
|
||||
});
|
||||
if (!added) {
|
||||
map.default.push(installation);
|
||||
}
|
||||
if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) === 0) {
|
||||
added = true;
|
||||
map[locale] = map[locale] || [];
|
||||
map[locale].push(installation);
|
||||
}
|
||||
});
|
||||
if (!added) {
|
||||
map.default.push(installation);
|
||||
}
|
||||
return map;
|
||||
}, {default: []});
|
||||
return map;
|
||||
},
|
||||
{ default: [] }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,8 +122,10 @@ export function validatePushType(where = {}, validPushTypes = []) {
|
||||
for (var i = 0; i < deviceTypes.length; i++) {
|
||||
var deviceType = deviceTypes[i];
|
||||
if (validPushTypes.indexOf(deviceType) < 0) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
deviceType + ' is not supported push type.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
deviceType + ' is not supported push type.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +133,7 @@ export function validatePushType(where = {}, validPushTypes = []) {
|
||||
export function applyDeviceTokenExists(where) {
|
||||
where = deepcopy(where);
|
||||
if (!where.hasOwnProperty('deviceToken')) {
|
||||
where['deviceToken'] = {'$exists': true};
|
||||
where['deviceToken'] = { $exists: true };
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
545
src/RestQuery.js
545
src/RestQuery.js
@@ -14,8 +14,14 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL'];
|
||||
// include
|
||||
// keys
|
||||
// redirectClassNameForKey
|
||||
function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, clientSDK) {
|
||||
|
||||
function RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
restWhere = {},
|
||||
restOptions = {},
|
||||
clientSDK
|
||||
) {
|
||||
this.config = config;
|
||||
this.auth = auth;
|
||||
this.className = className;
|
||||
@@ -29,17 +35,22 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
if (!this.auth.isMaster) {
|
||||
if (this.className == '_Session') {
|
||||
if (!this.auth.user) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Invalid session token');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_SESSION_TOKEN,
|
||||
'Invalid session token'
|
||||
);
|
||||
}
|
||||
this.restWhere = {
|
||||
'$and': [this.restWhere, {
|
||||
'user': {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.auth.user.id
|
||||
}
|
||||
}]
|
||||
$and: [
|
||||
this.restWhere,
|
||||
{
|
||||
user: {
|
||||
__type: 'Pointer',
|
||||
className: '_User',
|
||||
objectId: this.auth.user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -58,14 +69,18 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
// If we have keys, we probably want to force some includes (n-1 level)
|
||||
// See issue: https://github.com/parse-community/parse-server/issues/3185
|
||||
if (restOptions.hasOwnProperty('keys')) {
|
||||
const keysForInclude = restOptions.keys.split(',').filter((key) => {
|
||||
// At least 2 components
|
||||
return key.split(".").length > 1;
|
||||
}).map((key) => {
|
||||
// Slice the last component (a.b.c -> a.b)
|
||||
// Otherwise we'll include one level too much.
|
||||
return key.slice(0, key.lastIndexOf("."));
|
||||
}).join(',');
|
||||
const keysForInclude = restOptions.keys
|
||||
.split(',')
|
||||
.filter(key => {
|
||||
// At least 2 components
|
||||
return key.split('.').length > 1;
|
||||
})
|
||||
.map(key => {
|
||||
// Slice the last component (a.b.c -> a.b)
|
||||
// Otherwise we'll include one level too much.
|
||||
return key.slice(0, key.lastIndexOf('.'));
|
||||
})
|
||||
.join(',');
|
||||
|
||||
// Concat the possibly present include string with the one from the keys
|
||||
// Dedup / sorting is handle in 'include' case.
|
||||
@@ -73,79 +88,83 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
if (!restOptions.include || restOptions.include.length == 0) {
|
||||
restOptions.include = keysForInclude;
|
||||
} else {
|
||||
restOptions.include += "," + keysForInclude;
|
||||
restOptions.include += ',' + keysForInclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var option in restOptions) {
|
||||
switch(option) {
|
||||
case 'keys': {
|
||||
const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys);
|
||||
this.keys = Array.from(new Set(keys));
|
||||
break;
|
||||
}
|
||||
case 'count':
|
||||
this.doCount = true;
|
||||
break;
|
||||
case 'includeAll':
|
||||
this.includeAll = true;
|
||||
break;
|
||||
case 'distinct':
|
||||
case 'pipeline':
|
||||
case 'skip':
|
||||
case 'limit':
|
||||
case 'readPreference':
|
||||
this.findOptions[option] = restOptions[option];
|
||||
break;
|
||||
case 'order':
|
||||
var fields = restOptions.order.split(',');
|
||||
this.findOptions.sort = fields.reduce((sortMap, field) => {
|
||||
field = field.trim();
|
||||
if (field === '$score') {
|
||||
sortMap.score = {$meta: 'textScore'};
|
||||
} else if (field[0] == '-') {
|
||||
sortMap[field.slice(1)] = -1;
|
||||
} else {
|
||||
sortMap[field] = 1;
|
||||
}
|
||||
return sortMap;
|
||||
}, {});
|
||||
break;
|
||||
case 'include': {
|
||||
const paths = restOptions.include.split(',');
|
||||
if (paths.includes('*')) {
|
||||
this.includeAll = true;
|
||||
switch (option) {
|
||||
case 'keys': {
|
||||
const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys);
|
||||
this.keys = Array.from(new Set(keys));
|
||||
break;
|
||||
}
|
||||
// Load the existing includes (from keys)
|
||||
const pathSet = paths.reduce((memo, path) => {
|
||||
// Split each paths on . (a.b.c -> [a,b,c])
|
||||
// reduce to create all paths
|
||||
// ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true})
|
||||
return path.split('.').reduce((memo, path, index, parts) => {
|
||||
memo[parts.slice(0, index + 1).join('.')] = true;
|
||||
return memo;
|
||||
}, memo);
|
||||
}, {});
|
||||
case 'count':
|
||||
this.doCount = true;
|
||||
break;
|
||||
case 'includeAll':
|
||||
this.includeAll = true;
|
||||
break;
|
||||
case 'distinct':
|
||||
case 'pipeline':
|
||||
case 'skip':
|
||||
case 'limit':
|
||||
case 'readPreference':
|
||||
this.findOptions[option] = restOptions[option];
|
||||
break;
|
||||
case 'order':
|
||||
var fields = restOptions.order.split(',');
|
||||
this.findOptions.sort = fields.reduce((sortMap, field) => {
|
||||
field = field.trim();
|
||||
if (field === '$score') {
|
||||
sortMap.score = { $meta: 'textScore' };
|
||||
} else if (field[0] == '-') {
|
||||
sortMap[field.slice(1)] = -1;
|
||||
} else {
|
||||
sortMap[field] = 1;
|
||||
}
|
||||
return sortMap;
|
||||
}, {});
|
||||
break;
|
||||
case 'include': {
|
||||
const paths = restOptions.include.split(',');
|
||||
if (paths.includes('*')) {
|
||||
this.includeAll = true;
|
||||
break;
|
||||
}
|
||||
// Load the existing includes (from keys)
|
||||
const pathSet = paths.reduce((memo, path) => {
|
||||
// Split each paths on . (a.b.c -> [a,b,c])
|
||||
// reduce to create all paths
|
||||
// ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true})
|
||||
return path.split('.').reduce((memo, path, index, parts) => {
|
||||
memo[parts.slice(0, index + 1).join('.')] = true;
|
||||
return memo;
|
||||
}, memo);
|
||||
}, {});
|
||||
|
||||
this.include = Object.keys(pathSet).map((s) => {
|
||||
return s.split('.');
|
||||
}).sort((a, b) => {
|
||||
return a.length - b.length; // Sort by number of components
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'redirectClassNameForKey':
|
||||
this.redirectKey = restOptions.redirectClassNameForKey;
|
||||
this.redirectClassName = null;
|
||||
break;
|
||||
case 'includeReadPreference':
|
||||
case 'subqueryReadPreference':
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
||||
'bad option: ' + option);
|
||||
this.include = Object.keys(pathSet)
|
||||
.map(s => {
|
||||
return s.split('.');
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.length - b.length; // Sort by number of components
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'redirectClassNameForKey':
|
||||
this.redirectKey = restOptions.redirectClassNameForKey;
|
||||
this.redirectClassName = null;
|
||||
break;
|
||||
case 'includeReadPreference':
|
||||
case 'subqueryReadPreference':
|
||||
break;
|
||||
default:
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'bad option: ' + option
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,48 +175,63 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl
|
||||
// 'results' and 'count'.
|
||||
// TODO: consolidate the replaceX functions
|
||||
RestQuery.prototype.execute = function(executeOptions) {
|
||||
return Promise.resolve().then(() => {
|
||||
return this.buildRestWhere();
|
||||
}).then(() => {
|
||||
return this.handleIncludeAll();
|
||||
}).then(() => {
|
||||
return this.runFind(executeOptions);
|
||||
}).then(() => {
|
||||
return this.runCount();
|
||||
}).then(() => {
|
||||
return this.handleInclude();
|
||||
}).then(() => {
|
||||
return this.runAfterFindTrigger();
|
||||
}).then(() => {
|
||||
return this.response;
|
||||
});
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.buildRestWhere();
|
||||
})
|
||||
.then(() => {
|
||||
return this.handleIncludeAll();
|
||||
})
|
||||
.then(() => {
|
||||
return this.runFind(executeOptions);
|
||||
})
|
||||
.then(() => {
|
||||
return this.runCount();
|
||||
})
|
||||
.then(() => {
|
||||
return this.handleInclude();
|
||||
})
|
||||
.then(() => {
|
||||
return this.runAfterFindTrigger();
|
||||
})
|
||||
.then(() => {
|
||||
return this.response;
|
||||
});
|
||||
};
|
||||
|
||||
RestQuery.prototype.buildRestWhere = function() {
|
||||
return Promise.resolve().then(() => {
|
||||
return this.getUserAndRoleACL();
|
||||
}).then(() => {
|
||||
return this.redirectClassNameForKey();
|
||||
}).then(() => {
|
||||
return this.validateClientClassCreation();
|
||||
}).then(() => {
|
||||
return this.replaceSelect();
|
||||
}).then(() => {
|
||||
return this.replaceDontSelect();
|
||||
}).then(() => {
|
||||
return this.replaceInQuery();
|
||||
}).then(() => {
|
||||
return this.replaceNotInQuery();
|
||||
}).then(() => {
|
||||
return this.replaceEquality();
|
||||
});
|
||||
}
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return this.getUserAndRoleACL();
|
||||
})
|
||||
.then(() => {
|
||||
return this.redirectClassNameForKey();
|
||||
})
|
||||
.then(() => {
|
||||
return this.validateClientClassCreation();
|
||||
})
|
||||
.then(() => {
|
||||
return this.replaceSelect();
|
||||
})
|
||||
.then(() => {
|
||||
return this.replaceDontSelect();
|
||||
})
|
||||
.then(() => {
|
||||
return this.replaceInQuery();
|
||||
})
|
||||
.then(() => {
|
||||
return this.replaceNotInQuery();
|
||||
})
|
||||
.then(() => {
|
||||
return this.replaceEquality();
|
||||
});
|
||||
};
|
||||
|
||||
// Marks the query for a write attempt, so we read the proper ACL (write instead of read)
|
||||
RestQuery.prototype.forWrite = function() {
|
||||
this.isWrite = true;
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
// Uses the Auth object to get the list of roles, adds the user id
|
||||
RestQuery.prototype.getUserAndRoleACL = function() {
|
||||
@@ -208,8 +242,10 @@ RestQuery.prototype.getUserAndRoleACL = function() {
|
||||
this.findOptions.acl = ['*'];
|
||||
|
||||
if (this.auth.user) {
|
||||
return this.auth.getUserRoles().then((roles) => {
|
||||
this.findOptions.acl = this.findOptions.acl.concat(roles, [this.auth.user.id]);
|
||||
return this.auth.getUserRoles().then(roles => {
|
||||
this.findOptions.acl = this.findOptions.acl.concat(roles, [
|
||||
this.auth.user.id,
|
||||
]);
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
@@ -225,8 +261,9 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
}
|
||||
|
||||
// We need to change the class name based on the schema
|
||||
return this.config.database.redirectClassNameForKey(this.className, this.redirectKey)
|
||||
.then((newClassName) => {
|
||||
return this.config.database
|
||||
.redirectClassNameForKey(this.className, this.redirectKey)
|
||||
.then(newClassName => {
|
||||
this.className = newClassName;
|
||||
this.redirectClassName = newClassName;
|
||||
});
|
||||
@@ -234,15 +271,22 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& SchemaController.systemClasses.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema()
|
||||
if (
|
||||
this.config.allowClientClassCreation === false &&
|
||||
!this.auth.isMaster &&
|
||||
SchemaController.systemClasses.indexOf(this.className) === -1
|
||||
) {
|
||||
return this.config.database
|
||||
.loadSchema()
|
||||
.then(schemaController => schemaController.hasClass(this.className))
|
||||
.then(hasClass => {
|
||||
if (hasClass !== true) {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'This user is not allowed to access ' +
|
||||
'non-existent class: ' + this.className);
|
||||
'non-existent class: ' +
|
||||
this.className
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@@ -256,7 +300,7 @@ function transformInQuery(inQueryObject, className, results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
objectId: result.objectId,
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
@@ -280,12 +324,14 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
// The inQuery value must have precisely two keys - where and className
|
||||
var inQueryValue = inQueryObject['$inQuery'];
|
||||
if (!inQueryValue.where || !inQueryValue.className) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $inQuery');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $inQuery'
|
||||
);
|
||||
}
|
||||
|
||||
const additionalOptions = {
|
||||
redirectClassNameForKey: inQueryValue.redirectClassNameForKey
|
||||
redirectClassNameForKey: inQueryValue.redirectClassNameForKey,
|
||||
};
|
||||
|
||||
if (this.restOptions.subqueryReadPreference) {
|
||||
@@ -294,9 +340,13 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config, this.auth, inQueryValue.className,
|
||||
inQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
this.config,
|
||||
this.auth,
|
||||
inQueryValue.className,
|
||||
inQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
return subquery.execute().then(response => {
|
||||
transformInQuery(inQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceInQuery();
|
||||
@@ -309,7 +359,7 @@ function transformNotInQuery(notInQueryObject, className, results) {
|
||||
values.push({
|
||||
__type: 'Pointer',
|
||||
className: className,
|
||||
objectId: result.objectId
|
||||
objectId: result.objectId,
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
@@ -333,12 +383,14 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
// The notInQuery value must have precisely two keys - where and className
|
||||
var notInQueryValue = notInQueryObject['$notInQuery'];
|
||||
if (!notInQueryValue.where || !notInQueryValue.className) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $notInQuery');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $notInQuery'
|
||||
);
|
||||
}
|
||||
|
||||
const additionalOptions = {
|
||||
redirectClassNameForKey: notInQueryValue.redirectClassNameForKey
|
||||
redirectClassNameForKey: notInQueryValue.redirectClassNameForKey,
|
||||
};
|
||||
|
||||
if (this.restOptions.subqueryReadPreference) {
|
||||
@@ -347,19 +399,23 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config, this.auth, notInQueryValue.className,
|
||||
notInQueryValue.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
this.config,
|
||||
this.auth,
|
||||
notInQueryValue.className,
|
||||
notInQueryValue.where,
|
||||
additionalOptions
|
||||
);
|
||||
return subquery.execute().then(response => {
|
||||
transformNotInQuery(notInQueryObject, subquery.className, response.results);
|
||||
// Recurse to repeat
|
||||
return this.replaceNotInQuery();
|
||||
});
|
||||
};
|
||||
|
||||
const transformSelect = (selectObject, key ,objects) => {
|
||||
const transformSelect = (selectObject, key, objects) => {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(key.split('.').reduce((o,i)=>o[i], result));
|
||||
values.push(key.split('.').reduce((o, i) => o[i], result));
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
@@ -367,7 +423,7 @@ const transformSelect = (selectObject, key ,objects) => {
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Replaces a $select clause by running the subquery, if there is a
|
||||
// $select clause.
|
||||
@@ -383,17 +439,21 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
// The select value must have precisely two keys - query and key
|
||||
var selectValue = selectObject['$select'];
|
||||
// iOS SDK don't send where if not set, let it pass
|
||||
if (!selectValue.query ||
|
||||
!selectValue.key ||
|
||||
typeof selectValue.query !== 'object' ||
|
||||
!selectValue.query.className ||
|
||||
Object.keys(selectValue).length !== 2) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $select');
|
||||
if (
|
||||
!selectValue.query ||
|
||||
!selectValue.key ||
|
||||
typeof selectValue.query !== 'object' ||
|
||||
!selectValue.query.className ||
|
||||
Object.keys(selectValue).length !== 2
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $select'
|
||||
);
|
||||
}
|
||||
|
||||
const additionalOptions = {
|
||||
redirectClassNameForKey: selectValue.query.redirectClassNameForKey
|
||||
redirectClassNameForKey: selectValue.query.redirectClassNameForKey,
|
||||
};
|
||||
|
||||
if (this.restOptions.subqueryReadPreference) {
|
||||
@@ -402,19 +462,23 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config, this.auth, selectValue.query.className,
|
||||
selectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
this.config,
|
||||
this.auth,
|
||||
selectValue.query.className,
|
||||
selectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
return subquery.execute().then(response => {
|
||||
transformSelect(selectObject, selectValue.key, response.results);
|
||||
// Keep replacing $select clauses
|
||||
return this.replaceSelect();
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
var values = [];
|
||||
for (var result of objects) {
|
||||
values.push(key.split('.').reduce((o,i)=>o[i], result));
|
||||
values.push(key.split('.').reduce((o, i) => o[i], result));
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
@@ -422,7 +486,7 @@ const transformDontSelect = (dontSelectObject, key, objects) => {
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Replaces a $dontSelect clause by running the subquery, if there is a
|
||||
// $dontSelect clause.
|
||||
@@ -437,16 +501,20 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
|
||||
// The dontSelect value must have precisely two keys - query and key
|
||||
var dontSelectValue = dontSelectObject['$dontSelect'];
|
||||
if (!dontSelectValue.query ||
|
||||
!dontSelectValue.key ||
|
||||
typeof dontSelectValue.query !== 'object' ||
|
||||
!dontSelectValue.query.className ||
|
||||
Object.keys(dontSelectValue).length !== 2) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $dontSelect');
|
||||
if (
|
||||
!dontSelectValue.query ||
|
||||
!dontSelectValue.key ||
|
||||
typeof dontSelectValue.query !== 'object' ||
|
||||
!dontSelectValue.query.className ||
|
||||
Object.keys(dontSelectValue).length !== 2
|
||||
) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $dontSelect'
|
||||
);
|
||||
}
|
||||
const additionalOptions = {
|
||||
redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey
|
||||
redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey,
|
||||
};
|
||||
|
||||
if (this.restOptions.subqueryReadPreference) {
|
||||
@@ -455,16 +523,24 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
}
|
||||
|
||||
var subquery = new RestQuery(
|
||||
this.config, this.auth, dontSelectValue.query.className,
|
||||
dontSelectValue.query.where, additionalOptions);
|
||||
return subquery.execute().then((response) => {
|
||||
transformDontSelect(dontSelectObject, dontSelectValue.key, response.results);
|
||||
this.config,
|
||||
this.auth,
|
||||
dontSelectValue.query.className,
|
||||
dontSelectValue.query.where,
|
||||
additionalOptions
|
||||
);
|
||||
return subquery.execute().then(response => {
|
||||
transformDontSelect(
|
||||
dontSelectObject,
|
||||
dontSelectValue.key,
|
||||
response.results
|
||||
);
|
||||
// Keep replacing $dontSelect clauses
|
||||
return this.replaceDontSelect();
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const cleanResultOfSensitiveUserInfo = function (result, auth, config) {
|
||||
const cleanResultOfSensitiveUserInfo = function(result, auth, config) {
|
||||
delete result.password;
|
||||
|
||||
if (auth.isMaster || (auth.user && auth.user.id === result.objectId)) {
|
||||
@@ -476,9 +552,9 @@ const cleanResultOfSensitiveUserInfo = function (result, auth, config) {
|
||||
}
|
||||
};
|
||||
|
||||
const cleanResultAuthData = function (result) {
|
||||
const cleanResultAuthData = function(result) {
|
||||
if (result.authData) {
|
||||
Object.keys(result.authData).forEach((provider) => {
|
||||
Object.keys(result.authData).forEach(provider => {
|
||||
if (result.authData[provider] === null) {
|
||||
delete result.authData[provider];
|
||||
}
|
||||
@@ -490,7 +566,7 @@ const cleanResultAuthData = function (result) {
|
||||
}
|
||||
};
|
||||
|
||||
const replaceEqualityConstraint = (constraint) => {
|
||||
const replaceEqualityConstraint = constraint => {
|
||||
if (typeof constraint !== 'object') {
|
||||
return constraint;
|
||||
}
|
||||
@@ -507,12 +583,12 @@ const replaceEqualityConstraint = (constraint) => {
|
||||
}
|
||||
if (hasDirectConstraint && hasOperatorConstraint) {
|
||||
constraint['$eq'] = equalToObject;
|
||||
Object.keys(equalToObject).forEach((key) => {
|
||||
Object.keys(equalToObject).forEach(key => {
|
||||
delete constraint[key];
|
||||
});
|
||||
}
|
||||
return constraint;
|
||||
}
|
||||
};
|
||||
|
||||
RestQuery.prototype.replaceEquality = function() {
|
||||
if (typeof this.restWhere !== 'object') {
|
||||
@@ -521,18 +597,18 @@ RestQuery.prototype.replaceEquality = function() {
|
||||
for (const key in this.restWhere) {
|
||||
this.restWhere[key] = replaceEqualityConstraint(this.restWhere[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a promise for whether it was successful.
|
||||
// Populates this.response with an object that only has 'results'.
|
||||
RestQuery.prototype.runFind = function(options = {}) {
|
||||
if (this.findOptions.limit === 0) {
|
||||
this.response = {results: []};
|
||||
this.response = { results: [] };
|
||||
return Promise.resolve();
|
||||
}
|
||||
const findOptions = Object.assign({}, this.findOptions);
|
||||
if (this.keys) {
|
||||
findOptions.keys = this.keys.map((key) => {
|
||||
findOptions.keys = this.keys.map(key => {
|
||||
return key.split('.')[0];
|
||||
});
|
||||
}
|
||||
@@ -542,8 +618,9 @@ RestQuery.prototype.runFind = function(options = {}) {
|
||||
if (this.isWrite) {
|
||||
findOptions.isWrite = true;
|
||||
}
|
||||
return this.config.database.find(this.className, this.restWhere, findOptions)
|
||||
.then((results) => {
|
||||
return this.config.database
|
||||
.find(this.className, this.restWhere, findOptions)
|
||||
.then(results => {
|
||||
if (this.className === '_User') {
|
||||
for (var result of results) {
|
||||
cleanResultOfSensitiveUserInfo(result, this.auth, this.config);
|
||||
@@ -558,7 +635,7 @@ RestQuery.prototype.runFind = function(options = {}) {
|
||||
r.className = this.redirectClassName;
|
||||
}
|
||||
}
|
||||
this.response = {results: results};
|
||||
this.response = { results: results };
|
||||
});
|
||||
};
|
||||
|
||||
@@ -571,8 +648,9 @@ RestQuery.prototype.runCount = function() {
|
||||
this.findOptions.count = true;
|
||||
delete this.findOptions.skip;
|
||||
delete this.findOptions.limit;
|
||||
return this.config.database.find(this.className, this.restWhere, this.findOptions)
|
||||
.then((c) => {
|
||||
return this.config.database
|
||||
.find(this.className, this.restWhere, this.findOptions)
|
||||
.then(c => {
|
||||
this.response.count = c;
|
||||
});
|
||||
};
|
||||
@@ -582,13 +660,17 @@ RestQuery.prototype.handleIncludeAll = function() {
|
||||
if (!this.includeAll) {
|
||||
return;
|
||||
}
|
||||
return this.config.database.loadSchema()
|
||||
return this.config.database
|
||||
.loadSchema()
|
||||
.then(schemaController => schemaController.getOneSchema(this.className))
|
||||
.then(schema => {
|
||||
const includeFields = [];
|
||||
const keyFields = [];
|
||||
for (const field in schema.fields) {
|
||||
if (schema.fields[field].type && schema.fields[field].type === 'Pointer') {
|
||||
if (
|
||||
schema.fields[field].type &&
|
||||
schema.fields[field].type === 'Pointer'
|
||||
) {
|
||||
includeFields.push([field]);
|
||||
keyFields.push(field);
|
||||
}
|
||||
@@ -608,10 +690,15 @@ RestQuery.prototype.handleInclude = function() {
|
||||
return;
|
||||
}
|
||||
|
||||
var pathResponse = includePath(this.config, this.auth,
|
||||
this.response, this.include[0], this.restOptions);
|
||||
var pathResponse = includePath(
|
||||
this.config,
|
||||
this.auth,
|
||||
this.response,
|
||||
this.include[0],
|
||||
this.restOptions
|
||||
);
|
||||
if (pathResponse.then) {
|
||||
return pathResponse.then((newResponse) => {
|
||||
return pathResponse.then(newResponse => {
|
||||
this.response = newResponse;
|
||||
this.include = this.include.slice(1);
|
||||
return this.handleInclude();
|
||||
@@ -630,7 +717,11 @@ RestQuery.prototype.runAfterFindTrigger = function() {
|
||||
return;
|
||||
}
|
||||
// Avoid doing any setup for triggers if there is no 'afterFind' trigger for this class.
|
||||
const hasAfterFindHook = triggers.triggerExists(this.className, triggers.Types.afterFind, this.config.applicationId);
|
||||
const hasAfterFindHook = triggers.triggerExists(
|
||||
this.className,
|
||||
triggers.Types.afterFind,
|
||||
this.config.applicationId
|
||||
);
|
||||
if (!hasAfterFindHook) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -639,20 +730,28 @@ RestQuery.prototype.runAfterFindTrigger = function() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Run afterFind trigger and set the new results
|
||||
return triggers.maybeRunAfterFindTrigger(triggers.Types.afterFind, this.auth, this.className,this.response.results, this.config).then((results) => {
|
||||
// Ensure we properly set the className back
|
||||
if (this.redirectClassName) {
|
||||
this.response.results = results.map((object) => {
|
||||
if (object instanceof Parse.Object) {
|
||||
object = object.toJSON();
|
||||
}
|
||||
object.className = this.redirectClassName;
|
||||
return object;
|
||||
});
|
||||
} else {
|
||||
this.response.results = results;
|
||||
}
|
||||
});
|
||||
return triggers
|
||||
.maybeRunAfterFindTrigger(
|
||||
triggers.Types.afterFind,
|
||||
this.auth,
|
||||
this.className,
|
||||
this.response.results,
|
||||
this.config
|
||||
)
|
||||
.then(results => {
|
||||
// Ensure we properly set the className back
|
||||
if (this.redirectClassName) {
|
||||
this.response.results = results.map(object => {
|
||||
if (object instanceof Parse.Object) {
|
||||
object = object.toJSON();
|
||||
}
|
||||
object.className = this.redirectClassName;
|
||||
return object;
|
||||
});
|
||||
} else {
|
||||
this.response.results = results;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Adds included values to the response.
|
||||
@@ -698,42 +797,49 @@ function includePath(config, auth, response, path, restOptions = {}) {
|
||||
|
||||
if (restOptions.includeReadPreference) {
|
||||
includeRestOptions.readPreference = restOptions.includeReadPreference;
|
||||
includeRestOptions.includeReadPreference = restOptions.includeReadPreference;
|
||||
includeRestOptions.includeReadPreference =
|
||||
restOptions.includeReadPreference;
|
||||
}
|
||||
|
||||
const queryPromises = Object.keys(pointersHash).map((className) => {
|
||||
const queryPromises = Object.keys(pointersHash).map(className => {
|
||||
const objectIds = Array.from(pointersHash[className]);
|
||||
let where;
|
||||
if (objectIds.length === 1) {
|
||||
where = {'objectId': objectIds[0]};
|
||||
where = { objectId: objectIds[0] };
|
||||
} else {
|
||||
where = {'objectId': {'$in': objectIds}};
|
||||
where = { objectId: { $in: objectIds } };
|
||||
}
|
||||
var query = new RestQuery(config, auth, className, where, includeRestOptions);
|
||||
return query.execute({op: 'get'}).then((results) => {
|
||||
var query = new RestQuery(
|
||||
config,
|
||||
auth,
|
||||
className,
|
||||
where,
|
||||
includeRestOptions
|
||||
);
|
||||
return query.execute({ op: 'get' }).then(results => {
|
||||
results.className = className;
|
||||
return Promise.resolve(results);
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
// Get the objects for all these object ids
|
||||
return Promise.all(queryPromises).then((responses) => {
|
||||
return Promise.all(queryPromises).then(responses => {
|
||||
var replace = responses.reduce((replace, includeResponse) => {
|
||||
for (var obj of includeResponse.results) {
|
||||
obj.__type = 'Object';
|
||||
obj.className = includeResponse.className;
|
||||
|
||||
if (obj.className == "_User" && !auth.isMaster) {
|
||||
if (obj.className == '_User' && !auth.isMaster) {
|
||||
delete obj.sessionToken;
|
||||
delete obj.authData;
|
||||
}
|
||||
replace[obj.objectId] = obj;
|
||||
}
|
||||
return replace;
|
||||
}, {})
|
||||
}, {});
|
||||
|
||||
var resp = {
|
||||
results: replacePointers(response.results, path, replace)
|
||||
results: replacePointers(response.results, path, replace),
|
||||
};
|
||||
if (response.count) {
|
||||
resp.count = response.count;
|
||||
@@ -782,8 +888,9 @@ function findPointers(object, path) {
|
||||
// pointers inflated.
|
||||
function replacePointers(object, path, replace) {
|
||||
if (object instanceof Array) {
|
||||
return object.map((obj) => replacePointers(obj, path, replace))
|
||||
.filter((obj) => typeof obj !== 'undefined');
|
||||
return object
|
||||
.map(obj => replacePointers(obj, path, replace))
|
||||
.filter(obj => typeof obj !== 'undefined');
|
||||
}
|
||||
|
||||
if (typeof object !== 'object' || !object) {
|
||||
|
||||
1190
src/RestWrite.js
1190
src/RestWrite.js
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import rest from '../rest';
|
||||
import * as middleware from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import UsersRouter from './UsersRouter';
|
||||
import Parse from 'parse/node';
|
||||
import UsersRouter from './UsersRouter';
|
||||
|
||||
const BASE_KEYS = ['where', 'distinct', 'pipeline'];
|
||||
|
||||
@@ -37,9 +37,11 @@ const PIPELINE_KEYS = [
|
||||
const ALLOWED_KEYS = [...BASE_KEYS, ...PIPELINE_KEYS];
|
||||
|
||||
export class AggregateRouter extends ClassesRouter {
|
||||
|
||||
handleFind(req) {
|
||||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
const body = Object.assign(
|
||||
req.body,
|
||||
ClassesRouter.JSONFromQuery(req.query)
|
||||
);
|
||||
const options = {};
|
||||
if (body.distinct) {
|
||||
options.distinct = String(body.distinct);
|
||||
@@ -48,14 +50,23 @@ export class AggregateRouter extends ClassesRouter {
|
||||
if (typeof body.where === 'string') {
|
||||
body.where = JSON.parse(body.where);
|
||||
}
|
||||
return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK).then((response) => {
|
||||
for(const result of response.results) {
|
||||
if(typeof result === 'object') {
|
||||
UsersRouter.removeHiddenProperties(result);
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
body.where,
|
||||
options,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(response => {
|
||||
for (const result of response.results) {
|
||||
if (typeof result === 'object') {
|
||||
UsersRouter.removeHiddenProperties(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { response };
|
||||
});
|
||||
return { response };
|
||||
});
|
||||
}
|
||||
|
||||
/* Builds a pipeline from the body. Originally the body could be passed as a single object,
|
||||
@@ -87,13 +98,17 @@ export class AggregateRouter extends ClassesRouter {
|
||||
let pipeline = body.pipeline || body;
|
||||
|
||||
if (!Array.isArray(pipeline)) {
|
||||
pipeline = Object.keys(pipeline).map((key) => { return { [key]: pipeline[key] } });
|
||||
pipeline = Object.keys(pipeline).map(key => {
|
||||
return { [key]: pipeline[key] };
|
||||
});
|
||||
}
|
||||
|
||||
return pipeline.map((stage) => {
|
||||
return pipeline.map(stage => {
|
||||
const keys = Object.keys(stage);
|
||||
if (keys.length != 1) {
|
||||
throw new Error(`Pipeline stages should only have one key found ${keys.join(', ')}`);
|
||||
throw new Error(
|
||||
`Pipeline stages should only have one key found ${keys.join(', ')}`
|
||||
);
|
||||
}
|
||||
return AggregateRouter.transformStage(keys[0], stage);
|
||||
});
|
||||
@@ -126,7 +141,14 @@ export class AggregateRouter extends ClassesRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/aggregate/:className', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); });
|
||||
this.route(
|
||||
'GET',
|
||||
'/aggregate/:className',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleFind(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,9 @@ function trackEvent(req) {
|
||||
return analyticsController.trackEvent(req);
|
||||
}
|
||||
|
||||
|
||||
export class AnalyticsRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('POST','/events/AppOpened', appOpened);
|
||||
this.route('POST','/events/:eventName', trackEvent);
|
||||
this.route('POST', '/events/AppOpened', appOpened);
|
||||
this.route('POST', '/events/:eventName', trackEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,41 +3,84 @@ import rest from '../rest';
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class AudiencesRouter extends ClassesRouter {
|
||||
|
||||
className() {
|
||||
return '_Audience';
|
||||
}
|
||||
|
||||
handleFind(req) {
|
||||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
const body = Object.assign(
|
||||
req.body,
|
||||
ClassesRouter.JSONFromQuery(req.query)
|
||||
);
|
||||
const options = ClassesRouter.optionsFromBody(body);
|
||||
|
||||
return rest.find(req.config, req.auth, '_Audience', body.where, options, req.info.clientSDK)
|
||||
.then((response) => {
|
||||
|
||||
response.results.forEach((item) => {
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_Audience',
|
||||
body.where,
|
||||
options,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(response => {
|
||||
response.results.forEach(item => {
|
||||
item.query = JSON.parse(item.query);
|
||||
});
|
||||
|
||||
return {response: response};
|
||||
return { response: response };
|
||||
});
|
||||
}
|
||||
|
||||
handleGet(req) {
|
||||
return super.handleGet(req)
|
||||
.then((data) => {
|
||||
data.response.query = JSON.parse(data.response.query);
|
||||
return super.handleGet(req).then(data => {
|
||||
data.response.query = JSON.parse(data.response.query);
|
||||
|
||||
return data;
|
||||
});
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); });
|
||||
this.route('GET','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleGet(req); });
|
||||
this.route('POST','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleCreate(req); });
|
||||
this.route('PUT','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleUpdate(req); });
|
||||
this.route('DELETE','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleDelete(req); });
|
||||
this.route(
|
||||
'GET',
|
||||
'/push_audiences',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleFind(req);
|
||||
}
|
||||
);
|
||||
this.route(
|
||||
'GET',
|
||||
'/push_audiences/:objectId',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleGet(req);
|
||||
}
|
||||
);
|
||||
this.route(
|
||||
'POST',
|
||||
'/push_audiences',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleCreate(req);
|
||||
}
|
||||
);
|
||||
this.route(
|
||||
'PUT',
|
||||
'/push_audiences/:objectId',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleUpdate(req);
|
||||
}
|
||||
);
|
||||
this.route(
|
||||
'DELETE',
|
||||
'/push_audiences/:objectId',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.handleDelete(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import _ from 'lodash';
|
||||
import Parse from 'parse/node';
|
||||
import rest from '../rest';
|
||||
import _ from 'lodash';
|
||||
import Parse from 'parse/node';
|
||||
|
||||
const ALLOWED_GET_QUERY_KEYS = ['keys', 'include'];
|
||||
|
||||
export class ClassesRouter extends PromiseRouter {
|
||||
|
||||
className(req) {
|
||||
return req.params.className;
|
||||
}
|
||||
|
||||
handleFind(req) {
|
||||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
const body = Object.assign(
|
||||
req.body,
|
||||
ClassesRouter.JSONFromQuery(req.query)
|
||||
);
|
||||
const options = ClassesRouter.optionsFromBody(body);
|
||||
if (req.config.maxLimit && (body.limit > req.config.maxLimit)) {
|
||||
if (req.config.maxLimit && body.limit > req.config.maxLimit) {
|
||||
// Silently replace the limit on the query with the max configured
|
||||
options.limit = Number(req.config.maxLimit);
|
||||
}
|
||||
@@ -25,20 +26,34 @@ export class ClassesRouter extends PromiseRouter {
|
||||
if (typeof body.where === 'string') {
|
||||
body.where = JSON.parse(body.where);
|
||||
}
|
||||
return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK)
|
||||
.then((response) => {
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
body.where,
|
||||
options,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(response => {
|
||||
return { response: response };
|
||||
});
|
||||
}
|
||||
|
||||
// Returns a promise for a {response} object.
|
||||
handleGet(req) {
|
||||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
const body = Object.assign(
|
||||
req.body,
|
||||
ClassesRouter.JSONFromQuery(req.query)
|
||||
);
|
||||
const options = {};
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
if (ALLOWED_GET_QUERY_KEYS.indexOf(key) === -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Improper encode of parameter');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
'Improper encode of parameter'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,17 +64,27 @@ export class ClassesRouter extends PromiseRouter {
|
||||
options.include = String(body.include);
|
||||
}
|
||||
|
||||
return rest.get(req.config, req.auth, this.className(req), req.params.objectId, options, req.info.clientSDK)
|
||||
.then((response) => {
|
||||
return rest
|
||||
.get(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
req.params.objectId,
|
||||
options,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(response => {
|
||||
if (!response.results || response.results.length == 0) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'
|
||||
);
|
||||
}
|
||||
|
||||
if (this.className(req) === "_User") {
|
||||
|
||||
if (this.className(req) === '_User') {
|
||||
delete response.results[0].sessionToken;
|
||||
|
||||
const user = response.results[0];
|
||||
const user = response.results[0];
|
||||
|
||||
if (req.auth.user && user.objectId == req.auth.user.id) {
|
||||
// Force the session token
|
||||
@@ -71,18 +96,38 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
handleCreate(req) {
|
||||
return rest.create(req.config, req.auth, this.className(req), req.body, req.info.clientSDK);
|
||||
return rest.create(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
req.body,
|
||||
req.info.clientSDK
|
||||
);
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
const where = { objectId: req.params.objectId }
|
||||
return rest.update(req.config, req.auth, this.className(req), where, req.body, req.info.clientSDK);
|
||||
const where = { objectId: req.params.objectId };
|
||||
return rest.update(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
where,
|
||||
req.body,
|
||||
req.info.clientSDK
|
||||
);
|
||||
}
|
||||
|
||||
handleDelete(req) {
|
||||
return rest.del(req.config, req.auth, this.className(req), req.params.objectId, req.info.clientSDK)
|
||||
return rest
|
||||
.del(
|
||||
req.config,
|
||||
req.auth,
|
||||
this.className(req),
|
||||
req.params.objectId,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(() => {
|
||||
return {response: {}};
|
||||
return { response: {} };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,16 +140,28 @@ export class ClassesRouter extends PromiseRouter {
|
||||
json[key] = value;
|
||||
}
|
||||
}
|
||||
return json
|
||||
return json;
|
||||
}
|
||||
|
||||
static optionsFromBody(body) {
|
||||
const allowConstraints = ['skip', 'limit', 'order', 'count', 'keys',
|
||||
'include', 'includeAll', 'redirectClassNameForKey', 'where'];
|
||||
const allowConstraints = [
|
||||
'skip',
|
||||
'limit',
|
||||
'order',
|
||||
'count',
|
||||
'keys',
|
||||
'include',
|
||||
'includeAll',
|
||||
'redirectClassNameForKey',
|
||||
'where',
|
||||
];
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
if (allowConstraints.indexOf(key) === -1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_QUERY,
|
||||
`Invalid parameter for query: ${key}`
|
||||
);
|
||||
}
|
||||
}
|
||||
const options = {};
|
||||
@@ -135,11 +192,21 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); });
|
||||
this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); });
|
||||
this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); });
|
||||
this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); });
|
||||
this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); });
|
||||
this.route('GET', '/classes/:className', req => {
|
||||
return this.handleFind(req);
|
||||
});
|
||||
this.route('GET', '/classes/:className/:objectId', req => {
|
||||
return this.handleGet(req);
|
||||
});
|
||||
this.route('POST', '/classes/:className', req => {
|
||||
return this.handleCreate(req);
|
||||
});
|
||||
this.route('PUT', '/classes/:className/:objectId', req => {
|
||||
return this.handleUpdate(req);
|
||||
});
|
||||
this.route('DELETE', '/classes/:className/:objectId', req => {
|
||||
return this.handleDelete(req);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import Parse from 'parse/node';
|
||||
import rest from '../rest';
|
||||
const triggers = require('../triggers');
|
||||
const middleware = require('../middlewares');
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import Parse from 'parse/node';
|
||||
import rest from '../rest';
|
||||
const triggers = require('../triggers');
|
||||
const middleware = require('../middlewares');
|
||||
|
||||
function formatJobSchedule(job_schedule) {
|
||||
if (typeof job_schedule.startAfter === 'undefined') {
|
||||
@@ -14,63 +14,111 @@ function formatJobSchedule(job_schedule) {
|
||||
function validateJobSchedule(config, job_schedule) {
|
||||
const jobs = triggers.getJobs(config.applicationId) || {};
|
||||
if (job_schedule.jobName && !jobs[job_schedule.jobName]) {
|
||||
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Cannot Schedule a job that is not deployed');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
||||
'Cannot Schedule a job that is not deployed'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class CloudCodeRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET', '/cloud_code/jobs', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.getJobs);
|
||||
this.route('GET', '/cloud_code/jobs/data', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.getJobsData);
|
||||
this.route('POST', '/cloud_code/jobs', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.createJob);
|
||||
this.route('PUT', '/cloud_code/jobs/:objectId', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.editJob);
|
||||
this.route('DELETE', '/cloud_code/jobs/:objectId', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.deleteJob);
|
||||
this.route(
|
||||
'GET',
|
||||
'/cloud_code/jobs',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
CloudCodeRouter.getJobs
|
||||
);
|
||||
this.route(
|
||||
'GET',
|
||||
'/cloud_code/jobs/data',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
CloudCodeRouter.getJobsData
|
||||
);
|
||||
this.route(
|
||||
'POST',
|
||||
'/cloud_code/jobs',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
CloudCodeRouter.createJob
|
||||
);
|
||||
this.route(
|
||||
'PUT',
|
||||
'/cloud_code/jobs/:objectId',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
CloudCodeRouter.editJob
|
||||
);
|
||||
this.route(
|
||||
'DELETE',
|
||||
'/cloud_code/jobs/:objectId',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
CloudCodeRouter.deleteJob
|
||||
);
|
||||
}
|
||||
|
||||
static getJobs(req) {
|
||||
return rest.find(req.config, req.auth, '_JobSchedule', {}, {}).then((scheduledJobs) => {
|
||||
return {
|
||||
response: scheduledJobs.results
|
||||
}
|
||||
});
|
||||
return rest
|
||||
.find(req.config, req.auth, '_JobSchedule', {}, {})
|
||||
.then(scheduledJobs => {
|
||||
return {
|
||||
response: scheduledJobs.results,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static getJobsData(req) {
|
||||
const config = req.config;
|
||||
const jobs = triggers.getJobs(config.applicationId) || {};
|
||||
return rest.find(req.config, req.auth, '_JobSchedule', {}, {}).then((scheduledJobs) => {
|
||||
return {
|
||||
response: {
|
||||
in_use: scheduledJobs.results.map((job) => job.jobName),
|
||||
jobs: Object.keys(jobs),
|
||||
}
|
||||
};
|
||||
});
|
||||
return rest
|
||||
.find(req.config, req.auth, '_JobSchedule', {}, {})
|
||||
.then(scheduledJobs => {
|
||||
return {
|
||||
response: {
|
||||
in_use: scheduledJobs.results.map(job => job.jobName),
|
||||
jobs: Object.keys(jobs),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static createJob(req) {
|
||||
const { job_schedule } = req.body;
|
||||
validateJobSchedule(req.config, job_schedule);
|
||||
return rest.create(req.config, req.auth, '_JobSchedule', formatJobSchedule(job_schedule), req.client);
|
||||
return rest.create(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_JobSchedule',
|
||||
formatJobSchedule(job_schedule),
|
||||
req.client
|
||||
);
|
||||
}
|
||||
|
||||
static editJob(req) {
|
||||
const { objectId } = req.params;
|
||||
const { job_schedule } = req.body;
|
||||
validateJobSchedule(req.config, job_schedule);
|
||||
return rest.update(req.config, req.auth, '_JobSchedule', { objectId }, formatJobSchedule(job_schedule)).then((response) => {
|
||||
return {
|
||||
response
|
||||
}
|
||||
});
|
||||
return rest
|
||||
.update(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_JobSchedule',
|
||||
{ objectId },
|
||||
formatJobSchedule(job_schedule)
|
||||
)
|
||||
.then(response => {
|
||||
return {
|
||||
response,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static deleteJob(req) {
|
||||
const { objectId } = req.params;
|
||||
return rest.del(req.config, req.auth, '_JobSchedule', objectId).then((response) => {
|
||||
return {
|
||||
response
|
||||
}
|
||||
});
|
||||
return rest
|
||||
.del(req.config, req.auth, '_JobSchedule', objectId)
|
||||
.then(response => {
|
||||
return {
|
||||
response,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,63 @@
|
||||
import { version } from '../../package.json';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import { version } from '../../package.json';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class FeaturesRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, req => {
|
||||
const features = {
|
||||
globalConfig: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
hooks: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
cloudCode: {
|
||||
jobs: true,
|
||||
},
|
||||
logs: {
|
||||
level: true,
|
||||
size: true,
|
||||
order: true,
|
||||
until: true,
|
||||
from: true,
|
||||
},
|
||||
push: {
|
||||
immediatePush: req.config.hasPushSupport,
|
||||
scheduledPush: req.config.hasPushScheduledSupport,
|
||||
storedPushData: req.config.hasPushSupport,
|
||||
pushAudiences: true,
|
||||
localization: true,
|
||||
},
|
||||
schemas: {
|
||||
addField: true,
|
||||
removeField: true,
|
||||
addClass: true,
|
||||
removeClass: true,
|
||||
clearAllDataFromClass: true,
|
||||
exportClass: false,
|
||||
editClassLevelPermissions: true,
|
||||
editPointerPermissions: true,
|
||||
},
|
||||
};
|
||||
this.route(
|
||||
'GET',
|
||||
'/serverInfo',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
const features = {
|
||||
globalConfig: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
hooks: {
|
||||
create: true,
|
||||
read: true,
|
||||
update: true,
|
||||
delete: true,
|
||||
},
|
||||
cloudCode: {
|
||||
jobs: true,
|
||||
},
|
||||
logs: {
|
||||
level: true,
|
||||
size: true,
|
||||
order: true,
|
||||
until: true,
|
||||
from: true,
|
||||
},
|
||||
push: {
|
||||
immediatePush: req.config.hasPushSupport,
|
||||
scheduledPush: req.config.hasPushScheduledSupport,
|
||||
storedPushData: req.config.hasPushSupport,
|
||||
pushAudiences: true,
|
||||
localization: true,
|
||||
},
|
||||
schemas: {
|
||||
addField: true,
|
||||
removeField: true,
|
||||
addClass: true,
|
||||
removeClass: true,
|
||||
clearAllDataFromClass: true,
|
||||
exportClass: false,
|
||||
editClassLevelPermissions: true,
|
||||
editPointerPermissions: true,
|
||||
},
|
||||
};
|
||||
|
||||
return { response: {
|
||||
features: features,
|
||||
parseServerVersion: version,
|
||||
} };
|
||||
});
|
||||
return {
|
||||
response: {
|
||||
features: features,
|
||||
parseServerVersion: version,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
import logger from '../logger';
|
||||
import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import Parse from 'parse/node';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
import logger from '../logger';
|
||||
|
||||
export class FilesRouter {
|
||||
|
||||
expressRouter({ maxUploadSize = '20Mb' } = {}) {
|
||||
var router = express.Router();
|
||||
router.get('/files/:appId/:filename', this.getHandler);
|
||||
|
||||
router.post('/files', function(req, res, next) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename not provided.'));
|
||||
next(
|
||||
new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename not provided.')
|
||||
);
|
||||
});
|
||||
|
||||
router.post('/files/:filename',
|
||||
router.post(
|
||||
'/files/:filename',
|
||||
Middlewares.allowCrossDomain,
|
||||
BodyParser.raw({type: () => { return true; }, limit: maxUploadSize }), // Allow uploads without Content-Type, or with any Content-Type.
|
||||
BodyParser.raw({
|
||||
type: () => {
|
||||
return true;
|
||||
},
|
||||
limit: maxUploadSize,
|
||||
}), // Allow uploads without Content-Type, or with any Content-Type.
|
||||
Middlewares.handleParseHeaders,
|
||||
this.createHandler
|
||||
);
|
||||
|
||||
router.delete('/files/:filename',
|
||||
router.delete(
|
||||
'/files/:filename',
|
||||
Middlewares.allowCrossDomain,
|
||||
Middlewares.handleParseHeaders,
|
||||
Middlewares.enforceMasterKeyAccess,
|
||||
@@ -39,43 +46,55 @@ export class FilesRouter {
|
||||
const filename = req.params.filename;
|
||||
const contentType = mime.getType(filename);
|
||||
if (isFileStreamable(req, filesController)) {
|
||||
filesController.getFileStream(config, filename).then((stream) => {
|
||||
handleFileStream(stream, req, res, contentType);
|
||||
}).catch(() => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
filesController
|
||||
.getFileStream(config, filename)
|
||||
.then(stream => {
|
||||
handleFileStream(stream, req, res, contentType);
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
} else {
|
||||
filesController.getFileData(config, filename).then((data) => {
|
||||
res.status(200);
|
||||
res.set('Content-Type', contentType);
|
||||
res.set('Content-Length', data.length);
|
||||
res.end(data);
|
||||
}).catch(() => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
filesController
|
||||
.getFileData(config, filename)
|
||||
.then(data => {
|
||||
res.status(200);
|
||||
res.set('Content-Type', contentType);
|
||||
res.set('Content-Length', data.length);
|
||||
res.end(data);
|
||||
})
|
||||
.catch(() => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createHandler(req, res, next) {
|
||||
if (!req.body || !req.body.length) {
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR,
|
||||
'Invalid file upload.'));
|
||||
next(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.params.filename.length > 128) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename too long.'));
|
||||
next(
|
||||
new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) {
|
||||
next(new Parse.Error(Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename contains invalid characters.'));
|
||||
next(
|
||||
new Parse.Error(
|
||||
Parse.Error.INVALID_FILE_NAME,
|
||||
'Filename contains invalid characters.'
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,35 +103,53 @@ export class FilesRouter {
|
||||
const config = req.config;
|
||||
const filesController = config.filesController;
|
||||
|
||||
filesController.createFile(config, filename, req.body, contentType).then((result) => {
|
||||
res.status(201);
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
}).catch((e) => {
|
||||
logger.error(e.message, e);
|
||||
next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.'));
|
||||
});
|
||||
filesController
|
||||
.createFile(config, filename, req.body, contentType)
|
||||
.then(result => {
|
||||
res.status(201);
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error(e.message, e);
|
||||
next(
|
||||
new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
deleteHandler(req, res, next) {
|
||||
const filesController = req.config.filesController;
|
||||
filesController.deleteFile(req.config, req.params.filename).then(() => {
|
||||
res.status(200);
|
||||
// TODO: return useful JSON here?
|
||||
res.end();
|
||||
}).catch(() => {
|
||||
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
|
||||
'Could not delete file.'));
|
||||
});
|
||||
filesController
|
||||
.deleteFile(req.config, req.params.filename)
|
||||
.then(() => {
|
||||
res.status(200);
|
||||
// TODO: return useful JSON here?
|
||||
res.end();
|
||||
})
|
||||
.catch(() => {
|
||||
next(
|
||||
new Parse.Error(
|
||||
Parse.Error.FILE_DELETE_ERROR,
|
||||
'Could not delete file.'
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isFileStreamable(req, filesController){
|
||||
return req.get('Range') && typeof filesController.adapter.getFileStream === 'function';
|
||||
function isFileStreamable(req, filesController) {
|
||||
return (
|
||||
req.get('Range') &&
|
||||
typeof filesController.adapter.getFileStream === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
function getRange(req) {
|
||||
const parts = req.get('Range').replace(/bytes=/, "").split("-");
|
||||
const parts = req
|
||||
.get('Range')
|
||||
.replace(/bytes=/, '')
|
||||
.split('-');
|
||||
return { start: parseInt(parts[0], 10), end: parseInt(parts[1], 10) };
|
||||
}
|
||||
|
||||
@@ -121,12 +158,10 @@ function getRange(req) {
|
||||
function handleFileStream(stream, req, res, contentType) {
|
||||
const buffer_size = 1024 * 1024; //1024Kb
|
||||
// Range request, partiall stream the file
|
||||
let {
|
||||
start, end
|
||||
} = getRange(req);
|
||||
let { start, end } = getRange(req);
|
||||
|
||||
const notEnded = (!end && end !== 0);
|
||||
const notStarted = (!start && start !== 0);
|
||||
const notEnded = !end && end !== 0;
|
||||
const notStarted = !start && start !== 0;
|
||||
// No end provided, we want all bytes
|
||||
if (notEnded) {
|
||||
end = stream.length - 1;
|
||||
@@ -142,7 +177,7 @@ function handleFileStream(stream, req, res, contentType) {
|
||||
end = start + buffer_size - 1;
|
||||
}
|
||||
|
||||
const contentLength = (end - start) + 1;
|
||||
const contentLength = end - start + 1;
|
||||
|
||||
res.writeHead(206, {
|
||||
'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length,
|
||||
@@ -151,14 +186,14 @@ function handleFileStream(stream, req, res, contentType) {
|
||||
'Content-Type': contentType,
|
||||
});
|
||||
|
||||
stream.seek(start, function () {
|
||||
stream.seek(start, function() {
|
||||
// get gridFile stream
|
||||
const gridFileStream = stream.stream(true);
|
||||
let bufferAvail = 0;
|
||||
let remainingBytesToWrite = contentLength;
|
||||
let totalBytesWritten = 0;
|
||||
// write to response
|
||||
gridFileStream.on('data', function (data) {
|
||||
gridFileStream.on('data', function(data) {
|
||||
bufferAvail += data.length;
|
||||
if (bufferAvail > 0) {
|
||||
// slice returns the same buffer if overflowing
|
||||
|
||||
@@ -11,7 +11,7 @@ import { logger } from '../logger';
|
||||
|
||||
function parseObject(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => {
|
||||
return obj.map(item => {
|
||||
return parseObject(item);
|
||||
});
|
||||
} else if (obj && obj.__type == 'Date') {
|
||||
@@ -30,12 +30,20 @@ function parseParams(params) {
|
||||
}
|
||||
|
||||
export class FunctionsRouter extends PromiseRouter {
|
||||
|
||||
mountRoutes() {
|
||||
this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction);
|
||||
this.route('POST', '/jobs/:jobName', promiseEnforceMasterKeyAccess, function(req) {
|
||||
return FunctionsRouter.handleCloudJob(req);
|
||||
});
|
||||
this.route(
|
||||
'POST',
|
||||
'/functions/:functionName',
|
||||
FunctionsRouter.handleCloudFunction
|
||||
);
|
||||
this.route(
|
||||
'POST',
|
||||
'/jobs/:jobName',
|
||||
promiseEnforceMasterKeyAccess,
|
||||
function(req) {
|
||||
return FunctionsRouter.handleCloudJob(req);
|
||||
}
|
||||
);
|
||||
this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, function(req) {
|
||||
return FunctionsRouter.handleCloudJob(req);
|
||||
});
|
||||
@@ -57,27 +65,32 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
headers: req.config.headers,
|
||||
ip: req.config.ip,
|
||||
jobName,
|
||||
message: jobHandler.setMessage.bind(jobHandler)
|
||||
message: jobHandler.setMessage.bind(jobHandler),
|
||||
};
|
||||
|
||||
return jobHandler.setRunning(jobName, params).then((jobStatus) => {
|
||||
request.jobId = jobStatus.objectId
|
||||
return jobHandler.setRunning(jobName, params).then(jobStatus => {
|
||||
request.jobId = jobStatus.objectId;
|
||||
// run the function async
|
||||
process.nextTick(() => {
|
||||
Promise.resolve().then(() => {
|
||||
return jobFunction(request);
|
||||
}).then((result) => {
|
||||
jobHandler.setSucceeded(result);
|
||||
}, (error) => {
|
||||
jobHandler.setFailed(error);
|
||||
});
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
return jobFunction(request);
|
||||
})
|
||||
.then(
|
||||
result => {
|
||||
jobHandler.setSucceeded(result);
|
||||
},
|
||||
error => {
|
||||
jobHandler.setFailed(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
return {
|
||||
headers: {
|
||||
'X-Parse-Job-Status-Id': jobStatus.objectId
|
||||
'X-Parse-Job-Status-Id': jobStatus.objectId,
|
||||
},
|
||||
response: {}
|
||||
}
|
||||
response: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,8 +99,8 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
success: function(result) {
|
||||
resolve({
|
||||
response: {
|
||||
result: Parse._encode(result)
|
||||
}
|
||||
result: Parse._encode(result),
|
||||
},
|
||||
});
|
||||
},
|
||||
error: function(message) {
|
||||
@@ -106,17 +119,23 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
}
|
||||
reject(new Parse.Error(code, message));
|
||||
},
|
||||
message: message
|
||||
}
|
||||
message: message,
|
||||
};
|
||||
}
|
||||
|
||||
static handleCloudFunction(req) {
|
||||
const functionName = req.params.functionName;
|
||||
const applicationId = req.config.applicationId;
|
||||
const theFunction = triggers.getFunction(functionName, applicationId);
|
||||
const theValidator = triggers.getValidator(req.params.functionName, applicationId);
|
||||
const theValidator = triggers.getValidator(
|
||||
req.params.functionName,
|
||||
applicationId
|
||||
);
|
||||
if (!theFunction) {
|
||||
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
|
||||
throw new Parse.Error(
|
||||
Parse.Error.SCRIPT_FAILED,
|
||||
`Invalid function: "${functionName}"`
|
||||
);
|
||||
}
|
||||
let params = Object.assign({}, req.body, req.query);
|
||||
params = parseParams(params);
|
||||
@@ -128,53 +147,65 @@ export class FunctionsRouter extends PromiseRouter {
|
||||
log: req.config.loggerController,
|
||||
headers: req.config.headers,
|
||||
ip: req.config.ip,
|
||||
functionName
|
||||
functionName,
|
||||
};
|
||||
|
||||
if (theValidator && typeof theValidator === "function") {
|
||||
if (theValidator && typeof theValidator === 'function') {
|
||||
var result = theValidator(request);
|
||||
if (!result) {
|
||||
throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Validation failed.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.VALIDATION_ERROR,
|
||||
'Validation failed.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const userString = (req.auth && req.auth.user) ? req.auth.user.id : undefined;
|
||||
return new Promise(function(resolve, reject) {
|
||||
const userString =
|
||||
req.auth && req.auth.user ? req.auth.user.id : undefined;
|
||||
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
|
||||
const { success, error, message } = FunctionsRouter.createResponseObject((result) => {
|
||||
try {
|
||||
const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result));
|
||||
logger.info(
|
||||
`Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput }\n Result: ${cleanResult }`,
|
||||
{
|
||||
functionName,
|
||||
params,
|
||||
user: userString,
|
||||
}
|
||||
);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
const { success, error, message } = FunctionsRouter.createResponseObject(
|
||||
result => {
|
||||
try {
|
||||
const cleanResult = logger.truncateLogMessage(
|
||||
JSON.stringify(result.response.result)
|
||||
);
|
||||
logger.info(
|
||||
`Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
|
||||
{
|
||||
functionName,
|
||||
params,
|
||||
user: userString,
|
||||
}
|
||||
);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
error => {
|
||||
try {
|
||||
logger.error(
|
||||
`Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` +
|
||||
JSON.stringify(error),
|
||||
{
|
||||
functionName,
|
||||
error,
|
||||
params,
|
||||
user: userString,
|
||||
}
|
||||
);
|
||||
reject(error);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
}, (error) => {
|
||||
try {
|
||||
logger.error(
|
||||
`Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + JSON.stringify(error),
|
||||
{
|
||||
functionName,
|
||||
error,
|
||||
params,
|
||||
user: userString
|
||||
}
|
||||
);
|
||||
reject(error);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
return Promise.resolve().then(() => {
|
||||
return theFunction(request, { message });
|
||||
}).then(success, error);
|
||||
);
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return theFunction(request, { message });
|
||||
})
|
||||
.then(success, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
// global_config.js
|
||||
import Parse from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import Parse from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
return req.config.database.find('_GlobalConfig', { objectId: "1" }, { limit: 1 }).then((results) => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
}
|
||||
const globalConfig = results[0];
|
||||
return { response: { params: globalConfig.params } };
|
||||
});
|
||||
return req.config.database
|
||||
.find('_GlobalConfig', { objectId: '1' }, { limit: 1 })
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
// If there is no config in the database - return empty config.
|
||||
return { response: { params: {} } };
|
||||
}
|
||||
const globalConfig = results[0];
|
||||
return { response: { params: globalConfig.params } };
|
||||
});
|
||||
}
|
||||
|
||||
updateGlobalConfig(req) {
|
||||
if (req.auth.isReadOnly) {
|
||||
throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to update the config.');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
"read-only masterKey isn't allowed to update the config."
|
||||
);
|
||||
}
|
||||
const params = req.body.params;
|
||||
// Transform in dot notation to make sure it works
|
||||
@@ -25,12 +30,23 @@ export class GlobalConfigRouter extends PromiseRouter {
|
||||
acc[`params.${key}`] = params[key];
|
||||
return acc;
|
||||
}, {});
|
||||
return req.config.database.update('_GlobalConfig', {objectId: "1"}, update, {upsert: true}).then(() => ({ response: { result: true } }));
|
||||
return req.config.database
|
||||
.update('_GlobalConfig', { objectId: '1' }, update, { upsert: true })
|
||||
.then(() => ({ response: { result: true } }));
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/config', req => { return this.getGlobalConfig(req) });
|
||||
this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(req) });
|
||||
this.route('GET', '/config', req => {
|
||||
return this.getGlobalConfig(req);
|
||||
});
|
||||
this.route(
|
||||
'PUT',
|
||||
'/config',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
req => {
|
||||
return this.updateGlobalConfig(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class HooksRouter extends PromiseRouter {
|
||||
createHook(aHook, config) {
|
||||
return config.hooksController.createHook(aHook).then((hook) => ({response: hook}));
|
||||
return config.hooksController
|
||||
.createHook(aHook)
|
||||
.then(hook => ({ response: hook }));
|
||||
}
|
||||
|
||||
updateHook(aHook, config) {
|
||||
return config.hooksController.updateHook(aHook).then((hook) => ({response: hook}));
|
||||
return config.hooksController
|
||||
.updateHook(aHook)
|
||||
.then(hook => ({ response: hook }));
|
||||
}
|
||||
|
||||
handlePost(req) {
|
||||
@@ -18,67 +22,84 @@ export class HooksRouter extends PromiseRouter {
|
||||
handleGetFunctions(req) {
|
||||
var hooksController = req.config.hooksController;
|
||||
if (req.params.functionName) {
|
||||
return hooksController.getFunction(req.params.functionName).then((foundFunction) => {
|
||||
if (!foundFunction) {
|
||||
throw new Parse.Error(143, `no function named: ${req.params.functionName} is defined`);
|
||||
}
|
||||
return Promise.resolve({response: foundFunction});
|
||||
});
|
||||
return hooksController
|
||||
.getFunction(req.params.functionName)
|
||||
.then(foundFunction => {
|
||||
if (!foundFunction) {
|
||||
throw new Parse.Error(
|
||||
143,
|
||||
`no function named: ${req.params.functionName} is defined`
|
||||
);
|
||||
}
|
||||
return Promise.resolve({ response: foundFunction });
|
||||
});
|
||||
}
|
||||
|
||||
return hooksController.getFunctions().then((functions) => {
|
||||
return { response: functions || [] };
|
||||
}, (err) => {
|
||||
throw err;
|
||||
});
|
||||
return hooksController.getFunctions().then(
|
||||
functions => {
|
||||
return { response: functions || [] };
|
||||
},
|
||||
err => {
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleGetTriggers(req) {
|
||||
var hooksController = req.config.hooksController;
|
||||
if (req.params.className && req.params.triggerName) {
|
||||
|
||||
return hooksController.getTrigger(req.params.className, req.params.triggerName).then((foundTrigger) => {
|
||||
if (!foundTrigger) {
|
||||
throw new Parse.Error(143,`class ${req.params.className} does not exist`);
|
||||
}
|
||||
return Promise.resolve({response: foundTrigger});
|
||||
});
|
||||
return hooksController
|
||||
.getTrigger(req.params.className, req.params.triggerName)
|
||||
.then(foundTrigger => {
|
||||
if (!foundTrigger) {
|
||||
throw new Parse.Error(
|
||||
143,
|
||||
`class ${req.params.className} does not exist`
|
||||
);
|
||||
}
|
||||
return Promise.resolve({ response: foundTrigger });
|
||||
});
|
||||
}
|
||||
|
||||
return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] }));
|
||||
return hooksController
|
||||
.getTriggers()
|
||||
.then(triggers => ({ response: triggers || [] }));
|
||||
}
|
||||
|
||||
handleDelete(req) {
|
||||
var hooksController = req.config.hooksController;
|
||||
if (req.params.functionName) {
|
||||
return hooksController.deleteFunction(req.params.functionName).then(() => ({response: {}}))
|
||||
|
||||
return hooksController
|
||||
.deleteFunction(req.params.functionName)
|
||||
.then(() => ({ response: {} }));
|
||||
} else if (req.params.className && req.params.triggerName) {
|
||||
return hooksController.deleteTrigger(req.params.className, req.params.triggerName).then(() => ({response: {}}))
|
||||
return hooksController
|
||||
.deleteTrigger(req.params.className, req.params.triggerName)
|
||||
.then(() => ({ response: {} }));
|
||||
}
|
||||
return Promise.resolve({response: {}});
|
||||
return Promise.resolve({ response: {} });
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
var hook;
|
||||
if (req.params.functionName && req.body.url) {
|
||||
hook = {}
|
||||
hook = {};
|
||||
hook.functionName = req.params.functionName;
|
||||
hook.url = req.body.url;
|
||||
} else if (req.params.className && req.params.triggerName && req.body.url) {
|
||||
hook = {}
|
||||
hook = {};
|
||||
hook.className = req.params.className;
|
||||
hook.triggerName = req.params.triggerName;
|
||||
hook.url = req.body.url
|
||||
hook.url = req.body.url;
|
||||
} else {
|
||||
throw new Parse.Error(143, "invalid hook declaration");
|
||||
throw new Parse.Error(143, 'invalid hook declaration');
|
||||
}
|
||||
return this.updateHook(hook, req.config);
|
||||
}
|
||||
|
||||
handlePut(req) {
|
||||
var body = req.body;
|
||||
if (body.__op == "Delete") {
|
||||
if (body.__op == 'Delete') {
|
||||
return this.handleDelete(req);
|
||||
} else {
|
||||
return this.handleUpdate(req);
|
||||
@@ -86,14 +107,54 @@ export class HooksRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('GET', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('POST', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('POST', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('PUT', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route('PUT', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route(
|
||||
'GET',
|
||||
'/hooks/functions',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handleGetFunctions.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'GET',
|
||||
'/hooks/triggers',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handleGetTriggers.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'GET',
|
||||
'/hooks/functions/:functionName',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handleGetFunctions.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'GET',
|
||||
'/hooks/triggers/:className/:triggerName',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handleGetTriggers.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'POST',
|
||||
'/hooks/functions',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handlePost.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'POST',
|
||||
'/hooks/triggers',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handlePost.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'PUT',
|
||||
'/hooks/functions/:functionName',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handlePut.bind(this)
|
||||
);
|
||||
this.route(
|
||||
'PUT',
|
||||
'/hooks/triggers/:className/:triggerName',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.handlePut.bind(this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,81 +1,97 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
var request = require("request");
|
||||
var rest = require("../rest");
|
||||
var request = require('request');
|
||||
var rest = require('../rest');
|
||||
import Parse from 'parse/node';
|
||||
|
||||
// TODO move validation logic in IAPValidationController
|
||||
const IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt";
|
||||
const IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt";
|
||||
const IAP_SANDBOX_URL = 'https://sandbox.itunes.apple.com/verifyReceipt';
|
||||
const IAP_PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt';
|
||||
|
||||
const APP_STORE_ERRORS = {
|
||||
21000: "The App Store could not read the JSON object you provided.",
|
||||
21002: "The data in the receipt-data property was malformed or missing.",
|
||||
21003: "The receipt could not be authenticated.",
|
||||
21004: "The shared secret you provided does not match the shared secret on file for your account.",
|
||||
21005: "The receipt server is not currently available.",
|
||||
21006: "This receipt is valid but the subscription has expired.",
|
||||
21007: "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.",
|
||||
21008: "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead."
|
||||
}
|
||||
21000: 'The App Store could not read the JSON object you provided.',
|
||||
21002: 'The data in the receipt-data property was malformed or missing.',
|
||||
21003: 'The receipt could not be authenticated.',
|
||||
21004: 'The shared secret you provided does not match the shared secret on file for your account.',
|
||||
21005: 'The receipt server is not currently available.',
|
||||
21006: 'This receipt is valid but the subscription has expired.',
|
||||
21007: 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.',
|
||||
21008: 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.',
|
||||
};
|
||||
|
||||
function appStoreError(status) {
|
||||
status = parseInt(status);
|
||||
var errorString = APP_STORE_ERRORS[status] || "unknown error.";
|
||||
return { status: status, error: errorString }
|
||||
var errorString = APP_STORE_ERRORS[status] || 'unknown error.';
|
||||
return { status: status, error: errorString };
|
||||
}
|
||||
|
||||
function validateWithAppStore(url, receipt) {
|
||||
return new Promise(function(fulfill, reject) {
|
||||
request.post({
|
||||
url: url,
|
||||
body: { "receipt-data": receipt },
|
||||
json: true,
|
||||
}, function(err, res, body) {
|
||||
var status = body.status;
|
||||
if (status == 0) {
|
||||
// No need to pass anything, status is OK
|
||||
return fulfill();
|
||||
request.post(
|
||||
{
|
||||
url: url,
|
||||
body: { 'receipt-data': receipt },
|
||||
json: true,
|
||||
},
|
||||
function(err, res, body) {
|
||||
var status = body.status;
|
||||
if (status == 0) {
|
||||
// No need to pass anything, status is OK
|
||||
return fulfill();
|
||||
}
|
||||
// receipt is from test and should go to test
|
||||
return reject(body);
|
||||
}
|
||||
// receipt is from test and should go to test
|
||||
return reject(body);
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getFileForProductIdentifier(productIdentifier, req) {
|
||||
return rest.find(req.config, req.auth, '_Product', { productIdentifier: productIdentifier }, undefined, req.info.clientSDK).then(function(result){
|
||||
const products = result.results;
|
||||
if (!products || products.length != 1) {
|
||||
// Error not found or too many
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')
|
||||
}
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_Product',
|
||||
{ productIdentifier: productIdentifier },
|
||||
undefined,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(function(result) {
|
||||
const products = result.results;
|
||||
if (!products || products.length != 1) {
|
||||
// Error not found or too many
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'
|
||||
);
|
||||
}
|
||||
|
||||
var download = products[0].download;
|
||||
return Promise.resolve({response: download});
|
||||
});
|
||||
var download = products[0].download;
|
||||
return Promise.resolve({ response: download });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export class IAPValidationRouter extends PromiseRouter {
|
||||
|
||||
handleRequest(req) {
|
||||
let receipt = req.body.receipt;
|
||||
const productIdentifier = req.body.productIdentifier;
|
||||
|
||||
if (!receipt || ! productIdentifier) {
|
||||
if (!receipt || !productIdentifier) {
|
||||
// TODO: Error, malformed request
|
||||
throw new Parse.Error(Parse.Error.INVALID_JSON, "missing receipt or productIdentifier");
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_JSON,
|
||||
'missing receipt or productIdentifier'
|
||||
);
|
||||
}
|
||||
|
||||
// Transform the object if there
|
||||
// otherwise assume it's in Base64 already
|
||||
if (typeof receipt == "object") {
|
||||
if (receipt["__type"] == "Bytes") {
|
||||
if (typeof receipt == 'object') {
|
||||
if (receipt['__type'] == 'Bytes') {
|
||||
receipt = receipt.base64;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.TESTING == "1" && req.body.bypassAppStoreValidation) {
|
||||
if (process.env.TESTING == '1' && req.body.bypassAppStoreValidation) {
|
||||
return getFileForProductIdentifier(productIdentifier, req);
|
||||
}
|
||||
|
||||
@@ -84,28 +100,31 @@ export class IAPValidationRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
function errorCallback(error) {
|
||||
return Promise.resolve({response: appStoreError(error.status) });
|
||||
return Promise.resolve({ response: appStoreError(error.status) });
|
||||
}
|
||||
|
||||
return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(() => {
|
||||
|
||||
return successCallback();
|
||||
|
||||
}, (error) => {
|
||||
if (error.status == 21007) {
|
||||
return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(() => {
|
||||
return successCallback();
|
||||
}, (error) => {
|
||||
return errorCallback(error);
|
||||
return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(
|
||||
() => {
|
||||
return successCallback();
|
||||
},
|
||||
error => {
|
||||
if (error.status == 21007) {
|
||||
return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(
|
||||
() => {
|
||||
return successCallback();
|
||||
},
|
||||
error => {
|
||||
return errorCallback(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return errorCallback(error);
|
||||
});
|
||||
return errorCallback(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route("POST","/validate_purchase", this.handleRequest);
|
||||
this.route('POST', '/validate_purchase', this.handleRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,41 @@ export class InstallationsRouter extends ClassesRouter {
|
||||
}
|
||||
|
||||
handleFind(req) {
|
||||
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
|
||||
const body = Object.assign(
|
||||
req.body,
|
||||
ClassesRouter.JSONFromQuery(req.query)
|
||||
);
|
||||
const options = ClassesRouter.optionsFromBody(body);
|
||||
return rest.find(req.config, req.auth,
|
||||
'_Installation', body.where, options, req.info.clientSDK)
|
||||
.then((response) => {
|
||||
return {response: response};
|
||||
return rest
|
||||
.find(
|
||||
req.config,
|
||||
req.auth,
|
||||
'_Installation',
|
||||
body.where,
|
||||
options,
|
||||
req.info.clientSDK
|
||||
)
|
||||
.then(response => {
|
||||
return { response: response };
|
||||
});
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/installations', req => { return this.handleFind(req); });
|
||||
this.route('GET','/installations/:objectId', req => { return this.handleGet(req); });
|
||||
this.route('POST','/installations', req => { return this.handleCreate(req); });
|
||||
this.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); });
|
||||
this.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); });
|
||||
this.route('GET', '/installations', req => {
|
||||
return this.handleFind(req);
|
||||
});
|
||||
this.route('GET', '/installations/:objectId', req => {
|
||||
return this.handleGet(req);
|
||||
});
|
||||
this.route('POST', '/installations', req => {
|
||||
return this.handleCreate(req);
|
||||
});
|
||||
this.route('PUT', '/installations/:objectId', req => {
|
||||
return this.handleUpdate(req);
|
||||
});
|
||||
this.route('DELETE', '/installations/:objectId', req => {
|
||||
return this.handleDelete(req);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import * as middleware from '../middlewares';
|
||||
|
||||
export class LogsRouter extends PromiseRouter {
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/scriptlog', middleware.promiseEnforceMasterKeyAccess, this.validateRequest, (req) => {
|
||||
return this.handleGET(req);
|
||||
});
|
||||
this.route(
|
||||
'GET',
|
||||
'/scriptlog',
|
||||
middleware.promiseEnforceMasterKeyAccess,
|
||||
this.validateRequest,
|
||||
req => {
|
||||
return this.handleGET(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
validateRequest(req) {
|
||||
if (!req.config || !req.config.loggerController) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not available');
|
||||
throw new Parse.Error(
|
||||
Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not available'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,21 +40,21 @@ export class LogsRouter extends PromiseRouter {
|
||||
size = req.query.n;
|
||||
}
|
||||
|
||||
const order = req.query.order
|
||||
const order = req.query.order;
|
||||
const level = req.query.level;
|
||||
const options = {
|
||||
from,
|
||||
until,
|
||||
size,
|
||||
order,
|
||||
level
|
||||
level,
|
||||
};
|
||||
|
||||
return req.config.loggerController.getLogs(options).then((result) => {
|
||||
return req.config.loggerController.getLogs(options).then(result => {
|
||||
return Promise.resolve({
|
||||
response: result
|
||||
response: result,
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,17 +5,16 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import qs from 'querystring';
|
||||
|
||||
const public_html = path.resolve(__dirname, "../../public_html");
|
||||
const public_html = path.resolve(__dirname, '../../public_html');
|
||||
const views = path.resolve(__dirname, '../../views');
|
||||
|
||||
export class PublicAPIRouter extends PromiseRouter {
|
||||
|
||||
verifyEmail(req) {
|
||||
const { token, username } = req.query;
|
||||
const appId = req.params.appId;
|
||||
const config = Config.get(appId);
|
||||
|
||||
if(!config){
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
@@ -28,15 +27,18 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
const userController = config.userController;
|
||||
return userController.verifyEmail(username, token).then(() => {
|
||||
const params = qs.stringify({username});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.verifyEmailSuccessURL}?${params}`
|
||||
});
|
||||
}, ()=> {
|
||||
return this.invalidVerificationLink(req);
|
||||
})
|
||||
return userController.verifyEmail(username, token).then(
|
||||
() => {
|
||||
const params = qs.stringify({ username });
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.verifyEmailSuccessURL}?${params}`,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
return this.invalidVerificationLink(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resendVerificationEmail(req) {
|
||||
@@ -44,7 +46,7 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
const appId = req.params.appId;
|
||||
const config = Config.get(appId);
|
||||
|
||||
if(!config){
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
@@ -58,51 +60,60 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
|
||||
const userController = config.userController;
|
||||
|
||||
return userController.resendVerificationEmail(username).then(() => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.linkSendSuccessURL}`
|
||||
});
|
||||
}, ()=> {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.linkSendFailURL}`
|
||||
});
|
||||
})
|
||||
return userController.resendVerificationEmail(username).then(
|
||||
() => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.linkSendSuccessURL}`,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.linkSendFailURL}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
changePassword(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const config = Config.get(req.query.id);
|
||||
|
||||
if(!config){
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return resolve({
|
||||
status: 404,
|
||||
text: 'Not found.'
|
||||
text: 'Not found.',
|
||||
});
|
||||
}
|
||||
// Should we keep the file in memory or leave like that?
|
||||
fs.readFile(path.resolve(views, "choose_password"), 'utf-8', (err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
fs.readFile(
|
||||
path.resolve(views, 'choose_password'),
|
||||
'utf-8',
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
data = data.replace(
|
||||
'PARSE_SERVER_URL',
|
||||
`'${config.publicServerURL}'`
|
||||
);
|
||||
resolve({
|
||||
text: data,
|
||||
});
|
||||
}
|
||||
data = data.replace("PARSE_SERVER_URL", `'${config.publicServerURL}'`);
|
||||
resolve({
|
||||
text: data
|
||||
})
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
requestResetPassword(req) {
|
||||
|
||||
const config = req.config;
|
||||
|
||||
if(!config){
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
@@ -116,22 +127,29 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.checkResetTokenValidity(username, token).then(() => {
|
||||
const params = qs.stringify({token, id: config.applicationId, username, app: config.appName, });
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?${params}`
|
||||
})
|
||||
}, () => {
|
||||
return this.invalidLink(req);
|
||||
})
|
||||
return config.userController.checkResetTokenValidity(username, token).then(
|
||||
() => {
|
||||
const params = qs.stringify({
|
||||
token,
|
||||
id: config.applicationId,
|
||||
username,
|
||||
app: config.appName,
|
||||
});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?${params}`,
|
||||
});
|
||||
},
|
||||
() => {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetPassword(req) {
|
||||
|
||||
const config = req.config;
|
||||
|
||||
if(!config){
|
||||
if (!config) {
|
||||
this.invalidRequest();
|
||||
}
|
||||
|
||||
@@ -139,46 +157,55 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
const {
|
||||
username,
|
||||
token,
|
||||
new_password
|
||||
} = req.body;
|
||||
const { username, token, new_password } = req.body;
|
||||
|
||||
if (!username || !token || !new_password) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.updatePassword(username, token, new_password).then(() => {
|
||||
const params = qs.stringify({username: username});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.passwordResetSuccessURL}?${params}`
|
||||
});
|
||||
}, (err) => {
|
||||
const params = qs.stringify({username: username, token: token, id: config.applicationId, error:err, app:config.appName});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?${params}`
|
||||
});
|
||||
});
|
||||
|
||||
return config.userController
|
||||
.updatePassword(username, token, new_password)
|
||||
.then(
|
||||
() => {
|
||||
const params = qs.stringify({ username: username });
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.passwordResetSuccessURL}?${params}`,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
const params = qs.stringify({
|
||||
username: username,
|
||||
token: token,
|
||||
id: config.applicationId,
|
||||
error: err,
|
||||
app: config.appName,
|
||||
});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?${params}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
invalidLink(req) {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: req.config.invalidLinkURL
|
||||
location: req.config.invalidLinkURL,
|
||||
});
|
||||
}
|
||||
|
||||
invalidVerificationLink(req) {
|
||||
const config = req.config;
|
||||
if (req.query.username && req.params.appId) {
|
||||
const params = qs.stringify({username: req.query.username, appId: req.params.appId});
|
||||
const params = qs.stringify({
|
||||
username: req.query.username,
|
||||
appId: req.params.appId,
|
||||
});
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.invalidVerificationLinkURL}?${params}`
|
||||
location: `${config.invalidVerificationLinkURL}?${params}`,
|
||||
});
|
||||
} else {
|
||||
return this.invalidLink(req);
|
||||
@@ -187,15 +214,15 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
|
||||
missingPublicServerURL() {
|
||||
return Promise.resolve({
|
||||
text: 'Not found.',
|
||||
status: 404
|
||||
text: 'Not found.',
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
invalidRequest() {
|
||||
const error = new Error();
|
||||
error.status = 403;
|
||||
error.message = "unauthorized";
|
||||
error.message = 'unauthorized';
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -205,30 +232,59 @@ export class PublicAPIRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/apps/:appId/verify_email',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.verifyEmail(req); });
|
||||
this.route(
|
||||
'GET',
|
||||
'/apps/:appId/verify_email',
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.verifyEmail(req);
|
||||
}
|
||||
);
|
||||
|
||||
this.route('POST', '/apps/:appId/resend_verification_email',
|
||||
req => { this.setConfig(req); },
|
||||
req => { return this.resendVerificationEmail(req); });
|
||||
this.route(
|
||||
'POST',
|
||||
'/apps/:appId/resend_verification_email',
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.resendVerificationEmail(req);
|
||||
}
|
||||
);
|
||||
|
||||
this.route('GET','/apps/choose_password',
|
||||
req => { return this.changePassword(req); });
|
||||
this.route('GET', '/apps/choose_password', req => {
|
||||
return this.changePassword(req);
|
||||
});
|
||||
|
||||
this.route('POST','/apps/:appId/request_password_reset',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.resetPassword(req); });
|
||||
this.route(
|
||||
'POST',
|
||||
'/apps/:appId/request_password_reset',
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.resetPassword(req);
|
||||
}
|
||||
);
|
||||
|
||||
this.route('GET','/apps/:appId/request_password_reset',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.requestResetPassword(req); });
|
||||
this.route(
|
||||
'GET',
|
||||
'/apps/:appId/request_password_reset',
|
||||
req => {
|
||||
this.setConfig(req);
|
||||
},
|
||||
req => {
|
||||
return this.requestResetPassword(req);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
expressRouter() {
|
||||
const router = express.Router();
|
||||
router.use("/apps", express.static(public_html));
|
||||
router.use("/", super.expressRouter());
|
||||
router.use('/apps', express.static(public_html));
|
||||
router.use('/', super.expressRouter());
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user