Skip to content

Commit

Permalink
feat: Lazy load Parse.CoreManager controllers to add support for sw…
Browse files Browse the repository at this point in the history
…appable `CryptoController`, `LocalDatastoreController`, `StorageController`, `WebSocketController`, `ParseLiveQuery` (#2100)
  • Loading branch information
dplewis authored Apr 14, 2024
1 parent c58fdda commit fbd0ab1
Show file tree
Hide file tree
Showing 17 changed files with 278 additions and 104 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ A library that gives you access to the powerful Parse Server backend from your J

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

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

#### Core Manager

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).

```js
// Configuration example
Parse.CoreManager.set('REQUEST_ATTEMPT_LIMIT', 1)

// Controller example
Parse.CoreManager.setRESTController(MyRESTController);
```

## Compatibility

### Parse Server
Expand Down
104 changes: 104 additions & 0 deletions integration/test/ParseReactNativeTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use strict';

const Parse = require('../../react-native');
const { resolvingPromise } = require('../../lib/react-native/promiseUtils');
const CryptoController = require('../../lib/react-native/CryptoController');
const LocalDatastoreController = require('../../lib/react-native/LocalDatastoreController.default');
const StorageController = require('../../lib/react-native/StorageController.default');
const RESTController = require('../../lib/react-native/RESTController');

RESTController._setXHR(require('xmlhttprequest').XMLHttpRequest);

describe('Parse React Native', () => {
beforeEach(() => {
// Set up missing controllers and configurations
Parse.CoreManager.setWebSocketController(require('ws'));
Parse.CoreManager.setEventEmitter(require('events').EventEmitter);
Parse.CoreManager.setLocalDatastoreController(LocalDatastoreController);
Parse.CoreManager.setStorageController(StorageController);
Parse.CoreManager.setRESTController(RESTController);
Parse.CoreManager.setCryptoController(CryptoController);

Parse.initialize('integration');
Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse');
Parse.CoreManager.set('MASTER_KEY', 'notsosecret');
Parse.enableLocalDatastore();
});

afterEach(async () => {
await Parse.User.logOut();
Parse.Storage._clear();
});

it('can log in a user', async () => {
// Handle Storage Controller
await Parse.User.signUp('asdf', 'zxcv')
const user = await Parse.User.logIn('asdf', 'zxcv');
expect(user.get('username')).toBe('asdf');
expect(user.existed()).toBe(true);
});

it('can encrypt user', async () => {
// Handle Crypto Controller
Parse.User.enableUnsafeCurrentUser();
Parse.enableEncryptedUser();
Parse.secret = 'My Secret Key';
const user = new Parse.User();
user.setUsername('usernameENC');
user.setPassword('passwordENC');
await user.signUp();

const path = Parse.Storage.generatePath('currentUser');
const encryptedUser = Parse.Storage.getItem(path);

const crypto = Parse.CoreManager.getCryptoController();

const decryptedUser = crypto.decrypt(encryptedUser, Parse.CoreManager.get('ENCRYPTED_KEY'));
expect(JSON.parse(decryptedUser).objectId).toBe(user.id);

const currentUser = Parse.User.current();
expect(currentUser).toEqual(user);

const currentUserAsync = await Parse.User.currentAsync();
expect(currentUserAsync).toEqual(user);
await Parse.User.logOut();
Parse.CoreManager.set('ENCRYPTED_USER', false);
Parse.CoreManager.set('ENCRYPTED_KEY', null);
});

it('can pin saved object LDS', async () => {
// Handle LocalDatastore Controller
function LDS_KEY(object) {
return Parse.LocalDatastore.getKeyForObject(object);
}
const object = new Parse.Object('TestObject');
object.set('field', 'test');
await object.save();
await object.pin();
const localDatastore = await Parse.LocalDatastore._getAllContents();
const cachedObject = localDatastore[LDS_KEY(object)][0];
expect(Object.keys(localDatastore).length).toBe(2);
expect(cachedObject.objectId).toBe(object.id);
expect(cachedObject.field).toBe('test');
});

it('can subscribe to query', async () => {
// Handle WebSocket Controller
const object = new Parse.Object('TestObject');
await object.save();
const installationId = await Parse.CoreManager.getInstallationController().currentInstallationId();

const query = new Parse.Query('TestObject');
query.equalTo('objectId', object.id);
const subscription = await query.subscribe();
const promise = resolvingPromise();
subscription.on('update', (object, _, response) => {
expect(object.get('foo')).toBe('bar');
expect(response.installationId).toBe(installationId);
promise.resolve();
});
object.set({ foo: 'bar' });
await object.save();
await promise;
});
});
14 changes: 0 additions & 14 deletions src/LiveQueryClient.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* global WebSocket */

import CoreManager from './CoreManager';
import ParseObject from './ParseObject';
import LiveQuerySubscription from './LiveQuerySubscription';
Expand Down Expand Up @@ -502,16 +500,4 @@ class LiveQueryClient {
}
}

if (process.env.PARSE_BUILD === 'node') {
CoreManager.setWebSocketController(require('ws'));
} else if (process.env.PARSE_BUILD === 'browser') {
CoreManager.setWebSocketController(
typeof WebSocket === 'function' || typeof WebSocket === 'object' ? WebSocket : null
);
} else if (process.env.PARSE_BUILD === 'weapp') {
CoreManager.setWebSocketController(require('./Socket.weapp'));
} else if (process.env.PARSE_BUILD === 'react-native') {
CoreManager.setWebSocketController(WebSocket);
}

export default LiveQueryClient;
67 changes: 67 additions & 0 deletions src/LocalDatastoreController.default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @flow
*/
import { isLocalDatastoreKey } from './LocalDatastoreUtils';
import Storage from './Storage';

const LocalDatastoreController = {
async fromPinWithName(name: string): Array<Object> {
const values = await Storage.getItemAsync(name);
if (!values) {
return [];
}
const objects = JSON.parse(values);
return objects;
},

pinWithName(name: string, value: any) {
const values = JSON.stringify(value);
return Storage.setItemAsync(name, values);
},

unPinWithName(name: string) {
return Storage.removeItemAsync(name);
},

async getAllContents(): Object {
const keys = await Storage.getAllKeysAsync();
return keys.reduce(async (previousPromise, key) => {
const LDS = await previousPromise;
if (isLocalDatastoreKey(key)) {
const value = await Storage.getItemAsync(key);
try {
LDS[key] = JSON.parse(value);
} catch (error) {
console.error('Error getAllContents: ', error);
}
}
return LDS;
}, Promise.resolve({}));
},

// Used for testing
async getRawStorage(): Object {
const keys = await Storage.getAllKeysAsync();
return keys.reduce(async (previousPromise, key) => {
const LDS = await previousPromise;
const value = await Storage.getItemAsync(key);
LDS[key] = value;
return LDS;
}, Promise.resolve({}));
},

async clear(): Promise {
const keys = await Storage.getAllKeysAsync();

const toRemove = [];
for (const key of keys) {
if (isLocalDatastoreKey(key)) {
toRemove.push(key);
}
}
const promises = toRemove.map(this.unPinWithName);
return Promise.all(promises);
},
};

module.exports = LocalDatastoreController;
72 changes: 5 additions & 67 deletions src/LocalDatastoreController.js
Original file line number Diff line number Diff line change
@@ -1,67 +1,5 @@
/**
* @flow
*/
import { isLocalDatastoreKey } from './LocalDatastoreUtils';
import Storage from './Storage';

const LocalDatastoreController = {
async fromPinWithName(name: string): Array<Object> {
const values = await Storage.getItemAsync(name);
if (!values) {
return [];
}
const objects = JSON.parse(values);
return objects;
},

pinWithName(name: string, value: any) {
const values = JSON.stringify(value);
return Storage.setItemAsync(name, values);
},

unPinWithName(name: string) {
return Storage.removeItemAsync(name);
},

async getAllContents(): Object {
const keys = await Storage.getAllKeysAsync();
return keys.reduce(async (previousPromise, key) => {
const LDS = await previousPromise;
if (isLocalDatastoreKey(key)) {
const value = await Storage.getItemAsync(key);
try {
LDS[key] = JSON.parse(value);
} catch (error) {
console.error('Error getAllContents: ', error);
}
}
return LDS;
}, Promise.resolve({}));
},

// Used for testing
async getRawStorage(): Object {
const keys = await Storage.getAllKeysAsync();
return keys.reduce(async (previousPromise, key) => {
const LDS = await previousPromise;
const value = await Storage.getItemAsync(key);
LDS[key] = value;
return LDS;
}, Promise.resolve({}));
},

async clear(): Promise {
const keys = await Storage.getAllKeysAsync();

const toRemove = [];
for (const key of keys) {
if (isLocalDatastoreKey(key)) {
toRemove.push(key);
}
}
const promises = toRemove.map(this.unPinWithName);
return Promise.all(promises);
},
};

module.exports = LocalDatastoreController;
if (process.env.PARSE_BUILD === 'react-native') {
module.exports = require('./LocalDatastoreController.react-native');
} else {
module.exports = require('./LocalDatastoreController.default');
}
28 changes: 21 additions & 7 deletions src/Parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import Schema from './ParseSchema'
import Session from './ParseSession'
import Storage from './Storage'
import User from './ParseUser'
import LiveQuery from './ParseLiveQuery'
import ParseLiveQuery from './ParseLiveQuery'
import LiveQueryClient from './LiveQueryClient'
import LocalDatastoreController from './LocalDatastoreController';
import StorageController from './StorageController';
import WebSocketController from './WebSocketController';

/**
* Contains all Parse API classes and functions.
Expand Down Expand Up @@ -78,7 +81,7 @@ interface ParseType {
Session: typeof Session,
Storage: typeof Storage,
User: typeof User,
LiveQuery?: typeof LiveQuery,
LiveQuery: ParseLiveQuery,
LiveQueryClient: typeof LiveQueryClient,

initialize(applicationId: string, javaScriptKey: string): void,
Expand Down Expand Up @@ -146,7 +149,6 @@ const Parse: ParseType = {
Storage: Storage,
User: User,
LiveQueryClient: LiveQueryClient,
LiveQuery: undefined,
IndexedDB: undefined,
Hooks: undefined,
Parse: undefined,
Expand Down Expand Up @@ -181,9 +183,11 @@ const Parse: ParseType = {
CoreManager.set('MASTER_KEY', masterKey);
CoreManager.set('USE_MASTER_KEY', false);
CoreManager.setIfNeeded('EventEmitter', EventEmitter);

Parse.LiveQuery = new LiveQuery();
CoreManager.setIfNeeded('LiveQuery', Parse.LiveQuery);
CoreManager.setIfNeeded('LiveQuery', new ParseLiveQuery());
CoreManager.setIfNeeded('CryptoController', CryptoController);
CoreManager.setIfNeeded('LocalDatastoreController', LocalDatastoreController);
CoreManager.setIfNeeded('StorageController', StorageController);
CoreManager.setIfNeeded('WebSocketController', WebSocketController);

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

/**
* @member {ParseLiveQuery} Parse.LiveQuery
* @static
*/
set LiveQuery(liveQuery: ParseLiveQuery) {
CoreManager.setLiveQuery(liveQuery);
},
get LiveQuery() {
return CoreManager.getLiveQuery();
},

/**
* @member {string} Parse.liveQueryServerURL
* @static
Expand Down Expand Up @@ -433,7 +448,6 @@ const Parse: ParseType = {
},
};

CoreManager.setCryptoController(CryptoController);
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController(RESTController);

Expand Down
10 changes: 0 additions & 10 deletions src/Storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,3 @@ const Storage = {

module.exports = Storage;
export default Storage;

if (process.env.PARSE_BUILD === 'react-native') {
CoreManager.setStorageController(require('./StorageController.react-native'));
} else if (process.env.PARSE_BUILD === 'browser') {
CoreManager.setStorageController(require('./StorageController.browser'));
} else if (process.env.PARSE_BUILD === 'weapp') {
CoreManager.setStorageController(require('./StorageController.weapp'));
} else {
CoreManager.setStorageController(require('./StorageController.default'));
}
9 changes: 9 additions & 0 deletions src/StorageController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
if (process.env.PARSE_BUILD === 'react-native') {
module.exports = require('./StorageController.react-native');
} else if (process.env.PARSE_BUILD === 'browser') {
module.exports = require('./StorageController.browser');
} else if (process.env.PARSE_BUILD === 'weapp') {
module.exports = require('./StorageController.weapp');
} else {
module.exports = require('./StorageController.default');
}
Loading

0 comments on commit fbd0ab1

Please sign in to comment.