Add Indexes to Schema API (#4240)
* Add Indexes to Schema API * error handling * ci errors * postgres support * full text compound indexes * pg clean up * get indexes on startup * test compound index on startup * add default _id to index, full Text index on startup * lint * fix test
This commit is contained in:
committed by
Florent Vilmart
parent
6a1510729a
commit
4bccf96ae7
@@ -21,6 +21,9 @@ describe('MongoSchemaCollection', () => {
|
|||||||
"create":{"*":true},
|
"create":{"*":true},
|
||||||
"delete":{"*":true},
|
"delete":{"*":true},
|
||||||
"addField":{"*":true},
|
"addField":{"*":true},
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"name1":{"deviceToken":1}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"installationId":"string",
|
"installationId":"string",
|
||||||
@@ -66,7 +69,10 @@ describe('MongoSchemaCollection', () => {
|
|||||||
update: { '*': true },
|
update: { '*': true },
|
||||||
delete: { '*': true },
|
delete: { '*': true },
|
||||||
addField: { '*': true },
|
addField: { '*': true },
|
||||||
}
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: {deviceToken: 1}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const fullTextHelper = () => {
|
|||||||
const request = {
|
const request = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
subject: subjects[i]
|
subject: subjects[i],
|
||||||
|
comment: subjects[i],
|
||||||
},
|
},
|
||||||
path: "/1/classes/TestObject"
|
path: "/1/classes/TestObject"
|
||||||
};
|
};
|
||||||
@@ -280,42 +281,18 @@ describe('Parse.Query Full Text Search testing', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe_only_db('mongo')('Parse.Query Full Text Search testing', () => {
|
describe_only_db('mongo')('Parse.Query Full Text Search testing', () => {
|
||||||
it('fullTextSearch: $search, only one text index', (done) => {
|
it('fullTextSearch: does not create text index if compound index exist', (done) => {
|
||||||
return reconfigureServer({
|
fullTextHelper().then(() => {
|
||||||
appId: 'test',
|
return databaseAdapter.dropAllIndexes('TestObject');
|
||||||
restAPIKey: 'test',
|
|
||||||
publicServerURL: 'http://localhost:8378/1',
|
|
||||||
databaseAdapter: new MongoStorageAdapter({ uri: mongoURI })
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
return rp.post({
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
url: 'http://localhost:8378/1/batch',
|
}).then((indexes) => {
|
||||||
body: {
|
expect(indexes.length).toEqual(1);
|
||||||
requests: [
|
return databaseAdapter.createIndex('TestObject', {subject: 'text', comment: 'text'});
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
subject: "coffee is java"
|
|
||||||
},
|
|
||||||
path: "/1/classes/TestObject"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
subject: "java is coffee"
|
|
||||||
},
|
|
||||||
path: "/1/classes/TestObject"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
json: true,
|
|
||||||
headers: {
|
|
||||||
'X-Parse-Application-Id': 'test',
|
|
||||||
'X-Parse-REST-API-Key': 'test'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).then(() => {
|
|
||||||
return databaseAdapter.createIndex('TestObject', {random: 'text'});
|
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
|
}).then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(2);
|
||||||
const where = {
|
const where = {
|
||||||
subject: {
|
subject: {
|
||||||
$text: {
|
$text: {
|
||||||
@@ -334,12 +311,91 @@ describe_only_db('mongo')('Parse.Query Full Text Search testing', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
fail(`Should not be more than one text index: ${JSON.stringify(resp)}`);
|
expect(resp.results.length).toEqual(3);
|
||||||
done();
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
}).catch((err) => {
|
}).then((indexes) => {
|
||||||
expect(err.error.code).toEqual(Parse.Error.INTERNAL_SERVER_ERROR);
|
expect(indexes.length).toEqual(2);
|
||||||
done();
|
rp.get({
|
||||||
});
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fullTextSearch: does not create text index if schema compound index exist', (done) => {
|
||||||
|
fullTextHelper().then(() => {
|
||||||
|
return databaseAdapter.dropAllIndexes('TestObject');
|
||||||
|
}).then(() => {
|
||||||
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
|
}).then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(1);
|
||||||
|
return rp.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||||
|
json: true,
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'test',
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
indexes: {
|
||||||
|
text_test: { subject: 'text', comment: 'text'},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
|
}).then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(2);
|
||||||
|
const where = {
|
||||||
|
subject: {
|
||||||
|
$text: {
|
||||||
|
$search: {
|
||||||
|
$term: 'coffee'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return rp.post({
|
||||||
|
url: 'http://localhost:8378/1/classes/TestObject',
|
||||||
|
json: { where, '_method': 'GET' },
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-REST-API-Key': 'test'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).then((resp) => {
|
||||||
|
expect(resp.results.length).toEqual(3);
|
||||||
|
return databaseAdapter.getIndexes('TestObject');
|
||||||
|
}).then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(2);
|
||||||
|
rp.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': 'test',
|
||||||
|
'X-Parse-Master-Key': 'test',
|
||||||
|
},
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fullTextSearch: $diacriticSensitive - false', (done) => {
|
it('fullTextSearch: $diacriticSensitive - false', (done) => {
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ describe('SchemaController', () => {
|
|||||||
fooSixteen: {type: 'String'},
|
fooSixteen: {type: 'String'},
|
||||||
fooEighteen: {type: 'String'},
|
fooEighteen: {type: 'String'},
|
||||||
fooNineteen: {type: 'String'},
|
fooNineteen: {type: 'String'},
|
||||||
}, levelPermissions, config.database))
|
}, levelPermissions, {}, config.database))
|
||||||
.then(actualSchema => {
|
.then(actualSchema => {
|
||||||
const expectedSchema = {
|
const expectedSchema = {
|
||||||
className: 'NewClass',
|
className: 'NewClass',
|
||||||
@@ -304,6 +304,9 @@ describe('SchemaController', () => {
|
|||||||
fooNineteen: {type: 'String'},
|
fooNineteen: {type: 'String'},
|
||||||
},
|
},
|
||||||
classLevelPermissions: { ...levelPermissions },
|
classLevelPermissions: { ...levelPermissions },
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
expect(dd(actualSchema, expectedSchema)).toEqual(undefined);
|
||||||
|
|||||||
@@ -771,7 +771,7 @@ describe('schemas', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it_exclude_dbs(['postgres'])('lets you delete multiple fields and add fields', done => {
|
it('lets you delete multiple fields and add fields', done => {
|
||||||
var obj1 = hasAllPODobject();
|
var obj1 = hasAllPODobject();
|
||||||
obj1.save()
|
obj1.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -1756,4 +1756,605 @@ describe('schemas', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('cannot create index if field does not exist', 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: { aString: 1},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.code).toBe(Parse.Error.INVALID_QUERY);
|
||||||
|
expect(body.error).toBe('Field aString does not exist, cannot add index.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot create compound index if field does not exist', 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: {
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1, bString: 1},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.code).toBe(Parse.Error.INVALID_QUERY);
|
||||||
|
expect(body.error).toBe('Field bString does not exist, cannot add index.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows add index when you create a class', done => {
|
||||||
|
request.post({
|
||||||
|
url: 'http://localhost:8378/1/schemas',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
className: "NewClass",
|
||||||
|
fields: {
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toBe(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty index returns nothing', done => {
|
||||||
|
request.post({
|
||||||
|
url: 'http://localhost:8378/1/schemas',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
className: "NewClass",
|
||||||
|
fields: {
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
indexes: {},
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you add indexes', 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: {
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 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'},
|
||||||
|
aString: {type: 'String'}
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you add multiple indexes', 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: {
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1, dString: 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1, dString: 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'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1, dString: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(4);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you delete indexes', 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: {
|
||||||
|
aString: {type: 'String'},
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
}
|
||||||
|
})).toEqual(undefined);
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
indexes: {
|
||||||
|
name1: { __op: 'Delete' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(1);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you delete multiple indexes', 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: {
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
}
|
||||||
|
})).toEqual(undefined);
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
indexes: {
|
||||||
|
name1: { __op: 'Delete' },
|
||||||
|
name2: { __op: 'Delete' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lets you add and delete indexes', 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: {
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(dd(body, {
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name1: { aString: 1 },
|
||||||
|
name2: { bString: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
}
|
||||||
|
})).toEqual(undefined);
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
indexes: {
|
||||||
|
name1: { __op: 'Delete' },
|
||||||
|
name2: { __op: 'Delete' },
|
||||||
|
name4: { dString: 1 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body).toEqual({
|
||||||
|
className: 'NewClass',
|
||||||
|
fields: {
|
||||||
|
ACL: {type: 'ACL'},
|
||||||
|
createdAt: {type: 'Date'},
|
||||||
|
updatedAt: {type: 'Date'},
|
||||||
|
objectId: {type: 'String'},
|
||||||
|
aString: {type: 'String'},
|
||||||
|
bString: {type: 'String'},
|
||||||
|
cString: {type: 'String'},
|
||||||
|
dString: {type: 'String'},
|
||||||
|
},
|
||||||
|
classLevelPermissions: defaultClassLevelPermissions,
|
||||||
|
indexes: {
|
||||||
|
_id_: { _id: 1 },
|
||||||
|
name3: { cString: 1 },
|
||||||
|
name4: { dString: 1 },
|
||||||
|
}
|
||||||
|
});
|
||||||
|
config.database.adapter.getIndexes('NewClass').then((indexes) => {
|
||||||
|
expect(indexes.length).toEqual(3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot delete index that does not exist', 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: {
|
||||||
|
unknownIndex: { __op: 'Delete' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.code).toBe(Parse.Error.INVALID_QUERY);
|
||||||
|
expect(body.error).toBe('Index unknownIndex does not exist, cannot delete.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cannot update index that exist', 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: {
|
||||||
|
aString: {type: 'String'},
|
||||||
|
},
|
||||||
|
indexes: {
|
||||||
|
name1: { aString: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
request.put({
|
||||||
|
url: 'http://localhost:8378/1/schemas/NewClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
indexes: {
|
||||||
|
name1: { field2: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.code).toBe(Parse.Error.INVALID_QUERY);
|
||||||
|
expect(body.error).toBe('Index name1 exists, cannot update.');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
it_exclude_dbs(['postgres'])('get indexes on startup', (done) => {
|
||||||
|
const obj = new Parse.Object('TestObject');
|
||||||
|
obj.save().then(() => {
|
||||||
|
return reconfigureServer({
|
||||||
|
appId: 'test',
|
||||||
|
restAPIKey: 'test',
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
request.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
}, (error, response, body) => {
|
||||||
|
expect(body.indexes._id_).toBeDefined();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it_exclude_dbs(['postgres'])('get compound indexes on startup', (done) => {
|
||||||
|
const obj = new Parse.Object('TestObject');
|
||||||
|
obj.set('subject', 'subject');
|
||||||
|
obj.set('comment', 'comment');
|
||||||
|
obj.save().then(() => {
|
||||||
|
return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 'text'});
|
||||||
|
}).then(() => {
|
||||||
|
return reconfigureServer({
|
||||||
|
appId: 'test',
|
||||||
|
restAPIKey: 'test',
|
||||||
|
publicServerURL: 'http://localhost:8378/1',
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
request.get({
|
||||||
|
url: 'http://localhost:8378/1/schemas/TestObject',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,13 +63,20 @@ const defaultCLPS = Object.freeze({
|
|||||||
|
|
||||||
function mongoSchemaToParseSchema(mongoSchema) {
|
function mongoSchemaToParseSchema(mongoSchema) {
|
||||||
let clps = defaultCLPS;
|
let clps = defaultCLPS;
|
||||||
if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) {
|
let indexes = {}
|
||||||
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
if (mongoSchema._metadata) {
|
||||||
|
if (mongoSchema._metadata.class_permissions) {
|
||||||
|
clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions};
|
||||||
|
}
|
||||||
|
if (mongoSchema._metadata.indexes) {
|
||||||
|
indexes = {...mongoSchema._metadata.indexes};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
className: mongoSchema._id,
|
className: mongoSchema._id,
|
||||||
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
|
fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema),
|
||||||
classLevelPermissions: clps,
|
classLevelPermissions: clps,
|
||||||
|
indexes: indexes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const convertParseSchemaToMongoSchema = ({...schema}) => {
|
|||||||
|
|
||||||
// Returns { code, error } if invalid, or { result }, an object
|
// Returns { code, error } if invalid, or { result }, an object
|
||||||
// suitable for inserting into _SCHEMA collection, otherwise.
|
// suitable for inserting into _SCHEMA collection, otherwise.
|
||||||
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions) => {
|
const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions, indexes) => {
|
||||||
const mongoObject = {
|
const mongoObject = {
|
||||||
_id: className,
|
_id: className,
|
||||||
objectId: 'string',
|
objectId: 'string',
|
||||||
@@ -74,6 +74,11 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (indexes && typeof indexes === 'object' && Object.keys(indexes).length > 0) {
|
||||||
|
mongoObject._metadata = mongoObject._metadata || {};
|
||||||
|
mongoObject._metadata.indexes = indexes;
|
||||||
|
}
|
||||||
|
|
||||||
return mongoObject;
|
return mongoObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,11 +170,81 @@ export class MongoStorageAdapter {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields) {
|
||||||
|
if (submittedIndexes === undefined) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (Object.keys(existingIndexes).length === 0) {
|
||||||
|
existingIndexes = { _id_: { _id: 1} };
|
||||||
|
}
|
||||||
|
const deletePromises = [];
|
||||||
|
const insertedIndexes = [];
|
||||||
|
Object.keys(submittedIndexes).forEach(name => {
|
||||||
|
const field = submittedIndexes[name];
|
||||||
|
if (existingIndexes[name] && field.__op !== 'Delete') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`);
|
||||||
|
}
|
||||||
|
if (!existingIndexes[name] && field.__op === 'Delete') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`);
|
||||||
|
}
|
||||||
|
if (field.__op === 'Delete') {
|
||||||
|
const promise = this.dropIndex(className, name);
|
||||||
|
deletePromises.push(promise);
|
||||||
|
delete existingIndexes[name];
|
||||||
|
} else {
|
||||||
|
Object.keys(field).forEach(key => {
|
||||||
|
if (!fields.hasOwnProperty(key)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
existingIndexes[name] = field;
|
||||||
|
insertedIndexes.push({
|
||||||
|
key: field,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let insertPromise = Promise.resolve();
|
||||||
|
if (insertedIndexes.length > 0) {
|
||||||
|
insertPromise = this.createIndexes(className, insertedIndexes);
|
||||||
|
}
|
||||||
|
return Promise.all(deletePromises)
|
||||||
|
.then(() => insertPromise)
|
||||||
|
.then(() => this._schemaCollection())
|
||||||
|
.then(schemaCollection => schemaCollection.updateSchema(className, {
|
||||||
|
$set: { _metadata: { indexes: existingIndexes } }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIndexesFromMongo(className) {
|
||||||
|
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 } }
|
||||||
|
}));
|
||||||
|
}).catch(() => {
|
||||||
|
// Ignore if collection not found
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createClass(className, schema) {
|
createClass(className, schema) {
|
||||||
schema = convertParseSchemaToMongoSchema(schema);
|
schema = convertParseSchemaToMongoSchema(schema);
|
||||||
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions);
|
const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes);
|
||||||
mongoObject._id = className;
|
mongoObject._id = className;
|
||||||
return this._schemaCollection()
|
return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields)
|
||||||
|
.then(() => this._schemaCollection())
|
||||||
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
|
.then(schemaCollection => schemaCollection._collection.insertOne(mongoObject))
|
||||||
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0]))
|
.then(result => MongoSchemaCollection._TESTmongoSchemaToParseSchema(result.ops[0]))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -353,7 +428,7 @@ export class MongoStorageAdapter {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
readPreference = this._parseReadPreference(readPreference);
|
readPreference = this._parseReadPreference(readPreference);
|
||||||
return this.createTextIndexesIfNeeded(className, query)
|
return this.createTextIndexesIfNeeded(className, query, schema)
|
||||||
.then(() => this._adaptiveCollection(className))
|
.then(() => this._adaptiveCollection(className))
|
||||||
.then(collection => collection.find(mongoWhere, {
|
.then(collection => collection.find(mongoWhere, {
|
||||||
skip,
|
skip,
|
||||||
@@ -463,6 +538,11 @@ export class MongoStorageAdapter {
|
|||||||
.then(collection => collection._mongoCollection.createIndex(index));
|
.then(collection => collection._mongoCollection.createIndex(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createIndexes(className, indexes) {
|
||||||
|
return this._adaptiveCollection(className)
|
||||||
|
.then(collection => collection._mongoCollection.createIndexes(indexes));
|
||||||
|
}
|
||||||
|
|
||||||
createIndexesIfNeeded(className, fieldName, type) {
|
createIndexesIfNeeded(className, fieldName, type) {
|
||||||
if (type && type.type === 'Polygon') {
|
if (type && type.type === 'Polygon') {
|
||||||
const index = {
|
const index = {
|
||||||
@@ -473,20 +553,26 @@ export class MongoStorageAdapter {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
createTextIndexesIfNeeded(className, query) {
|
createTextIndexesIfNeeded(className, query, schema) {
|
||||||
for(const fieldName in query) {
|
for(const fieldName in query) {
|
||||||
if (!query[fieldName] || !query[fieldName].$text) {
|
if (!query[fieldName] || !query[fieldName].$text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const index = {
|
const existingIndexes = schema.indexes;
|
||||||
[fieldName]: 'text'
|
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.createIndex(className, index)
|
return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.code === 85) {
|
if (error.code === 85) { // Index exist with different options
|
||||||
throw new Parse.Error(
|
return this.setIndexesFromMongo(className);
|
||||||
Parse.Error.INTERNAL_SERVER_ERROR,
|
|
||||||
'Only one text index is supported, please delete all text indexes to use new field.');
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
@@ -498,6 +584,26 @@ export class MongoStorageAdapter {
|
|||||||
return this._adaptiveCollection(className)
|
return this._adaptiveCollection(className)
|
||||||
.then(collection => collection._mongoCollection.indexes());
|
.then(collection => collection._mongoCollection.indexes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dropIndex(className, index) {
|
||||||
|
return this._adaptiveCollection(className)
|
||||||
|
.then(collection => collection._mongoCollection.dropIndex(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
dropAllIndexes(className) {
|
||||||
|
return this._adaptiveCollection(className)
|
||||||
|
.then(collection => collection._mongoCollection.dropIndexes());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSchemaWithIndexes() {
|
||||||
|
return this.getAllClasses()
|
||||||
|
.then((classes) => {
|
||||||
|
const promises = classes.map((schema) => {
|
||||||
|
return this.setIndexesFromMongo(schema.className);
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MongoStorageAdapter;
|
export default MongoStorageAdapter;
|
||||||
|
|||||||
@@ -98,10 +98,15 @@ const toParseSchema = (schema) => {
|
|||||||
if (schema.classLevelPermissions) {
|
if (schema.classLevelPermissions) {
|
||||||
clps = {...emptyCLPS, ...schema.classLevelPermissions};
|
clps = {...emptyCLPS, ...schema.classLevelPermissions};
|
||||||
}
|
}
|
||||||
|
let indexes = {};
|
||||||
|
if (schema.indexes) {
|
||||||
|
indexes = {...schema.indexes};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
className: schema.className,
|
className: schema.className,
|
||||||
fields: schema.fields,
|
fields: schema.fields,
|
||||||
classLevelPermissions: clps,
|
classLevelPermissions: clps,
|
||||||
|
indexes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,12 +613,64 @@ export class PostgresStorageAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIndexesWithSchemaFormat(className, submittedIndexes, existingIndexes = {}, fields, conn) {
|
||||||
|
conn = conn || this._client;
|
||||||
|
if (submittedIndexes === undefined) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if (Object.keys(existingIndexes).length === 0) {
|
||||||
|
existingIndexes = { _id_: { _id: 1} };
|
||||||
|
}
|
||||||
|
const deletedIndexes = [];
|
||||||
|
const insertedIndexes = [];
|
||||||
|
Object.keys(submittedIndexes).forEach(name => {
|
||||||
|
const field = submittedIndexes[name];
|
||||||
|
if (existingIndexes[name] && field.__op !== 'Delete') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`);
|
||||||
|
}
|
||||||
|
if (!existingIndexes[name] && field.__op === 'Delete') {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`);
|
||||||
|
}
|
||||||
|
if (field.__op === 'Delete') {
|
||||||
|
deletedIndexes.push(name);
|
||||||
|
delete existingIndexes[name];
|
||||||
|
} else {
|
||||||
|
Object.keys(field).forEach(key => {
|
||||||
|
if (!fields.hasOwnProperty(key)) {
|
||||||
|
throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
existingIndexes[name] = field;
|
||||||
|
insertedIndexes.push({
|
||||||
|
key: field,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let insertPromise = Promise.resolve();
|
||||||
|
if (insertedIndexes.length > 0) {
|
||||||
|
insertPromise = this.createIndexes(className, insertedIndexes, conn);
|
||||||
|
}
|
||||||
|
let deletePromise = Promise.resolve();
|
||||||
|
if (deletedIndexes.length > 0) {
|
||||||
|
deletePromise = this.dropIndexes(className, deletedIndexes, conn);
|
||||||
|
}
|
||||||
|
return deletePromise
|
||||||
|
.then(() => insertPromise)
|
||||||
|
.then(() => this._ensureSchemaCollectionExists())
|
||||||
|
.then(() => {
|
||||||
|
const values = [className, 'schema', 'indexes', JSON.stringify(existingIndexes)]
|
||||||
|
return conn.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createClass(className, schema) {
|
createClass(className, schema) {
|
||||||
return this._client.tx(t => {
|
return this._client.tx(t => {
|
||||||
const q1 = this.createTable(className, schema, t);
|
const q1 = this.createTable(className, schema, t);
|
||||||
const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema });
|
const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema });
|
||||||
|
const q3 = this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields, t);
|
||||||
|
|
||||||
return t.batch([q1, q2]);
|
return t.batch([q1, q2, q3]);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return toParseSchema(schema)
|
return toParseSchema(schema)
|
||||||
@@ -1548,6 +1605,25 @@ export class PostgresStorageAdapter {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createIndexes(className, indexes, conn) {
|
||||||
|
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]);
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
|
dropIndexes(className, indexes, conn) {
|
||||||
|
return (conn || this._client).tx(t => t.batch(indexes.map(i => t.none('DROP INDEX $1:name', i))));
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexes(className) {
|
||||||
|
const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}';
|
||||||
|
return this._client.any(qs, {className});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSchemaWithIndexes() {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertPolygonToSQL(polygon) {
|
function convertPolygonToSQL(polygon) {
|
||||||
|
|||||||
@@ -1029,9 +1029,11 @@ DatabaseController.prototype.performInitialization = function() {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const indexPromise = this.adapter.updateSchemaWithIndexes();
|
||||||
|
|
||||||
// Create tables for volatile classes
|
// Create tables for volatile classes
|
||||||
const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas });
|
const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas });
|
||||||
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]);
|
return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinTableName(className, key) {
|
function joinTableName(className, key) {
|
||||||
|
|||||||
@@ -287,18 +287,28 @@ const convertAdapterSchemaToParseSchema = ({...schema}) => {
|
|||||||
schema.fields.password = { type: 'String' };
|
schema.fields.password = { type: 'String' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schema.indexes && Object.keys(schema.indexes).length === 0) {
|
||||||
|
delete schema.indexes;
|
||||||
|
}
|
||||||
|
|
||||||
return schema;
|
return schema;
|
||||||
}
|
}
|
||||||
|
|
||||||
const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({
|
const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}) => {
|
||||||
className,
|
const defaultSchema = {
|
||||||
fields: {
|
className,
|
||||||
...defaultColumns._Default,
|
fields: {
|
||||||
...(defaultColumns[className] || {}),
|
...defaultColumns._Default,
|
||||||
...fields,
|
...(defaultColumns[className] || {}),
|
||||||
},
|
...fields,
|
||||||
classLevelPermissions,
|
},
|
||||||
});
|
classLevelPermissions,
|
||||||
|
};
|
||||||
|
if (indexes && Object.keys(indexes).length !== 0) {
|
||||||
|
defaultSchema.indexes = indexes;
|
||||||
|
}
|
||||||
|
return defaultSchema;
|
||||||
|
};
|
||||||
|
|
||||||
const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks};
|
const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks};
|
||||||
const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }
|
const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }
|
||||||
@@ -344,6 +354,7 @@ export default class SchemaController {
|
|||||||
_dbAdapter;
|
_dbAdapter;
|
||||||
data;
|
data;
|
||||||
perms;
|
perms;
|
||||||
|
indexes;
|
||||||
|
|
||||||
constructor(databaseAdapter, schemaCache) {
|
constructor(databaseAdapter, schemaCache) {
|
||||||
this._dbAdapter = databaseAdapter;
|
this._dbAdapter = databaseAdapter;
|
||||||
@@ -352,6 +363,8 @@ export default class SchemaController {
|
|||||||
this.data = {};
|
this.data = {};
|
||||||
// this.perms[className][operation] tells you the acl-style permissions
|
// this.perms[className][operation] tells you the acl-style permissions
|
||||||
this.perms = {};
|
this.perms = {};
|
||||||
|
// this.indexes[className][operation] tells you the indexes
|
||||||
|
this.indexes = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadData(options = {clearCache: false}) {
|
reloadData(options = {clearCache: false}) {
|
||||||
@@ -370,9 +383,11 @@ export default class SchemaController {
|
|||||||
.then(allSchemas => {
|
.then(allSchemas => {
|
||||||
const data = {};
|
const data = {};
|
||||||
const perms = {};
|
const perms = {};
|
||||||
|
const indexes = {};
|
||||||
allSchemas.forEach(schema => {
|
allSchemas.forEach(schema => {
|
||||||
data[schema.className] = injectDefaultSchema(schema).fields;
|
data[schema.className] = injectDefaultSchema(schema).fields;
|
||||||
perms[schema.className] = schema.classLevelPermissions;
|
perms[schema.className] = schema.classLevelPermissions;
|
||||||
|
indexes[schema.className] = schema.indexes;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inject the in-memory classes
|
// Inject the in-memory classes
|
||||||
@@ -380,13 +395,16 @@ export default class SchemaController {
|
|||||||
const schema = injectDefaultSchema({ className });
|
const schema = injectDefaultSchema({ className });
|
||||||
data[className] = schema.fields;
|
data[className] = schema.fields;
|
||||||
perms[className] = schema.classLevelPermissions;
|
perms[className] = schema.classLevelPermissions;
|
||||||
|
indexes[className] = schema.indexes;
|
||||||
});
|
});
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.perms = perms;
|
this.perms = perms;
|
||||||
|
this.indexes = indexes;
|
||||||
delete this.reloadDataPromise;
|
delete this.reloadDataPromise;
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.data = {};
|
this.data = {};
|
||||||
this.perms = {};
|
this.perms = {};
|
||||||
|
this.indexes = {};
|
||||||
delete this.reloadDataPromise;
|
delete this.reloadDataPromise;
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@@ -424,7 +442,8 @@ export default class SchemaController {
|
|||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
className,
|
className,
|
||||||
fields: this.data[className],
|
fields: this.data[className],
|
||||||
classLevelPermissions: this.perms[className]
|
classLevelPermissions: this.perms[className],
|
||||||
|
indexes: this.indexes[className]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return this._cache.getOneSchema(className).then((cached) => {
|
return this._cache.getOneSchema(className).then((cached) => {
|
||||||
@@ -449,13 +468,13 @@ export default class SchemaController {
|
|||||||
// on success, and rejects with an error on fail. Ensure you
|
// on success, and rejects with an error on fail. Ensure you
|
||||||
// have authorization (master key, or client class creation
|
// have authorization (master key, or client class creation
|
||||||
// enabled) before calling this function.
|
// enabled) before calling this function.
|
||||||
addClassIfNotExists(className, fields = {}, classLevelPermissions) {
|
addClassIfNotExists(className, fields = {}, classLevelPermissions, indexes = {}) {
|
||||||
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
|
var validationError = this.validateNewClass(className, fields, classLevelPermissions);
|
||||||
if (validationError) {
|
if (validationError) {
|
||||||
return Promise.reject(validationError);
|
return Promise.reject(validationError);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
|
return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, indexes, className }))
|
||||||
.then(convertAdapterSchemaToParseSchema)
|
.then(convertAdapterSchemaToParseSchema)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return this._cache.clear().then(() => {
|
return this._cache.clear().then(() => {
|
||||||
@@ -471,7 +490,7 @@ export default class SchemaController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClass(className, submittedFields, classLevelPermissions, database) {
|
updateClass(className, submittedFields, classLevelPermissions, indexes, database) {
|
||||||
return this.getOneSchema(className)
|
return this.getOneSchema(className)
|
||||||
.then(schema => {
|
.then(schema => {
|
||||||
const existingFields = schema.fields;
|
const existingFields = schema.fields;
|
||||||
@@ -509,7 +528,6 @@ export default class SchemaController {
|
|||||||
if (deletedFields.length > 0) {
|
if (deletedFields.length > 0) {
|
||||||
deletePromise = this.deleteFields(deletedFields, className, database);
|
deletePromise = this.deleteFields(deletedFields, className, database);
|
||||||
}
|
}
|
||||||
|
|
||||||
return deletePromise // Delete Everything
|
return deletePromise // Delete Everything
|
||||||
.then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values
|
.then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -520,12 +538,20 @@ export default class SchemaController {
|
|||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
})
|
})
|
||||||
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
|
.then(() => this.setPermissions(className, classLevelPermissions, newSchema))
|
||||||
|
.then(() => this._dbAdapter.setIndexesWithSchemaFormat(className, indexes, schema.indexes, newSchema))
|
||||||
|
.then(() => this.reloadData({ clearCache: true }))
|
||||||
//TODO: Move this logic into the database adapter
|
//TODO: Move this logic into the database adapter
|
||||||
.then(() => ({
|
.then(() => {
|
||||||
className: className,
|
const reloadedSchema = {
|
||||||
fields: this.data[className],
|
className: className,
|
||||||
classLevelPermissions: this.perms[className]
|
fields: this.data[className],
|
||||||
}));
|
classLevelPermissions: this.perms[className],
|
||||||
|
};
|
||||||
|
if (this.indexes[className] && Object.keys(this.indexes[className]).length !== 0) {
|
||||||
|
reloadedSchema.indexes = this.indexes[className];
|
||||||
|
}
|
||||||
|
return reloadedSchema;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error === undefined) {
|
if (error === undefined) {
|
||||||
@@ -620,8 +646,7 @@ export default class SchemaController {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
validateCLP(perms, newSchema);
|
validateCLP(perms, newSchema);
|
||||||
return this._dbAdapter.setClassLevelPermissions(className, perms)
|
return this._dbAdapter.setClassLevelPermissions(className, perms);
|
||||||
.then(() => this.reloadData({ clearCache: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns a promise that resolves successfully to the new schema
|
// Returns a promise that resolves successfully to the new schema
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function createSchema(req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return req.config.database.loadSchema({ clearCache: true})
|
return req.config.database.loadSchema({ clearCache: true})
|
||||||
.then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions))
|
.then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions, req.body.indexes))
|
||||||
.then(schema => ({ response: schema }));
|
.then(schema => ({ response: schema }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ function modifySchema(req) {
|
|||||||
const className = req.params.className;
|
const className = req.params.className;
|
||||||
|
|
||||||
return req.config.database.loadSchema({ clearCache: true})
|
return req.config.database.loadSchema({ clearCache: true})
|
||||||
.then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database))
|
.then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.body.indexes, req.config.database))
|
||||||
.then(result => ({response: result}));
|
.then(result => ({response: result}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user