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

feat: Lazy load Parse.CoreManager controllers to add support for swappable CryptoController, LocalDatastoreController, StorageController, WebSocketController, ParseLiveQuery #2100

Merged
merged 7 commits into from
Apr 14, 2024
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
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
Loading