Skip to content

Commit fbd0ab1

Browse files
authored
feat: Lazy load Parse.CoreManager controllers to add support for swappable CryptoController, LocalDatastoreController, StorageController, WebSocketController, ParseLiveQuery (#2100)
1 parent c58fdda commit fbd0ab1

17 files changed

+278
-104
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ A library that gives you access to the powerful Parse Server backend from your J
2828

2929
- [Getting Started](#getting-started)
3030
- [Using Parse on Different Platforms](#using-parse-on-different-platforms)
31+
- [Core Manager](#core-manager)
3132
- [Compatibility](#compatibility)
3233
- [Parse Server](#parse-server)
3334
- [Node.js](#nodejs)
@@ -89,6 +90,18 @@ $ npm install @types/parse
8990

9091
Types are updated manually after every release. If a definition doesn't exist, please submit a pull request to [@types/parse][types-parse]
9192

93+
#### Core Manager
94+
95+
The SDK has a [Core Manager](src/CoreManager.js) that handles all configurations and controllers. These modules can be swapped out for customization before you initialize the SDK. For full list of all available modules take a look at the [Core Manager Documentation](src/CoreManager.js).
96+
97+
```js
98+
// Configuration example
99+
Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1)
100+
101+
// Controller example
102+
Parse.CoreManager.setRESTController(MyRESTController);
103+
```
104+
92105
## Compatibility
93106

94107
### Parse Server
+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
3+
const Parse = require('../../react-native');
4+
const { resolvingPromise } = require('../../lib/react-native/promiseUtils');
5+
const CryptoController = require('../../lib/react-native/CryptoController');
6+
const LocalDatastoreController = require('../../lib/react-native/LocalDatastoreController.default');
7+
const StorageController = require('../../lib/react-native/StorageController.default');
8+
const RESTController = require('../../lib/react-native/RESTController');
9+
10+
RESTController._setXHR(require('xmlhttprequest').XMLHttpRequest);
11+
12+
describe('Parse React Native', () => {
13+
beforeEach(() => {
14+
// Set up missing controllers and configurations
15+
Parse.CoreManager.setWebSocketController(require('ws'));
16+
Parse.CoreManager.setEventEmitter(require('events').EventEmitter);
17+
Parse.CoreManager.setLocalDatastoreController(LocalDatastoreController);
18+
Parse.CoreManager.setStorageController(StorageController);
19+
Parse.CoreManager.setRESTController(RESTController);
20+
Parse.CoreManager.setCryptoController(CryptoController);
21+
22+
Parse.initialize('integration');
23+
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
24+
Parse.CoreManager.set('MASTER_KEY', 'notsosecret');
25+
Parse.enableLocalDatastore();
26+
});
27+
28+
afterEach(async () => {
29+
await Parse.User.logOut();
30+
Parse.Storage._clear();
31+
});
32+
33+
it('can log in a user', async () => {
34+
// Handle Storage Controller
35+
await Parse.User.signUp('asdf', 'zxcv')
36+
const user = await Parse.User.logIn('asdf', 'zxcv');
37+
expect(user.get('username')).toBe('asdf');
38+
expect(user.existed()).toBe(true);
39+
});
40+
41+
it('can encrypt user', async () => {
42+
// Handle Crypto Controller
43+
Parse.User.enableUnsafeCurrentUser();
44+
Parse.enableEncryptedUser();
45+
Parse.secret = 'My Secret Key';
46+
const user = new Parse.User();
47+
user.setUsername('usernameENC');
48+
user.setPassword('passwordENC');
49+
await user.signUp();
50+
51+
const path = Parse.Storage.generatePath('currentUser');
52+
const encryptedUser = Parse.Storage.getItem(path);
53+
54+
const crypto = Parse.CoreManager.getCryptoController();
55+
56+
const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
57+
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);
58+
59+
const currentUser = Parse.User.current();
60+
expect(currentUser).toEqual(user);
61+
62+
const currentUserAsync = await Parse.User.currentAsync();
63+
expect(currentUserAsync).toEqual(user);
64+
await Parse.User.logOut();
65+
Parse.CoreManager.set('ENCRYPTED_USER', false);
66+
Parse.CoreManager.set('ENCRYPTED_KEY', null);
67+
});
68+
69+
it('can pin saved object LDS', async () => {
70+
// Handle LocalDatastore Controller
71+
function LDS_KEY(object) {
72+
return Parse.LocalDatastore.getKeyForObject(object);
73+
}
74+
const object = new Parse.Object('TestObject');
75+
object.set('field', 'test');
76+
await object.save();
77+
await object.pin();
78+
const localDatastore = await Parse.LocalDatastore._getAllContents();
79+
const cachedObject = localDatastore[LDS_KEY(object)][0];
80+
expect(Object.keys(localDatastore).length).toBe(2);
81+
expect(cachedObject.objectId).toBe(object.id);
82+
expect(cachedObject.field).toBe('test');
83+
});
84+
85+
it('can subscribe to query', async () => {
86+
// Handle WebSocket Controller
87+
const object = new Parse.Object('TestObject');
88+
await object.save();
89+
const installationId = await Parse.CoreManager.getInstallationController().currentInstallationId();
90+
91+
const query = new Parse.Query('TestObject');
92+
query.equalTo('objectId', object.id);
93+
const subscription = await query.subscribe();
94+
const promise = resolvingPromise();
95+
subscription.on('update', (object, _, response) => {
96+
expect(object.get('foo')).toBe('bar');
97+
expect(response.installationId).toBe(installationId);
98+
promise.resolve();
99+
});
100+
object.set({ foo: 'bar' });
101+
await object.save();
102+
await promise;
103+
});
104+
});

src/LiveQueryClient.js

-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* global WebSocket */
2-
31
import CoreManager from './CoreManager';
42
import ParseObject from './ParseObject';
53
import LiveQuerySubscription from './LiveQuerySubscription';
@@ -502,16 +500,4 @@ class LiveQueryClient {
502500
}
503501
}
504502

505-
if (process.env.PARSE_BUILD === 'node') {
506-
CoreManager.setWebSocketController(require('ws'));
507-
} else if (process.env.PARSE_BUILD === 'browser') {
508-
CoreManager.setWebSocketController(
509-
typeof WebSocket === 'function' || typeof WebSocket === 'object' ? WebSocket : null
510-
);
511-
} else if (process.env.PARSE_BUILD === 'weapp') {
512-
CoreManager.setWebSocketController(require('./Socket.weapp'));
513-
} else if (process.env.PARSE_BUILD === 'react-native') {
514-
CoreManager.setWebSocketController(WebSocket);
515-
}
516-
517503
export default LiveQueryClient;
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @flow
3+
*/
4+
import { isLocalDatastoreKey } from './LocalDatastoreUtils';
5+
import Storage from './Storage';
6+
7+
const LocalDatastoreController = {
8+
async fromPinWithName(name: string): Array<Object> {
9+
const values = await Storage.getItemAsync(name);
10+
if (!values) {
11+
return [];
12+
}
13+
const objects = JSON.parse(values);
14+
return objects;
15+
},
16+
17+
pinWithName(name: string, value: any) {
18+
const values = JSON.stringify(value);
19+
return Storage.setItemAsync(name, values);
20+
},
21+
22+
unPinWithName(name: string) {
23+
return Storage.removeItemAsync(name);
24+
},
25+
26+
async getAllContents(): Object {
27+
const keys = await Storage.getAllKeysAsync();
28+
return keys.reduce(async (previousPromise, key) => {
29+
const LDS = await previousPromise;
30+
if (isLocalDatastoreKey(key)) {
31+
const value = await Storage.getItemAsync(key);
32+
try {
33+
LDS[key] = JSON.parse(value);
34+
} catch (error) {
35+
console.error('Error getAllContents: ', error);
36+
}
37+
}
38+
return LDS;
39+
}, Promise.resolve({}));
40+
},
41+
42+
// Used for testing
43+
async getRawStorage(): Object {
44+
const keys = await Storage.getAllKeysAsync();
45+
return keys.reduce(async (previousPromise, key) => {
46+
const LDS = await previousPromise;
47+
const value = await Storage.getItemAsync(key);
48+
LDS[key] = value;
49+
return LDS;
50+
}, Promise.resolve({}));
51+
},
52+
53+
async clear(): Promise {
54+
const keys = await Storage.getAllKeysAsync();
55+
56+
const toRemove = [];
57+
for (const key of keys) {
58+
if (isLocalDatastoreKey(key)) {
59+
toRemove.push(key);
60+
}
61+
}
62+
const promises = toRemove.map(this.unPinWithName);
63+
return Promise.all(promises);
64+
},
65+
};
66+
67+
module.exports = LocalDatastoreController;

src/LocalDatastoreController.js

+5-67
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,5 @@
1-
/**
2-
* @flow
3-
*/
4-
import { isLocalDatastoreKey } from './LocalDatastoreUtils';
5-
import Storage from './Storage';
6-
7-
const LocalDatastoreController = {
8-
async fromPinWithName(name: string): Array<Object> {
9-
const values = await Storage.getItemAsync(name);
10-
if (!values) {
11-
return [];
12-
}
13-
const objects = JSON.parse(values);
14-
return objects;
15-
},
16-
17-
pinWithName(name: string, value: any) {
18-
const values = JSON.stringify(value);
19-
return Storage.setItemAsync(name, values);
20-
},
21-
22-
unPinWithName(name: string) {
23-
return Storage.removeItemAsync(name);
24-
},
25-
26-
async getAllContents(): Object {
27-
const keys = await Storage.getAllKeysAsync();
28-
return keys.reduce(async (previousPromise, key) => {
29-
const LDS = await previousPromise;
30-
if (isLocalDatastoreKey(key)) {
31-
const value = await Storage.getItemAsync(key);
32-
try {
33-
LDS[key] = JSON.parse(value);
34-
} catch (error) {
35-
console.error('Error getAllContents: ', error);
36-
}
37-
}
38-
return LDS;
39-
}, Promise.resolve({}));
40-
},
41-
42-
// Used for testing
43-
async getRawStorage(): Object {
44-
const keys = await Storage.getAllKeysAsync();
45-
return keys.reduce(async (previousPromise, key) => {
46-
const LDS = await previousPromise;
47-
const value = await Storage.getItemAsync(key);
48-
LDS[key] = value;
49-
return LDS;
50-
}, Promise.resolve({}));
51-
},
52-
53-
async clear(): Promise {
54-
const keys = await Storage.getAllKeysAsync();
55-
56-
const toRemove = [];
57-
for (const key of keys) {
58-
if (isLocalDatastoreKey(key)) {
59-
toRemove.push(key);
60-
}
61-
}
62-
const promises = toRemove.map(this.unPinWithName);
63-
return Promise.all(promises);
64-
},
65-
};
66-
67-
module.exports = LocalDatastoreController;
1+
if (process.env.PARSE_BUILD === 'react-native') {
2+
module.exports = require('./LocalDatastoreController.react-native');
3+
} else {
4+
module.exports = require('./LocalDatastoreController.default');
5+
}

src/Parse.ts

+21-7
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ import Schema from './ParseSchema'
3030
import Session from './ParseSession'
3131
import Storage from './Storage'
3232
import User from './ParseUser'
33-
import LiveQuery from './ParseLiveQuery'
33+
import ParseLiveQuery from './ParseLiveQuery'
3434
import LiveQueryClient from './LiveQueryClient'
35+
import LocalDatastoreController from './LocalDatastoreController';
36+
import StorageController from './StorageController';
37+
import WebSocketController from './WebSocketController';
3538

3639
/**
3740
* Contains all Parse API classes and functions.
@@ -78,7 +81,7 @@ interface ParseType {
7881
Session: typeof Session,
7982
Storage: typeof Storage,
8083
User: typeof User,
81-
LiveQuery?: typeof LiveQuery,
84+
LiveQuery: ParseLiveQuery,
8285
LiveQueryClient: typeof LiveQueryClient,
8386

8487
initialize(applicationId: string, javaScriptKey: string): void,
@@ -146,7 +149,6 @@ const Parse: ParseType = {
146149
Storage: Storage,
147150
User: User,
148151
LiveQueryClient: LiveQueryClient,
149-
LiveQuery: undefined,
150152
IndexedDB: undefined,
151153
Hooks: undefined,
152154
Parse: undefined,
@@ -181,9 +183,11 @@ const Parse: ParseType = {
181183
CoreManager.set('MASTER_KEY', masterKey);
182184
CoreManager.set('USE_MASTER_KEY', false);
183185
CoreManager.setIfNeeded('EventEmitter', EventEmitter);
184-
185-
Parse.LiveQuery = new LiveQuery();
186-
CoreManager.setIfNeeded('LiveQuery', Parse.LiveQuery);
186+
CoreManager.setIfNeeded('LiveQuery', new ParseLiveQuery());
187+
CoreManager.setIfNeeded('CryptoController', CryptoController);
188+
CoreManager.setIfNeeded('LocalDatastoreController', LocalDatastoreController);
189+
CoreManager.setIfNeeded('StorageController', StorageController);
190+
CoreManager.setIfNeeded('WebSocketController', WebSocketController);
187191

188192
if (process.env.PARSE_BUILD === 'browser') {
189193
Parse.IndexedDB = CoreManager.setIfNeeded('IndexedDBStorageController', IndexedDBStorageController);
@@ -289,6 +293,17 @@ const Parse: ParseType = {
289293
return CoreManager.get('SERVER_AUTH_TYPE');
290294
},
291295

296+
/**
297+
* @member {ParseLiveQuery} Parse.LiveQuery
298+
* @static
299+
*/
300+
set LiveQuery(liveQuery: ParseLiveQuery) {
301+
CoreManager.setLiveQuery(liveQuery);
302+
},
303+
get LiveQuery() {
304+
return CoreManager.getLiveQuery();
305+
},
306+
292307
/**
293308
* @member {string} Parse.liveQueryServerURL
294309
* @static
@@ -433,7 +448,6 @@ const Parse: ParseType = {
433448
},
434449
};
435450

436-
CoreManager.setCryptoController(CryptoController);
437451
CoreManager.setInstallationController(InstallationController);
438452
CoreManager.setRESTController(RESTController);
439453

src/Storage.js

-10
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,3 @@ const Storage = {
9797

9898
module.exports = Storage;
9999
export default Storage;
100-
101-
if (process.env.PARSE_BUILD === 'react-native') {
102-
CoreManager.setStorageController(require('./StorageController.react-native'));
103-
} else if (process.env.PARSE_BUILD === 'browser') {
104-
CoreManager.setStorageController(require('./StorageController.browser'));
105-
} else if (process.env.PARSE_BUILD === 'weapp') {
106-
CoreManager.setStorageController(require('./StorageController.weapp'));
107-
} else {
108-
CoreManager.setStorageController(require('./StorageController.default'));
109-
}

src/StorageController.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
if (process.env.PARSE_BUILD === 'react-native') {
2+
module.exports = require('./StorageController.react-native');
3+
} else if (process.env.PARSE_BUILD === 'browser') {
4+
module.exports = require('./StorageController.browser');
5+
} else if (process.env.PARSE_BUILD === 'weapp') {
6+
module.exports = require('./StorageController.weapp');
7+
} else {
8+
module.exports = require('./StorageController.default');
9+
}

0 commit comments

Comments
 (0)