diff --git a/index.js b/index.js index 0bce0c729a..d7c12a2ed7 100644 --- a/index.js +++ b/index.js @@ -111,6 +111,7 @@ function ParseServer(args) { router.merge(require('./push')); router.merge(require('./installations')); router.merge(require('./functions')); + router.merge(require('./schemas')); batch.mountOnto(router); diff --git a/package.json b/package.json index 95ef2f3a35..95003e813f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "codecov": "^1.0.1", + "deep-diff": "^0.3.3", "istanbul": "^0.4.2", "jasmine": "^2.3.2", "mongodb-runner": "^3.1.15" diff --git a/schemas.js b/schemas.js new file mode 100644 index 0000000000..88b0da38fe --- /dev/null +++ b/schemas.js @@ -0,0 +1,69 @@ +// schemas.js + +var express = require('express'), + PromiseRouter = require('./PromiseRouter'); + +var router = new PromiseRouter(); + +function mongoFieldTypeToApiResponseType(type) { + if (type[0] === '*') { + return { + type: 'Pointer', + targetClass: type.slice(1), + }; + } + if (type.startsWith('relation<')) { + return { + type: 'Relation', + targetClass: type.slice('relation<'.length, type.length - 1), + }; + } + switch (type) { + case 'number': return {type: 'Number'}; + case 'string': return {type: 'String'}; + case 'boolean': return {type: 'Boolean'}; + case 'date': return {type: 'Date'}; + case 'object': return {type: 'Object'}; + case 'array': return {type: 'Array'}; + case 'geopoint': return {type: 'GeoPoint'}; + case 'file': return {type: 'File'}; + } +} + +function mongoSchemaAPIResponseFields(schema) { + fieldNames = Object.keys(schema).filter(key => key !== '_id'); + response = {}; + fieldNames.forEach(fieldName => { + response[fieldName] = mongoFieldTypeToApiResponseType(schema[fieldName]); + }); + response.ACL = {type: 'ACL'}; + response.createdAt = {type: 'Date'}; + response.updatedAt = {type: 'Date'}; + response.objectId = {type: 'String'}; + return response; +} + +function mongoSchemaToSchemaAPIResponse(schema) { + return { + className: schema._id, + fields: mongoSchemaAPIResponseFields(schema), + }; +} + +function getAllSchemas(req) { + if (!req.auth.isMaster) { + return Promise.resolve({ + status: 401, + response: {error: 'unauthorized'}, + }); + } + return req.config.database.collection('_SCHEMA') + .then(coll => coll.find({}).toArray()) + .then(schemas => ({response: { + results: schemas.map(mongoSchemaToSchemaAPIResponse) + }})); +} + +router.route('GET', '/schemas', getAllSchemas); + +module.exports = router; diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js new file mode 100644 index 0000000000..a4d2f6188c --- /dev/null +++ b/spec/schemas.spec.js @@ -0,0 +1,110 @@ +var request = require('request'); +var dd = require('deep-diff'); + +describe('schemas', () => { + it('requires the master key to get all schemas', (done) => { + request.get({ + url: 'http://localhost:8378/1/schemas', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, (error, response, body) => { + expect(response.statusCode).toEqual(401); + expect(body.error).toEqual('unauthorized'); + done(); + }); + }); + + it('responds with empty list when there are no schemas', done => { + request.get({ + url: 'http://localhost:8378/1/schemas', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, (error, response, body) => { + expect(body.results).toEqual([]); + done(); + }); + }); + + it('responds with a list of schemas after creating objects', done => { + var obj1 = new Parse.Object('HasAllPOD'); + obj1.set('aNumber', 5); + obj1.set('aString', 'string'); + obj1.set('aBool', true); + obj1.set('aDate', new Date()); + obj1.set('aObject', {k1: 'value', k2: true, k3: 5}); + obj1.set('aArray', ['contents', true, 5]); + obj1.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0})); + obj1.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' })); + var obj1ACL = new Parse.ACL(); + obj1ACL.setPublicWriteAccess(false); + obj1.setACL(obj1ACL); + + obj1.save().then(savedObj1 => { + var obj2 = new Parse.Object('HasPointersAndRelations'); + obj2.set('aPointer', savedObj1); + var relation = obj2.relation('aRelation'); + relation.add(obj1); + return obj2.save(); + }).then(() => { + request.get({ + url: 'http://localhost:8378/1/schemas', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, (error, response, body) => { + var expected = { + results: [ + { + className: 'HasAllPOD', + fields: { + //Default fields + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + //Custom fields + aNumber: {type: 'Number'}, + aString: {type: 'String'}, + aBool: {type: 'Boolean'}, + aDate: {type: 'Date'}, + aObject: {type: 'Object'}, + aArray: {type: 'Array'}, + aGeoPoint: {type: 'GeoPoint'}, + aFile: {type: 'File'} + }, + }, + { + className: 'HasPointersAndRelations', + fields: { + //Default fields + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + //Custom fields + aPointer: { + type: 'Pointer', + targetClass: 'HasAllPOD', + }, + aRelation: { + type: 'Relation', + targetClass: 'HasAllPOD', + }, + }, + } + ] + }; + expect(body).toEqual(expected); + done(); + }) + }); + }); +});