Skip to content

Unique indexes #1971

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jun 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
49df78f
Add unique indexing
drew-gross May 27, 2016
4c84650
Add unique indexing for username/email
drew-gross May 28, 2016
6584e62
WIP
drew-gross May 31, 2016
aa1c51b
Finish unique indexes
drew-gross Jun 1, 2016
895eaf1
Notes on how to upgrade to 2.3.0 safely
drew-gross Jun 2, 2016
f0353e6
index on unique-indexes: c454180 Revert "Log objects rather than JSON…
drew-gross Jun 6, 2016
44f9ee5
reconfigure username/email tests
drew-gross Jun 6, 2016
1e557e8
Start dealing with test shittyness
drew-gross Jun 7, 2016
d346b29
Remove tests for files that we are removing
drew-gross Jun 7, 2016
8fe737f
most tests passing
drew-gross Jun 7, 2016
3da4ce0
fix failing test
drew-gross Jun 7, 2016
0f2c723
Make specific server config for tests async
drew-gross Jun 7, 2016
74036ad
Fix more tests
drew-gross Jun 7, 2016
1bc15d2
fix more tests
drew-gross Jun 7, 2016
2f2f010
Fix another test
drew-gross Jun 7, 2016
8a3e52e
fix more tests
drew-gross Jun 7, 2016
c2c85f7
Fix email validation
drew-gross Jun 7, 2016
4c35ef2
move some stuff around
drew-gross Jun 8, 2016
b16d22c
Destroy server to ensure all connections are gone
drew-gross Jun 8, 2016
f827c73
Fix broken cloud code
drew-gross Jun 8, 2016
c290c35
Save callback to variable
drew-gross Jun 8, 2016
dccc1db
no need to delete non existant cloud
drew-gross Jun 8, 2016
05e5495
undo
drew-gross Jun 8, 2016
3e7a03e
Fix all tests where connections are left open after server closes.
drew-gross Jun 8, 2016
7e1349c
Fix issues caused by missing gridstore adapter
drew-gross Jun 8, 2016
0dc918a
Update guide for 2.3.0 and fix final tests
drew-gross Jun 8, 2016
a3f023b
use strict
drew-gross Jun 8, 2016
cbbc590
don't use features that won't work in node 4
drew-gross Jun 8, 2016
1a0859d
Fix syntax error
drew-gross Jun 8, 2016
0b0518f
Fix typos
drew-gross Jun 9, 2016
b88a0b2
Add duplicate finding command
drew-gross Jun 9, 2016
1b8aaaf
Update 2.3.0.md
drew-gross Jun 9, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions 2.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Upgrading Parse Server to version 2.3.0

Parse Server version 2.3.0 begins using unique indexes to ensure User's username and email are unique. This is not a backwards incompatable change, but it may in some cases cause a significant performance regression until the index finishes building. Building the unique index before upgrading your Parse Server version will eliminate the performance impact, and is a recommended step before upgrading any app to Parse Server 2.3.0. New apps starting with version 2.3.0 do not need to take any steps before beginning their project.

If you are using MongoDB in Cluster or Replica Set mode, we recommend reading Mongo's [documentation on index building](https://docs.mongodb.com/v3.0/tutorial/build-indexes-on-replica-sets/) first. If you are not using these features, you can execute the following commands from the Mongo shell to build the unique index. You may also want to create a backup first.

```js
// Select the database that your Parse App uses
use parse;

// Select the collection your Parse App uses for users. For migrated apps, this probably includes a collectionPrefix.
var coll = db['your_prefix:_User'];

// You can check if the indexes already exists by running coll.getIndexes()
coll.getIndexes();

// The indexes you want should look like this. If they already exist, you can skip creating them.
{
"v" : 1,
"unique" : true,
"key" : {
"username" : 1
},
"name" : "username_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}

{
"v" : 1,
"unique" : true,
"key" : {
"email" : 1
},
"name" : "email_1",
"ns" : "parse.your_prefix:_User",
"background" : true,
"sparse" : true
}

// Create the username index.
// "background: true" is mandatory and avoids downtime while the index builds.
// "sparse: true" is also mandatory because Parse Server uses sparse indexes.
coll.ensureIndex({ username: 1 }, { background: true, unique: true, sparse: true });

// Create the email index.
// "background: true" is still mandatory.
// "sparse: true" is also mandatory both because Parse Server uses sparse indexes, and because email addresses are not required by the Parse API.
coll.ensureIndex({ email: 1 }, { background: true, unique: true, sparse: true });
```

There are some issues you may run into during this process:

## Mongo complains that the index already exists, but with different options

In this case, you will need to remove the incorrect index. If your app relies on the existence of the index in order to be performant, you can create a new index, with "-1" for the direction of the field, so that it counts as different options. Then, drop the conflicting index, and create the unique index.

## There is already non-unique data in the username or email field

This is possible if you have explicitly set some user's emails to null. If this is bogus data, and those null fields shoud be unset, you can unset the null emails with this command. If your app relies on the difference between null and unset emails, you will need to upgrade your app to treat null and unset emails the same before building the index and upgrading to Parse Server 2.3.0.

```js
coll.update({ email: { $exists: true, $eq: null } }, { $unset: { email: '' } }, { multi: true })
```

## There is already non-unique data in the username or email field, and it's not nulls

This is possible due to a race condition in previous versions of Parse Server. If you have this problem, it is unlikely that you have a lot of rows with duplicate data. We recommend you clean up the data manually, by removing or modifying the offending rows.

This command, can be used to find the duplicate data:

```js
coll.aggregate([
{$match: {"username": {"$ne": null}}},
{$group: {_id: "$username", uniqueIds: {$addToSet: "$_id"}, count: {$sum: 1}}},
{$match: {count: {"$gt": 1}}},
{$project: {id: "$uniqueIds", username: "$_id", _id : 0} },
{$unwind: "$id" },
{$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates collection. Remove this line to just output the list.
], {allowDiskUse:true})
```
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"parse-server-simple-mailgun-adapter": "^1.0.0",
"redis": "^2.5.0-1",
"request": "^2.65.0",
"request-promise": "^3.0.0",
"tv4": "^1.2.7",
"winston": "^2.1.1",
"winston-daily-rotate-file": "^1.0.1",
Expand Down
149 changes: 76 additions & 73 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
"use strict"
const Parse = require("parse/node");
const request = require('request');
const rp = require('request-promise');
const InMemoryCacheAdapter = require('../src/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter;

describe('Cloud Code', () => {
it('can load absolute cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: __dirname + '/cloud/cloudCodeRelativeFile.js'
});
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
reconfigureServer({ cloud: __dirname + '/cloud/cloudCodeRelativeFile.js' })
.then(() => {
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
})
});

it('can load relative cloud code file', done => {
setServerConfiguration({
serverURL: 'http://localhost:8378/1',
appId: 'test',
masterKey: 'test',
cloud: './spec/cloud/cloudCodeAbsoluteFile.js'
});
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' })
.then(() => {
Parse.Cloud.run('cloudCodeInFile', {}, result => {
expect(result).toEqual('It is possible to define cloud code in a file.');
done();
});
})
});

it('can create functions', done => {
Expand Down Expand Up @@ -511,67 +506,75 @@ describe('Cloud Code', () => {
});

it('clears out the user cache for all sessions when the user is changed', done => {
let session1;
let session2;
let user;
const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 });
setServerConfiguration(Object.assign({}, defaultConfiguration, { cacheAdapter: cacheAdapter }));
Parse.Cloud.define('checkStaleUser', (request, response) => {
response.success(request.user.get('data'));
});
reconfigureServer({ cacheAdapter })
.then(() => {
Parse.Cloud.define('checkStaleUser', (request, response) => {
response.success(request.user.get('data'));
});

let user = new Parse.User();
user.set('username', 'test');
user.set('password', 'moon-y');
user.set('data', 'first data');
user.signUp()
user = new Parse.User();
user.set('username', 'test');
user.set('password', 'moon-y');
user.set('data', 'first data');
return user.signUp();
})
.then(user => {
let session1 = user.getSessionToken();
request.get({
url: 'http://localhost:8378/1/login?username=test&password=moon-y',
session1 = user.getSessionToken();
return rp({
uri: 'http://localhost:8378/1/login?username=test&password=moon-y',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
}, (error, response, body) => {
let session2 = body.sessionToken;

//Ensure both session tokens are in the cache
Parse.Cloud.run('checkStaleUser')
.then(() => {
request.post({
url: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}, (error, response, body) => {
Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)])
.then(cachedVals => {
expect(cachedVals[0].objectId).toEqual(user.id);
expect(cachedVals[1].objectId).toEqual(user.id);

//Change with session 1 and then read with session 2.
user.set('data', 'second data');
user.save()
.then(() => {
request.post({
url: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}, (error, response, body) => {
expect(body.result).toEqual('second data');
done();
})
});
});
});
});
});
})
})
.then(body => {
session2 = body.sessionToken;

//Ensure both session tokens are in the cache
return Parse.Cloud.run('checkStaleUser')
})
.then(() => rp({
method: 'POST',
uri: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}))
.then(() => Parse.Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)]))
.then(cachedVals => {
expect(cachedVals[0].objectId).toEqual(user.id);
expect(cachedVals[1].objectId).toEqual(user.id);

//Change with session 1 and then read with session 2.
user.set('data', 'second data');
return user.save()
})
.then(() => rp({
method: 'POST',
uri: 'http://localhost:8378/1/functions/checkStaleUser',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': session2,
}
}))
.then(body => {
expect(body.result).toEqual('second data');
done();
})
.catch(error => {
fail(JSON.stringify(error));
done();
});
});

Expand Down
23 changes: 0 additions & 23 deletions spec/DatabaseAdapter.spec.js

This file was deleted.

18 changes: 0 additions & 18 deletions spec/DatabaseController.spec.js

This file was deleted.

6 changes: 3 additions & 3 deletions spec/FileLoggerAdapter.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ describe('error logs', () => {
describe('verbose logs', () => {

it("mask sensitive information in _User class", (done) => {
let customConfig = Object.assign({}, defaultConfiguration, {verbose: true});
setServerConfiguration(customConfig);
createTestUser().then(() => {
reconfigureServer({ verbose: true })
.then(() => createTestUser())
.then(() => {
let fileLoggerAdapter = new FileLoggerAdapter();
return fileLoggerAdapter.query({
from: new Date(Date.now() - 500),
Expand Down
29 changes: 15 additions & 14 deletions spec/Parse.Push.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,27 @@ describe('Parse.Push', () => {
}
}

setServerConfiguration({
return reconfigureServer({
appId: Parse.applicationId,
masterKey: Parse.masterKey,
serverURL: Parse.serverURL,
push: {
adapter: pushAdapter
}
})
.then(() => {
var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
});

var installations = [];
while(installations.length != 10) {
var installation = new Parse.Object("_Installation");
installation.set("installationId", "installation_"+installations.length);
installation.set("deviceToken","device_token_"+installations.length)
installation.set("badge", installations.length);
installation.set("originalBadge", installations.length);
installation.set("deviceType", "ios");
installations.push(installation);
}
return Parse.Object.saveAll(installations);
}

it('should properly send push', (done) => {
Expand Down Expand Up @@ -110,7 +111,7 @@ describe('Parse.Push', () => {
'X-Parse-Application-Id': 'test',
},
}, (error, response, body) => {
expect(body.results.length).toEqual(0);
expect(body.error).toEqual('unauthorized');
done();
});
});
Expand Down
Loading