diff --git a/spec/ParsePolygon.spec.js b/spec/ParsePolygon.spec.js index 2527a09e55..628ab1fa68 100644 --- a/spec/ParsePolygon.spec.js +++ b/spec/ParsePolygon.spec.js @@ -1,7 +1,14 @@ const TestObject = Parse.Object.extend('TestObject'); const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const Config = require('../src/Config'); const rp = require('request-promise'); + +const masterKeyHeaders = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', +}; + const defaultHeaders = { 'X-Parse-Application-Id': 'test', 'X-Parse-Rest-API-Key': 'rest' @@ -272,6 +279,39 @@ describe('Parse.Polygon testing', () => { }); }); +describe_only_db('postgres')('schemas', () => { + it('can create polygon index', (done) => { + const adapter = Config.get('test').database.adapter; + const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]]; + const polygon = new Parse.Polygon(coords); + const obj = new TestObject(); + obj.set('polygon', polygon); + reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + return obj.save(); + }).then(() => { + return adapter.getIndexes('TestObject'); + }).then((indexes) => { + rp.get({ + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body.indexes).toEqual(indexes); + expect(indexes._id_._id).toEqual(1); + expect(indexes.polygon_2dsphere.polygon).toEqual(1); + done(); + }); + }).catch(error => { + console.log(error); + done(); + }); + }); +}); + describe_only_db('mongo')('Parse.Polygon testing', () => { beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently()); @@ -313,11 +353,10 @@ describe_only_db('mongo')('Parse.Polygon testing', () => { equal(resp.polygon2, polygon); return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - equal(indexes.length, 4); - equal(indexes[0].key, {_id: 1}); - equal(indexes[1].key, {location: '2d'}); - equal(indexes[2].key, {polygon: '2dsphere'}); - equal(indexes[3].key, {polygon2: '2dsphere'}); + equal(indexes._id_, {_id: 1}); + equal(indexes.location_2d, {location: '2d'}); + equal(indexes.polygon_2dsphere, {polygon: '2dsphere'}); + equal(indexes.polygon2_2dsphere, {polygon2: '2dsphere'}); done(); }, done.fail); }); diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index eac29c9268..589edb8423 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -287,12 +287,13 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = }).then(() => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(1); + expect(indexes._id_).toBeDefined(); return databaseAdapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); }).then(() => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes._id_).toBeDefined(); + expect(indexes.subject_text_comment_text).toBeDefined(); const where = { subject: { $text: { @@ -314,7 +315,8 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = expect(resp.results.length).toEqual(3); return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes._id_).toBeDefined(); + expect(indexes.subject_text_comment_text).toBeDefined(); rp.get({ url: 'http://localhost:8378/1/schemas/TestObject', headers: { @@ -324,10 +326,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = json: true, }, (error, response, body) => { expect(body.indexes._id_).toBeDefined(); - expect(body.indexes._id_._id).toEqual(1); expect(body.indexes.subject_text_comment_text).toBeDefined(); - expect(body.indexes.subject_text_comment_text.subject).toEqual('text'); - expect(body.indexes.subject_text_comment_text.comment).toEqual('text'); done(); }); }).catch(done.fail); @@ -339,7 +338,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = }).then(() => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(1); + expect(indexes._id_).toBeDefined(); return rp.put({ url: 'http://localhost:8378/1/schemas/TestObject', json: true, @@ -357,7 +356,8 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = }).then(() => { return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes._id_).toBeDefined(); + expect(indexes.text_test).toBeDefined(); const where = { subject: { $text: { @@ -379,7 +379,8 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = expect(resp.results.length).toEqual(3); return databaseAdapter.getIndexes('TestObject'); }).then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes._id_).toBeDefined(); + expect(indexes.text_test).toBeDefined(); rp.get({ url: 'http://localhost:8378/1/schemas/TestObject', headers: { @@ -389,10 +390,7 @@ describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () = json: true, }, (error, response, body) => { expect(body.indexes._id_).toBeDefined(); - expect(body.indexes._id_._id).toEqual(1); expect(body.indexes.text_test).toBeDefined(); - expect(body.indexes.text_test.subject).toEqual('text'); - expect(body.indexes.text_test.comment).toEqual('text'); done(); }); }).catch(done.fail); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index c2f738e693..fa171bb567 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -606,7 +606,7 @@ describe('SchemaController', () => { }); }); - it('creates non-custom classes which include relation field', done => { + it_only_db('mongo')('creates non-custom classes which include relation field', done => { config.database.loadSchema() //as `_Role` is always created by default, we only get it here .then(schema => schema.getOneSchema('_Role')) @@ -630,6 +630,44 @@ describe('SchemaController', () => { delete: { '*': true }, addField: { '*': true }, }, + indexes: { + _id_: { _id: 1 }, + name_1: { name: 1 }, + }, + }; + expect(dd(actualSchema, expectedSchema)).toEqual(undefined); + done(); + }); + }); + + it_only_db('postgres')('creates non-custom classes which include relation field', done => { + config.database.loadSchema() + //as `_Role` is always created by default, we only get it here + .then(schema => schema.getOneSchema('_Role')) + .then(actualSchema => { + const expectedSchema = { + className: '_Role', + fields: { + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + createdAt: { type: 'Date' }, + ACL: { type: 'ACL' }, + name: { type: 'String' }, + users: { type: 'Relation', targetClass: '_User' }, + roles: { type: 'Relation', targetClass: '_Role' }, + }, + classLevelPermissions: { + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true }, + }, + indexes: { + _id_: { _id: 1 }, + unique_name: { name: 1 }, + }, }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index e8ec3de67b..0ef90b2266 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -45,6 +45,10 @@ const defaultClassLevelPermissions = { } } +const defaultIndex = { + _id_: { _id: 1 }, +}; + const plainOldDataSchema = { className: 'HasAllPOD', fields: { @@ -101,7 +105,12 @@ const userSchema = { "authData": {"type": "Object"} }, "classLevelPermissions": defaultClassLevelPermissions, -} + indexes: { + _id_: { _id: 1 }, + email_1: { email: 1 }, + username_1: { username: 1 } + }, +}; const roleSchema = { "className": "_Role", @@ -115,7 +124,50 @@ const roleSchema = { "roles": {"type":"Relation", "targetClass":"_Role"} }, "classLevelPermissions": defaultClassLevelPermissions, -} + indexes: { + _id_: { _id: 1 }, + name_1: { name: 1 }, + }, +}; + +const pgUserSchema = { + "className": "_User", + "fields": { + "objectId": {"type": "String"}, + "createdAt": {"type": "Date"}, + "updatedAt": {"type": "Date"}, + "ACL": {"type": "ACL"}, + "username": {"type": "String"}, + "password": {"type": "String"}, + "email": {"type": "String"}, + "emailVerified": {"type": "Boolean"}, + "authData": {"type": "Object"} + }, + "classLevelPermissions": defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + unique_email: { email: 1 }, + unique_username: { username: 1 } + }, +}; + +const pgRoleSchema = { + "className": "_Role", + "fields": { + "objectId": {"type": "String"}, + "createdAt": {"type": "Date"}, + "updatedAt": {"type": "Date"}, + "ACL": {"type": "ACL"}, + "name": {"type":"String"}, + "users": {"type":"Relation", "targetClass":"_User"}, + "roles": {"type":"Relation", "targetClass":"_Role"} + }, + "classLevelPermissions": defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + unique_name: { name: 1 }, + }, +}; const noAuthHeaders = { 'X-Parse-Application-Id': 'test', @@ -178,21 +230,35 @@ describe('schemas', () => { }); }); - it('creates _User schema when server starts', done => { + it_only_db('mongo')('creates _User schema when server starts', done => { + request.get({ + url: 'http://localhost:8378/1/schemas', + json: true, + headers: masterKeyHeaders, + }, (error, response, body) => { + const expected = { + results: [userSchema, roleSchema] + }; + expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined); + done(); + }); + }); + + it_only_db('postgres')('creates _User schema when server starts', done => { request.get({ url: 'http://localhost:8378/1/schemas', json: true, headers: masterKeyHeaders, }, (error, response, body) => { const expected = { - results: [userSchema,roleSchema] + results: [pgUserSchema, pgRoleSchema] }; expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined); done(); }); }); - it('responds with a list of schemas after creating objects', done => { + it_only_db('mongo')('responds with a list of schemas after creating objects', done => { const obj1 = hasAllPODobject(); obj1.save().then(savedObj1 => { const obj2 = new Parse.Object('HasPointersAndRelations'); @@ -215,6 +281,29 @@ describe('schemas', () => { }); }); + it_only_db('postgres')('responds with a list of schemas after creating objects', done => { + const obj1 = hasAllPODobject(); + obj1.save().then(savedObj1 => { + const obj2 = new Parse.Object('HasPointersAndRelations'); + obj2.set('aPointer', savedObj1); + const relation = obj2.relation('aRelation'); + relation.add(obj1); + return obj2.save(); + }).then(() => { + request.get({ + url: 'http://localhost:8378/1/schemas', + json: true, + headers: masterKeyHeaders, + }, (error, response, body) => { + const expected = { + results: [pgUserSchema,pgRoleSchema,plainOldDataSchema,pointersAndRelationsSchema] + }; + expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined); + done(); + }) + }); + }); + it('responds with a single schema', done => { const obj = hasAllPODobject(); obj.save().then(() => { @@ -347,7 +436,8 @@ describe('schemas', () => { foo: {type: 'Number'}, ptr: {type: 'Pointer', targetClass: 'SomeClass'}, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: defaultIndex, }); done(); }); @@ -411,7 +501,8 @@ describe('schemas', () => { updatedAt: {type: 'Date'}, objectId: {type: 'String'}, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: defaultIndex, }); done(); }); @@ -633,7 +724,8 @@ describe('schemas', () => { "updatedAt": {"type": "Date"}, "newField": {"type": "String"}, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: defaultIndex, })).toEqual(undefined); request.get({ url: 'http://localhost:8378/1/schemas/NewClass', @@ -649,7 +741,8 @@ describe('schemas', () => { objectId: {type: 'String'}, newField: {type: 'String'}, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: defaultIndex, }); done(); }); @@ -657,7 +750,7 @@ describe('schemas', () => { }) }); - it('lets you add fields to system schema', done => { + it_only_db('mongo')('lets you add fields to system schema', done => { request.post({ url: 'http://localhost:8378/1/schemas/_User', headers: masterKeyHeaders, @@ -687,7 +780,12 @@ describe('schemas', () => { newField: {type: 'String'}, ACL: {type: 'ACL'} }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + email_1: { email: 1 }, + username_1: { username: 1 } + }, })).toBeUndefined(); request.get({ url: 'http://localhost:8378/1/schemas/_User', @@ -708,7 +806,81 @@ describe('schemas', () => { newField: {type: 'String'}, ACL: {type: 'ACL'} }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + email_1: { email: 1 }, + username_1: { username: 1 } + }, + })).toBeUndefined(); + done(); + }); + }); + }) + }); + + it_only_db('postgres')('lets you add fields to system schema', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: {type: 'String'} + } + } + }, (error, response, body) => { + expect(dd(body,{ + className: '_User', + fields: { + objectId: {type: 'String'}, + updatedAt: {type: 'Date'}, + createdAt: {type: 'Date'}, + username: {type: 'String'}, + password: {type: 'String'}, + email: {type: 'String'}, + emailVerified: {type: 'Boolean'}, + authData: {type: 'Object'}, + newField: {type: 'String'}, + ACL: {type: 'ACL'} + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + unique_email: { email: 1 }, + unique_username: { username: 1 } + }, + })).toBeUndefined(); + request.get({ + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true + }, (error, response, body) => { + expect(dd(body,{ + className: '_User', + fields: { + objectId: {type: 'String'}, + updatedAt: {type: 'Date'}, + createdAt: {type: 'Date'}, + username: {type: 'String'}, + password: {type: 'String'}, + email: {type: 'String'}, + emailVerified: {type: 'Boolean'}, + authData: {type: 'Object'}, + newField: {type: 'String'}, + ACL: {type: 'ACL'} + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + unique_email: { email: 1 }, + unique_username: { username: 1 } + }, })).toBeUndefined(); done(); }); @@ -1837,7 +2009,7 @@ describe('schemas', () => { }) }); - it('can create index on default field', done => { + it_only_db('mongo')('can create index on default field', done => { request.post({ url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, @@ -1850,12 +2022,49 @@ describe('schemas', () => { json: true, body: { indexes: { - name1: { createdAt: 1}, + name1: { createdAt: 1 }, + name2: { updatedAt: 1 }, } } }, (error, response, body) => { - expect(body.indexes.name1).toEqual({ createdAt: 1}); - done(); + expect(body.indexes.name1).toEqual({ createdAt: 1 }); + expect(body.indexes.name2).toEqual({ updatedAt: 1 }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes._id_).toEqual({ _id: 1 }); + expect(indexes.name1).toEqual({ _created_at: 1 }); + expect(indexes.name2).toEqual({ _updated_at: 1 }); + done(); + }); + }); + }) + }); + + it_only_db('postgres')('can create index on default field', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { createdAt: 1 }, + name2: { updatedAt: 1 }, + } + } + }, (error, response, body) => { + expect(body.indexes.name1).toEqual({ createdAt: 1 }); + expect(body.indexes.name2).toEqual({ updatedAt: 1 }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes._id_).toEqual({ _id: 1 }); + expect(indexes.name1).toEqual({ createdAt: 1 }); + expect(indexes.name2).toEqual({ updatedAt: 1 }); + done(); + }); }); }) }); @@ -1913,17 +2122,18 @@ describe('schemas', () => { }, classLevelPermissions: defaultClassLevelPermissions, indexes: { + _id_: { _id: 1 }, name1: { aString: 1}, }, }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toBe(2); + expect(indexes).toEqual(body.indexes); done(); }); }); }); - it('empty index returns nothing', done => { + it('empty index returns default', done => { request.post({ url: 'http://localhost:8378/1/schemas', headers: masterKeyHeaders, @@ -1946,6 +2156,7 @@ describe('schemas', () => { aString: {type: 'String'} }, classLevelPermissions: defaultClassLevelPermissions, + indexes: defaultIndex, }); done(); }); @@ -2007,7 +2218,137 @@ describe('schemas', () => { } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes).toEqual(body.indexes); + done(); + }); + }); + }); + }) + }); + + it_only_db('mongo')('lets you add pointer index', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + indexes: { + aPointer_1: { aPointer: 1 }, + }, + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + aPointer_1: { aPointer: 1}, + } + })).toEqual(undefined); + request.get({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + aPointer_1: { aPointer: 1 }, + } + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes._id_).toEqual({ _id: 1 }); + expect(indexes.aPointer_1).toEqual({ _p_aPointer: 1 }); + done(); + }); + }); + }); + }) + }); + + it_only_db('postgres')('lets you add pointer index', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, () => { + request.put({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + indexes: { + aPointer_1: { aPointer: 1 }, + }, + } + }, (error, response, body) => { + expect(dd(body, { + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + aPointer_1: { aPointer: 1}, + } + })).toEqual(undefined); + request.get({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: {type: 'ACL'}, + createdAt: {type: 'Date'}, + updatedAt: {type: 'Date'}, + objectId: {type: 'String'}, + aPointer: { type: 'Pointer', targetClass: 'NewClass' } + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + aPointer_1: { aPointer: 1 }, + } + }); + config.database.adapter.getIndexes('NewClass').then((indexes) => { + expect(indexes._id_).toEqual({ _id: 1 }); + expect(indexes.aPointer_1).toEqual({ aPointer: 1 }); done(); }); }); @@ -2086,7 +2427,7 @@ describe('schemas', () => { }, }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(4); + expect(indexes).toEqual(body.indexes); done(); }); }); @@ -2154,7 +2495,7 @@ describe('schemas', () => { } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(1); + expect(indexes).toEqual(body.indexes); done(); }); }); @@ -2234,7 +2575,7 @@ describe('schemas', () => { } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(2); + expect(indexes).toEqual(body.indexes); done(); }); }); @@ -2319,7 +2660,7 @@ describe('schemas', () => { } }); config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(3); + expect(indexes).toEqual(body.indexes); done(); }); }); @@ -2389,7 +2730,7 @@ describe('schemas', () => { }) }); - it_exclude_dbs(['postgres'])('get indexes on startup', (done) => { + it('get indexes on startup', (done) => { const obj = new Parse.Object('TestObject'); obj.save().then(() => { return reconfigureServer({ @@ -2409,7 +2750,7 @@ describe('schemas', () => { }); }); - it_exclude_dbs(['postgres'])('get compound indexes on startup', (done) => { + it('get compound indexes on startup', (done) => { const obj = new Parse.Object('TestObject'); obj.set('subject', 'subject'); obj.set('comment', 'comment'); @@ -2460,5 +2801,71 @@ describe('schemas', () => { done(); }); }); + + it('invalid string index', (done) => { + const obj = new Parse.Object('TestObject'); + obj.set('name', 'parse'); + obj.save().then(() => { + const index = { + name: 'invalid' + }; + const schema = new Parse.Schema('TestObject'); + schema.addIndex('string_index', index); + return schema.update(); + }).then(done.fail).catch((error) => { + expect(error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); + }); + }); + + it('invalid geopoint index', (done) => { + const obj = new Parse.Object('TestObject'); + const geoPoint = new Parse.GeoPoint(22, 11); + obj.set('location', geoPoint); + obj.save().then(() => { + const index = { + location: 'invalid' + }; + const schema = new Parse.Schema('TestObject'); + schema.addIndex('geo_index', index); + return schema.update(); + }).then(done.fail).catch((error) => { + expect(error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); + }); + }); + + it('invalid polygon index', (done) => { + const points = [[0,0],[0,1],[1,1],[1,0]]; + const polygon = new Parse.Polygon(points); + const obj = new Parse.Object('TestObject'); + obj.set('bounds', polygon); + obj.save().then(() => { + const index = { + bounds: 'invalid' + }; + const schema = new Parse.Schema('TestObject'); + schema.addIndex('poly_index', index); + return schema.update(); + }).then(done.fail).catch((error) => { + expect(error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); + }); + }); + + it('valid polygon index', (done) => { + const points = [[0,0],[0,1],[1,1],[1,0]]; + const polygon = new Parse.Polygon(points); + const obj = new Parse.Object('TestObject'); + obj.set('bounds', polygon); + obj.save().then(() => { + const index = { + bounds: '2dsphere' + }; + const schema = new Parse.Schema('TestObject'); + schema.addIndex('valid_index', index); + return schema.update(); + }).then(done).catch(done.fail); + }); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index a150be1a40..a5bc8d98f8 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -17,6 +17,7 @@ import { transformWhere, transformUpdate, transformPointerString, + transformIndexes, } from './MongoTransform'; // @flow-disable-next import Parse from 'parse/node'; @@ -216,7 +217,10 @@ export class MongoStorageAdapter implements StorageAdapter { const deletePromises = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { - const field = submittedIndexes[name]; + if (name === '_id_') { + return; + } + const field = Object.assign({}, submittedIndexes[name]); if (existingIndexes[name] && field.__op !== 'Delete') { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); } @@ -232,8 +236,26 @@ export class MongoStorageAdapter implements StorageAdapter { if (!fields.hasOwnProperty(key)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); } + const value = field[key]; + const type = fields[key].type; + if (type === 'String') { + if (value !== 1 && value !== -1 && value !== 'text') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } else if (type === 'GeoPoint' || type === 'Polygon') { + if (value !== '2d' && value !== '2dsphere') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } else { + if (value !== 1 && value !== -1) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } + const transformedKey = transformKey(className, key, { fields }); + delete field[key]; + field[transformedKey] = value; }); - existingIndexes[name] = field; + existingIndexes[name] = submittedIndexes[name]; insertedIndexes.push({ key: field, name, @@ -253,19 +275,8 @@ export class MongoStorageAdapter implements StorageAdapter { .catch(err => this.handleError(err)); } - setIndexesFromMongo(className: string) { + setIndexesFromDB(className: string) { return this.getIndexes(className).then((indexes) => { - indexes = indexes.reduce((obj, index) => { - if (index.key._fts) { - delete index.key._fts; - delete index.key._ftsx; - for (const field in index.weights) { - index.key[field] = 'text'; - } - } - obj[index.name] = index.key; - return obj; - }, {}); return this._schemaCollection() .then(schemaCollection => schemaCollection.updateSchema(className, { $set: { '_metadata.indexes': indexes } @@ -747,7 +758,7 @@ export class MongoStorageAdapter implements StorageAdapter { } performInitialization(): Promise { - return Promise.resolve(); + return this.updateSchemaWithIndexes(); } createIndex(className: string, index: any) { @@ -756,7 +767,7 @@ export class MongoStorageAdapter implements StorageAdapter { .catch(err => this.handleError(err)); } - createIndexes(className: string, indexes: any) { + createIndexes(className: string, indexes: any): Promise { return this._adaptiveCollection(className) .then(collection => collection._mongoCollection.createIndexes(indexes)) .catch(err => this.handleError(err)); @@ -791,18 +802,25 @@ export class MongoStorageAdapter implements StorageAdapter { return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields) .catch((error) => { if (error.code === 85) { // Index exist with different options - return this.setIndexesFromMongo(className); + return this.setIndexesFromDB(className); } - throw error; + return this.handleError(error); }); } return Promise.resolve(); } - getIndexes(className: string) { + getIndexes(className: string): Promise { return this._adaptiveCollection(className) .then(collection => collection._mongoCollection.indexes()) - .catch(err => this.handleError(err)); + .then((indexes) => transformIndexes(indexes)) + .catch(err => { + // Collection doesn't exist + if (err.code == 26) { + return {}; + } + return this.handleError(err) + }); } dropIndex(className: string, index: any) { @@ -821,7 +839,7 @@ export class MongoStorageAdapter implements StorageAdapter { return this.getAllClasses() .then((classes) => { const promises = classes.map((schema) => { - return this.setIndexesFromMongo(schema.className); + return this.setIndexesFromDB(schema.className); }); return Promise.all(promises); }) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 453bddef70..f822beb451 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -123,6 +123,20 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc return {key, value}; } +const transformIndexes = indexes => { + return indexes.reduce((obj, index) => { + if (index.key._fts) { + delete index.key._fts; + delete index.key._ftsx; + for (const field in index.weights) { + index.key[field] = 'text'; + } + } + obj[index.name] = index.key; + return obj; + }, {}); +} + const isRegex = value => { return value && (value instanceof RegExp) } @@ -1463,4 +1477,5 @@ module.exports = { relativeTimeToDate, transformConstraint, transformPointerString, + transformIndexes, }; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 2df5abb66d..eb89bebf32 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -752,6 +752,9 @@ export class PostgresStorageAdapter implements StorageAdapter { const deletedIndexes = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { + if (name === '_id_') { + return; + } const field = submittedIndexes[name]; if (existingIndexes[name] && field.__op !== 'Delete') { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); @@ -763,15 +766,33 @@ export class PostgresStorageAdapter implements StorageAdapter { deletedIndexes.push(name); delete existingIndexes[name]; } else { + let indexType = 'btree'; Object.keys(field).forEach(key => { if (!fields.hasOwnProperty(key)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); } + const value = field[key]; + const type = fields[key].type; + if (type === 'String') { + if (value !== 1 && value !== -1 && value !== 'text') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } else if (type === 'GeoPoint' || type === 'Polygon') { + indexType = 'gist'; + if (value !== '2d' && value !== '2dsphere') { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } else { + if (value !== 1 && value !== -1) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid index: ${value} for field ${key}`); + } + } }); existingIndexes[name] = field; insertedIndexes.push({ - key: field, name, + type: indexType, + key: field, }); } }); @@ -787,6 +808,19 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } + setIndexesFromDB(className: string, conn: any) { + conn = conn || this._client; + return this.getIndexes(className, conn).then((indexes) => { + return this._ensureSchemaCollectionExists(conn).then(() => { + const params = {className, json: 'schema', key: 'indexes', value: indexes} + return conn.none(`UPDATE "_SCHEMA" SET $ = json_object_set_key($, $, $::jsonb) WHERE "className"=$ `, params); + }); + }).catch(() => { + // Ignore if collection not found + return Promise.resolve(); + }); + } + createClass(className: string, schema: SchemaType, conn: ?any) { conn = conn || this._client; return conn.tx('create-class', t => { @@ -917,6 +951,7 @@ export class PostgresStorageAdapter implements StorageAdapter { } else { const path = `{fields,${fieldName}}`; yield t.none('UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $, $) WHERE "className"=$', {path, type, className}); + return yield self.createIndexesIfNeeded(className, fieldName, type, t); } }); } @@ -1399,7 +1434,7 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } - find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys }: QueryOptions) { + find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys }: QueryOptions): Promise { debug('find', className, query, {skip, limit, sort, keys }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; @@ -1451,13 +1486,14 @@ export class PostgresStorageAdapter implements StorageAdapter { const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`; debug(qs, values); - return this._client.any(qs, values) - .catch(error => { + return this.createTextIndexesIfNeeded(className, query, schema) + .then(() => this._client.any(qs, values)) + .catch(err => { // Query on non existing table, don't crash - if (error.code !== PostgresRelationDoesNotExistError) { - throw error; + if (err.code === PostgresRelationDoesNotExistError) { + return []; } - return []; + throw err; }) .then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema))); } @@ -1817,7 +1853,8 @@ export class PostgresStorageAdapter implements StorageAdapter { } throw err; }) - .then(() => this.schemaUpgrade(schema.className, schema)); + .then(() => this.schemaUpgrade(schema.className, schema)) + .then(() => this.updateSchemaWithIndexes()); }); return Promise.all(promises) .then(() => { @@ -1842,14 +1879,77 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } + createIndex(className: string, index: any, type: string = 'btree', conn: ?any): Promise { + conn = conn || this._client; + const indexNames = []; + for (const key in index) { + indexNames.push(`${key}_${index[key]}`); + } + const values = [indexNames.join('_'), className, type, index]; + return conn.none('CREATE INDEX $1:name ON $2:name USING $3:name ($4:name)', values) + .then(() => this.setIndexesFromDB(className, conn)) + .catch((error) => { + if (error.code === PostgresDuplicateRelationError) { + // Index already exists, Ignore error. + return Promise.resolve(); + } else { + throw error; + } + }); + } + createIndexes(className: string, indexes: any, conn: ?any): Promise { return (conn || this._client).tx(t => t.batch(indexes.map(i => { - return t.none('CREATE INDEX $1:name ON $2:name ($3:name)', [i.name, className, i.key]); + const type = i.type || 'btree'; + const values = [i.name, className, type, i.key]; + return t.none('CREATE INDEX $1:name ON $2:name USING $3:name ($4:name)', values); }))); } createIndexesIfNeeded(className: string, fieldName: string, type: any, conn: ?any): Promise { - return (conn || this._client).none('CREATE INDEX $1:name ON $2:name ($3:name)', [fieldName, className, type]); + conn = conn || this._client; + const self = this; + return conn.tx('create-indexes-if-needed', function * (t) { + if (type && (type.type === 'Polygon')) { + const index = { + [fieldName]: '2dsphere' + }; + + yield self.createIndex(className, index, 'gist', t); + } + }); + } + + createTextIndexesIfNeeded(className: string, query: QueryType, schema: SchemaType): Promise { + for(const fieldName in query) { + if (!query[fieldName] || !query[fieldName].$text) { + continue; + } + let promise = Promise.resolve(); + let existingIndexes = schema.indexes; + if (!existingIndexes) { + promise = this.setIndexesFromDB(className).then(() => { + return this.getClass(className); + }); + } + return promise.then((dbSchema) => { + if (dbSchema) { + existingIndexes = dbSchema.indexes; + } + for (const key in existingIndexes) { + const index = existingIndexes[key]; + if (index.hasOwnProperty(fieldName)) { + return Promise.resolve(); + } + } + const indexName = `${fieldName}_text`; + const textIndex = { + [indexName]: { [fieldName]: 'text' } + }; + return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields); + }); + } + return Promise.resolve(); } dropIndexes(className: string, indexes: any, conn: any): Promise { @@ -1857,13 +1957,56 @@ export class PostgresStorageAdapter implements StorageAdapter { return (conn || this._client).tx(t => t.none(this._pgp.helpers.concat(queries))); } - getIndexes(className: string) { + dropAllIndexes(className: string) { + return this.getClass(className).then((schema) => { + const batch = []; + return this._client.tx(t => { + for (const key in schema.indexes) { + batch.push(t.none('DROP INDEX $1:name', key)); + } + return t.batch(batch); + }); + }); + } + + getIndexes(className: string, conn: ?any): Promise { const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}'; - return this._client.any(qs, {className}); + return (conn || this._client).any(qs, {className}).then((indexes) => { + return indexes.reduce((obj, index) => { + // Get field from index definition + const indexdef = index.indexdef.replace(/[\"]/g, ""); + const regExp = /\(([^)]+)\)/; + let name = index.indexname; + const value = regExp.exec(indexdef)[1]; + const fields = value.split(', '); + let key = {}; + fields.forEach((field) => { + if (field === 'objectId') { + key = { _id: 1 }; + name = '_id_'; + return; + } + const textIndex = `${field}_text`; + if (name.includes(textIndex)) { + key[field] = 'text'; + } else { + key[field] = 1; + } + }); + obj[name] = key; + return obj; + }, {}); + }); } - updateSchemaWithIndexes(): Promise { - return Promise.resolve(); + updateSchemaWithIndexes() { + return this.getAllClasses() + .then((classes) => { + const promises = classes.map((schema) => { + return this.setIndexesFromDB(schema.className); + }); + return Promise.all(promises); + }); } } diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index ed2c2e0701..4101d17bcd 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -49,7 +49,7 @@ export interface StorageAdapter { // Indexing createIndexes(className: string, indexes: any, conn: ?any): Promise; - getIndexes(className: string, connection: ?any): Promise; + getIndexes(className: string, connection: ?any): Promise; updateSchemaWithIndexes(): Promise; setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any, fields: any, conn: ?any): Promise; } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 372be94f81..791463cc4f 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -1057,7 +1057,7 @@ class DatabaseController { // TODO: create indexes on first creation of a _User object. Otherwise it's impossible to // have a Parse app without it having a _User collection. - performInitialization() { + performInitialization(): Promise { const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } }; @@ -1087,11 +1087,11 @@ class DatabaseController { throw error; }); - const indexPromise = this.adapter.updateSchemaWithIndexes(); - // Create tables for volatile classes - const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); - return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]); + return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness]).then(() => { + // Create tables for volatile classes + return this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); + }); } static _validateQuery: ((any) => void) diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index f21c14c217..38a9a98580 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -48,8 +48,11 @@ function createSchema(req) { throw new Parse.Error(135, `POST ${req.path} needs a class name.`); } + const indexes = req.body.indexes || {}; + indexes._id_ = { _id: 1 }; + return req.config.database.loadSchema({ clearCache: true}) - .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions, req.body.indexes)) + .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions, indexes)) .then(schema => ({ response: schema })); }