Skip to content
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

LocalDatastore can update from server #734

Merged
merged 6 commits into from
Mar 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 25 additions & 2 deletions integration/test/ParseLocalDatastoreTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,29 @@ function runTest(controller) {
assert.equal(localDatastore[`Item_${fetchedItems[0].id}`].foo, 'changed');
assert.equal(localDatastore[`Item_${fetchedItems[1].id}`].foo, 'changed');
});

it(`${controller.name} can update Local Datastore from network`, async () => {
const parent = new TestObject();
const child = new Item();
const grandchild = new Item();
child.set('grandchild', grandchild);
parent.set('field', 'test');
parent.set('child', child);
await Parse.Object.saveAll([parent, child, grandchild]);
await parent.pin();

// Updates child with { foo: 'changed' }
const params = { id: child.id };
await Parse.Cloud.run('TestFetchFromLocalDatastore', params);

Parse.LocalDatastore.isSyncing = false;

await Parse.LocalDatastore.updateFromServer();

const updatedLDS = await Parse.LocalDatastore._getAllContents();
const childJSON = updatedLDS[`${child.className}_${child.id}`];
assert.equal(childJSON.foo, 'changed');
});
});

describe(`Parse Query Pinning (${controller.name})`, () => {
Expand Down Expand Up @@ -2344,7 +2367,7 @@ function runTest(controller) {
});
});

it('supports withinPolygon', async () => {
it(`${controller.name} supports withinPolygon`, async () => {
const sacramento = new TestObject();
sacramento.set('location', new Parse.GeoPoint(38.52, -121.50));
sacramento.set('name', 'Sacramento');
Expand Down Expand Up @@ -2373,7 +2396,7 @@ function runTest(controller) {
assert.equal(results.length, 1);
});

it('supports polygonContains', async () => {
it(`${controller.name} supports polygonContains`, async () => {
const p1 = [[0,0], [0,1], [1,1], [1,0]];
const p2 = [[0,0], [0,2], [2,2], [2,0]];
const p3 = [[10,10], [10,15], [15,15], [15,10], [10,10]];
Expand Down
61 changes: 61 additions & 0 deletions src/LocalDatastore.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import CoreManager from './CoreManager';

import type ParseObject from './ParseObject';
import ParseQuery from './ParseQuery';

const DEFAULT_PIN = '_default';
const PIN_PREFIX = 'parsePin_';
Expand Down Expand Up @@ -259,6 +260,65 @@ const LocalDatastore = {
}
},

/**
* Updates Local Datastore from Server
*
* <pre>
* await Parse.LocalDatastore.updateFromServer();
* </pre>
*
* @static
*/
async updateFromServer() {
if (!this.checkIfEnabled() || this.isSyncing) {
return;
}
const localDatastore = await this._getAllContents();
const keys = [];
for (const key in localDatastore) {
if (key !== DEFAULT_PIN && !key.startsWith(PIN_PREFIX)) {
keys.push(key);
}
}
if (keys.length === 0) {
return;
}
this.isSyncing = true;
const pointersHash = {};
for (const key of keys) {
const [className, objectId] = key.split('_');
if (!(className in pointersHash)) {
pointersHash[className] = new Set();
}
pointersHash[className].add(objectId);
}
const queryPromises = Object.keys(pointersHash).map(className => {
const objectIds = Array.from(pointersHash[className]);
const query = new ParseQuery(className);
query.limit(objectIds.length);
if (objectIds.length === 1) {
query.equalTo('objectId', objectIds[0]);
} else {
query.containedIn('objectId', objectIds);
}
return query.find();
});
try {
const responses = await Promise.all(queryPromises);
const objects = [].concat.apply([], responses);
const pinPromises = objects.map((object) => {
const objectKey = this.getKeyForObject(object);
return this.pinWithName(objectKey, object._toFullJSON());
});
await Promise.all(pinPromises);
this.isSyncing = false;
} catch(error) {
console.log('Error syncing LocalDatastore'); // eslint-disable-line
dplewis marked this conversation as resolved.
Show resolved Hide resolved
console.log(error); // eslint-disable-line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason not to use parse's logger so this stuff shows up in aggregated logs?

see middlewares, MogoTransform, PromiseRouter, etc. for examples

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like

const Logger = require('parse-server/lib/logger');

Logger.logger.error(`Error syncing LocalDatastore: ${error}`);
console.log('Error syncing LocalDatastore', error);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not follow the exact pattern we have already?

i.e.

https://github.com/parse-community/parse-server/blob/master/src/PromiseRouter.js#L10

import log from './logger';
...
log.error('message', object);
...

and remove the console.log and the eslint exception comment?

Copy link
Member Author

@dplewis dplewis Mar 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats the parse-server repo, there isn't a logger in this repo that I know of.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha. riiiight. yeah, lgtm. ;) . I don't use the client js sdk is the only lame excuse i have for that one.

this.isSyncing = false;
}
},

getKeyForObject(object: any) {
const objectId = object.objectId || object._getId();
return `${object.className}_${objectId}`;
Expand All @@ -282,6 +342,7 @@ const LocalDatastore = {
LocalDatastore.DEFAULT_PIN = DEFAULT_PIN;
LocalDatastore.PIN_PREFIX = PIN_PREFIX;
LocalDatastore.isEnabled = false;
LocalDatastore.isSyncing = false;

module.exports = LocalDatastore;

Expand Down
145 changes: 145 additions & 0 deletions src/__tests__/LocalDatastore-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,21 @@ const mockLocalStorageController = {
clear: jest.fn(),
};
jest.setMock('../ParseObject', MockObject);

const mockQueryFind = jest.fn();
jest.mock('../ParseQuery', () => {
return jest.fn().mockImplementation(function () {
this.equalTo = jest.fn();
this.containedIn = jest.fn();
this.limit = jest.fn();
this.find = mockQueryFind;
});
});

const CoreManager = require('../CoreManager');
const LocalDatastore = require('../LocalDatastore');
const ParseObject = require('../ParseObject');
const ParseQuery = require('../ParseQuery');
const RNDatastoreController = require('../LocalDatastoreController.react-native');
const BrowserDatastoreController = require('../LocalDatastoreController.browser');
const DefaultDatastoreController = require('../LocalDatastoreController.default');
Expand Down Expand Up @@ -572,6 +584,139 @@ describe('LocalDatastore', () => {
LocalDatastore._traverse(object, encountered);
expect(encountered).toEqual({ 'Item_1234': object });
});

it('do not sync if disabled', async () => {
LocalDatastore.isEnabled = false;
jest.spyOn(mockLocalStorageController, 'getAllContents');

await LocalDatastore.updateFromServer();
expect(LocalDatastore.isSyncing).toBe(false);
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(0);
});

it('do not sync if syncing', async () => {
LocalDatastore.isEnabled = true;
LocalDatastore.isSyncing = true;

jest.spyOn(mockLocalStorageController, 'getAllContents');
await LocalDatastore.updateFromServer();

expect(LocalDatastore.isSyncing).toBe(true);
expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(0);
});

it('updateFromServer empty LDS', async () => {
LocalDatastore.isEnabled = true;
LocalDatastore.isSyncing = false;
const LDS = {};

mockLocalStorageController
.getAllContents
.mockImplementationOnce(() => LDS);

jest.spyOn(mockLocalStorageController, 'pinWithName');
await LocalDatastore.updateFromServer();

expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(0);
});

it('updateFromServer on one object', async () => {
LocalDatastore.isEnabled = true;
LocalDatastore.isSyncing = false;
const object = new ParseObject('Item');
const LDS = {
[`Item_${object.id}`]: object._toFullJSON(),
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${object.id}`],
[LocalDatastore.DEFAULT_PIN]: [`Item_${object.id}`],
};

mockLocalStorageController
.getAllContents
.mockImplementationOnce(() => LDS);

object.set('updatedField', 'foo');
mockQueryFind.mockImplementationOnce(() => Promise.resolve([object]));

await LocalDatastore.updateFromServer();

expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
expect(ParseQuery).toHaveBeenCalledTimes(1);
const mockQueryInstance = ParseQuery.mock.instances[0];

expect(mockQueryInstance.equalTo.mock.calls.length).toBe(1);
expect(mockQueryFind).toHaveBeenCalledTimes(1);
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(1);
});

it('updateFromServer handle error', async () => {
LocalDatastore.isEnabled = true;
LocalDatastore.isSyncing = false;
const object = new ParseObject('Item');
const LDS = {
[`Item_${object.id}`]: object._toFullJSON(),
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${object.id}`],
[LocalDatastore.DEFAULT_PIN]: [`Item_${object.id}`],
};

mockLocalStorageController
.getAllContents
.mockImplementationOnce(() => LDS);

object.set('updatedField', 'foo');
mockQueryFind.mockImplementationOnce(() => {
expect(LocalDatastore.isSyncing).toBe(true);
return Promise.reject('Unable to connect to the Parse API')
});

jest.spyOn(console, 'log');
await LocalDatastore.updateFromServer();

expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
expect(ParseQuery).toHaveBeenCalledTimes(1);
const mockQueryInstance = ParseQuery.mock.instances[0];

expect(mockQueryInstance.equalTo.mock.calls.length).toBe(1);
expect(mockQueryFind).toHaveBeenCalledTimes(1);
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(0);
expect(console.log).toHaveBeenCalledTimes(2);
expect(LocalDatastore.isSyncing).toBe(false);
});

it('updateFromServer on mixed object', async () => {
LocalDatastore.isEnabled = true;
LocalDatastore.isSyncing = false;
const obj1 = new ParseObject('Item');
const obj2 = new ParseObject('Item');
const obj3 = new ParseObject('TestObject');
const LDS = {
[`Item_${obj1.id}`]: obj1._toFullJSON(),
[`Item_${obj2.id}`]: obj2._toFullJSON(),
[`TestObject_${obj3.id}`]: obj3._toFullJSON(),
[`${LocalDatastore.PIN_PREFIX}_testPinName`]: [`Item_${obj1.id}`],
[LocalDatastore.DEFAULT_PIN]: [`Item_${obj1.id}`],
};

mockLocalStorageController
.getAllContents
.mockImplementationOnce(() => LDS);

mockQueryFind
.mockImplementationOnce(() => Promise.resolve([obj1, obj2]))
.mockImplementationOnce(() => Promise.resolve([obj3]));

await LocalDatastore.updateFromServer();

expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1);
expect(ParseQuery).toHaveBeenCalledTimes(2);

const mockQueryInstance1 = ParseQuery.mock.instances[0];
const mockQueryInstance2 = ParseQuery.mock.instances[1];

expect(mockQueryInstance1.containedIn.mock.calls.length).toBe(1);
expect(mockQueryInstance2.equalTo.mock.calls.length).toBe(1);
expect(mockQueryFind).toHaveBeenCalledTimes(2);
expect(mockLocalStorageController.pinWithName).toHaveBeenCalledTimes(3);
});
});

describe('BrowserDatastoreController', async () => {
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/ParseObject-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const mockLocalDatastore = {
_updateObjectIfPinned: jest.fn(),
_destroyObjectIfPinned: jest.fn(),
_updateLocalIdForObject: jest.fn(),
updateFromServer: jest.fn(),
_clear: jest.fn(),
getKeyForObject: jest.fn(),
checkIfEnabled: jest.fn(() => {
Expand Down