Merge branch 'master' of https://github.com/ParsePlatform/parse-server into mcdonald-gcs-adapter
Get GCSAdapter up to snuff with FilesController + FilesControllerTestFactory * 'master' of https://github.com/ParsePlatform/parse-server: (102 commits) Remove duplicated instructions Release and Changelog for 2.1.4 fixes missing coverage with sh script Fix update system schema Adds optional COVERAGE Allows to pass no where in $select clause Sanitize objectId in Fix delete schema when actual collection does not exist Fix replace query overwrite the existing query object. Fix create system class with relation/pointer Use throws syntax for errors in SchemasRouter. Completely migrate SchemasRouter to new MongoCollection API. Add tests that verify installationId in Cloud Code triggers. Propagate installationId in all Cloud Code triggers. Add test expiresAt should be a Date, not a string. Fixes #776 Fix missing 'let/var' in OneSignalPushAdapter.spec. Don't run any afterSave hooks if none are registered. Fix : remove query count limit Flatten custom operations in request.object in afterSave hooks. ...
This commit is contained in:
@@ -1,35 +1,43 @@
|
||||
export function loadAdapter(adapter, defaultAdapter, options) {
|
||||
|
||||
export function loadAdapter(options, defaultAdapter) {
|
||||
let adapter;
|
||||
|
||||
// We have options and options have adapter key
|
||||
if (options) {
|
||||
// Pass an adapter as a module name, a function or an instance
|
||||
if (typeof options == "string" || typeof options == "function" || options.constructor != Object) {
|
||||
adapter = options;
|
||||
if (!adapter)
|
||||
{
|
||||
if (!defaultAdapter) {
|
||||
return options;
|
||||
}
|
||||
if (options.adapter) {
|
||||
adapter = options.adapter;
|
||||
// Load from the default adapter when no adapter is set
|
||||
return loadAdapter(defaultAdapter, undefined, options);
|
||||
} else if (typeof adapter === "function") {
|
||||
try {
|
||||
return adapter(options);
|
||||
} catch(e) {
|
||||
var Adapter = adapter;
|
||||
return new Adapter(options);
|
||||
}
|
||||
}
|
||||
|
||||
if (!adapter) {
|
||||
adapter = defaultAdapter;
|
||||
}
|
||||
|
||||
// This is a string, require the module
|
||||
if (typeof adapter === "string") {
|
||||
} else if (typeof adapter === "string") {
|
||||
adapter = require(adapter);
|
||||
// If it's define as a module, get the default
|
||||
if (adapter.default) {
|
||||
adapter = adapter.default;
|
||||
}
|
||||
|
||||
return loadAdapter(adapter, undefined, options);
|
||||
} else if (adapter.module) {
|
||||
return loadAdapter(adapter.module, undefined, adapter.options);
|
||||
} else if (adapter.class) {
|
||||
return loadAdapter(adapter.class, undefined, adapter.options);
|
||||
} else if (adapter.adapter) {
|
||||
return loadAdapter(adapter.adapter, undefined, adapter.options);
|
||||
} else {
|
||||
// Try to load the defaultAdapter with the options
|
||||
// The default adapter should throw if the options are
|
||||
// incompatible
|
||||
try {
|
||||
return loadAdapter(defaultAdapter, undefined, adapter);
|
||||
} catch (e) {};
|
||||
}
|
||||
// From there it's either a function or an object
|
||||
// if it's an function, instanciate and pass the options
|
||||
if (typeof adapter === "function") {
|
||||
var Adapter = adapter;
|
||||
adapter = new Adapter(options);
|
||||
}
|
||||
return adapter;
|
||||
// return the adapter as is as it's unusable otherwise
|
||||
return adapter;
|
||||
}
|
||||
|
||||
export default loadAdapter;
|
||||
|
||||
23
src/Adapters/Email/MailAdapter.js
Normal file
23
src/Adapters/Email/MailAdapter.js
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
/*
|
||||
Mail Adapter prototype
|
||||
A MailAdapter should implement at least sendMail()
|
||||
*/
|
||||
export class MailAdapter {
|
||||
/*
|
||||
* A method for sending mail
|
||||
* @param options would have the parameters
|
||||
* - to: the recipient
|
||||
* - text: the raw text of the message
|
||||
* - subject: the subject of the email
|
||||
*/
|
||||
sendMail(options) {}
|
||||
|
||||
/* You can implement those methods if you want
|
||||
* to provide HTML templates etc...
|
||||
*/
|
||||
// sendVerificationEmail({ link, appName, user }) {}
|
||||
// sendPasswordResetEmail({ link, appName, user }) {}
|
||||
}
|
||||
|
||||
export default MailAdapter;
|
||||
32
src/Adapters/Email/SimpleMailgunAdapter.js
Normal file
32
src/Adapters/Email/SimpleMailgunAdapter.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import Mailgun from 'mailgun-js';
|
||||
|
||||
let SimpleMailgunAdapter = mailgunOptions => {
|
||||
if (!mailgunOptions || !mailgunOptions.apiKey || !mailgunOptions.domain) {
|
||||
throw 'SimpleMailgunAdapter requires an API Key and domain.';
|
||||
}
|
||||
let mailgun = Mailgun(mailgunOptions);
|
||||
|
||||
let sendMail = ({to, subject, text}) => {
|
||||
let data = {
|
||||
from: mailgunOptions.fromAddress,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: text,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
mailgun.messages().send(data, (err, body) => {
|
||||
if (typeof err !== 'undefined') {
|
||||
reject(err);
|
||||
}
|
||||
resolve(body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
sendMail: sendMail
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SimpleMailgunAdapter
|
||||
@@ -8,11 +8,23 @@
|
||||
// * getFileLocation(config, request, filename)
|
||||
//
|
||||
// Default is GridStoreAdapter, which requires mongo
|
||||
// and for the API server to be using the ExportAdapter
|
||||
// and for the API server to be using the DatabaseController with Mongo
|
||||
// database adapter.
|
||||
|
||||
export class FilesAdapter {
|
||||
createFile(config, filename, data) { }
|
||||
/* this method is responsible to store the file in order to be retrived later by it's file name
|
||||
*
|
||||
*
|
||||
* @param config the current config
|
||||
* @param filename the filename to save
|
||||
* @param data the buffer of data from the file
|
||||
* @param contentType the supposed contentType
|
||||
* @discussion the contentType can be undefined if the controller was not able to determine it
|
||||
*
|
||||
* @return a promise that should fail if the storage didn't succeed
|
||||
*
|
||||
*/
|
||||
createFile(config, filename: string, data, contentType: string) { }
|
||||
|
||||
deleteFile(config, filename) { }
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
// GCSAdapter
|
||||
// Store Parse Files in Google Cloud Storage: https://cloud.google.com/storage
|
||||
import * as gcloud from 'gcloud';
|
||||
import { storage } from 'gcloud';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import requiredParameter from '../../requiredParameter';
|
||||
|
||||
export class GCSAdapter extends FilesAdapter {
|
||||
// GCS Project ID and the name of a corresponding Keyfile are required.
|
||||
// Unlike the S3 adapter, you must create a new Cloud Storage bucket, as this is not created automatically.
|
||||
// See https://googlecloudplatform.github.io/gcloud-node/#/docs/master/guides/authentication
|
||||
// for more details.
|
||||
constructor(
|
||||
projectId,
|
||||
keyFilename,
|
||||
bucket,
|
||||
projectId = requiredParameter('GCSAdapter requires a GCP Project ID'),
|
||||
keyFilename = requiredParameter('GCSAdapter requires a GCP keyfile'),
|
||||
bucket = requiredParameter('GCSAdapter requires a GCS bucket name'),
|
||||
{ bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
@@ -20,21 +22,25 @@ export class GCSAdapter extends FilesAdapter {
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
this._directAccess = directAccess;
|
||||
|
||||
let gcsOptions = {
|
||||
let options = {
|
||||
projectId: projectId,
|
||||
keyFilename: keyFilename
|
||||
};
|
||||
|
||||
this._gcsClient = new gcloud.storage(gcsOptions);
|
||||
this._gcsClient = new storage(options);
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in GCS.
|
||||
// Resolves the promise or fails with an error.
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
let params = {
|
||||
contentType: contentType || 'application/octet-stream'
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
// gcloud supports upload(file) not upload(bytes), so we need to stream.
|
||||
var uploadStream = file.createWriteStream();
|
||||
var uploadStream = file.createWriteStream(params);
|
||||
uploadStream.on('error', (err) => {
|
||||
return reject(err);
|
||||
}).on('finish', () => {
|
||||
@@ -61,6 +67,7 @@ export class GCSAdapter extends FilesAdapter {
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
file.delete((err, res) => {
|
||||
console.log("delete: ", filename, err, res);
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
@@ -74,11 +81,19 @@ export class GCSAdapter extends FilesAdapter {
|
||||
getFileData(config, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let file = this._gcsClient.bucket(this._bucket).file(this._bucketPrefix + filename);
|
||||
file.download((err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
// Check for existence, since gcloud-node seemed to be caching the result
|
||||
file.exists((err, exists) => {
|
||||
if (exists) {
|
||||
file.download((err, data) => {
|
||||
console.log("get: ", filename, err, data);
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
return resolve(data);
|
||||
});
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,28 +1,47 @@
|
||||
// GridStoreAdapter
|
||||
//
|
||||
// Stores files in Mongo using GridStore
|
||||
// Requires the database adapter to be based on mongoclient
|
||||
/**
|
||||
GridStoreAdapter
|
||||
Stores files in Mongo using GridStore
|
||||
Requires the database adapter to be based on mongoclient
|
||||
|
||||
import { GridStore } from 'mongodb';
|
||||
@flow weak
|
||||
*/
|
||||
|
||||
import { MongoClient, GridStore, Db} from 'mongodb';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
|
||||
export class GridStoreAdapter extends FilesAdapter {
|
||||
_databaseURI: string;
|
||||
_connectionPromise: Promise<Db>;
|
||||
|
||||
constructor(mongoDatabaseURI: string) {
|
||||
super();
|
||||
this._databaseURI = mongoDatabaseURI;
|
||||
this._connect();
|
||||
}
|
||||
|
||||
_connect() {
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = MongoClient.connect(this._databaseURI);
|
||||
}
|
||||
return this._connectionPromise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file
|
||||
// Returns a promise
|
||||
createFile(config, filename, data) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
createFile(config, filename: string, data, contentType) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.write(data);
|
||||
}).then((gridStore) => {
|
||||
}).then(gridStore => {
|
||||
return gridStore.close();
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'w');
|
||||
deleteFile(config, filename: string) {
|
||||
return this._connect().then(database => {
|
||||
let gridStore = new GridStore(database, filename, 'w');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
return gridStore.unlink();
|
||||
@@ -31,13 +50,14 @@ export class GridStoreAdapter extends FilesAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
getFileData(config, filename) {
|
||||
return config.database.connect().then(() => {
|
||||
return GridStore.exist(config.database.db, filename);
|
||||
}).then(() => {
|
||||
let gridStore = new GridStore(config.database.db, filename, 'r');
|
||||
return gridStore.open();
|
||||
}).then((gridStore) => {
|
||||
getFileData(config, filename: string) {
|
||||
return this._connect().then(database => {
|
||||
return GridStore.exist(database, filename)
|
||||
.then(() => {
|
||||
let gridStore = new GridStore(database, filename, 'r');
|
||||
return gridStore.open();
|
||||
});
|
||||
}).then(gridStore => {
|
||||
return gridStore.read();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,23 +4,38 @@
|
||||
|
||||
import * as AWS from 'aws-sdk';
|
||||
import { FilesAdapter } from './FilesAdapter';
|
||||
import requiredParameter from '../../requiredParameter';
|
||||
|
||||
const DEFAULT_S3_REGION = "us-east-1";
|
||||
|
||||
function parseS3AdapterOptions(...options) {
|
||||
if (options.length === 1 && typeof options[0] == "object") {
|
||||
return options;
|
||||
}
|
||||
|
||||
const additionalOptions = options[3] || {};
|
||||
|
||||
return {
|
||||
accessKey: options[0],
|
||||
secretKey: options[1],
|
||||
bucket: options[2],
|
||||
region: additionalOptions.region
|
||||
}
|
||||
}
|
||||
|
||||
export class S3Adapter extends FilesAdapter {
|
||||
// Creates an S3 session.
|
||||
// Providing AWS access and secret keys is mandatory
|
||||
// Region and bucket will use sane defaults if omitted
|
||||
constructor(
|
||||
accessKey,
|
||||
secretKey,
|
||||
bucket,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}
|
||||
) {
|
||||
accessKey = requiredParameter('S3Adapter requires an accessKey'),
|
||||
secretKey = requiredParameter('S3Adapter requires a secretKey'),
|
||||
bucket,
|
||||
{ region = DEFAULT_S3_REGION,
|
||||
bucketPrefix = '',
|
||||
directAccess = false } = {}) {
|
||||
super();
|
||||
|
||||
|
||||
this._region = region;
|
||||
this._bucket = bucket;
|
||||
this._bucketPrefix = bucketPrefix;
|
||||
@@ -33,11 +48,27 @@ export class S3Adapter extends FilesAdapter {
|
||||
};
|
||||
AWS.config._region = this._region;
|
||||
this._s3Client = new AWS.S3(s3Options);
|
||||
this._hasBucket = false;
|
||||
}
|
||||
|
||||
createBucket() {
|
||||
var promise;
|
||||
if (this._hasBucket) {
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = new Promise((resolve, reject) => {
|
||||
this._s3Client.createBucket(() => {
|
||||
this._hasBucket = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
// For a given config object, filename, and data, store a file in S3
|
||||
// Returns a promise containing the S3 object creation response
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename,
|
||||
Body: data
|
||||
@@ -45,26 +76,33 @@ export class S3Adapter extends FilesAdapter {
|
||||
if (this._directAccess) {
|
||||
params.ACL = "public-read"
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
if (contentType) {
|
||||
params.ContentType = contentType;
|
||||
}
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.upload(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(config, filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename
|
||||
};
|
||||
this._s3Client.deleteObject(params, (err, data) =>{
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let params = {
|
||||
Key: this._bucketPrefix + filename
|
||||
};
|
||||
this._s3Client.deleteObject(params, (err, data) =>{
|
||||
if(err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -73,12 +111,18 @@ export class S3Adapter extends FilesAdapter {
|
||||
// Returns a promise that succeeds with the buffer result from S3
|
||||
getFileData(config, filename) {
|
||||
let params = {Key: this._bucketPrefix + filename};
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data.Body);
|
||||
return this.createBucket().then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._s3Client.getObject(params, (err, data) => {
|
||||
if (err !== null) {
|
||||
return reject(err);
|
||||
}
|
||||
// Something happend here...
|
||||
if (data && !data.Body) {
|
||||
return reject(data);
|
||||
}
|
||||
resolve(data.Body);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,7 +101,6 @@ let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
|
||||
export class FileLoggerAdapter extends LoggerAdapter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this._logsFolder = options.logsFolder || LOGS_FOLDER;
|
||||
|
||||
// check logs folder exists
|
||||
|
||||
@@ -18,6 +18,10 @@ export class OneSignalPushAdapter extends PushAdapter {
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
this.OneSignalConfig = {};
|
||||
const { oneSignalAppId, oneSignalApiKey } = pushConfig;
|
||||
if (!oneSignalAppId || !oneSignalApiKey) {
|
||||
throw "Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey";
|
||||
}
|
||||
this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId'];
|
||||
this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey'];
|
||||
|
||||
|
||||
@@ -14,6 +14,10 @@ export class ParsePushAdapter extends PushAdapter {
|
||||
super(pushConfig);
|
||||
this.validPushTypes = ['ios', 'android'];
|
||||
this.senderMap = {};
|
||||
// used in PushController for Dashboard Features
|
||||
this.feature = {
|
||||
immediatePush: true
|
||||
};
|
||||
let pushTypes = Object.keys(pushConfig);
|
||||
|
||||
for (let pushType of pushTypes) {
|
||||
|
||||
76
src/Adapters/Storage/Mongo/MongoCollection.js
Normal file
76
src/Adapters/Storage/Mongo/MongoCollection.js
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let Collection = mongodb.Collection;
|
||||
|
||||
export default class MongoCollection {
|
||||
_mongoCollection:Collection;
|
||||
|
||||
constructor(mongoCollection:Collection) {
|
||||
this._mongoCollection = mongoCollection;
|
||||
}
|
||||
|
||||
// Does a find with "smart indexing".
|
||||
// Currently this just means, if it needs a geoindex and there is
|
||||
// none, then build the geoindex.
|
||||
// This could be improved a lot but it's not clear if that's a good
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
find(query, { skip, limit, sort } = {}) {
|
||||
return this._rawFind(query, { skip, limit, sort })
|
||||
.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
|
||||
let key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return this._mongoCollection.createIndex(index)
|
||||
// Retry, but just once.
|
||||
.then(() => this._rawFind(query, { skip, limit, sort }));
|
||||
});
|
||||
}
|
||||
|
||||
_rawFind(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection
|
||||
.find(query, { skip, limit, sort })
|
||||
.toArray();
|
||||
}
|
||||
|
||||
count(query, { skip, limit, sort } = {}) {
|
||||
return this._mongoCollection.count(query, { skip, limit, sort });
|
||||
}
|
||||
|
||||
// Atomically finds and updates an object based on query.
|
||||
// The result is the promise with an object that was in the database !AFTER! changes.
|
||||
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
|
||||
findOneAndUpdate(query, update) {
|
||||
// arguments: query, sort, update, options(optional)
|
||||
// Setting `new` option to true makes it return the after document, not the before one.
|
||||
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
})
|
||||
}
|
||||
|
||||
// Atomically find and delete an object based on query.
|
||||
// The result is the promise with an object that was in the database before deleting.
|
||||
// Postgres Note: Translates directly to `DELETE * FROM ... RETURNING *`, which will return data after delete is done.
|
||||
findOneAndDelete(query) {
|
||||
// arguments: query, sort
|
||||
return this._mongoCollection.findAndRemove(query, []).then(document => {
|
||||
// Value is the object where mongo returns multiple fields.
|
||||
return document.value;
|
||||
});
|
||||
}
|
||||
|
||||
drop() {
|
||||
return this._mongoCollection.drop();
|
||||
}
|
||||
}
|
||||
68
src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Normal file
68
src/Adapters/Storage/Mongo/MongoStorageAdapter.js
Normal file
@@ -0,0 +1,68 @@
|
||||
|
||||
import MongoCollection from './MongoCollection';
|
||||
|
||||
let mongodb = require('mongodb');
|
||||
let MongoClient = mongodb.MongoClient;
|
||||
|
||||
export class MongoStorageAdapter {
|
||||
// Private
|
||||
_uri: string;
|
||||
// Public
|
||||
connectionPromise;
|
||||
database;
|
||||
|
||||
constructor(uri: string) {
|
||||
this._uri = uri;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.connectionPromise) {
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = MongoClient.connect(this._uri).then(database => {
|
||||
this.database = database;
|
||||
});
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
collection(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collection(name);
|
||||
});
|
||||
}
|
||||
|
||||
adaptiveCollection(name: string) {
|
||||
return this.connect()
|
||||
.then(() => this.database.collection(name))
|
||||
.then(rawCollection => new MongoCollection(rawCollection));
|
||||
}
|
||||
|
||||
collectionExists(name: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.listCollections({ name: name }).toArray();
|
||||
}).then(collections => {
|
||||
return collections.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
dropCollection(name: string) {
|
||||
return this.collection(name).then(collection => collection.drop());
|
||||
}
|
||||
// Used for testing only right now.
|
||||
collectionsContaining(match: string) {
|
||||
return this.connect().then(() => {
|
||||
return this.database.collections();
|
||||
}).then(collections => {
|
||||
return collections.filter(collection => {
|
||||
if (collection.namespace.match(/\.system\./)) {
|
||||
return false;
|
||||
}
|
||||
return (collection.collectionName.indexOf(match) == 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MongoStorageAdapter;
|
||||
module.exports = MongoStorageAdapter; // Required for tests
|
||||
37
src/Auth.js
37
src/Auth.js
@@ -7,10 +7,11 @@ import cache from './cache';
|
||||
// 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, isMaster, userObject) {
|
||||
function Auth({ config, isMaster = false, user, installationId } = {}) {
|
||||
this.config = config;
|
||||
this.installationId = installationId;
|
||||
this.isMaster = isMaster;
|
||||
this.user = userObject;
|
||||
this.user = user;
|
||||
|
||||
// Assuming a users roles won't change during a single request, we'll
|
||||
// only load them once.
|
||||
@@ -33,19 +34,19 @@ Auth.prototype.couldUpdateUserId = function(userId) {
|
||||
|
||||
// A helper to get a master-level Auth object
|
||||
function master(config) {
|
||||
return new Auth(config, true, null);
|
||||
return new Auth({ config, isMaster: true });
|
||||
}
|
||||
|
||||
// A helper to get a nobody-level Auth object
|
||||
function nobody(config) {
|
||||
return new Auth(config, false, null);
|
||||
return new Auth({ config, isMaster: false });
|
||||
}
|
||||
|
||||
// Returns a promise that resolves to an Auth object
|
||||
var getAuthForSessionToken = function(config, sessionToken) {
|
||||
var cachedUser = cache.getUser(sessionToken);
|
||||
var getAuthForSessionToken = function({ config, sessionToken, installationId } = {}) {
|
||||
var cachedUser = cache.users.get(sessionToken);
|
||||
if (cachedUser) {
|
||||
return Promise.resolve(new Auth(config, false, cachedUser));
|
||||
return Promise.resolve(new Auth({ config, isMaster: false, installationId, user: cachedUser }));
|
||||
}
|
||||
var restOptions = {
|
||||
limit: 1,
|
||||
@@ -65,9 +66,9 @@ var getAuthForSessionToken = function(config, sessionToken) {
|
||||
delete obj.password;
|
||||
obj['className'] = '_User';
|
||||
obj['sessionToken'] = sessionToken;
|
||||
var userObject = Parse.Object.fromJSON(obj);
|
||||
cache.setUser(sessionToken, userObject);
|
||||
return new Auth(config, false, userObject);
|
||||
let userObject = Parse.Object.fromJSON(obj);
|
||||
cache.users.set(sessionToken, userObject);
|
||||
return new Auth({ config, isMaster: false, installationId, user: userObject });
|
||||
});
|
||||
};
|
||||
|
||||
@@ -159,6 +160,22 @@ Auth.prototype._getAllRoleNamesForId = function(roleID) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
var roleIDs = results.map(r => r.objectId);
|
||||
|
||||
var parentRolesPromises = roleIDs.map( (roleId) => {
|
||||
return this._getAllRoleNamesForId(roleId);
|
||||
});
|
||||
parentRolesPromises.push(Promise.resolve(roleIDs));
|
||||
return Promise.all(parentRolesPromises);
|
||||
}).then(function(results){
|
||||
// Flatten
|
||||
let roleIDs = results.reduce( (memo, result) => {
|
||||
if (typeof result == "object") {
|
||||
memo = memo.concat(result);
|
||||
} else {
|
||||
memo.push(result);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
return Promise.resolve(roleIDs);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,15 +7,12 @@ import cache from './cache';
|
||||
export class Config {
|
||||
constructor(applicationId: string, mount: string) {
|
||||
let DatabaseAdapter = require('./DatabaseAdapter');
|
||||
|
||||
let cacheInfo = cache.apps[applicationId];
|
||||
this.valid = !!cacheInfo;
|
||||
if (!this.valid) {
|
||||
let cacheInfo = cache.apps.get(applicationId);
|
||||
if (!cacheInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applicationId = applicationId;
|
||||
this.collectionPrefix = cacheInfo.collectionPrefix || '';
|
||||
this.masterKey = cacheInfo.masterKey;
|
||||
this.clientKey = cacheInfo.clientKey;
|
||||
this.javascriptKey = cacheInfo.javascriptKey;
|
||||
@@ -25,16 +22,64 @@ export class Config {
|
||||
this.facebookAppIds = cacheInfo.facebookAppIds;
|
||||
this.enableAnonymousUsers = cacheInfo.enableAnonymousUsers;
|
||||
this.allowClientClassCreation = cacheInfo.allowClientClassCreation;
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, this.collectionPrefix);
|
||||
this.database = DatabaseAdapter.getDatabaseConnection(applicationId, cacheInfo.collectionPrefix);
|
||||
|
||||
this.serverURL = cacheInfo.serverURL;
|
||||
this.publicServerURL = cacheInfo.publicServerURL;
|
||||
this.verifyUserEmails = cacheInfo.verifyUserEmails;
|
||||
this.appName = cacheInfo.appName;
|
||||
|
||||
this.hooksController = cacheInfo.hooksController;
|
||||
this.filesController = cacheInfo.filesController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.pushController = cacheInfo.pushController;
|
||||
this.loggerController = cacheInfo.loggerController;
|
||||
this.userController = cacheInfo.userController;
|
||||
this.oauth = cacheInfo.oauth;
|
||||
|
||||
this.customPages = cacheInfo.customPages || {};
|
||||
this.mount = mount;
|
||||
}
|
||||
}
|
||||
|
||||
static validate(options) {
|
||||
this.validateEmailConfiguration({verifyUserEmails: options.verifyUserEmails,
|
||||
appName: options.appName,
|
||||
publicServerURL: options.publicServerURL})
|
||||
}
|
||||
|
||||
static validateEmailConfiguration({verifyUserEmails, appName, publicServerURL}) {
|
||||
if (verifyUserEmails) {
|
||||
if (typeof appName !== 'string') {
|
||||
throw 'An app name is required when using email verification.';
|
||||
}
|
||||
if (typeof publicServerURL !== 'string') {
|
||||
throw 'A public server url is required when using email verification.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get invalidLinkURL() {
|
||||
return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`;
|
||||
}
|
||||
|
||||
get verifyEmailSuccessURL() {
|
||||
return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`;
|
||||
}
|
||||
|
||||
get choosePasswordURL() {
|
||||
return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`;
|
||||
}
|
||||
|
||||
get requestResetPasswordURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`;
|
||||
}
|
||||
|
||||
get passwordResetSuccessURL() {
|
||||
return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`;
|
||||
}
|
||||
|
||||
get verifyEmailURL() {
|
||||
return `${this.publicServerURL}/apps/${this.applicationId}/verify_email`;
|
||||
}
|
||||
};
|
||||
|
||||
export default Config;
|
||||
module.exports = Config;
|
||||
|
||||
@@ -10,13 +10,20 @@ based on the parameters passed
|
||||
|
||||
// _adapter is private, use Symbol
|
||||
var _adapter = Symbol();
|
||||
import Config from '../Config';
|
||||
|
||||
export class AdaptableController {
|
||||
|
||||
constructor(adapter) {
|
||||
constructor(adapter, appId, options) {
|
||||
this.options = options;
|
||||
this.appId = appId;
|
||||
this.adapter = adapter;
|
||||
this.setFeature();
|
||||
}
|
||||
|
||||
// sets features for Dashboard to consume from features router
|
||||
setFeature() {}
|
||||
|
||||
set adapter(adapter) {
|
||||
this.validateAdapter(adapter);
|
||||
this[_adapter] = adapter;
|
||||
@@ -26,12 +33,15 @@ export class AdaptableController {
|
||||
return this[_adapter];
|
||||
}
|
||||
|
||||
get config() {
|
||||
return new Config(this.appId);
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
throw new Error("Subclasses should implement expectedAdapterType()");
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
|
||||
if (!adapter) {
|
||||
throw new Error(this.constructor.name+" requires an adapter");
|
||||
}
|
||||
@@ -56,10 +66,9 @@ export class AdaptableController {
|
||||
}, {});
|
||||
|
||||
if (Object.keys(mismatches).length > 0) {
|
||||
console.error(adapter, mismatches);
|
||||
throw new Error("Adapter prototype don't match expected prototype");
|
||||
throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdaptableController;
|
||||
export default AdaptableController;
|
||||
|
||||
@@ -2,18 +2,17 @@
|
||||
// Parse database.
|
||||
|
||||
var mongodb = require('mongodb');
|
||||
var MongoClient = mongodb.MongoClient;
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
var Schema = require('./Schema');
|
||||
var transform = require('./transform');
|
||||
var Schema = require('./../Schema');
|
||||
var transform = require('./../transform');
|
||||
|
||||
// options can contain:
|
||||
// collectionPrefix: the string to put in front of every collection name.
|
||||
function ExportAdapter(mongoURI, options = {}) {
|
||||
this.mongoURI = mongoURI;
|
||||
function DatabaseController(adapter, { collectionPrefix } = {}) {
|
||||
this.adapter = adapter;
|
||||
|
||||
this.collectionPrefix = options.collectionPrefix;
|
||||
this.collectionPrefix = collectionPrefix;
|
||||
|
||||
// We don't want a mutable this.schema, because then you could have
|
||||
// one request that uses different schemas for different parts of
|
||||
@@ -25,25 +24,13 @@ function ExportAdapter(mongoURI, options = {}) {
|
||||
|
||||
// Connects to the database. Returns a promise that resolves when the
|
||||
// connection is successful.
|
||||
// this.db will be populated with a Mongo "Db" object when the
|
||||
// promise resolves successfully.
|
||||
ExportAdapter.prototype.connect = function() {
|
||||
if (this.connectionPromise) {
|
||||
// There's already a connection in progress.
|
||||
return this.connectionPromise;
|
||||
}
|
||||
|
||||
this.connectionPromise = Promise.resolve().then(() => {
|
||||
return MongoClient.connect(this.mongoURI);
|
||||
}).then((db) => {
|
||||
this.db = db;
|
||||
});
|
||||
return this.connectionPromise;
|
||||
DatabaseController.prototype.connect = function() {
|
||||
return this.adapter.connect();
|
||||
};
|
||||
|
||||
// Returns a promise for a Mongo collection.
|
||||
// Generally just for internal use.
|
||||
ExportAdapter.prototype.collection = function(className) {
|
||||
DatabaseController.prototype.collection = function(className) {
|
||||
if (!Schema.classNameIsValid(className)) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME,
|
||||
'invalid className: ' + className);
|
||||
@@ -51,10 +38,20 @@ ExportAdapter.prototype.collection = function(className) {
|
||||
return this.rawCollection(className);
|
||||
};
|
||||
|
||||
ExportAdapter.prototype.rawCollection = function(className) {
|
||||
return this.connect().then(() => {
|
||||
return this.db.collection(this.collectionPrefix + className);
|
||||
});
|
||||
DatabaseController.prototype.adaptiveCollection = function(className) {
|
||||
return this.adapter.adaptiveCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.collectionExists = function(className) {
|
||||
return this.adapter.collectionExists(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.rawCollection = function(className) {
|
||||
return this.adapter.collection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
DatabaseController.prototype.dropCollection = function(className) {
|
||||
return this.adapter.dropCollection(this.collectionPrefix + className);
|
||||
};
|
||||
|
||||
function returnsTrue() {
|
||||
@@ -64,7 +61,7 @@ function returnsTrue() {
|
||||
// Returns a promise for a schema object.
|
||||
// If we are provided a acceptor, then we run it on the schema.
|
||||
// If the schema isn't accepted, we reload it at most once.
|
||||
ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
if (!this.schemaPromise) {
|
||||
this.schemaPromise = this.collection('_SCHEMA').then((coll) => {
|
||||
@@ -88,8 +85,8 @@ ExportAdapter.prototype.loadSchema = function(acceptor = returnsTrue) {
|
||||
|
||||
// Returns a promise for the classname that is related to the given
|
||||
// classname through the key.
|
||||
// TODO: make this not in the ExportAdapter interface
|
||||
ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
|
||||
// TODO: make this not in the DatabaseController interface
|
||||
DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
|
||||
return this.loadSchema().then((schema) => {
|
||||
var t = schema.getExpectedType(className, key);
|
||||
var match = t.match(/^relation<(.*)>$/);
|
||||
@@ -105,7 +102,7 @@ ExportAdapter.prototype.redirectClassNameForKey = function(className, key) {
|
||||
// Returns a promise that resolves to the new schema.
|
||||
// This does not update this.schema, because in a situation like a
|
||||
// batch request, that could confuse other users of the schema.
|
||||
ExportAdapter.prototype.validateObject = function(className, object, query) {
|
||||
DatabaseController.prototype.validateObject = function(className, object, query) {
|
||||
return this.loadSchema().then((schema) => {
|
||||
return schema.validateObject(className, object, query);
|
||||
});
|
||||
@@ -113,7 +110,7 @@ ExportAdapter.prototype.validateObject = function(className, object, query) {
|
||||
|
||||
// Like transform.untransformObject but you need to provide a className.
|
||||
// Filters out any data that shouldn't be on this REST-formatted object.
|
||||
ExportAdapter.prototype.untransformObject = function(
|
||||
DatabaseController.prototype.untransformObject = function(
|
||||
schema, isMaster, aclGroup, className, mongoObject) {
|
||||
var object = transform.untransformObject(schema, className, mongoObject);
|
||||
|
||||
@@ -138,65 +135,59 @@ ExportAdapter.prototype.untransformObject = function(
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
ExportAdapter.prototype.update = function(className, query, update, options) {
|
||||
DatabaseController.prototype.update = function(className, query, update, options) {
|
||||
var acceptor = function(schema) {
|
||||
return schema.hasKeys(className, Object.keys(query));
|
||||
};
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
var mongoUpdate, schema;
|
||||
return this.loadSchema(acceptor).then((s) => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
return Promise.resolve();
|
||||
}).then(() => {
|
||||
|
||||
return this.handleRelationUpdates(className, query.objectId, update);
|
||||
}).then(() => {
|
||||
return this.collection(className);
|
||||
}).then((coll) => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
return this.loadSchema(acceptor)
|
||||
.then(s => {
|
||||
schema = s;
|
||||
if (!isMaster) {
|
||||
return schema.validatePermission(className, aclGroup, 'update');
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
|
||||
return coll.findAndModify(mongoWhere, {}, mongoUpdate, {});
|
||||
}).then((result) => {
|
||||
if (!result.value) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
if (result.lastErrorObject.n != 1) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
|
||||
var response = {};
|
||||
var inc = mongoUpdate['$inc'];
|
||||
if (inc) {
|
||||
for (var key in inc) {
|
||||
response[key] = (result.value[key] || 0) + inc[key];
|
||||
return Promise.resolve();
|
||||
})
|
||||
.then(() => this.handleRelationUpdates(className, query.objectId, update))
|
||||
.then(() => this.adaptiveCollection(className))
|
||||
.then(collection => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (options.acl) {
|
||||
var writePerms = [
|
||||
{_wperm: {'$exists': false}}
|
||||
];
|
||||
for (var entry of options.acl) {
|
||||
writePerms.push({_wperm: {'$in': [entry]}});
|
||||
}
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': writePerms}]};
|
||||
}
|
||||
}
|
||||
return response;
|
||||
});
|
||||
mongoUpdate = transform.transformUpdate(schema, className, update);
|
||||
return collection.findOneAndUpdate(mongoWhere, mongoUpdate);
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found.'));
|
||||
}
|
||||
|
||||
let response = {};
|
||||
let inc = mongoUpdate['$inc'];
|
||||
if (inc) {
|
||||
Object.keys(inc).forEach(key => {
|
||||
response[key] = result[key];
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
// Processes relation-updating operations from a REST-format update.
|
||||
// Returns a promise that resolves successfully when these are
|
||||
// processed.
|
||||
// This mutates update.
|
||||
ExportAdapter.prototype.handleRelationUpdates = function(className,
|
||||
DatabaseController.prototype.handleRelationUpdates = function(className,
|
||||
objectId,
|
||||
update) {
|
||||
var pending = [];
|
||||
@@ -243,7 +234,7 @@ ExportAdapter.prototype.handleRelationUpdates = function(className,
|
||||
|
||||
// Adds a relation.
|
||||
// Returns a promise that resolves successfully iff the add was successful.
|
||||
ExportAdapter.prototype.addRelation = function(key, fromClassName,
|
||||
DatabaseController.prototype.addRelation = function(key, fromClassName,
|
||||
fromId, toId) {
|
||||
var doc = {
|
||||
relatedId: toId,
|
||||
@@ -258,7 +249,7 @@ ExportAdapter.prototype.addRelation = function(key, fromClassName,
|
||||
// Removes a relation.
|
||||
// Returns a promise that resolves successfully iff the remove was
|
||||
// successful.
|
||||
ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||
DatabaseController.prototype.removeRelation = function(key, fromClassName,
|
||||
fromId, toId) {
|
||||
var doc = {
|
||||
relatedId: toId,
|
||||
@@ -277,7 +268,7 @@ ExportAdapter.prototype.removeRelation = function(key, fromClassName,
|
||||
// acl: a list of strings. If the object to be updated has an ACL,
|
||||
// one of the provided strings must provide the caller with
|
||||
// write permissions.
|
||||
ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||
DatabaseController.prototype.destroy = function(className, query, options = {}) {
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
|
||||
@@ -320,7 +311,7 @@ ExportAdapter.prototype.destroy = function(className, query, options = {}) {
|
||||
|
||||
// Inserts an object into the database.
|
||||
// Returns a promise that resolves successfully iff the object saved.
|
||||
ExportAdapter.prototype.create = function(className, object, options) {
|
||||
DatabaseController.prototype.create = function(className, object, options) {
|
||||
var schema;
|
||||
var isMaster = !('acl' in options);
|
||||
var aclGroup = options.acl || [];
|
||||
@@ -346,28 +337,21 @@ ExportAdapter.prototype.create = function(className, object, options) {
|
||||
// This should only be used for testing - use 'find' for normal code
|
||||
// to avoid Mongo-format dependencies.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
ExportAdapter.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.collection(className).then((coll) => {
|
||||
return coll.find(query, options).toArray();
|
||||
});
|
||||
DatabaseController.prototype.mongoFind = function(className, query, options = {}) {
|
||||
return this.adaptiveCollection(className)
|
||||
.then(collection => collection.find(query, options));
|
||||
};
|
||||
|
||||
// Deletes everything in the database matching the current collectionPrefix
|
||||
// Won't delete collections in the system namespace
|
||||
// Returns a promise.
|
||||
ExportAdapter.prototype.deleteEverything = function() {
|
||||
DatabaseController.prototype.deleteEverything = function() {
|
||||
this.schemaPromise = null;
|
||||
|
||||
return this.connect().then(() => {
|
||||
return this.db.collections();
|
||||
}).then((colls) => {
|
||||
var promises = [];
|
||||
for (var coll of colls) {
|
||||
if (!coll.namespace.match(/\.system\./) &&
|
||||
coll.collectionName.indexOf(this.collectionPrefix) === 0) {
|
||||
promises.push(coll.drop());
|
||||
}
|
||||
}
|
||||
return this.adapter.collectionsContaining(this.collectionPrefix).then(collections => {
|
||||
let promises = collections.map(collection => {
|
||||
return collection.drop();
|
||||
});
|
||||
return Promise.all(promises);
|
||||
});
|
||||
};
|
||||
@@ -376,13 +360,11 @@ ExportAdapter.prototype.deleteEverything = function() {
|
||||
function keysForQuery(query) {
|
||||
var sublist = query['$and'] || query['$or'];
|
||||
if (sublist) {
|
||||
var answer = new Set();
|
||||
for (var subquery of sublist) {
|
||||
for (var key of keysForQuery(subquery)) {
|
||||
answer.add(key);
|
||||
}
|
||||
}
|
||||
return answer;
|
||||
let answer = sublist.reduce((memo, subquery) => {
|
||||
return memo.concat(keysForQuery(subquery));
|
||||
}, []);
|
||||
|
||||
return new Set(answer);
|
||||
}
|
||||
|
||||
return new Set(Object.keys(query));
|
||||
@@ -390,59 +372,74 @@ function keysForQuery(query) {
|
||||
|
||||
// Returns a promise for a list of related ids given an owning id.
|
||||
// className here is the owning className.
|
||||
ExportAdapter.prototype.relatedIds = function(className, key, owningId) {
|
||||
var joinTable = '_Join:' + key + ':' + className;
|
||||
return this.collection(joinTable).then((coll) => {
|
||||
return coll.find({owningId: owningId}).toArray();
|
||||
}).then((results) => {
|
||||
return results.map(r => r.relatedId);
|
||||
});
|
||||
DatabaseController.prototype.relatedIds = function(className, key, owningId) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({owningId : owningId}))
|
||||
.then(results => results.map(r => r.relatedId));
|
||||
};
|
||||
|
||||
// Returns a promise for a list of owning ids given some related ids.
|
||||
// className here is the owning className.
|
||||
ExportAdapter.prototype.owningIds = function(className, key, relatedIds) {
|
||||
var joinTable = '_Join:' + key + ':' + className;
|
||||
return this.collection(joinTable).then((coll) => {
|
||||
return coll.find({relatedId: {'$in': relatedIds}}).toArray();
|
||||
}).then((results) => {
|
||||
return results.map(r => r.owningId);
|
||||
});
|
||||
DatabaseController.prototype.owningIds = function(className, key, relatedIds) {
|
||||
return this.adaptiveCollection(joinTableName(className, key))
|
||||
.then(coll => coll.find({ relatedId: { '$in': relatedIds } }))
|
||||
.then(results => results.map(r => r.owningId));
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $in on relation fields, or
|
||||
// equal-to-pointer constraints on relation fields.
|
||||
// Returns a promise that resolves when query is mutated
|
||||
// TODO: this only handles one of these at a time - make it handle more
|
||||
ExportAdapter.prototype.reduceInRelation = function(className, query, schema) {
|
||||
DatabaseController.prototype.reduceInRelation = function(className, query, schema) {
|
||||
|
||||
// Search for an in-relation or equal-to-relation
|
||||
for (var key in query) {
|
||||
if (query[key] &&
|
||||
(query[key]['$in'] || query[key].__type == 'Pointer')) {
|
||||
var t = schema.getExpectedType(className, key);
|
||||
var match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
// Make it sequential for now, not sure of paralleization side effects
|
||||
if (query['$or']) {
|
||||
let ors = query['$or'];
|
||||
return Promise.all(ors.map((aQuery, index) => {
|
||||
return this.reduceInRelation(className, aQuery, schema).then((aQuery) => {
|
||||
query['$or'][index] = aQuery;
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
let promises = Object.keys(query).map((key) => {
|
||||
if (query[key] && (query[key]['$in'] || query[key].__type == 'Pointer')) {
|
||||
let t = schema.getExpectedType(className, key);
|
||||
let match = t ? t.match(/^relation<(.*)>$/) : false;
|
||||
if (!match) {
|
||||
continue;
|
||||
return Promise.resolve(query);
|
||||
}
|
||||
var relatedClassName = match[1];
|
||||
var relatedIds;
|
||||
let relatedClassName = match[1];
|
||||
let relatedIds;
|
||||
if (query[key]['$in']) {
|
||||
relatedIds = query[key]['$in'].map(r => r.objectId);
|
||||
} else {
|
||||
relatedIds = [query[key].objectId];
|
||||
}
|
||||
return this.owningIds(className, key, relatedIds).then((ids) => {
|
||||
delete query[key];
|
||||
query.objectId = {'$in': ids};
|
||||
delete query[key];
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
return Promise.resolve(query);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve(query);
|
||||
})
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
return Promise.resolve(query);
|
||||
})
|
||||
};
|
||||
|
||||
// Modifies query so that it no longer has $relatedTo
|
||||
// Returns a promise that resolves when query is mutated
|
||||
ExportAdapter.prototype.reduceRelationKeys = function(className, query) {
|
||||
DatabaseController.prototype.reduceRelationKeys = function(className, query) {
|
||||
|
||||
if (query['$or']) {
|
||||
return Promise.all(query['$or'].map((aQuery) => {
|
||||
return this.reduceRelationKeys(className, aQuery);
|
||||
}));
|
||||
}
|
||||
|
||||
var relatedTo = query['$relatedTo'];
|
||||
if (relatedTo) {
|
||||
return this.relatedIds(
|
||||
@@ -450,43 +447,22 @@ ExportAdapter.prototype.reduceRelationKeys = function(className, query) {
|
||||
relatedTo.key,
|
||||
relatedTo.object.objectId).then((ids) => {
|
||||
delete query['$relatedTo'];
|
||||
query['objectId'] = {'$in': ids};
|
||||
this.addInObjectIdsIds(ids, query);
|
||||
return this.reduceRelationKeys(className, query);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Does a find with "smart indexing".
|
||||
// Currently this just means, if it needs a geoindex and there is
|
||||
// none, then build the geoindex.
|
||||
// This could be improved a lot but it's not clear if that's a good
|
||||
// idea. Or even if this behavior is a good idea.
|
||||
ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
return coll.find(where, options).toArray()
|
||||
.then((result) => {
|
||||
return result;
|
||||
}, (error) => {
|
||||
// Check for "no geoindex" error
|
||||
if (!error.message.match(/unable to find index for .geoNear/) ||
|
||||
error.code != 17007) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Figure out what key needs an index
|
||||
var key = error.message.match(/field=([A-Za-z_0-9]+) /)[1];
|
||||
if (!key) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var index = {};
|
||||
index[key] = '2d';
|
||||
//TODO: condiser moving index creation logic into Schema.js
|
||||
return coll.createIndex(index).then(() => {
|
||||
// Retry, but just once.
|
||||
return coll.find(where, options).toArray();
|
||||
});
|
||||
});
|
||||
};
|
||||
DatabaseController.prototype.addInObjectIdsIds = function(ids, query) {
|
||||
if (typeof query.objectId == 'string') {
|
||||
query.objectId = {'$in': [query.objectId]};
|
||||
}
|
||||
query.objectId = query.objectId || {};
|
||||
let queryIn = [].concat(query.objectId['$in'] || [], ids || []);
|
||||
// make a set and spread to remove duplicates
|
||||
query.objectId = {'$in': [...new Set(queryIn)]};
|
||||
return query;
|
||||
}
|
||||
|
||||
// Runs a query on the database.
|
||||
// Returns a promise that resolves to a list of items.
|
||||
@@ -502,7 +478,7 @@ ExportAdapter.prototype.smartFind = function(coll, where, options) {
|
||||
// TODO: make userIds not needed here. The db adapter shouldn't know
|
||||
// anything about users, ideally. Then, improve the format of the ACL
|
||||
// arg to work like the others.
|
||||
ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
DatabaseController.prototype.find = function(className, query, options = {}) {
|
||||
var mongoOptions = {};
|
||||
if (options.skip) {
|
||||
mongoOptions.skip = options.skip;
|
||||
@@ -541,8 +517,8 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
}).then(() => {
|
||||
return this.reduceInRelation(className, query, schema);
|
||||
}).then(() => {
|
||||
return this.collection(className);
|
||||
}).then((coll) => {
|
||||
return this.adaptiveCollection(className);
|
||||
}).then(collection => {
|
||||
var mongoWhere = transform.transformWhere(schema, className, query);
|
||||
if (!isMaster) {
|
||||
var orParts = [
|
||||
@@ -555,9 +531,10 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
mongoWhere = {'$and': [mongoWhere, {'$or': orParts}]};
|
||||
}
|
||||
if (options.count) {
|
||||
return coll.count(mongoWhere, mongoOptions);
|
||||
delete mongoOptions.limit;
|
||||
return collection.count(mongoWhere, mongoOptions);
|
||||
} else {
|
||||
return this.smartFind(coll, mongoWhere, mongoOptions)
|
||||
return collection.find(mongoWhere, mongoOptions)
|
||||
.then((mongoResults) => {
|
||||
return mongoResults.map((r) => {
|
||||
return this.untransformObject(
|
||||
@@ -568,4 +545,8 @@ ExportAdapter.prototype.find = function(className, query, options = {}) {
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = ExportAdapter;
|
||||
function joinTableName(className, key) {
|
||||
return `_Join:${key}:${className}`;
|
||||
}
|
||||
|
||||
module.exports = DatabaseController;
|
||||
@@ -3,6 +3,8 @@ import { Parse } from 'parse/node';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { FilesAdapter } from '../Adapters/Files/FilesAdapter';
|
||||
import path from 'path';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesController extends AdaptableController {
|
||||
|
||||
@@ -10,10 +12,22 @@ export class FilesController extends AdaptableController {
|
||||
return this.adapter.getFileData(config, filename);
|
||||
}
|
||||
|
||||
createFile(config, filename, data) {
|
||||
createFile(config, filename, data, contentType) {
|
||||
|
||||
let extname = path.extname(filename);
|
||||
|
||||
const hasExtension = extname.length > 0;
|
||||
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
filename = filename + '.' + mime.extension(contentType);
|
||||
} else if (hasExtension && !contentType) {
|
||||
contentType = mime.lookup(filename);
|
||||
}
|
||||
|
||||
filename = randomHexString(32) + '_' + filename;
|
||||
|
||||
var location = this.adapter.getFileLocation(config, filename);
|
||||
return this.adapter.createFile(config, filename, data).then(() => {
|
||||
return this.adapter.createFile(config, filename, data, contentType).then(() => {
|
||||
return Promise.resolve({
|
||||
url: location,
|
||||
name: filename
|
||||
|
||||
@@ -3,9 +3,18 @@ import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import { PushAdapter } from '../Adapters/Push/PushAdapter';
|
||||
import deepcopy from 'deepcopy';
|
||||
import features from '../features';
|
||||
|
||||
const FEATURE_NAME = 'push';
|
||||
const UNSUPPORTED_BADGE_KEY = "unsupported";
|
||||
|
||||
export class PushController extends AdaptableController {
|
||||
|
||||
setFeature() {
|
||||
features.setFeature(FEATURE_NAME, this.adapter.feature || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the deviceType parameter in qury condition is valid or not.
|
||||
* @param {Object} where A query condition
|
||||
@@ -51,7 +60,55 @@ export class PushController extends AdaptableController {
|
||||
body['expiration_time'] = PushController.getExpirationTime(body);
|
||||
// TODO: If the req can pass the checking, we return immediately instead of waiting
|
||||
// pushes to be sent. We probably change this behaviour in the future.
|
||||
rest.find(config, auth, '_Installation', where).then(function(response) {
|
||||
let badgeUpdate = Promise.resolve();
|
||||
|
||||
if (body.badge) {
|
||||
var op = {};
|
||||
if (body.badge == "Increment") {
|
||||
op = {'$inc': {'badge': 1}}
|
||||
} else if (Number(body.badge)) {
|
||||
op = {'$set': {'badge': body.badge } }
|
||||
} else {
|
||||
throw "Invalid value for badge, expected number or 'Increment'";
|
||||
}
|
||||
let updateWhere = deepcopy(where);
|
||||
|
||||
// Only on iOS!
|
||||
updateWhere.deviceType = 'ios';
|
||||
|
||||
// TODO: @nlutsenko replace with better thing
|
||||
badgeUpdate = config.database.rawCollection("_Installation").then((coll) => {
|
||||
return coll.update(updateWhere, op, { multi: true });
|
||||
});
|
||||
}
|
||||
|
||||
return badgeUpdate.then(() => {
|
||||
return rest.find(config, auth, '_Installation', where)
|
||||
}).then((response) => {
|
||||
if (body.badge && body.badge == "Increment") {
|
||||
// Collect the badges to reduce the # of calls
|
||||
let badgeInstallationsMap = response.results.reduce((map, installation) => {
|
||||
let badge = installation.badge;
|
||||
if (installation.deviceType != "ios") {
|
||||
badge = UNSUPPORTED_BADGE_KEY;
|
||||
}
|
||||
map[badge] = map[badge] || [];
|
||||
map[badge].push(installation);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// Map the on the badges count and return the send result
|
||||
let promises = Object.keys(badgeInstallationsMap).map((badge) => {
|
||||
let payload = deepcopy(body);
|
||||
if (badge == UNSUPPORTED_BADGE_KEY) {
|
||||
delete payload.badge;
|
||||
} else {
|
||||
payload.badge = parseInt(badge);
|
||||
}
|
||||
return pushAdapter.send(payload, badgeInstallationsMap[badge]);
|
||||
});
|
||||
return Promise.all(promises);
|
||||
}
|
||||
return pushAdapter.send(body, response.results);
|
||||
});
|
||||
}
|
||||
|
||||
203
src/Controllers/UserController.js
Normal file
203
src/Controllers/UserController.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { randomString } from '../cryptoUtils';
|
||||
import { inflate } from '../triggers';
|
||||
import AdaptableController from './AdaptableController';
|
||||
import MailAdapter from '../Adapters/Email/MailAdapter';
|
||||
|
||||
var DatabaseAdapter = require('../DatabaseAdapter');
|
||||
var RestWrite = require('../RestWrite');
|
||||
var RestQuery = require('../RestQuery');
|
||||
var hash = require('../password').hash;
|
||||
var Auth = require('../Auth');
|
||||
|
||||
export class UserController extends AdaptableController {
|
||||
|
||||
constructor(adapter, appId, options = {}) {
|
||||
super(adapter, appId, options);
|
||||
}
|
||||
|
||||
validateAdapter(adapter) {
|
||||
// Allow no adapter
|
||||
if (!adapter && !this.shouldVerifyEmails) {
|
||||
return;
|
||||
}
|
||||
super.validateAdapter(adapter);
|
||||
}
|
||||
|
||||
expectedAdapterType() {
|
||||
return MailAdapter;
|
||||
}
|
||||
|
||||
get shouldVerifyEmails() {
|
||||
return this.options.verifyUserEmails;
|
||||
}
|
||||
|
||||
setEmailVerifyToken(user) {
|
||||
if (this.shouldVerifyEmails) {
|
||||
user._email_verify_token = randomString(25);
|
||||
user.emailVerified = false;
|
||||
}
|
||||
}
|
||||
|
||||
verifyEmail(username, token) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
// Trying to verify email when not enabled
|
||||
// TODO: Better error here.
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate({
|
||||
username: username,
|
||||
_email_verify_token: token
|
||||
}, {$set: {emailVerified: true}});
|
||||
})
|
||||
.then(document => {
|
||||
if (!document) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return document;
|
||||
});
|
||||
}
|
||||
|
||||
checkResetTokenValidity(username, token) {
|
||||
return this.config.database.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
return collection.find({
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, { limit: 1 });
|
||||
})
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return results[0];
|
||||
});
|
||||
}
|
||||
|
||||
getUserIfNeeded(user) {
|
||||
if (user.username && user.email) {
|
||||
return Promise.resolve(user);
|
||||
}
|
||||
var where = {};
|
||||
if (user.username) {
|
||||
where.username = user.username;
|
||||
}
|
||||
if (user.email) {
|
||||
where.email = user.email;
|
||||
}
|
||||
|
||||
var query = new RestQuery(this.config, Auth.master(this.config), '_User', where);
|
||||
return query.execute().then(function(result){
|
||||
if (result.results.length != 1) {
|
||||
return Promise.reject();
|
||||
}
|
||||
return result.results[0];
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
sendVerificationEmail(user) {
|
||||
if (!this.shouldVerifyEmails) {
|
||||
return;
|
||||
}
|
||||
// We may need to fetch the user in case of update email
|
||||
this.getUserIfNeeded(user).then((user) => {
|
||||
const token = encodeURIComponent(user._email_verify_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
let link = `${this.config.verifyEmailURL}?token=${token}&username=${username}`;
|
||||
let options = {
|
||||
appName: this.config.appName,
|
||||
link: link,
|
||||
user: inflate('_User', user),
|
||||
};
|
||||
if (this.adapter.sendVerificationEmail) {
|
||||
this.adapter.sendVerificationEmail(options);
|
||||
} else {
|
||||
this.adapter.sendMail(this.defaultVerificationEmail(options));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setPasswordResetToken(email) {
|
||||
let token = randomString(25);
|
||||
return this.config.database
|
||||
.adaptiveCollection('_User')
|
||||
.then(collection => {
|
||||
// Need direct database access because verification token is not a parse field
|
||||
return collection.findOneAndUpdate(
|
||||
{ email: email}, // query
|
||||
{ $set: { _perishable_token: token } } // update
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sendPasswordResetEmail(email) {
|
||||
if (!this.adapter) {
|
||||
throw "Trying to send a reset password but no adapter is set";
|
||||
// TODO: No adapter?
|
||||
return;
|
||||
}
|
||||
|
||||
return this.setPasswordResetToken(email).then((user) => {
|
||||
|
||||
const token = encodeURIComponent(user._perishable_token);
|
||||
const username = encodeURIComponent(user.username);
|
||||
let link = `${this.config.requestResetPasswordURL}?token=${token}&username=${username}`
|
||||
|
||||
let 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));
|
||||
}
|
||||
|
||||
return Promise.resolve(user);
|
||||
});
|
||||
}
|
||||
|
||||
updatePassword(username, token, password, config) {
|
||||
return this.checkResetTokenValidity(username, token).then(() => {
|
||||
return updateUserPassword(username, token, password, this.config);
|
||||
});
|
||||
}
|
||||
|
||||
defaultVerificationEmail({link, user, appName, }) {
|
||||
let text = "Hi,\n\n" +
|
||||
"You are being asked to confirm the e-mail address " + user.email + " with " + appName + "\n\n" +
|
||||
"" +
|
||||
"Click here to confirm it:\n" + link;
|
||||
let to = user.get("email");
|
||||
let subject = 'Please verify your e-mail for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
|
||||
defaultResetPasswordEmail({link, user, appName, }) {
|
||||
let text = "Hi,\n\n" +
|
||||
"You requested to reset your password for " + appName + ".\n\n" +
|
||||
"" +
|
||||
"Click here to reset it:\n" + link;
|
||||
let to = user.get("email");
|
||||
let subject = 'Password Reset for ' + appName;
|
||||
return { text, to, subject };
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this private
|
||||
function updateUserPassword(username, token, password, config) {
|
||||
var write = new RestWrite(config, Auth.master(config), '_User', {
|
||||
username: username,
|
||||
_perishable_token: token
|
||||
}, {password: password, _perishable_token: null }, undefined);
|
||||
return write.execute();
|
||||
}
|
||||
|
||||
export default UserController;
|
||||
@@ -13,14 +13,17 @@
|
||||
// * destroy(className, query, options)
|
||||
// * This list is incomplete and the database process is not fully modularized.
|
||||
//
|
||||
// Default is ExportAdapter, which uses mongo.
|
||||
// Default is MongoStorageAdapter.
|
||||
|
||||
var ExportAdapter = require('./ExportAdapter');
|
||||
import DatabaseController from './Controllers/DatabaseController';
|
||||
import MongoStorageAdapter from './Adapters/Storage/Mongo/MongoStorageAdapter';
|
||||
|
||||
var adapter = ExportAdapter;
|
||||
var dbConnections = {};
|
||||
var databaseURI = 'mongodb://localhost:27017/parse';
|
||||
var appDatabaseURIs = {};
|
||||
const DefaultDatabaseURI = 'mongodb://localhost:27017/parse';
|
||||
|
||||
let adapter = MongoStorageAdapter;
|
||||
let dbConnections = {};
|
||||
let databaseURI = DefaultDatabaseURI;
|
||||
let appDatabaseURIs = {};
|
||||
|
||||
function setAdapter(databaseAdapter) {
|
||||
adapter = databaseAdapter;
|
||||
@@ -46,10 +49,11 @@ function getDatabaseConnection(appId: string, collectionPrefix: string) {
|
||||
}
|
||||
|
||||
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
|
||||
dbConnections[appId] = new adapter(dbURI, {
|
||||
|
||||
let storageAdapter = new adapter(dbURI);
|
||||
dbConnections[appId] = new DatabaseController(storageAdapter, {
|
||||
collectionPrefix: collectionPrefix
|
||||
});
|
||||
dbConnections[appId].connect();
|
||||
return dbConnections[appId];
|
||||
}
|
||||
|
||||
@@ -59,5 +63,6 @@ module.exports = {
|
||||
setAdapter: setAdapter,
|
||||
setDatabaseURI: setDatabaseURI,
|
||||
setAppDatabaseURI: setAppDatabaseURI,
|
||||
clearDatabaseURIs: clearDatabaseURIs
|
||||
clearDatabaseURIs: clearDatabaseURIs,
|
||||
defaultDatabaseURI: databaseURI
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// themselves use our routing information, without disturbing express
|
||||
// components that external developers may be modifying.
|
||||
|
||||
import express from 'express';
|
||||
|
||||
export default class PromiseRouter {
|
||||
// Each entry should be an object with:
|
||||
// path: the path to route, in express format
|
||||
@@ -15,8 +17,8 @@ export default class PromiseRouter {
|
||||
// status: optional. the http status code. defaults to 200
|
||||
// response: a json object with the content of the response
|
||||
// location: optional. a location header
|
||||
constructor() {
|
||||
this.routes = [];
|
||||
constructor(routes = []) {
|
||||
this.routes = routes;
|
||||
this.mountRoutes();
|
||||
}
|
||||
|
||||
@@ -47,17 +49,11 @@ export default class PromiseRouter {
|
||||
if (handlers.length > 1) {
|
||||
const length = handlers.length;
|
||||
handler = function(req) {
|
||||
var next = function(i, req, res) {
|
||||
if (i == length) {
|
||||
return res;
|
||||
}
|
||||
let result = handlers[i](req);
|
||||
if (!result || typeof result.then !== "function") {
|
||||
result = Promise.resolve(result);
|
||||
}
|
||||
return result.then((res) => (next(i+1, req, res)));
|
||||
}
|
||||
return next(0, req);
|
||||
return handlers.reduce((promise, handler) => {
|
||||
return promise.then((result) => {
|
||||
return handler(req);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +121,29 @@ export default class PromiseRouter {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
expressApp() {
|
||||
var expressApp = express();
|
||||
for (var route of this.routes) {
|
||||
switch(route.method) {
|
||||
case 'POST':
|
||||
expressApp.post(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'GET':
|
||||
expressApp.get(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'PUT':
|
||||
expressApp.put(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
case 'DELETE':
|
||||
expressApp.delete(route.path, makeExpressHandler(route.handler));
|
||||
break;
|
||||
default:
|
||||
throw 'unexpected code branch';
|
||||
}
|
||||
}
|
||||
return expressApp;
|
||||
}
|
||||
}
|
||||
|
||||
// Global flag. Set this to true to log every request and response.
|
||||
@@ -142,15 +161,24 @@ function makeExpressHandler(promiseHandler) {
|
||||
JSON.stringify(req.body, null, 2));
|
||||
}
|
||||
promiseHandler(req).then((result) => {
|
||||
if (!result.response) {
|
||||
console.log('BUG: the handler did not include a "response" field');
|
||||
if (!result.response && !result.location && !result.text) {
|
||||
console.log('BUG: the handler did not include a "response" or a "location" field');
|
||||
throw 'control should not get here';
|
||||
}
|
||||
if (PromiseRouter.verbose) {
|
||||
console.log('response:', JSON.stringify(result.response, null, 2));
|
||||
console.log('response:', JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
var status = result.status || 200;
|
||||
res.status(status);
|
||||
|
||||
if (result.text) {
|
||||
return res.send(result.text);
|
||||
}
|
||||
|
||||
if (result.location && !result.response) {
|
||||
return res.redirect(result.location);
|
||||
}
|
||||
if (result.location) {
|
||||
res.set('Location', result.location);
|
||||
}
|
||||
|
||||
@@ -165,7 +165,9 @@ RestQuery.prototype.redirectClassNameForKey = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestQuery.prototype.validateClientClassCreation = function() {
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema().then((schema) => {
|
||||
return schema.hasClass(this.className)
|
||||
}).then((hasClass) => {
|
||||
@@ -212,7 +214,11 @@ RestQuery.prototype.replaceInQuery = function() {
|
||||
});
|
||||
}
|
||||
delete inQueryObject['$inQuery'];
|
||||
inQueryObject['$in'] = values;
|
||||
if (Array.isArray(inQueryObject['$in'])) {
|
||||
inQueryObject['$in'] = inQueryObject['$in'].concat(values);
|
||||
} else {
|
||||
inQueryObject['$in'] = values;
|
||||
}
|
||||
|
||||
// Recurse to repeat
|
||||
return this.replaceInQuery();
|
||||
@@ -249,7 +255,11 @@ RestQuery.prototype.replaceNotInQuery = function() {
|
||||
});
|
||||
}
|
||||
delete notInQueryObject['$notInQuery'];
|
||||
notInQueryObject['$nin'] = values;
|
||||
if (Array.isArray(notInQueryObject['$nin'])) {
|
||||
notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values);
|
||||
} else {
|
||||
notInQueryObject['$nin'] = values;
|
||||
}
|
||||
|
||||
// Recurse to repeat
|
||||
return this.replaceNotInQuery();
|
||||
@@ -269,11 +279,11 @@ 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 ||
|
||||
!selectValue.query.where ||
|
||||
Object.keys(selectValue).length !== 2) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_QUERY,
|
||||
'improper usage of $select');
|
||||
@@ -288,7 +298,11 @@ RestQuery.prototype.replaceSelect = function() {
|
||||
values.push(result[selectValue.key]);
|
||||
}
|
||||
delete selectObject['$select'];
|
||||
selectObject['$in'] = values;
|
||||
if (Array.isArray(selectObject['$in'])) {
|
||||
selectObject['$in'] = selectObject['$in'].concat(values);
|
||||
} else {
|
||||
selectObject['$in'] = values;
|
||||
}
|
||||
|
||||
// Keep replacing $select clauses
|
||||
return this.replaceSelect();
|
||||
@@ -327,7 +341,11 @@ RestQuery.prototype.replaceDontSelect = function() {
|
||||
values.push(result[dontSelectValue.key]);
|
||||
}
|
||||
delete dontSelectObject['$dontSelect'];
|
||||
dontSelectObject['$nin'] = values;
|
||||
if (Array.isArray(dontSelectObject['$nin'])) {
|
||||
dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values);
|
||||
} else {
|
||||
dontSelectObject['$nin'] = values;
|
||||
}
|
||||
|
||||
// Keep replacing $dontSelect clauses
|
||||
return this.replaceDontSelect();
|
||||
@@ -507,7 +525,7 @@ function replacePointers(object, path, replace) {
|
||||
}
|
||||
|
||||
if (path.length == 0) {
|
||||
if (object.__type == 'Pointer' && replace[object.objectId]) {
|
||||
if (object.__type == 'Pointer') {
|
||||
return replace[object.objectId];
|
||||
}
|
||||
return object;
|
||||
|
||||
@@ -67,12 +67,12 @@ RestWrite.prototype.execute = function() {
|
||||
return this.handleInstallation();
|
||||
}).then(() => {
|
||||
return this.handleSession();
|
||||
}).then(() => {
|
||||
return this.validateAuthData();
|
||||
}).then(() => {
|
||||
return this.runBeforeTrigger();
|
||||
}).then(() => {
|
||||
return this.setRequiredFieldsIfNeeded();
|
||||
}).then(() => {
|
||||
return this.validateAuthData();
|
||||
}).then(() => {
|
||||
return this.transformUser();
|
||||
}).then(() => {
|
||||
@@ -109,7 +109,9 @@ RestWrite.prototype.getUserAndRoleACL = function() {
|
||||
|
||||
// Validates this operation against the allowClientClassCreation config.
|
||||
RestWrite.prototype.validateClientClassCreation = function() {
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster) {
|
||||
let sysClass = ['_User', '_Installation', '_Role', '_Session', '_Product'];
|
||||
if (this.config.allowClientClassCreation === false && !this.auth.isMaster
|
||||
&& sysClass.indexOf(this.className) === -1) {
|
||||
return this.config.database.loadSchema().then((schema) => {
|
||||
return schema.hasClass(this.className)
|
||||
}).then((hasClass) => {
|
||||
@@ -134,6 +136,10 @@ RestWrite.prototype.validateSchema = function() {
|
||||
// Runs any beforeSave triggers against this operation.
|
||||
// Any change leads to our data being mutated.
|
||||
RestWrite.prototype.runBeforeTrigger = function() {
|
||||
if (this.response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid doing any setup for triggers if there is no 'beforeSave' trigger for this class.
|
||||
if (!triggers.triggerExists(this.className, triggers.Types.beforeSave, this.config.applicationId)) {
|
||||
return Promise.resolve();
|
||||
@@ -459,12 +465,18 @@ RestWrite.prototype.transformUser = function() {
|
||||
'address');
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
}).then(() => {
|
||||
// We updated the email, send a new validation
|
||||
this.storage['sendVerificationEmail'] = true;
|
||||
this.config.userController.setEmailVerifyToken(this.data);
|
||||
return Promise.resolve();
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// Handles any followup logic
|
||||
RestWrite.prototype.handleFollowup = function() {
|
||||
|
||||
if (this.storage && this.storage['clearSessions']) {
|
||||
var sessionQuery = {
|
||||
user: {
|
||||
@@ -474,9 +486,16 @@ RestWrite.prototype.handleFollowup = function() {
|
||||
}
|
||||
};
|
||||
delete this.storage['clearSessions'];
|
||||
return this.config.database.destroy('_Session', sessionQuery)
|
||||
this.config.database.destroy('_Session', sessionQuery)
|
||||
.then(this.handleFollowup.bind(this));
|
||||
}
|
||||
|
||||
if (this.storage && this.storage['sendVerificationEmail']) {
|
||||
delete this.storage['sendVerificationEmail'];
|
||||
// Fire and forget!
|
||||
this.config.userController.sendVerificationEmail(this.data);
|
||||
this.handleFollowup.bind(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Handles the _Role class specialness.
|
||||
@@ -592,6 +611,9 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
|
||||
var promise = Promise.resolve();
|
||||
|
||||
var idMatch; // Will be a match on either objectId or installationId
|
||||
var deviceTokenMatches = [];
|
||||
|
||||
if (this.query && this.query.objectId) {
|
||||
promise = promise.then(() => {
|
||||
return this.config.database.find('_Installation', {
|
||||
@@ -601,22 +623,22 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
||||
'Object not found for update.');
|
||||
}
|
||||
var existing = results[0];
|
||||
if (this.data.installationId && existing.installationId &&
|
||||
this.data.installationId !== existing.installationId) {
|
||||
idMatch = results[0];
|
||||
if (this.data.installationId && idMatch.installationId &&
|
||||
this.data.installationId !== idMatch.installationId) {
|
||||
throw new Parse.Error(136,
|
||||
'installationId may not be changed in this ' +
|
||||
'operation');
|
||||
}
|
||||
if (this.data.deviceToken && existing.deviceToken &&
|
||||
this.data.deviceToken !== existing.deviceToken &&
|
||||
!this.data.installationId && !existing.installationId) {
|
||||
if (this.data.deviceToken && idMatch.deviceToken &&
|
||||
this.data.deviceToken !== idMatch.deviceToken &&
|
||||
!this.data.installationId && !idMatch.installationId) {
|
||||
throw new Parse.Error(136,
|
||||
'deviceToken may not be changed in this ' +
|
||||
'operation');
|
||||
}
|
||||
if (this.data.deviceType && this.data.deviceType &&
|
||||
this.data.deviceType !== existing.deviceType) {
|
||||
this.data.deviceType !== idMatch.deviceType) {
|
||||
throw new Parse.Error(136,
|
||||
'deviceType may not be changed in this ' +
|
||||
'operation');
|
||||
@@ -627,8 +649,6 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
}
|
||||
|
||||
// Check if we already have installations for the installationId/deviceToken
|
||||
var installationMatch;
|
||||
var deviceTokenMatches = [];
|
||||
promise = promise.then(() => {
|
||||
if (this.data.installationId) {
|
||||
return this.config.database.find('_Installation', {
|
||||
@@ -639,7 +659,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
}).then((results) => {
|
||||
if (results && results.length) {
|
||||
// We only take the first match by installationId
|
||||
installationMatch = results[0];
|
||||
idMatch = results[0];
|
||||
}
|
||||
if (this.data.deviceToken) {
|
||||
return this.config.database.find(
|
||||
@@ -651,7 +671,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
if (results) {
|
||||
deviceTokenMatches = results;
|
||||
}
|
||||
if (!installationMatch) {
|
||||
if (!idMatch) {
|
||||
if (!deviceTokenMatches.length) {
|
||||
return;
|
||||
} else if (deviceTokenMatches.length == 1 &&
|
||||
@@ -689,14 +709,14 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
// Exactly one device token match and it doesn't have an installation
|
||||
// ID. This is the one case where we want to merge with the existing
|
||||
// object.
|
||||
var delQuery = {objectId: installationMatch.objectId};
|
||||
var delQuery = {objectId: idMatch.objectId};
|
||||
return this.config.database.destroy('_Installation', delQuery)
|
||||
.then(() => {
|
||||
return deviceTokenMatches[0]['objectId'];
|
||||
});
|
||||
} else {
|
||||
if (this.data.deviceToken &&
|
||||
installationMatch.deviceToken != this.data.deviceToken) {
|
||||
idMatch.deviceToken != this.data.deviceToken) {
|
||||
// We're setting the device token on an existing installation, so
|
||||
// we should try cleaning out old installations that match this
|
||||
// device token.
|
||||
@@ -712,7 +732,7 @@ RestWrite.prototype.handleInstallation = function() {
|
||||
this.config.database.destroy('_Installation', delQuery);
|
||||
}
|
||||
// In non-merge scenarios, just return the installation match id
|
||||
return installationMatch.objectId;
|
||||
return idMatch.objectId;
|
||||
}
|
||||
}
|
||||
}).then((objId) => {
|
||||
@@ -762,8 +782,10 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
// Run an update
|
||||
return this.config.database.update(
|
||||
this.className, this.query, this.data, this.runOptions).then((resp) => {
|
||||
this.response = resp;
|
||||
this.response.updatedAt = this.updatedAt;
|
||||
resp.updatedAt = this.updatedAt;
|
||||
this.response = {
|
||||
response: resp
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Set the default ACL for the new _User
|
||||
@@ -794,22 +816,33 @@ RestWrite.prototype.runDatabaseOperation = function() {
|
||||
|
||||
// Returns nothing - doesn't wait for the trigger.
|
||||
RestWrite.prototype.runAfterTrigger = function() {
|
||||
if (!this.response || !this.response.response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class.
|
||||
if (!triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
var extraData = {className: this.className};
|
||||
if (this.query && this.query.objectId) {
|
||||
extraData.objectId = this.query.objectId;
|
||||
}
|
||||
|
||||
// Build the inflated object, different from beforeSave, originalData is not empty
|
||||
// since developers can change data in the beforeSave.
|
||||
var inflatedObject = triggers.inflate(extraData, this.originalData);
|
||||
inflatedObject._finishFetch(this.data);
|
||||
// Build the original object, we only do this for a update write.
|
||||
var originalObject;
|
||||
let originalObject;
|
||||
if (this.query && this.query.objectId) {
|
||||
originalObject = triggers.inflate(extraData, this.originalData);
|
||||
}
|
||||
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, inflatedObject, originalObject, this.config.applicationId);
|
||||
// Build the inflated object, different from beforeSave, originalData is not empty
|
||||
// since developers can change data in the beforeSave.
|
||||
let updatedObject = triggers.inflate(extraData, this.originalData);
|
||||
updatedObject.set(Parse._decode(undefined, this.data));
|
||||
updatedObject._handleSaveResponse(this.response.response, this.response.status || 200);
|
||||
|
||||
triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config.applicationId);
|
||||
};
|
||||
|
||||
// A helper to figure out what location this operation happens at.
|
||||
@@ -825,4 +858,5 @@ RestWrite.prototype.objectId = function() {
|
||||
return this.data.objectId || this.query.objectId;
|
||||
};
|
||||
|
||||
export default RestWrite;
|
||||
module.exports = RestWrite;
|
||||
|
||||
@@ -85,10 +85,7 @@ export class ClassesRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
return rest.update(req.config, req.auth, req.params.className, req.params.objectId, req.body)
|
||||
.then((response) => {
|
||||
return {response: response};
|
||||
});
|
||||
return rest.update(req.config, req.auth, req.params.className, req.params.objectId, req.body);
|
||||
}
|
||||
|
||||
handleDelete(req) {
|
||||
|
||||
15
src/Routers/FeaturesRouter.js
Normal file
15
src/Routers/FeaturesRouter.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { version } from '../../package.json';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
import { getFeatures } from '../features';
|
||||
|
||||
export class FeaturesRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, () => {
|
||||
return { response: {
|
||||
features: getFeatures(),
|
||||
parseServerVersion: version,
|
||||
} };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import express from 'express';
|
||||
import BodyParser from 'body-parser';
|
||||
import * as Middlewares from '../middlewares';
|
||||
import { randomHexString } from '../cryptoUtils';
|
||||
import mime from 'mime';
|
||||
import Config from '../Config';
|
||||
import mime from 'mime';
|
||||
|
||||
export class FilesRouter {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class FilesRouter {
|
||||
var contentType = mime.lookup(filename);
|
||||
res.set('Content-Type', contentType);
|
||||
res.end(data);
|
||||
}).catch(() => {
|
||||
}).catch((err) => {
|
||||
res.status(404);
|
||||
res.set('Content-Type', 'text/plain');
|
||||
res.end('File not found.');
|
||||
@@ -66,20 +66,13 @@ export class FilesRouter {
|
||||
'Filename contains invalid characters.'));
|
||||
return;
|
||||
}
|
||||
let extension = '';
|
||||
|
||||
// Not very safe there.
|
||||
const hasExtension = req.params.filename.indexOf('.') > 0;
|
||||
const filename = req.params.filename;
|
||||
const contentType = req.get('Content-type');
|
||||
if (!hasExtension && contentType && mime.extension(contentType)) {
|
||||
extension = '.' + mime.extension(contentType);
|
||||
}
|
||||
|
||||
const filename = req.params.filename + extension;
|
||||
const config = req.config;
|
||||
const filesController = config.filesController;
|
||||
|
||||
filesController.createFile(config, filename, req.body).then((result) => {
|
||||
filesController.createFile(config, filename, req.body, contentType).then((result) => {
|
||||
res.status(201);
|
||||
res.set('Location', result.url);
|
||||
res.json(result);
|
||||
|
||||
42
src/Routers/GlobalConfigRouter.js
Normal file
42
src/Routers/GlobalConfigRouter.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// global_config.js
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class GlobalConfigRouter extends PromiseRouter {
|
||||
getGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOne({'_id': 1}))
|
||||
.then(globalConfig => ({response: { params: globalConfig.params }}))
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config does not exist',
|
||||
}
|
||||
}));
|
||||
}
|
||||
updateGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
|
||||
.then(response => {
|
||||
return { response: { result: true } }
|
||||
})
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config cannot be updated',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/config', req => { return this.getGlobalConfig(req) });
|
||||
this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(req) });
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalConfigRouter;
|
||||
@@ -1,15 +1,9 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import { HooksController } from '../Controllers/HooksController';
|
||||
|
||||
function enforceMasterKeyAccess(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
throw new Parse.Error(403, "unauthorized: master key is required");
|
||||
}
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class HooksRouter extends PromiseRouter {
|
||||
|
||||
createHook(aHook, config) {
|
||||
return config.hooksController.createHook(aHook).then( (hook) => ({response: hook}));
|
||||
};
|
||||
@@ -93,14 +87,14 @@ export class HooksRouter extends PromiseRouter {
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/hooks/functions', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('GET', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handleGetFunctions.bind(this));
|
||||
this.route('GET', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, this.handleGetTriggers.bind(this));
|
||||
this.route('POST', '/hooks/functions', enforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('POST', '/hooks/triggers', enforceMasterKeyAccess, this.handlePost.bind(this));
|
||||
this.route('PUT', '/hooks/functions/:functionName', enforceMasterKeyAccess, this.handlePut.bind(this));
|
||||
this.route('PUT', '/hooks/triggers/:className/:triggerName', enforceMasterKeyAccess, 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,25 +1,22 @@
|
||||
import { Parse } from 'parse/node';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
|
||||
// only allow request with master key
|
||||
let enforceSecurity = (auth) => {
|
||||
if (!auth || !auth.isMaster) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.OPERATION_FORBIDDEN,
|
||||
'Clients aren\'t allowed to perform the ' +
|
||||
'get' + ' operation on logs.'
|
||||
);
|
||||
}
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
export class LogsRouter extends PromiseRouter {
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/logs', (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 availabe');
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a promise for a {response} object.
|
||||
// query params:
|
||||
// level (optional) Level of logging you want to query for (info || error)
|
||||
@@ -27,28 +24,25 @@ export class LogsRouter extends PromiseRouter {
|
||||
// until (optional) End time for the search. Defaults to current time.
|
||||
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
|
||||
// size (optional) Number of rows returned by search. Defaults to 10
|
||||
// n same as size, overrides size if set
|
||||
handleGET(req) {
|
||||
if (!req.config || !req.config.loggerController) {
|
||||
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
|
||||
'Logger adapter is not availabe');
|
||||
}
|
||||
|
||||
let promise = new Parse.Promise();
|
||||
let from = req.query.from;
|
||||
let until = req.query.until;
|
||||
const from = req.query.from;
|
||||
const until = req.query.until;
|
||||
let size = req.query.size;
|
||||
let order = req.query.order
|
||||
let level = req.query.level;
|
||||
enforceSecurity(req.auth);
|
||||
if (req.query.n) {
|
||||
size = req.query.n;
|
||||
}
|
||||
|
||||
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 Promise.resolve({
|
||||
response: result
|
||||
|
||||
159
src/Routers/PublicAPIRouter.js
Normal file
159
src/Routers/PublicAPIRouter.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import UserController from '../Controllers/UserController';
|
||||
import Config from '../Config';
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
let public_html = path.resolve(__dirname, "../../public_html");
|
||||
let views = path.resolve(__dirname, '../../views');
|
||||
|
||||
export class PublicAPIRouter extends PromiseRouter {
|
||||
|
||||
verifyEmail(req) {
|
||||
let { token, username }= req.query;
|
||||
let appId = req.params.appId;
|
||||
let config = new Config(appId);
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
if (!token || !username) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
let userController = config.userController;
|
||||
return userController.verifyEmail(username, token).then( () => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.verifyEmailSuccessURL}?username=${username}`
|
||||
});
|
||||
}, ()=> {
|
||||
return this.invalidLink(req);
|
||||
})
|
||||
}
|
||||
|
||||
changePassword(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let config = new Config(req.query.id);
|
||||
if (!config.publicServerURL) {
|
||||
return resolve({
|
||||
status: 404,
|
||||
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);
|
||||
}
|
||||
data = data.replace("PARSE_SERVER_URL", `'${config.publicServerURL}'`);
|
||||
resolve({
|
||||
text: data
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
requestResetPassword(req) {
|
||||
|
||||
let config = req.config;
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
let { username, token } = req.query;
|
||||
|
||||
if (!username || !token) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.checkResetTokenValidity(username, token).then( (user) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&app=${config.appName}`
|
||||
})
|
||||
}, () => {
|
||||
return this.invalidLink(req);
|
||||
})
|
||||
}
|
||||
|
||||
resetPassword(req) {
|
||||
|
||||
let config = req.config;
|
||||
|
||||
if (!config.publicServerURL) {
|
||||
return this.missingPublicServerURL();
|
||||
}
|
||||
|
||||
let {
|
||||
username,
|
||||
token,
|
||||
new_password
|
||||
} = req.body;
|
||||
|
||||
if (!username || !token || !new_password) {
|
||||
return this.invalidLink(req);
|
||||
}
|
||||
|
||||
return config.userController.updatePassword(username, token, new_password).then((result) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: config.passwordResetSuccessURL
|
||||
});
|
||||
}, (err) => {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: `${config.choosePasswordURL}?token=${token}&id=${config.applicationId}&username=${username}&error=${err}&app=${config.appName}`
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
invalidLink(req) {
|
||||
return Promise.resolve({
|
||||
status: 302,
|
||||
location: req.config.invalidLinkURL
|
||||
});
|
||||
}
|
||||
|
||||
missingPublicServerURL() {
|
||||
return Promise.resolve({
|
||||
text: 'Not found.',
|
||||
status: 404
|
||||
});
|
||||
}
|
||||
|
||||
setConfig(req) {
|
||||
req.config = new Config(req.params.appId);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET','/apps/:appId/verify_email',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.verifyEmail(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('GET','/apps/:appId/request_password_reset',
|
||||
req => { this.setConfig(req) },
|
||||
req => { return this.requestResetPassword(req); });
|
||||
}
|
||||
|
||||
expressApp() {
|
||||
let router = express();
|
||||
router.use("/apps", express.static(public_html));
|
||||
router.use("/", super.expressApp());
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
export default PublicAPIRouter;
|
||||
@@ -1,27 +1,17 @@
|
||||
// schemas.js
|
||||
|
||||
var express = require('express'),
|
||||
Parse = require('parse/node').Parse,
|
||||
Schema = require('../Schema');
|
||||
Parse = require('parse/node').Parse,
|
||||
Schema = require('../Schema');
|
||||
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
|
||||
// TODO: refactor in a SchemaController at one point...
|
||||
function masterKeyRequiredResponse() {
|
||||
return Promise.resolve({
|
||||
status: 401,
|
||||
response: {error: 'master key not specified'},
|
||||
})
|
||||
}
|
||||
import * as middleware from "../middlewares";
|
||||
|
||||
function classNameMismatchResponse(bodyClass, pathClass) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class name mismatch between ' + bodyClass + ' and ' + pathClass,
|
||||
}
|
||||
});
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INVALID_CLASS_NAME,
|
||||
`Class name mismatch between ${bodyClass} and ${pathClass}.`
|
||||
);
|
||||
}
|
||||
|
||||
function mongoSchemaAPIResponseFields(schema) {
|
||||
@@ -45,65 +35,43 @@ function mongoSchemaToSchemaAPIResponse(schema) {
|
||||
}
|
||||
|
||||
function getAllSchemas(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
return req.config.database.collection('_SCHEMA')
|
||||
.then(coll => coll.find({}).toArray())
|
||||
.then(schemas => ({response: {
|
||||
results: schemas.map(mongoSchemaToSchemaAPIResponse)
|
||||
}}));
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(collection => collection.find({}))
|
||||
.then(schemas => schemas.map(mongoSchemaToSchemaAPIResponse))
|
||||
.then(schemas => ({ response: { results: schemas }}));
|
||||
}
|
||||
|
||||
function getOneSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
return req.config.database.collection('_SCHEMA')
|
||||
.then(coll => coll.findOne({'_id': req.params.className}))
|
||||
.then(schema => ({response: mongoSchemaToSchemaAPIResponse(schema)}))
|
||||
.catch(() => ({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 103,
|
||||
error: 'class ' + req.params.className + ' does not exist',
|
||||
}
|
||||
}));
|
||||
const className = req.params.className;
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(collection => collection.find({ '_id': className }, { limit: 1 }))
|
||||
.then(results => {
|
||||
if (results.length != 1) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
return results[0];
|
||||
})
|
||||
.then(schema => ({ response: mongoSchemaToSchemaAPIResponse(schema) }));
|
||||
}
|
||||
|
||||
function createSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
if (req.params.className && req.body.className) {
|
||||
if (req.params.className != req.body.className) {
|
||||
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||
}
|
||||
}
|
||||
var className = req.params.className || req.body.className;
|
||||
|
||||
const className = req.params.className || req.body.className;
|
||||
if (!className) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 135,
|
||||
error: 'POST ' + req.path + ' needs class name',
|
||||
},
|
||||
});
|
||||
throw new Parse.Error(135, `POST ${req.path} needs a class name.`);
|
||||
}
|
||||
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
|
||||
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }))
|
||||
.catch(error => ({
|
||||
status: 400,
|
||||
response: error,
|
||||
}));
|
||||
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
|
||||
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }));
|
||||
}
|
||||
|
||||
function modifySchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
|
||||
if (req.body.className && req.body.className != req.params.className) {
|
||||
return classNameMismatchResponse(req.body.className, req.params.className);
|
||||
}
|
||||
@@ -112,168 +80,115 @@ function modifySchema(req) {
|
||||
var className = req.params.className;
|
||||
|
||||
return req.config.database.loadSchema()
|
||||
.then(schema => {
|
||||
if (!schema.data[className]) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + req.params.className + ' does not exist',
|
||||
.then(schema => {
|
||||
if (!schema.data[className]) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`);
|
||||
}
|
||||
|
||||
let existingFields = schema.data[className];
|
||||
Object.keys(submittedFields).forEach(name => {
|
||||
let field = submittedFields[name];
|
||||
if (existingFields[name] && field.__op !== 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
||||
}
|
||||
if (!existingFields[name] && field.__op === 'Delete') {
|
||||
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
var existingFields = schema.data[className];
|
||||
|
||||
for (var submittedFieldName in submittedFields) {
|
||||
if (existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op !== 'Delete') {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'field ' + submittedFieldName + ' exists, cannot update',
|
||||
}
|
||||
});
|
||||
let newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
|
||||
let mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
|
||||
if (!mongoObject.result) {
|
||||
throw new Parse.Error(mongoObject.code, mongoObject.error);
|
||||
}
|
||||
|
||||
if (!existingFields[submittedFieldName] && submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'field ' + submittedFieldName + ' does not exist, cannot delete',
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields);
|
||||
var mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className);
|
||||
if (!mongoObject.result) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: mongoObject,
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
let deletionPromises = [];
|
||||
Object.keys(submittedFields).forEach(submittedFieldName => {
|
||||
if (submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
let promise = schema.deleteField(submittedFieldName, className, req.config.database);
|
||||
deletionPromises.push(promise);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
||||
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
||||
var deletionPromises = []
|
||||
Object.keys(submittedFields).forEach(submittedFieldName => {
|
||||
if (submittedFields[submittedFieldName].__op === 'Delete') {
|
||||
var promise = req.config.database.connect()
|
||||
.then(() => schema.deleteField(
|
||||
submittedFieldName,
|
||||
className,
|
||||
req.config.database.db,
|
||||
req.config.database.collectionPrefix
|
||||
));
|
||||
deletionPromises.push(promise);
|
||||
}
|
||||
return Promise.all(deletionPromises)
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(deletionPromises)
|
||||
.then(() => new Promise((resolve, reject) => {
|
||||
schema.collection.update({_id: className}, mongoObject.result, {w: 1}, (err, docs) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve({ response: mongoSchemaToSchemaAPIResponse(mongoObject.result)});
|
||||
})
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// A helper function that removes all join tables for a schema. Returns a promise.
|
||||
var removeJoinTables = (database, prefix, mongoSchema) => {
|
||||
var removeJoinTables = (database, mongoSchema) => {
|
||||
return Promise.all(Object.keys(mongoSchema)
|
||||
.filter(field => mongoSchema[field].startsWith('relation<'))
|
||||
.map(field => {
|
||||
var joinCollectionName = prefix + '_Join:' + field + ':' + mongoSchema._id;
|
||||
return new Promise((resolve, reject) => {
|
||||
database.dropCollection(joinCollectionName, (err, results) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
});
|
||||
let collectionName = `_Join:${field}:${mongoSchema._id}`;
|
||||
return database.dropCollection(collectionName);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function deleteSchema(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return masterKeyRequiredResponse();
|
||||
}
|
||||
|
||||
if (!Schema.classNameIsValid(req.params.className)) {
|
||||
return Promise.resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: Schema.invalidClassNameMessage(req.params.className),
|
||||
}
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, Schema.invalidClassNameMessage(req.params.className));
|
||||
}
|
||||
|
||||
return req.config.database.collection(req.params.className)
|
||||
.then(coll => new Promise((resolve, reject) => {
|
||||
coll.count((err, count) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (count > 0) {
|
||||
resolve({
|
||||
status: 400,
|
||||
response: {
|
||||
code: 255,
|
||||
error: 'class ' + req.params.className + ' not empty, contains ' + count + ' objects, cannot drop schema',
|
||||
}
|
||||
});
|
||||
} else {
|
||||
coll.drop((err, reply) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
req.config.database.collection('_SCHEMA')
|
||||
.then(coll => new Promise((resolve, reject) => {
|
||||
coll.findAndRemove({ _id: req.params.className }, [], (err, doc) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else if (doc.value === null) {
|
||||
//tried to delete non-existant class
|
||||
resolve({ response: {}});
|
||||
} else {
|
||||
removeJoinTables(req.config.database.db, req.config.database.collectionPrefix, doc.value)
|
||||
.then(resolve, reject);
|
||||
}
|
||||
});
|
||||
}))
|
||||
.then(resolve.bind(undefined, {response: {}}), reject);
|
||||
}
|
||||
});
|
||||
return req.config.database.collectionExists(req.params.className)
|
||||
.then(exist => {
|
||||
if (!exist) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return req.config.database.adaptiveCollection(req.params.className)
|
||||
.then(collection => {
|
||||
return collection.count()
|
||||
.then(count => {
|
||||
if (count > 0) {
|
||||
throw new Parse.Error(255, `Class ${req.params.className} is not empty, contains ${count} objects, cannot drop schema.`);
|
||||
}
|
||||
return collection.drop();
|
||||
})
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
// We've dropped the collection now, so delete the item from _SCHEMA
|
||||
// and clear the _Join collections
|
||||
return req.config.database.adaptiveCollection('_SCHEMA')
|
||||
.then(coll => coll.findOneAndDelete({_id: req.params.className}))
|
||||
.then(document => {
|
||||
if (document === null) {
|
||||
//tried to delete non-existent class
|
||||
return Promise.resolve();
|
||||
}
|
||||
return removeJoinTables(req.config.database, document);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
// Success
|
||||
return { response: {} };
|
||||
}, error => {
|
||||
if (error.message == 'ns not found') {
|
||||
// If they try to delete a non-existent class, that's fine, just let them.
|
||||
return { response: {} };
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}))
|
||||
.catch( (error) => {
|
||||
if (error.message == 'ns not found') {
|
||||
// If they try to delete a non-existant class, thats fine, just let them.
|
||||
return Promise.resolve({ response: {} });
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
export class SchemasRouter extends PromiseRouter {
|
||||
mountRoutes() {
|
||||
this.route('GET', '/schemas', getAllSchemas);
|
||||
this.route('GET', '/schemas/:className', getOneSchema);
|
||||
this.route('POST', '/schemas', createSchema);
|
||||
this.route('POST', '/schemas/:className', createSchema);
|
||||
this.route('PUT', '/schemas/:className', modifySchema);
|
||||
this.route('DELETE', '/schemas/:className', deleteSchema);
|
||||
this.route('GET', '/schemas', middleware.promiseEnforceMasterKeyAccess, getAllSchemas);
|
||||
this.route('GET', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, getOneSchema);
|
||||
this.route('POST', '/schemas', middleware.promiseEnforceMasterKeyAccess, createSchema);
|
||||
this.route('POST', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, createSchema);
|
||||
this.route('PUT', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, modifySchema);
|
||||
this.route('DELETE', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, deleteSchema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// These methods handle the User-related routes.
|
||||
|
||||
import deepcopy from 'deepcopy';
|
||||
import deepcopy from 'deepcopy';
|
||||
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import ClassesRouter from './ClassesRouter';
|
||||
import PromiseRouter from '../PromiseRouter';
|
||||
import rest from '../rest';
|
||||
import Auth from '../Auth';
|
||||
import passwordCrypto from '../password';
|
||||
import RestWrite from '../RestWrite';
|
||||
import { newToken } from '../cryptoUtils';
|
||||
import RestWrite from '../RestWrite';
|
||||
let cryptoUtils = require('../cryptoUtils');
|
||||
let triggers = require('../triggers');
|
||||
|
||||
export class UsersRouter extends ClassesRouter {
|
||||
handleFind(req) {
|
||||
@@ -25,7 +26,18 @@ export class UsersRouter extends ClassesRouter {
|
||||
let data = deepcopy(req.body);
|
||||
req.body = data;
|
||||
req.params.className = '_User';
|
||||
|
||||
//req.config.userController.setEmailVerifyToken(req.body);
|
||||
|
||||
return super.handleCreate(req);
|
||||
|
||||
// if (req.config.verifyUserEmails) {
|
||||
// // Send email as fire-and-forget once the user makes it into the DB.
|
||||
// p.then(() => {
|
||||
// req.config.userController.sendVerificationEmail(req.body);
|
||||
// });
|
||||
// }
|
||||
// return p;
|
||||
}
|
||||
|
||||
handleUpdate(req) {
|
||||
@@ -75,7 +87,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
}
|
||||
|
||||
let user;
|
||||
return req.database.find('_User', { username: req.body.username })
|
||||
return req.config.database.find('_User', { username: req.body.username })
|
||||
.then((results) => {
|
||||
if (!results.length) {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
@@ -87,7 +99,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
|
||||
}
|
||||
|
||||
let token = 'r:' + newToken();
|
||||
let token = 'r:' + cryptoUtils.newToken();
|
||||
user.sessionToken = token;
|
||||
delete user.password;
|
||||
|
||||
@@ -140,6 +152,23 @@ export class UsersRouter extends ClassesRouter {
|
||||
}
|
||||
return Promise.resolve(success);
|
||||
}
|
||||
|
||||
handleResetRequest(req) {
|
||||
let { email } = req.body;
|
||||
if (!email) {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email");
|
||||
}
|
||||
let userController = req.config.userController;
|
||||
|
||||
return userController.sendPasswordResetEmail(email).then((token) => {
|
||||
return Promise.resolve({
|
||||
response: {}
|
||||
});
|
||||
}, (err) => {
|
||||
throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `no user found with email ${email}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
mountRoutes() {
|
||||
this.route('GET', '/users', req => { return this.handleFind(req); });
|
||||
@@ -150,9 +179,7 @@ export class UsersRouter extends ClassesRouter {
|
||||
this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
|
||||
this.route('GET', '/login', req => { return this.handleLogIn(req); });
|
||||
this.route('POST', '/logout', req => { return this.handleLogOut(req); });
|
||||
this.route('POST', '/requestPasswordReset', () => {
|
||||
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
|
||||
});
|
||||
this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
178
src/Schema.js
178
src/Schema.js
@@ -10,7 +10,7 @@
|
||||
// keeping it this way for now.
|
||||
//
|
||||
// In API-handling code, you should only use the Schema class via the
|
||||
// ExportAdapter. This will let us replace the schema logic for
|
||||
// DatabaseController. This will let us replace the schema logic for
|
||||
// different databases.
|
||||
// TODO: hide all schema logic inside the database adapter.
|
||||
|
||||
@@ -48,13 +48,13 @@ var defaultColumns = {
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Role: {
|
||||
"name": {type:'String'},
|
||||
"users": {type:'Relation',className:'_User'},
|
||||
"roles": {type:'Relation',className:'_Role'}
|
||||
"users": {type:'Relation', targetClass:'_User'},
|
||||
"roles": {type:'Relation', targetClass:'_Role'}
|
||||
},
|
||||
// The additional default columns for the _User collection (in addition to DefaultCols)
|
||||
_Session: {
|
||||
"restricted": {type:'Boolean'},
|
||||
"user": {type:'Pointer', className:'_User'},
|
||||
"user": {type:'Pointer', targetClass:'_User'},
|
||||
"installationId": {type:'String'},
|
||||
"sessionToken": {type:'String'},
|
||||
"expiresAt": {type:'Date'},
|
||||
@@ -73,7 +73,8 @@ var defaultColumns = {
|
||||
|
||||
|
||||
var requiredColumns = {
|
||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"]
|
||||
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
||||
_Role: ["name", "ACL"]
|
||||
}
|
||||
|
||||
// Valid classes must:
|
||||
@@ -307,8 +308,12 @@ function mongoFieldTypeToSchemaAPIType(type) {
|
||||
// is done in mongoSchemaFromFieldsAndClassName.
|
||||
function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
var newSchema = {};
|
||||
let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]);
|
||||
for (var oldField in mongoObject) {
|
||||
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
|
||||
if (!fieldIsDeleted) {
|
||||
newSchema[oldField] = mongoFieldTypeToSchemaAPIType(mongoObject[oldField]);
|
||||
@@ -317,6 +322,9 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
}
|
||||
for (var newField in putRequest) {
|
||||
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
|
||||
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
|
||||
continue;
|
||||
}
|
||||
newSchema[newField] = putRequest[newField];
|
||||
}
|
||||
}
|
||||
@@ -332,29 +340,22 @@ function buildMergedSchemaObject(mongoObject, putRequest) {
|
||||
// enabled) before calling this function.
|
||||
Schema.prototype.addClassIfNotExists = function(className, fields) {
|
||||
if (this.data[className]) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
}
|
||||
|
||||
var mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||
|
||||
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
|
||||
if (!mongoObject.result) {
|
||||
return Promise.reject(mongoObject);
|
||||
}
|
||||
|
||||
return this.collection.insertOne(mongoObject.result)
|
||||
.then(result => result.ops[0])
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' already exists',
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
.then(result => result.ops[0])
|
||||
.catch(error => {
|
||||
if (error.code === 11000) { //Mongo's duplicate key error
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
};
|
||||
|
||||
// Returns a promise that resolves successfully to the new schema
|
||||
@@ -500,80 +501,47 @@ Schema.prototype.validateField = function(className, key, type, freeze) {
|
||||
|
||||
// Passing the database and prefix is necessary in order to drop relation collections
|
||||
// and remove fields from objects. Ideally the database would belong to
|
||||
// a database adapter and this fuction would close over it or access it via member.
|
||||
Schema.prototype.deleteField = function(fieldName, className, database, prefix) {
|
||||
// a database adapter and this function would close over it or access it via member.
|
||||
Schema.prototype.deleteField = function(fieldName, className, database) {
|
||||
if (!classNameIsValid(className)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: invalidClassNameMessage(className),
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
||||
}
|
||||
|
||||
if (!fieldNameIsValid(fieldName)) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'invalid field name: ' + fieldName,
|
||||
});
|
||||
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
||||
}
|
||||
|
||||
//Don't allow deleting the default fields.
|
||||
if (!fieldNameIsValidForClass(fieldName, className)) {
|
||||
return Promise.reject({
|
||||
code: 136,
|
||||
error: 'field ' + fieldName + ' cannot be changed',
|
||||
});
|
||||
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
||||
}
|
||||
|
||||
return this.reload()
|
||||
.then(schema => {
|
||||
return schema.hasClass(className)
|
||||
.then(hasClass => {
|
||||
if (!hasClass) {
|
||||
return Promise.reject({
|
||||
code: Parse.Error.INVALID_CLASS_NAME,
|
||||
error: 'class ' + className + ' does not exist',
|
||||
});
|
||||
}
|
||||
.then(schema => {
|
||||
return schema.hasClass(className)
|
||||
.then(hasClass => {
|
||||
if (!hasClass) {
|
||||
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
||||
}
|
||||
if (!schema.data[className][fieldName]) {
|
||||
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
||||
}
|
||||
|
||||
if (!schema.data[className][fieldName]) {
|
||||
return Promise.reject({
|
||||
code: 255,
|
||||
error: 'field ' + fieldName + ' does not exist, cannot delete',
|
||||
});
|
||||
}
|
||||
if (schema.data[className][fieldName].startsWith('relation<')) {
|
||||
//For relations, drop the _Join table
|
||||
return database.dropCollection(`_Join:${fieldName}:${className}`);
|
||||
}
|
||||
|
||||
if (schema.data[className][fieldName].startsWith('relation<')) {
|
||||
//For relations, drop the _Join table
|
||||
return database.dropCollection(prefix + '_Join:' + fieldName + ':' + className)
|
||||
//Save the _SCHEMA object
|
||||
// for non-relations, remove all the data.
|
||||
// This is necessary to ensure that the data is still gone if they add the same field.
|
||||
return database.collection(className)
|
||||
.then(collection => {
|
||||
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ? '_p_' + fieldName : fieldName;
|
||||
return collection.update({}, { "$unset": { [mongoFieldName] : null } }, { multi: true });
|
||||
});
|
||||
})
|
||||
// Save the _SCHEMA object
|
||||
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}));
|
||||
} else {
|
||||
//for non-relations, remove all the data. This is necessary to ensure that the data is still gone
|
||||
//if they add the same field.
|
||||
return new Promise((resolve, reject) => {
|
||||
database.collection(prefix + className, (err, coll) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
var mongoFieldName = schema.data[className][fieldName].startsWith('*') ?
|
||||
'_p_' + fieldName :
|
||||
fieldName;
|
||||
return coll.update({}, {
|
||||
"$unset": { [mongoFieldName] : null },
|
||||
}, {
|
||||
multi: true,
|
||||
})
|
||||
//Save the _SCHEMA object
|
||||
.then(() => this.collection.update({ _id: className }, { $unset: {[fieldName]: null }}))
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Given a schema promise, construct another schema promise that
|
||||
// validates this field once the schema loads.
|
||||
@@ -626,7 +594,7 @@ Schema.prototype.validateRequiredColumns = function(className, object, query) {
|
||||
if (!columns || columns.length == 0) {
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
|
||||
var missingColumns = columns.filter(function(column){
|
||||
if (query && query.objectId) {
|
||||
if (object[column] && typeof object[column] === "object") {
|
||||
@@ -636,15 +604,15 @@ Schema.prototype.validateRequiredColumns = function(className, object, query) {
|
||||
// Not trying to do anything there
|
||||
return false;
|
||||
}
|
||||
return !object[column]
|
||||
return !object[column]
|
||||
});
|
||||
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Parse.Error(
|
||||
Parse.Error.INCORRECT_TYPE,
|
||||
missingColumns[0]+' is required.');
|
||||
}
|
||||
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
@@ -731,19 +699,31 @@ function getObjectType(obj) {
|
||||
if (obj instanceof Array) {
|
||||
return 'array';
|
||||
}
|
||||
if (obj.__type === 'Pointer' && obj.className) {
|
||||
return '*' + obj.className;
|
||||
}
|
||||
if (obj.__type === 'File' && obj.name) {
|
||||
return 'file';
|
||||
}
|
||||
if (obj.__type === 'Date' && obj.iso) {
|
||||
return 'date';
|
||||
}
|
||||
if (obj.__type == 'GeoPoint' &&
|
||||
obj.latitude != null &&
|
||||
obj.longitude != null) {
|
||||
return 'geopoint';
|
||||
if (obj.__type){
|
||||
switch(obj.__type) {
|
||||
case 'Pointer' :
|
||||
if(obj.className) {
|
||||
return '*' + obj.className;
|
||||
}
|
||||
case 'File' :
|
||||
if(obj.name) {
|
||||
return 'file';
|
||||
}
|
||||
case 'Date' :
|
||||
if(obj.iso) {
|
||||
return 'date';
|
||||
}
|
||||
case 'GeoPoint' :
|
||||
if(obj.latitude != null && obj.longitude != null) {
|
||||
return 'geopoint';
|
||||
}
|
||||
case 'Bytes' :
|
||||
if(obj.base64) {
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
|
||||
}
|
||||
}
|
||||
if (obj['$ne']) {
|
||||
return getObjectType(obj['$ne']);
|
||||
|
||||
58
src/cache.js
58
src/cache.js
@@ -1,45 +1,35 @@
|
||||
export var apps = {};
|
||||
export var stats = {};
|
||||
export var isLoaded = false;
|
||||
export var users = {};
|
||||
/** @flow weak */
|
||||
|
||||
export function getApp(app, callback) {
|
||||
if (apps[app]) return callback(true, apps[app]);
|
||||
return callback(false);
|
||||
export function CacheStore<KeyType, ValueType>() {
|
||||
let dataStore: {[id:KeyType]:ValueType} = {};
|
||||
return {
|
||||
get: (key: KeyType): ValueType => {
|
||||
return dataStore[key];
|
||||
},
|
||||
set(key: KeyType, value: ValueType): void {
|
||||
dataStore[key] = value;
|
||||
},
|
||||
remove(key: KeyType): void {
|
||||
delete dataStore[key];
|
||||
},
|
||||
clear(): void {
|
||||
dataStore = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateStat(key, value) {
|
||||
stats[key] = value;
|
||||
}
|
||||
|
||||
export function getUser(sessionToken) {
|
||||
if (users[sessionToken]) return users[sessionToken];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function setUser(sessionToken, userObject) {
|
||||
users[sessionToken] = userObject;
|
||||
}
|
||||
|
||||
export function clearUser(sessionToken) {
|
||||
delete users[sessionToken];
|
||||
}
|
||||
const apps = CacheStore();
|
||||
const users = CacheStore();
|
||||
|
||||
//So far used only in tests
|
||||
export function clearCache() {
|
||||
apps = {};
|
||||
stats = {};
|
||||
users = {};
|
||||
export function clearCache(): void {
|
||||
apps.clear();
|
||||
users.clear();
|
||||
}
|
||||
|
||||
export default {
|
||||
apps,
|
||||
stats,
|
||||
isLoaded,
|
||||
getApp,
|
||||
updateStat,
|
||||
clearUser,
|
||||
getUser,
|
||||
setUser,
|
||||
users,
|
||||
clearCache,
|
||||
CacheStore
|
||||
};
|
||||
|
||||
@@ -42,14 +42,6 @@ if (program.args.length > 0 ) {
|
||||
console.log(`Configuation loaded from ${jsonPath}`)
|
||||
}
|
||||
|
||||
if (!program.appId || !program.masterKey || !program.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error(colors.red("ERROR: appId, masterKey and serverURL are required"));
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
options = Object.keys(definitions).reduce(function (options, key) {
|
||||
if (program[key]) {
|
||||
options[key] = program[key];
|
||||
@@ -61,6 +53,14 @@ if (!options.serverURL) {
|
||||
options.serverURL = `http://localhost:${options.port}${options.mountPath}`;
|
||||
}
|
||||
|
||||
if (!options.appId || !options.masterKey || !options.serverURL) {
|
||||
program.outputHelp();
|
||||
console.error("");
|
||||
console.error(colors.red("ERROR: appId, masterKey and serverURL are required"));
|
||||
console.error("");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const api = new ParseServer(options);
|
||||
app.use(options.mountPath, api);
|
||||
|
||||
@@ -36,6 +36,12 @@ module.exports = function(options) {
|
||||
options.followRedirect = options.followRedirects == true;
|
||||
|
||||
request(options, (error, response, body) => {
|
||||
if (error) {
|
||||
if (callbacks.error) {
|
||||
callbacks.error(error);
|
||||
}
|
||||
return promise.reject(error);
|
||||
}
|
||||
var httpResponse = {};
|
||||
httpResponse.status = response.statusCode;
|
||||
httpResponse.headers = response.headers;
|
||||
@@ -46,7 +52,7 @@ module.exports = function(options) {
|
||||
httpResponse.data = JSON.parse(response.body);
|
||||
} catch (e) {}
|
||||
// Consider <200 && >= 400 as errors
|
||||
if (error || httpResponse.status <200 || httpResponse.status >=400) {
|
||||
if (httpResponse.status < 200 || httpResponse.status >= 400) {
|
||||
if (callbacks.error) {
|
||||
callbacks.error(httpResponse);
|
||||
}
|
||||
|
||||
91
src/features.js
Normal file
91
src/features.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* features.js
|
||||
* Feature config file that holds information on the features that are currently
|
||||
* available on Parse Server. This is primarily created to work with an UI interface
|
||||
* like the web dashboard. The list of features will change depending on the your
|
||||
* app, choice of adapter as well as Parse Server version. This approach will enable
|
||||
* the dashboard to be built independently and still support these use cases.
|
||||
*
|
||||
*
|
||||
* Default features and feature options are listed in the features object.
|
||||
*
|
||||
* featureSwitch is a convenient way to turn on/off features without changing the config
|
||||
*
|
||||
* Features that use Adapters should specify the feature options through
|
||||
* the setFeature method in your controller and feature
|
||||
* Reference PushController and ParsePushAdapter as an example.
|
||||
*
|
||||
* NOTE: When adding new endpoints be sure to update this list both (features, featureSwitch)
|
||||
* if you are planning to have a UI consume it.
|
||||
*/
|
||||
|
||||
// default features
|
||||
let features = {
|
||||
globalConfig: {
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
hooks: {
|
||||
create: false,
|
||||
read: false,
|
||||
update: false,
|
||||
delete: false,
|
||||
},
|
||||
logs: {
|
||||
level: false,
|
||||
size: false,
|
||||
order: false,
|
||||
until: false,
|
||||
from: false,
|
||||
},
|
||||
push: {
|
||||
immediatePush: false,
|
||||
scheduledPush: false,
|
||||
storedPushData: false,
|
||||
pushAudiences: false,
|
||||
},
|
||||
schemas: {
|
||||
addField: true,
|
||||
removeField: true,
|
||||
addClass: true,
|
||||
removeClass: true,
|
||||
clearAllDataFromClass: false,
|
||||
exportClass: false,
|
||||
},
|
||||
};
|
||||
|
||||
// master switch for features
|
||||
let featuresSwitch = {
|
||||
globalConfig: true,
|
||||
hooks: true,
|
||||
logs: true,
|
||||
push: true,
|
||||
schemas: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* set feature config options
|
||||
*/
|
||||
function setFeature(key, value) {
|
||||
features[key] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* get feature config options
|
||||
*/
|
||||
function getFeatures() {
|
||||
let result = {};
|
||||
Object.keys(features).forEach((key) => {
|
||||
if (featuresSwitch[key] && features[key]) {
|
||||
result[key] = features[key];
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getFeatures,
|
||||
setFeature,
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
// global_config.js
|
||||
|
||||
var Parse = require('parse/node').Parse;
|
||||
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
var router = new PromiseRouter();
|
||||
|
||||
function getGlobalConfig(req) {
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOne({'_id': 1}))
|
||||
.then(globalConfig => ({response: { params: globalConfig.params }}))
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config does not exist',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function updateGlobalConfig(req) {
|
||||
if (!req.auth.isMaster) {
|
||||
return Promise.resolve({
|
||||
status: 401,
|
||||
response: {error: 'unauthorized'},
|
||||
});
|
||||
}
|
||||
|
||||
return req.config.database.rawCollection('_GlobalConfig')
|
||||
.then(coll => coll.findOneAndUpdate({ _id: 1 }, { $set: req.body }))
|
||||
.then(response => {
|
||||
return { response: { result: true } }
|
||||
})
|
||||
.catch(() => ({
|
||||
status: 404,
|
||||
response: {
|
||||
code: Parse.Error.INVALID_KEY_NAME,
|
||||
error: 'config cannot be updated',
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
router.route('GET', '/config', getGlobalConfig);
|
||||
router.route('PUT', '/config', updateGlobalConfig);
|
||||
|
||||
module.exports = router;
|
||||
137
src/index.js
137
src/index.js
@@ -10,43 +10,48 @@ var batch = require('./batch'),
|
||||
multer = require('multer'),
|
||||
Parse = require('parse/node').Parse;
|
||||
|
||||
//import passwordReset from './passwordReset';
|
||||
import cache from './cache';
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
|
||||
import Config from './Config';
|
||||
import parseServerPackage from '../package.json';
|
||||
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
|
||||
import PromiseRouter from './PromiseRouter';
|
||||
import requiredParameter from './requiredParameter';
|
||||
import { AnalyticsRouter } from './Routers/AnalyticsRouter';
|
||||
import { ClassesRouter } from './Routers/ClassesRouter';
|
||||
import { FeaturesRouter } from './Routers/FeaturesRouter';
|
||||
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
|
||||
import { FilesController } from './Controllers/FilesController';
|
||||
import { FilesRouter } from './Routers/FilesRouter';
|
||||
import { FunctionsRouter } from './Routers/FunctionsRouter';
|
||||
import { GCSAdapter } from './Adapters/Files/GCSAdapter';
|
||||
import { GlobalConfigRouter } from './Routers/GlobalConfigRouter';
|
||||
import { GridStoreAdapter } from './Adapters/Files/GridStoreAdapter';
|
||||
import { HooksController } from './Controllers/HooksController';
|
||||
import { HooksRouter } from './Routers/HooksRouter';
|
||||
import { IAPValidationRouter } from './Routers/IAPValidationRouter';
|
||||
import { InstallationsRouter } from './Routers/InstallationsRouter';
|
||||
import { loadAdapter } from './Adapters/AdapterLoader';
|
||||
import { LoggerController } from './Controllers/LoggerController';
|
||||
import { LogsRouter } from './Routers/LogsRouter';
|
||||
import { PublicAPIRouter } from './Routers/PublicAPIRouter';
|
||||
import { PushController } from './Controllers/PushController';
|
||||
import { PushRouter } from './Routers/PushRouter';
|
||||
import { randomString } from './cryptoUtils';
|
||||
import { RolesRouter } from './Routers/RolesRouter';
|
||||
import { S3Adapter } from './Adapters/Files/S3Adapter';
|
||||
import { SchemasRouter } from './Routers/SchemasRouter';
|
||||
import { SessionsRouter } from './Routers/SessionsRouter';
|
||||
import { setFeature } from './features';
|
||||
import { UserController } from './Controllers/UserController';
|
||||
import { UsersRouter } from './Routers/UsersRouter';
|
||||
|
||||
// Mutate the Parse object to add the Cloud Code handlers
|
||||
addParseCloud();
|
||||
|
||||
// ParseServer works like a constructor of an express app.
|
||||
// The args that we understand are:
|
||||
// "databaseAdapter": a class like ExportAdapter providing create, find,
|
||||
// "databaseAdapter": a class like DatabaseController providing create, find,
|
||||
// update, and delete
|
||||
// "filesAdapter": a class like GridStoreAdapter providing create, get,
|
||||
// and delete
|
||||
@@ -73,11 +78,12 @@ addParseCloud();
|
||||
function ParseServer({
|
||||
appId = requiredParameter('You must provide an appId!'),
|
||||
masterKey = requiredParameter('You must provide a masterKey!'),
|
||||
appName,
|
||||
databaseAdapter,
|
||||
filesAdapter,
|
||||
push,
|
||||
loggerAdapter,
|
||||
databaseURI,
|
||||
databaseURI = DatabaseAdapter.defaultDatabaseURI,
|
||||
cloud,
|
||||
collectionPrefix = '',
|
||||
clientKey,
|
||||
@@ -90,9 +96,18 @@ function ParseServer({
|
||||
allowClientClassCreation = true,
|
||||
oauth = {},
|
||||
serverURL = requiredParameter('You must provide a serverURL!'),
|
||||
maxUploadSize = '20mb'
|
||||
maxUploadSize = '20mb',
|
||||
verifyUserEmails = false,
|
||||
emailAdapter,
|
||||
publicServerURL,
|
||||
customPages = {
|
||||
invalidLink: undefined,
|
||||
verifyEmailSuccess: undefined,
|
||||
choosePassword: undefined,
|
||||
passwordResetSuccess: undefined
|
||||
},
|
||||
}) {
|
||||
|
||||
setFeature('serverVersion', parseServerPackage.version);
|
||||
// Initialize the node client SDK automatically
|
||||
Parse.initialize(appId, javascriptKey || 'unused', masterKey);
|
||||
Parse.serverURL = serverURL;
|
||||
@@ -116,19 +131,24 @@ function ParseServer({
|
||||
}
|
||||
}
|
||||
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, GridStoreAdapter);
|
||||
const filesControllerAdapter = loadAdapter(filesAdapter, () => {
|
||||
return new GridStoreAdapter(databaseURI);
|
||||
});
|
||||
const pushControllerAdapter = loadAdapter(push, ParsePushAdapter);
|
||||
const loggerControllerAdapter = loadAdapter(loggerAdapter, FileLoggerAdapter);
|
||||
|
||||
const emailControllerAdapter = loadAdapter(emailAdapter);
|
||||
// We pass the options and the base class for the adatper,
|
||||
// Note that passing an instance would work too
|
||||
const filesController = new FilesController(filesControllerAdapter);
|
||||
const pushController = new PushController(pushControllerAdapter);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter);
|
||||
const filesController = new FilesController(filesControllerAdapter, appId);
|
||||
const pushController = new PushController(pushControllerAdapter, appId);
|
||||
const loggerController = new LoggerController(loggerControllerAdapter, appId);
|
||||
const hooksController = new HooksController(appId, collectionPrefix);
|
||||
const userController = new UserController(emailControllerAdapter, appId, { verifyUserEmails });
|
||||
|
||||
cache.apps[appId] = {
|
||||
|
||||
cache.apps.set(appId, {
|
||||
masterKey: masterKey,
|
||||
serverURL: serverURL,
|
||||
collectionPrefix: collectionPrefix,
|
||||
clientKey: clientKey,
|
||||
javascriptKey: javascriptKey,
|
||||
@@ -140,25 +160,34 @@ function ParseServer({
|
||||
pushController: pushController,
|
||||
loggerController: loggerController,
|
||||
hooksController: hooksController,
|
||||
userController: userController,
|
||||
verifyUserEmails: verifyUserEmails,
|
||||
enableAnonymousUsers: enableAnonymousUsers,
|
||||
allowClientClassCreation: allowClientClassCreation,
|
||||
oauth: oauth
|
||||
};
|
||||
oauth: oauth,
|
||||
appName: appName,
|
||||
publicServerURL: publicServerURL,
|
||||
customPages: customPages,
|
||||
});
|
||||
|
||||
// To maintain compatibility. TODO: Remove in v2.1
|
||||
// To maintain compatibility. TODO: Remove in some version that breaks backwards compatability
|
||||
if (process.env.FACEBOOK_APP_ID) {
|
||||
cache.apps[appId]['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
cache.apps.get(appId)['facebookAppIds'].push(process.env.FACEBOOK_APP_ID);
|
||||
}
|
||||
|
||||
Config.validate(cache.apps.get(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('/', new FilesRouter().getExpressRouter({
|
||||
maxUploadSize: maxUploadSize
|
||||
}));
|
||||
|
||||
api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressApp());
|
||||
|
||||
// TODO: separate this from the regular ParseServer object
|
||||
if (process.env.TESTING == 1) {
|
||||
api.use('/', require('./testing-routes').router);
|
||||
@@ -180,24 +209,27 @@ function ParseServer({
|
||||
new SchemasRouter(),
|
||||
new PushRouter(),
|
||||
new LogsRouter(),
|
||||
new IAPValidationRouter()
|
||||
new IAPValidationRouter(),
|
||||
new FeaturesRouter(),
|
||||
];
|
||||
|
||||
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
|
||||
routers.push(require('./global_config'));
|
||||
routers.push(new GlobalConfigRouter());
|
||||
}
|
||||
|
||||
if (process.env.PARSE_EXPERIMENTAL_HOOKS_ENABLED || process.env.TESTING) {
|
||||
routers.push(new HooksRouter());
|
||||
}
|
||||
|
||||
let appRouter = new PromiseRouter();
|
||||
routers.forEach((router) => {
|
||||
appRouter.merge(router);
|
||||
});
|
||||
let routes = routers.reduce((memo, router) => {
|
||||
return memo.concat(router.routes);
|
||||
}, []);
|
||||
|
||||
let appRouter = new PromiseRouter(routes);
|
||||
|
||||
batch.mountOnto(appRouter);
|
||||
|
||||
appRouter.mountOnto(api);
|
||||
api.use(appRouter.expressApp());
|
||||
|
||||
api.use(middlewares.handleParseErrors);
|
||||
|
||||
@@ -221,13 +253,6 @@ function addParseCloud() {
|
||||
global.Parse = Parse;
|
||||
}
|
||||
|
||||
function getClassName(parseClass) {
|
||||
if (parseClass && parseClass.className) {
|
||||
return parseClass.className;
|
||||
}
|
||||
return parseClass;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ParseServer: ParseServer,
|
||||
S3Adapter: S3Adapter,
|
||||
|
||||
@@ -35,7 +35,7 @@ function handleParseHeaders(req, res, next) {
|
||||
|
||||
var fileViaJSON = false;
|
||||
|
||||
if (!info.appId || !cache.apps[info.appId]) {
|
||||
if (!info.appId || !cache.apps.get(info.appId)) {
|
||||
// See if we can find the app id on the body.
|
||||
if (req.body instanceof Buffer) {
|
||||
// The only chance to find the app id is if this is a file
|
||||
@@ -44,12 +44,10 @@ function handleParseHeaders(req, res, next) {
|
||||
fileViaJSON = true;
|
||||
}
|
||||
|
||||
if (req.body && req.body._ApplicationId
|
||||
&& cache.apps[req.body._ApplicationId]
|
||||
&& (
|
||||
!info.masterKey
|
||||
||
|
||||
cache.apps[req.body._ApplicationId]['masterKey'] === info.masterKey)
|
||||
if (req.body &&
|
||||
req.body._ApplicationId &&
|
||||
cache.apps.get(req.body._ApplicationId) &&
|
||||
(!info.masterKey || cache.apps.get(req.body._ApplicationId).masterKey === info.masterKey)
|
||||
) {
|
||||
info.appId = req.body._ApplicationId;
|
||||
info.javascriptKey = req.body._JavaScriptKey || '';
|
||||
@@ -84,15 +82,14 @@ function handleParseHeaders(req, res, next) {
|
||||
req.body = new Buffer(base64, 'base64');
|
||||
}
|
||||
|
||||
info.app = cache.apps[info.appId];
|
||||
info.app = cache.apps.get(info.appId);
|
||||
req.config = new Config(info.appId, mount);
|
||||
req.database = req.config.database;
|
||||
req.info = info;
|
||||
|
||||
var isMaster = (info.masterKey === req.config.masterKey);
|
||||
|
||||
if (isMaster) {
|
||||
req.auth = new auth.Auth(req.config, true);
|
||||
req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
@@ -117,23 +114,23 @@ function handleParseHeaders(req, res, next) {
|
||||
}
|
||||
|
||||
if (!info.sessionToken) {
|
||||
req.auth = new auth.Auth(req.config, false);
|
||||
req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: false });
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
return auth.getAuthForSessionToken(
|
||||
req.config, info.sessionToken).then((auth) => {
|
||||
return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken })
|
||||
.then((auth) => {
|
||||
if (auth) {
|
||||
req.auth = auth;
|
||||
next();
|
||||
}
|
||||
}).catch((error) => {
|
||||
})
|
||||
.catch((error) => {
|
||||
// TODO: Determine the correct error scenario.
|
||||
console.log(error);
|
||||
throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var allowCrossDomain = function(req, res, next) {
|
||||
@@ -177,6 +174,9 @@ var handleParseErrors = function(err, req, res, next) {
|
||||
|
||||
res.status(httpStatus);
|
||||
res.json({code: err.code, error: err.message});
|
||||
} else if (err.status && err.message) {
|
||||
res.status(err.status);
|
||||
res.json({error: err.message});
|
||||
} else {
|
||||
console.log('Uncaught internal server error.', err, err.stack);
|
||||
res.status(500);
|
||||
@@ -194,6 +194,16 @@ function enforceMasterKeyAccess(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
function promiseEnforceMasterKeyAccess(request) {
|
||||
if (!request.auth.isMaster) {
|
||||
let error = new Error();
|
||||
error.status = 403;
|
||||
error.message = "unauthorized: master key is required";
|
||||
throw error;
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function invalidRequest(req, res) {
|
||||
res.status(403);
|
||||
res.end('{"error":"unauthorized"}');
|
||||
@@ -204,5 +214,6 @@ module.exports = {
|
||||
allowMethodOverride: allowMethodOverride,
|
||||
handleParseErrors: handleParseErrors,
|
||||
handleParseHeaders: handleParseHeaders,
|
||||
enforceMasterKeyAccess: enforceMasterKeyAccess
|
||||
enforceMasterKeyAccess: enforceMasterKeyAccess,
|
||||
promiseEnforceMasterKeyAccess
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/* @flow */
|
||||
export default (errorMessage: string) => {throw errorMessage}
|
||||
/** @flow */
|
||||
export default (errorMessage: string): any => { throw errorMessage }
|
||||
|
||||
@@ -46,7 +46,7 @@ function del(config, auth, className, objectId) {
|
||||
.then((response) => {
|
||||
if (response && response.results && response.results.length) {
|
||||
response.results[0].className = className;
|
||||
cache.clearUser(response.results[0].sessionToken);
|
||||
cache.users.remove(response.results[0].sessionToken);
|
||||
inflatedObject = Parse.Object.fromJSON(response.results[0]);
|
||||
return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config.applicationId);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ var router = express.Router();
|
||||
// creates a unique app in the cache, with a collection prefix
|
||||
function createApp(req, res) {
|
||||
var appId = cryptoUtils.randomHexString(32);
|
||||
cache.apps[appId] = {
|
||||
// TODO: (nlutsenko) This doesn't work and should die, since there are no controllers on this configuration.
|
||||
cache.apps.set(appId, {
|
||||
'collectionPrefix': appId + '_',
|
||||
'masterKey': 'master'
|
||||
};
|
||||
});
|
||||
var keys = {
|
||||
'application_id': appId,
|
||||
'client_key': 'unused',
|
||||
@@ -31,7 +32,7 @@ function clearApp(req, res) {
|
||||
if (!req.auth.isMaster) {
|
||||
return res.status(401).send({"error": "unauthorized"});
|
||||
}
|
||||
req.database.deleteEverything().then(() => {
|
||||
return req.config.database.deleteEverything().then(() => {
|
||||
res.status(200).send({});
|
||||
});
|
||||
}
|
||||
@@ -41,8 +42,8 @@ function dropApp(req, res) {
|
||||
if (!req.auth.isMaster) {
|
||||
return res.status(401).send({"error": "unauthorized"});
|
||||
}
|
||||
req.database.deleteEverything().then(() => {
|
||||
delete cache.apps[req.config.applicationId];
|
||||
return req.config.database.deleteEverything().then(() => {
|
||||
cache.apps.remove(req.config.applicationId);
|
||||
res.status(200).send({});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,12 @@ export function transformKeyValue(schema, className, restKey, restValue, options
|
||||
key = '_updated_at';
|
||||
timeField = true;
|
||||
break;
|
||||
case '_email_verify_token':
|
||||
key = "_email_verify_token";
|
||||
break;
|
||||
case '_perishable_token':
|
||||
key = "_perishable_token";
|
||||
break;
|
||||
case 'sessionToken':
|
||||
case '_session_token':
|
||||
key = '_session_token';
|
||||
@@ -638,7 +644,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
|
||||
break;
|
||||
case 'expiresAt':
|
||||
case '_expiresAt':
|
||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key])).iso;
|
||||
restObject['expiresAt'] = Parse._encode(new Date(mongoObject[key]));
|
||||
break;
|
||||
default:
|
||||
// Check other auth data keys
|
||||
@@ -649,7 +655,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
|
||||
restObject['authData'][provider] = mongoObject[key];
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (key.indexOf('_p_') == 0) {
|
||||
var newKey = key.substring(3);
|
||||
var expected;
|
||||
|
||||
@@ -110,12 +110,11 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb
|
||||
if (auth.user) {
|
||||
request['user'] = auth.user;
|
||||
}
|
||||
// TODO: Add installation to Auth?
|
||||
if (auth.installationId) {
|
||||
request['installationId'] = auth.installationId;
|
||||
}
|
||||
return request;
|
||||
};
|
||||
}
|
||||
|
||||
// Creates the response object, and uses the request object to pass data
|
||||
// The API will call this with REST API formatted objects, this will
|
||||
@@ -157,8 +156,8 @@ export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObj
|
||||
var response = getResponseObject(request, resolve, reject);
|
||||
// Force the current Parse app before the trigger
|
||||
Parse.applicationId = applicationId;
|
||||
Parse.javascriptKey = cache.apps[applicationId].javascriptKey || '';
|
||||
Parse.masterKey = cache.apps[applicationId].masterKey;
|
||||
Parse.javascriptKey = cache.apps.get(applicationId).javascriptKey || '';
|
||||
Parse.masterKey = cache.apps.get(applicationId).masterKey;
|
||||
trigger(request, response);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user