Endpoints for audiences CRUD (#3861)

This commit is contained in:
Antonio Davi Macedo Coelho de Castro
2017-06-21 02:54:13 -03:00
committed by Natan Rolnik
parent e94991b368
commit 4509d25471
11 changed files with 448 additions and 74 deletions

287
spec/AudienceRouter.spec.js Normal file
View File

@@ -0,0 +1,287 @@
var auth = require('../src/Auth');
var Config = require('../src/Config');
var rest = require('../src/rest');
var AudiencesRouter = require('../src/Routers/AudiencesRouter').AudiencesRouter;
describe('AudiencesRouter', () => {
it('uses find condition from request.body', (done) => {
var config = new Config('test');
var androidAudienceRequest = {
'name': 'Android Users',
'query': '{ "test": "android" }'
};
var iosAudienceRequest = {
'name': 'Iphone Users',
'query': '{ "test": "ios" }'
};
var request = {
config: config,
auth: auth.master(config),
body: {
where: {
query: '{ "test": "android" }'
}
},
query: {},
info: {}
};
var router = new AudiencesRouter();
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
})
.then(() => {
return router.handleFind(request);
})
.then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
})
.catch((err) => {
fail(JSON.stringify(err));
done();
});
});
it('uses find condition from request.query', (done) => {
var config = new Config('test');
var androidAudienceRequest = {
'name': 'Android Users',
'query': '{ "test": "android" }'
};
var iosAudienceRequest = {
'name': 'Iphone Users',
'query': '{ "test": "ios" }'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
where: {
'query': '{ "test": "android" }'
}
},
info: {}
};
var router = new AudiencesRouter();
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
})
.then(() => {
return router.handleFind(request);
})
.then((res) => {
var results = res.response.results;
expect(results.length).toEqual(1);
done();
})
.catch((err) => {
fail(err);
done();
});
});
it('query installations with limit = 0', (done) => {
var config = new Config('test');
var androidAudienceRequest = {
'name': 'Android Users',
'query': '{ "test": "android" }'
};
var iosAudienceRequest = {
'name': 'Iphone Users',
'query': '{ "test": "ios" }'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0
},
info: {}
};
new Config('test');
var router = new AudiencesRouter();
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
})
.then(() => {
return router.handleFind(request);
})
.then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
done();
})
.catch((err) => {
fail(JSON.stringify(err));
done();
});
});
it('query installations with count = 1', done => {
var config = new Config('test');
var androidAudienceRequest = {
'name': 'Android Users',
'query': '{ "test": "android" }'
};
var iosAudienceRequest = {
'name': 'Iphone Users',
'query': '{ "test": "ios" }'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
count: 1
},
info: {}
};
var router = new AudiencesRouter();
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
.then(() => rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest))
.then(() => router.handleFind(request))
.then((res) => {
var response = res.response;
expect(response.results.length).toEqual(2);
expect(response.count).toEqual(2);
done();
})
.catch(error => {
fail(JSON.stringify(error));
done();
})
});
it('query installations with limit = 0 and count = 1', (done) => {
var config = new Config('test');
var androidAudienceRequest = {
'name': 'Android Users',
'query': '{ "test": "android" }'
};
var iosAudienceRequest = {
'name': 'Iphone Users',
'query': '{ "test": "ios" }'
};
var request = {
config: config,
auth: auth.master(config),
body: {},
query: {
limit: 0,
count: 1
},
info: {}
};
var router = new AudiencesRouter();
rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest)
.then(() => {
return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest);
})
.then(() => {
return router.handleFind(request);
})
.then((res) => {
var response = res.response;
expect(response.results.length).toEqual(0);
expect(response.count).toEqual(2);
done();
})
.catch((err) => {
fail(JSON.stringify(err));
done();
});
});
it('should create, read, update and delete audiences throw api', (done) => {
Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true })
.then(() => {
Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then((results) => {
expect(results.results.length).toEqual(1);
expect(results.results[0].name).toEqual('My Audience');
expect(results.results[0].query.deviceType).toEqual('ios');
Parse._request('GET', `push_audiences/${results.results[0].objectId}`, {}, { useMasterKey: true }).then((results) => {
expect(results.name).toEqual('My Audience');
expect(results.query.deviceType).toEqual('ios');
Parse._request('PUT', `push_audiences/${results.objectId}`, { name: 'My Audience 2' }, { useMasterKey: true }).then(() => {
Parse._request('GET', `push_audiences/${results.objectId}`, {}, { useMasterKey: true }).then((results) => {
expect(results.name).toEqual('My Audience 2');
expect(results.query.deviceType).toEqual('ios');
Parse._request('DELETE', `push_audiences/${results.objectId}`, {}, { useMasterKey: true }).then(() => {
Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then((results) => {
expect(results.results.length).toEqual(0);
done();
});
});
});
});
});
});
});
});
it('should only create with master key', (done) => {
Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })})
.then(
() => {},
(error) => {
expect(error.message).toEqual('unauthorized: master key is required');
done();
}
);
});
it('should only find with master key', (done) => {
Parse._request('GET', 'push_audiences', {})
.then(
() => {},
(error) => {
expect(error.message).toEqual('unauthorized: master key is required');
done();
}
);
});
it('should only get with master key', (done) => {
Parse._request('GET', `push_audiences/someId`, {})
.then(
() => {},
(error) => {
expect(error.message).toEqual('unauthorized: master key is required');
done();
}
);
});
it('should only update with master key', (done) => {
Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2' })
.then(
() => {},
(error) => {
expect(error.message).toEqual('unauthorized: master key is required');
done();
}
);
});
it('should only delete with master key', (done) => {
Parse._request('DELETE', `push_audiences/someId`, {})
.then(
() => {},
(error) => {
expect(error.message).toEqual('unauthorized: master key is required');
done();
}
);
});
});

View File

@@ -915,15 +915,15 @@ describe('Parse.Query testing', () => {
it("order by descending number and string, with space", function(done) {
var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] });
var makeBoxedNumber = function (num, i) {
return new BoxedNumber({number: num, string: strings[i]});
};
Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then(
function() {
function () {
var query = new Parse.Query(BoxedNumber);
query.descending("number, string");
query.find(expectSuccess({
success: function(results) {
success: function (results) {
equal(results.length, 4);
equal(results[0].get("number"), 3);
equal(results[0].get("string"), "c");
@@ -936,7 +936,8 @@ describe('Parse.Query testing', () => {
done();
}
}));
}, (err) => {
},
(err) => {
jfail(err);
done();
});

View File

@@ -175,28 +175,28 @@ describe('rest query', () => {
const p0 = rp.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
where: '{"foo":{"$ne": "baz"}}',
limit: 1
}).replace('=', '%3D'),
}).then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
});
url: 'http://localhost:8378/1/classes/TestParameterEncode?' + querystring.stringify({
where: '{"foo":{"$ne": "baz"}}',
limit: 1
}).replace('=', '%3D'),
})
.then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
});
const p1 = rp.get({
headers: headers,
url: 'http://localhost:8378/1/classes/TestParameterEncode?'
+ querystring.stringify({
limit: 1
}).replace('=', '%3D'),
}).then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
});
url: 'http://localhost:8378/1/classes/TestParameterEncode?' + querystring.stringify({
limit: 1
}).replace('=', '%3D'),
})
.then(fail, (response) => {
const error = response.error;
var b = JSON.parse(error);
expect(b.code).toEqual(Parse.Error.INVALID_QUERY);
});
return Promise.all([p0, p1]);
}).then(done).catch((err) => {
jfail(err);

View File

@@ -667,11 +667,11 @@ export class PostgresStorageAdapter {
const joins = results.reduce((list, schema) => {
return list.concat(joinTablesForSchema(schema.schema));
}, []);
const classes = ['_SCHEMA','_PushStatus','_JobStatus','_JobSchedule','_Hooks','_GlobalConfig', ...results.map(result => result.className), ...joins];
return this._client.tx(t=>t.batch(classes.map(className=>t.none('DROP TABLE IF EXISTS $<className:name>', { className }))));
const classes = ['_SCHEMA', '_PushStatus', '_JobStatus', '_JobSchedule', '_Hooks', '_GlobalConfig', '_Audience', ...results.map(result => result.className), ...joins];
return this._client.tx(t=>t.batch(classes.map(className=>t.none('DROP TABLE IF EXISTS $<className:name>', {className}))));
}, error => {
if (error.code === PostgresRelationDoesNotExistError) {
// No _SCHEMA collection. Don't delete anything.
// No _SCHEMA collection. Don't delete anything.
return;
} else {
throw error;

View File

@@ -690,11 +690,12 @@ DatabaseController.prototype.reduceRelationKeys = function(className, query) {
return this.relatedIds(
relatedTo.object.className,
relatedTo.key,
relatedTo.object.objectId).then((ids) => {
delete query['$relatedTo'];
this.addInObjectIdsIds(ids, query);
return this.reduceRelationKeys(className, query);
});
relatedTo.object.objectId)
.then((ids) => {
delete query['$relatedTo'];
this.addInObjectIdsIds(ids, query);
return this.reduceRelationKeys(className, query);
});
}
};

View File

@@ -114,6 +114,11 @@ const defaultColumns = Object.freeze({
_GlobalConfig: {
"objectId": {type: 'String'},
"params": {type: 'Object'}
},
_Audience: {
"objectId": {type:'String'},
"name": {type:'String'},
"query": {type:'String'} //storing query as JSON string to prevent "Nested keys should not contain the '$' or '.' characters" error
}
});
@@ -122,9 +127,9 @@ const requiredColumns = Object.freeze({
_Role: ["name", "ACL"]
});
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus', '_JobSchedule']);
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus', '_JobSchedule', '_Audience']);
const volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig', '_JobSchedule']);
const volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig', '_JobSchedule', '_Audience']);
// 10 alpha numberic chars + uppercase
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
@@ -306,7 +311,11 @@ const _JobScheduleSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
fields: {},
classLevelPermissions: {}
}));
const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema];
const _AudienceSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
className: "_Audience",
fields: defaultColumns._Audience
}));
const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, _AudienceSchema];
const dbTypeMatchesObjectType = (dbType, objectType) => {
if (dbType.type !== objectType.type) return false;

View File

@@ -49,6 +49,7 @@ import { SessionsRouter } from './Routers/SessionsRouter';
import { UserController } from './Controllers/UserController';
import { UsersRouter } from './Routers/UsersRouter';
import { PurgeRouter } from './Routers/PurgeRouter';
import { AudiencesRouter } from './Routers/AudiencesRouter';
import DatabaseController from './Controllers/DatabaseController';
import SchemaCache from './Controllers/SchemaCache';
@@ -379,7 +380,8 @@ class ParseServer {
new GlobalConfigRouter(),
new PurgeRouter(),
new HooksRouter(),
new CloudCodeRouter()
new CloudCodeRouter(),
new AudiencesRouter()
];
const routes = routers.reduce((memo, router) => {

View File

@@ -197,11 +197,11 @@ RestQuery.prototype.redirectClassNameForKey = function() {
}
// We need to change the class name based on the schema
return this.config.database.redirectClassNameForKey(
this.className, this.redirectKey).then((newClassName) => {
this.className = newClassName;
this.redirectClassName = newClassName;
});
return this.config.database.redirectClassNameForKey(this.className, this.redirectKey)
.then((newClassName) => {
this.className = newClassName;
this.redirectClassName = newClassName;
});
};
// Validates this operation against the allowClientClassCreation config.
@@ -491,24 +491,24 @@ RestQuery.prototype.runFind = function(options = {}) {
if (options.op) {
findOptions.op = options.op;
}
return this.config.database.find(
this.className, this.restWhere, findOptions).then((results) => {
if (this.className === '_User') {
for (var result of results) {
cleanResultOfSensitiveUserInfo(result, this.auth, this.config);
cleanResultAuthData(result);
return this.config.database.find(this.className, this.restWhere, findOptions)
.then((results) => {
if (this.className === '_User') {
for (var result of results) {
cleanResultOfSensitiveUserInfo(result, this.auth, this.config);
cleanResultAuthData(result);
}
}
}
this.config.filesController.expandFilesInObject(this.config, results);
this.config.filesController.expandFilesInObject(this.config, results);
if (this.redirectClassName) {
for (var r of results) {
r.className = this.redirectClassName;
if (this.redirectClassName) {
for (var r of results) {
r.className = this.redirectClassName;
}
}
}
this.response = {results: results};
});
this.response = {results: results};
});
};
// Returns a promise for whether it was successful.
@@ -520,10 +520,10 @@ RestQuery.prototype.runCount = function() {
this.findOptions.count = true;
delete this.findOptions.skip;
delete this.findOptions.limit;
return this.config.database.find(
this.className, this.restWhere, this.findOptions).then((c) => {
this.response.count = c;
});
return this.config.database.find(this.className, this.restWhere, this.findOptions)
.then((c) => {
this.response.count = c;
});
};
// Augments this.response with data at the paths provided in this.include.

View File

@@ -0,0 +1,71 @@
import ClassesRouter from './ClassesRouter';
import rest from '../rest';
import * as middleware from '../middlewares';
export class AudiencesRouter extends ClassesRouter {
handleFind(req) {
const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
var options = {};
if (body.skip) {
options.skip = Number(body.skip);
}
if (body.limit || body.limit === 0) {
options.limit = Number(body.limit);
}
if (body.order) {
options.order = String(body.order);
}
if (body.count) {
options.count = true;
}
if (body.include) {
options.include = String(body.include);
}
return rest.find(req.config, req.auth, '_Audience', body.where, options, req.info.clientSDK)
.then((response) => {
response.results.forEach((item) => {
item.query = JSON.parse(item.query);
});
return {response: response};
});
}
handleGet(req) {
req.params.className = '_Audience';
return super.handleGet(req)
.then((data) => {
data.response.query = JSON.parse(data.response.query);
return data;
});
}
handleCreate(req) {
req.params.className = '_Audience';
return super.handleCreate(req);
}
handleUpdate(req) {
req.params.className = '_Audience';
return super.handleUpdate(req);
}
handleDelete(req) {
req.params.className = '_Audience';
return super.handleDelete(req);
}
mountRoutes() {
this.route('GET','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); });
this.route('GET','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleGet(req); });
this.route('POST','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleCreate(req); });
this.route('PUT','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleUpdate(req); });
this.route('DELETE','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleDelete(req); });
}
}
export default AudiencesRouter;

View File

@@ -32,7 +32,7 @@ export class FeaturesRouter extends PromiseRouter {
immediatePush: req.config.hasPushSupport,
scheduledPush: req.config.hasPushScheduledSupport,
storedPushData: req.config.hasPushSupport,
pushAudiences: false,
pushAudiences: true,
},
schemas: {
addField: true,

View File

@@ -128,26 +128,29 @@ export class FunctionsRouter extends PromiseRouter {
var response = FunctionsRouter.createResponseObject((result) => {
try {
const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result));
logger.info(`Ran cloud function ${functionName} for user ${userString} `
+ `with:\n Input: ${cleanInput }\n Result: ${cleanResult }`, {
functionName,
params,
user: userString,
});
logger.info(
`Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput }\n Result: ${cleanResult }`,
{
functionName,
params,
user: userString,
}
);
resolve(result);
} catch (e) {
reject(e);
}
}, (error) => {
try {
logger.error(`Failed running cloud function ${functionName} for `
+ `user ${userString} with:\n Input: ${cleanInput}\n Error: `
+ JSON.stringify(error), {
functionName,
error,
params,
user: userString
});
logger.error(
`Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + JSON.stringify(error),
{
functionName,
error,
params,
user: userString
}
);
reject(error);
} catch (e) {
reject(e);