diff --git a/.github/workflows/shiftleft-analysis.yml b/.github/workflows/shiftleft-analysis.yml
deleted file mode 100644
index 831315f..0000000
--- a/.github/workflows/shiftleft-analysis.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# This workflow integrates ShiftLeft Scan with GitHub's code scanning feature
-# ShiftLeft Scan is a free open-source security tool for modern DevOps teams
-# Visit https://slscan.io/en/latest/integrations/code-scan for help
-name: ShiftLeft Scan
-
-# This section configures the trigger for the workflow. Feel free to customize depending on your convention
-on: push
-
-jobs:
- Scan-Build:
- # Scan runs on ubuntu, mac and windows
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- # Instructions
- # 1. Setup JDK, Node.js, Python etc depending on your project type
- # 2. Compile or build the project before invoking scan
- # Example: mvn compile, or npm install or pip install goes here
- # 3. Invoke ShiftLeft Scan with the github token. Leave the workspace empty to use relative url
-
- - name: Perform ShiftLeft Scan
- uses: ShiftLeftSecurity/scan-action@master
- env:
- WORKSPACE: ""
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- SCAN_AUTO_BUILD: true
- with:
- output: reports
- # Scan auto-detects the languages in your project. To override uncomment the below variable and set the type
- # type: credscan,java
- # type: python
-
- - name: Upload report
- uses: github/codeql-action/upload-sarif@v1
- with:
- sarif_file: reports
diff --git a/companion-packages/meteorrn-local/README.md b/companion-packages/meteorrn-local/README.md
index 3d03b5c..b5842f8 100644
--- a/companion-packages/meteorrn-local/README.md
+++ b/companion-packages/meteorrn-local/README.md
@@ -6,8 +6,7 @@ This package introduces the `Local.Collection`, which will mirror the specified
### Caveats
- This package (currently) works by creating a second local Mongo Collection. This means you are esentially keeping two copies of each document (that you store locally) in memory. This issue can be mitigated by keeping an "age" or "version" on all your documents, and only publishing documents that have been changed from local
-- This package (currently) does not support the automatical removal/expiry of documents. Once a document has been inserted into the local database, it is there forever (unless you manually call `remove` on the Local.Collection)
-- Performing `.insert`, `.update`, `.remove`, etc on a Local.Collection only makes those modifications to the in-memory minimongo. Those changes won't be sent to the server, and those changes (currently) dont trigger the saving procedure, so they will not be committed to the disk (unless a remote change is made afterwards)
+- This package (currently) does not support the automatic removal/expiry of documents. Once a document has been inserted into the local database, it is there forever (unless you manually call `remove` on the Local.Collection)
### Usage:
diff --git a/companion-packages/meteorrn-local/index.js b/companion-packages/meteorrn-local/index.js
index 19c5ac0..cf7c572 100644
--- a/companion-packages/meteorrn-local/index.js
+++ b/companion-packages/meteorrn-local/index.js
@@ -93,6 +93,24 @@ const Local = {
});
};
+
+ LocalCol.insert = LocalCol.__insert;
+ LocalCol.update = LocalCol.__update;
+ LocalCol.remove = LocalCol.__remove;
+
+ LocalCol.insert = (...args) => {
+ LocalCol.__insert(...args);
+ storeLocalCol();
+ };
+ LocalCol.update = (...args) => {
+ LocalCol.__update(...args);
+ storeLocalCol();
+ };
+ LocalCol.remove = (...args) => {
+ LocalCol.__remove(...args);
+ storeLocalCol();
+ };
+
LocalCol.loadPromise = loadData();
LocalCol.save = storeLocalCol;
diff --git a/docs/api.md b/docs/api.md
index f5b1a76..3af63e0 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -1,6 +1,13 @@
-## Meteor
+# Meteor React Native Docs
+
+Table of Contents
+- [Meteor](#meteor)
+- [Tracker](#tracker)
+
+
Meteor
`import Meteor from '@meteorrn/core';`
+
### `Meteor.connect(url, options)`
Connect to the Meteor Server
@@ -46,60 +53,123 @@ Returns true if attempting to login
### `Meteor.logoutOtherClients`
-## withTracker
-`import { withTracker } from '@meteorrn/core'`;
-The `withTracker` component is used the same way as [`meteor/react-meteor-data`](https://guide.meteor.com/react.html#using-withTracker)
-```javascript
-export default withTracker(() => {
- let handle = Meteor.subscribe("mySubscription");
- let loading = !handle.ready();
- let myStuff = Stuff.find({}).fetch();
-
- return {
- myStuff
- };
-})(MyComponent);
-```
+Tracker
+`import { withTracker, useTracker } from '@meteorrn/core'`;
+
+
+#### `withTracker(trackerFunc)(Component)`
+Creates a new Tracker
+
+**Arguments:**
+ * trackerFunc - Function which will be re-run reactively when it's dependencies are updated. Must return an object that is passed as properties to `Component`
+ * Component - React Component which will receive properties from trackerFunc
+
+
+#### `useTracker(trackerFunc)` => `React Hook`
+Creates a new Tracker React Hook. Can only be used inside a function component. See React Docs for more info.
+
+**Arguments:**
+ * trackerFunc - Function which will be re-run reactively when it's dependencies are updated.
+
+
## ReactiveDict
`import { ReactiveDict } from '@meteorrn/core'`
-https://atmospherejs.com/meteor/reactive-dict
+#### `new ReactiveDict()` => *`ReactiveDict`*
+Creates a new reactive dictionary
+
+
+#### *`ReactiveDict`*
+
+***ReactiveDict* Methods:**
+ * .get(key) - Gets value of key (Reactive)
+ * .set(key, value) - Sets value of key
+
## Mongo
`import { Mongo } from '@meteorrn/core';`
-#### `Mongo.Collection(collectionName, options)`
-*collectionName*: Name of the remote collection, or pass `null` for a client-side collection
+#### `new Mongo.Collection(collectionName, options)` => `Collection`
+Creates and returns a *Collection*
-**options**:
- * [.insert(doc, callback)](http://docs.meteor.com/#/full/insert)
- * [.update(id, modifier, [options], [callback])](http://docs.meteor.com/#/full/update)
- * [.remove(id, callback(err, countRemoved))](http://docs.meteor.com/#/full/remove)
+**Arguments**
+ * collectionName - Name of the remote collection, or pass `null` for a client-side collection
+
+
+#### *`Collection`*
+
+***Collection* Methods:**
+ * .insert(document) - Inserts document into collection
+ * .update(query, modifications) - Updates document in collection
+ * .remove(query) - Removes document from collection
+ * .find(query) => *`Cursor`* - Returns a Cursor
+ * .findOne(query) => Document - Retrieves first matching Document
+
+
+#### *`Cursor`*
+
+***Cursor* Methods:**
+ * .obsrve() - Mirrors Meteor's observe behavior. Accepts object with the properties `added`, `changed`, and `removed`.
+ * .fetch() => `[Document]` - Retrieves an array of matching documents
-#### *Cursor*.observe
-Mirrors Meteor's observe behavior. Accepts object with the properties `added`, `changed`, and `removed`.
## Accounts
`import { Accounts } from '@meteorrn/core';`
-* [Accounts.createUser](http://docs.meteor.com/#/full/accounts_createuser)
-* [Accounts.changePassword](http://docs.meteor.com/#/full/accounts_forgotpassword)
+
+#### `Accounts.createUser(user, callback)`
+Creates a user
+
+**Arguments**
+ * user - The user object
+ * callback - Called with a single error object or null on success
+
+
+#### `Accounts.changePassword(oldPassword, newPassword)`
+Changes a user's password
+
+**Arguments**
+ * oldPassword - The user's current password
+ * newPassword - The user's new password
+
+
+#### `Accounts.onLogin(callback)`
+Registers a callback to be called when user is logged in
+
+**Arguments**
+ * callback
+
+
+#### `Accounts.onLoginFailure(callback)`
+Registers a callback to be called when login fails
+
+**Arguments**
+ * callback
+
+
+#### `Accounts._hashPassword(plaintext)` => `{algorithm:"sha-256", digest:"..."}`
+Hashes a password using the sha-256 algorithm. Returns an object formatted for use in accounts calls. You can access the raw hashed string using the digest property.
+
+**Arguments**
+ * plaintext - The plaintext string you want to hash
+
+Other:
+
* [Accounts.forgotPassword](http://docs.meteor.com/#/full/accounts_changepassword)
* [Accounts.resetPassword](http://docs.meteor.com/#/full/accounts_resetpassword)
-* [Accounts.onLogin](http://docs.meteor.com/#/full/accounts_onlogin)
-* [Accounts.onLoginFailure](http://docs.meteor.com/#/full/accounts_onloginfailure)
-* `Accounts._hashPassword` - SHA-256 hashes password, for use with methods that may require authentication
-## enableVerbose
+
+
+## Verbosity
`import { enableVerbose } from '@meteorrn/core';`
-Enables verbose mode which logs detailed information about accounts. **Note:** this will expose login tokens and other private information to the console.
+Verbose Mode logs detailed information from various places around MeteorRN. **Note:** this will expose login tokens and other private information to the console.
+
-````
-enableVerbose()
-````
\ No newline at end of file
+#### `enableVerbose()`
+Enables verbose mode
diff --git a/package.json b/package.json
index 7775f82..1f6414e 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
"name": "@meteorrn/core",
- "version": "2.0.15",
+ "version": "2.1.0",
"description": "Full Meteor Client for React Native",
- "main": "src/Meteor.js",
+ "main": "src/index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/TheRealNate/meteor-react-native.git"
diff --git a/src/Meteor.js b/src/Meteor.js
index 4a0c227..b933083 100644
--- a/src/Meteor.js
+++ b/src/Meteor.js
@@ -1,5 +1,3 @@
-import NetInfo from "@react-native-community/netinfo";
-
import { name as packageName } from '../package.json';
if(packageName !== "@meteorrn/core") {
@@ -16,36 +14,32 @@ import Mongo from './Mongo';
import { Collection, runObservers, localCollections } from './Collection';
import call from './Call';
-import withTracker from './components/ReactMeteorData';
+import withTracker from './components/withTracker';
+import useTracker from './components/useTracker';
import ReactiveDict from './ReactiveDict';
-import User from './user/User';
-import Accounts from './user/Accounts';
-
let isVerbose = false;
-module.exports = {
+const Meteor = {
isVerbose,
enableVerbose() {
isVerbose = true;
},
Random,
- Accounts,
Mongo,
Tracker: Trackr,
EJSON,
ReactiveDict,
Collection,
- collection(name, options) {
- console.error("Meteor.collection is deprecated. Use Mongo.Collection");
- return new Collection(name, options);
+ collection() {
+ throw new Error("Meteor.collection is deprecated. Use Mongo.Collection");
},
withTracker,
+ useTracker,
getData() {
return Data;
},
- ...User,
status() {
return {
connected: Data.ddp ? Data.ddp.status == 'connected' : false,
@@ -84,7 +78,7 @@ module.exports = {
if((!endpoint.startsWith("ws") || !endpoint.endsWith("/websocket")) && !options.suppressUrlErrors) {
throw new Error(`Your url "${endpoint}" may be in the wrong format. It should start with "ws://" or "wss://" and end with "/websocket", e.g. "wss://myapp.meteor.com/websocket". To disable this warning, connect with option "suppressUrlErrors" as true, e.g. Meteor.connect("${endpoint}", {suppressUrlErrors:true});`);
}
-
+
if (!options.AsyncStorage) {
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
@@ -103,12 +97,18 @@ module.exports = {
SocketConstructor: WebSocket,
...options,
});
-
- NetInfo.addEventListener(({type, isConnected, isInternetReachable, isWifiEnabled}) => {
- if (isConnected && Data.ddp.autoReconnect) {
- Data.ddp.connect();
- }
- });
+
+ try {
+ const NetInfo = require("@react-native-community/netinfo").default;
+ NetInfo.addEventListener(({type, isConnected, isInternetReachable, isWifiEnabled}) => {
+ if (isConnected && Data.ddp.autoReconnect) {
+ Data.ddp.connect();
+ }
+ });
+ }
+ catch(e) {
+ console.warn("Warning: NetInfo not installed, so DDP will not automatically reconnect");
+ }
Data.ddp.on('connected', () => {
// Clear the collections of any stale data in case this is a reconnect
@@ -155,9 +155,9 @@ module.exports = {
_id: message.id,
...message.fields,
};
-
+
Data.db[message.collection].upsert(document);
-
+
runObservers("added", message.collection, document);
});
@@ -192,18 +192,18 @@ module.exports = {
...message.fields,
...unset,
};
-
+
const oldDocument = Data.db[message.collection].findOne({_id:message.id});
-
+
Data.db[message.collection].upsert(document);
-
- runObservers("changed", message.collection, document, oldDocument);
+
+ runObservers("changed", message.collection, document, oldDocument);
}
});
Data.ddp.on('removed', message => {
if(Data.db[message.collection]) {
- const oldDocument = Data.db[message.collection].findOne({_id:message.id});
+ const oldDocument = Data.db[message.collection].findOne({_id:message.id});
Data.db[message.collection].del(message.id);
runObservers("removed", message.collection, oldDocument);
}
@@ -350,4 +350,4 @@ module.exports = {
},
};
-export default module.exports;
\ No newline at end of file
+export default Meteor;
diff --git a/src/components/MeteorDataManager.js b/src/components/MeteorDataManager.js
deleted file mode 100644
index c6b75bd..0000000
--- a/src/components/MeteorDataManager.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import Trackr from 'trackr';
-import Data from '../Data';
-
-// A class to keep the state and utility methods needed to manage
-// the Meteor data for a component.
-class MeteorDataManager {
- constructor(component) {
- this.component = component;
- this.computation = null;
- this.oldData = null;
- this._meteorDataDep = new Trackr.Dependency();
- this._meteorDataChangedCallback = () => {
- this._meteorDataDep.changed();
- };
-
- Data.onChange(this._meteorDataChangedCallback);
- }
-
- dispose() {
- if (this.computation) {
- this.computation.stop();
- this.computation = null;
- }
-
- Data.offChange(this._meteorDataChangedCallback);
- }
-
- calculateData() {
- const component = this.component;
-
- if (!component.getMeteorData) {
- return null;
- }
-
- if (this.computation) {
- this.computation.stop();
- this.computation = null;
- }
-
- let data;
- // Use Tracker.nonreactive in case we are inside a Tracker Computation.
- // This can happen if someone calls `ReactDOM.render` inside a Computation.
- // In that case, we want to opt out of the normal behavior of nested
- // Computations, where if the outer one is invalidated or stopped,
- // it stops the inner one.
-
- this.computation = Trackr.nonreactive(() => {
- return Trackr.autorun(c => {
- this._meteorDataDep.depend();
- if (c.firstRun) {
- const savedSetState = component.setState;
- try {
- component.setState = () => {
- throw new Error(
- "Can't call `setState` inside `getMeteorData` as this could cause an endless" +
- ' loop. To respond to Meteor data changing, consider making this component' +
- ' a "wrapper component" that only fetches data and passes it in as props to' +
- ' a child component. Then you can use `componentWillReceiveProps` in that' +
- ' child component.'
- );
- };
-
- data = component.getMeteorData();
- } finally {
- component.setState = savedSetState;
- }
- } else {
- // Stop this computation instead of using the re-run.
- // We use a brand-new autorun for each call to getMeteorData
- // to capture dependencies on any reactive data sources that
- // are accessed. The reason we can't use a single autorun
- // for the lifetime of the component is that Tracker only
- // re-runs autoruns at flush time, while we need to be able to
- // re-call getMeteorData synchronously whenever we want, e.g.
- // from componentWillUpdate.
- c.stop();
- // Calling forceUpdate() triggers componentWillUpdate which
- // recalculates getMeteorData() and re-renders the component.
- try {
- component.forceUpdate();
- } catch (e) {
- console.error(e);
- }
- }
- });
- });
-
- return data;
- }
-
- updateData(newData) {
- const component = this.component;
- const oldData = this.oldData;
-
- if (!(newData && typeof newData === 'object')) {
- throw new Error('Expected object returned from getMeteorData');
- }
- // update componentData in place based on newData
- for (let key in newData) {
- component.data[key] = newData[key];
- }
- // if there is oldData (which is every time this method is called
- // except the first), delete keys in newData that aren't in
- // oldData. don't interfere with other keys, in case we are
- // co-existing with something else that writes to a component's
- // this.data.
- if (oldData) {
- for (let key in oldData) {
- if (!(key in newData)) {
- delete component.data[key];
- }
- }
- }
- this.oldData = newData;
- }
-}
-
-export default MeteorDataManager;
diff --git a/src/components/ReactMeteorData.js b/src/components/ReactMeteorData.js
deleted file mode 100644
index 21963a7..0000000
--- a/src/components/ReactMeteorData.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import React from 'react';
-import EJSON from 'ejson';
-
-import Data from '../Data';
-import MeteorDataManager from './MeteorDataManager';
-
-const ReactMeteorData = {
- UNSAFE_componentWillMount() {
- Data.waitDdpReady(() => {
- if (this.getMeteorData) {
- this.data = {};
- this._meteorDataManager = new MeteorDataManager(this);
- const newData = this._meteorDataManager.calculateData();
- this._meteorDataManager.updateData(newData);
- }
- });
- },
-
- UNSAFE_componentWillUpdate(nextProps, nextState) {
- if (this.startMeteorSubscriptions) {
- if (
- !EJSON.equals(this.state, nextState) ||
- !EJSON.equals(this.props, nextProps)
- ) {
- this._meteorSubscriptionsManager._meteorDataChangedCallback();
- }
- }
-
- if (this.getMeteorData) {
- const saveProps = this.props;
- const saveState = this.state;
- let newData;
- try {
- // Temporarily assign this.state and this.props,
- // so that they are seen by getMeteorData!
- // This is a simulation of how the proposed Observe API
- // for React will work, which calls observe() after
- // componentWillUpdate and after props and state are
- // updated, but before render() is called.
- // See https://github.com/facebook/react/issues/3398.
- this.props = nextProps;
- this.state = nextState;
- newData = this._meteorDataManager.calculateData();
- } finally {
- this.props = saveProps;
- this.state = saveState;
- }
-
- this._meteorDataManager.updateData(newData);
- }
- },
-
- componentWillUnmount() {
- if (this._meteorDataManager) {
- this._meteorDataManager.dispose();
- }
-
- if (this._meteorSubscriptionsManager) {
- this._meteorSubscriptionsManager.dispose();
- }
- },
-};
-
-export { ReactMeteorData };
-
-class ReactComponent extends React.Component {}
-Object.assign(ReactComponent.prototype, ReactMeteorData);
-class ReactPureComponent extends React.PureComponent {}
-Object.assign(ReactPureComponent.prototype, ReactMeteorData);
-
-export default function connect(options) {
- let expandedOptions = options;
- if (typeof options === 'function') {
- expandedOptions = {
- getMeteorData: options,
- };
- }
-
- const { getMeteorData, pure = true } = expandedOptions;
-
- const BaseComponent = pure ? ReactPureComponent : ReactComponent;
- return WrappedComponent =>
- class ReactMeteorDataComponent extends BaseComponent {
- getMeteorData() {
- return getMeteorData(this.props);
- }
- render() {
- return ;
- }
- };
-}
diff --git a/src/components/useTracker.js b/src/components/useTracker.js
new file mode 100644
index 0000000..b32a510
--- /dev/null
+++ b/src/components/useTracker.js
@@ -0,0 +1,30 @@
+import { useEffect, useState } from 'react';
+import Tracker from 'trackr';
+import Data from '../Data';
+
+export default (trackerFn, deps = []) => {
+ const [response, setResponse] = useState(trackerFn());
+ const meteorDataDep = new Tracker.Dependency();
+ let computation = null;
+ const dataChangedCallback = () => {
+ meteorDataDep.changed();
+ };
+
+ const stopComputation = () => {
+ computation && computation.stop();
+ computation = null;
+ };
+
+ Data.onChange(dataChangedCallback);
+
+ useEffect(() => {
+ stopComputation();
+ Tracker.autorun(currentComputation => {
+ meteorDataDep.depend();
+ computation = currentComputation;
+ setResponse(trackerFn());
+ });
+ return () => { stopComputation(); Data.offChange(dataChangedCallback); };
+ }, deps);
+ return response;
+};
diff --git a/src/components/withTracker.js b/src/components/withTracker.js
new file mode 100644
index 0000000..cede3e0
--- /dev/null
+++ b/src/components/withTracker.js
@@ -0,0 +1,16 @@
+import React, { forwardRef, memo } from 'react';
+import useTracker from './useTracker';
+
+export default function withTracker (options) {
+ return Component => {
+ const expandedOptions = typeof options === 'function' ? { getMeteorData: options } : options;
+ const { getMeteorData, pure = true } = expandedOptions;
+
+ const WithTracker = forwardRef((props, ref) => {
+ const data = useTracker(() => getMeteorData(props) || {});
+ return React.createElement(Component, {ref, ...props, ...data});
+ });
+
+ return pure ? memo(WithTracker) : WithTracker;
+ };
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..7b5303f
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,12 @@
+import Meteor from './Meteor.js';
+import User from './user/User.js';
+import Accounts from './user/Accounts.js';
+
+Object.assign(Meteor, User);
+
+const {
+ withTracker, Mongo, packageInterface, ReactiveDict
+} = Meteor;
+
+export { Accounts, withTracker, Mongo, packageInterface, ReactiveDict };
+export default Meteor;
\ No newline at end of file
diff --git a/src/user/Accounts.js b/src/user/Accounts.js
index d63b8df..e6d4f81 100644
--- a/src/user/Accounts.js
+++ b/src/user/Accounts.js
@@ -8,9 +8,6 @@ class AccountsPassword {
_hashPassword = hashPassword;
createUser = (options, callback = () => {}) => {
- if (options.username) options.username = options.username;
- if (options.email) options.email = options.email;
-
// Replace password with the hashed password.
options.password = hashPassword(options.password);
@@ -76,4 +73,4 @@ class AccountsPassword {
}
}
-export default new AccountsPassword();
\ No newline at end of file
+export default new AccountsPassword();