Socket
Socket
Sign inDemoInstall

redux-orm

Package Overview
Dependencies
7
Maintainers
1
Versions
70
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.8.4 to 0.9.0-rc.0

lib/constants.js

25

gulpfile.js
const gulp = require('gulp');
const ghPages = require('gulp-gh-pages');
const rename = require('gulp-rename');
gulp.task('deploy', () =>
gulp.src([
'./dist/**/*',
'./docs/**/*',
], { base: '.' })
.pipe(rename(filepath => {
if (filepath.dirname.startsWith('docs')) {
// Collapses `docs` parent directory
// so index.html ends up at the root.
// `dist` remains in the dist folder.
const withoutDocs = filepath.dirname.substring('docs'.length);
const withoutLeadingSlash = withoutDocs.startsWith('/')
? withoutDocs.substring(1)
: withoutDocs;
filepath.dirname = withoutLeadingSlash;
}
}))
.pipe(ghPages())
);
gulp.task('deploy', () => {
return gulp.src('./docs/**/*')
.pipe(ghPages());
});
{
"name": "redux-orm",
"version": "0.8.4",
"version": "0.9.0-rc.0",
"description": "Simple ORM to manage and query your state trees",
"main": "dist/redux-orm.js",
"main": "lib/index.js",
"scripts": {

@@ -24,3 +24,2 @@ "test": "make test",

"babel-eslint": "^6.0.4",
"babel-loader": "^6.2.5",
"babel-plugin-transform-runtime": "^6.8.0",

@@ -37,13 +36,10 @@ "babel-preset-es2015": "^6.6.0",

"gulp-gh-pages": "^0.5.4",
"gulp-rename": "^1.2.2",
"jsdoc": "^3.4.1",
"mocha": "^2.2.5",
"sinon": "^1.17.2",
"sinon-chai": "^2.8.0",
"webpack": "^1.13.2",
"yargs": "^5.0.0"
"sinon-chai": "^2.8.0"
},
"dependencies": {
"babel-runtime": "^6.6.1",
"immutable-ops": "^0.4.2",
"immutable-ops": "^0.5.2",
"lodash": "^4.12.0",

@@ -50,0 +46,0 @@ "reselect": "^2.0.1"

@@ -6,4 +6,8 @@ redux-orm

See a [a guide to creating a simple app with Redux-ORM](https://github.com/tommikaikkonen/redux-orm-primer) (includes the source).
See a [a guide to creating a simple app with Redux-ORM](https://github.com/tommikaikkonen/redux-orm-primer) (includes the source). Its README is not updated for 0.9 yet but the [code has a branch for it](https://github.com/tommikaikkonen/redux-orm-primer/tree/migrate_to_0_9).
**The 0.9 which is in the release candidate stage, brings big breaking changes to the API. Please look at the [migration guide](https://github.com/tommikaikkonen/redux-orm/wiki/0.9-Migration-Guide) if you're migrating from earlier versions.**
Looking for the 0.8 docs? Read the [old README.md in the repo](https://github.com/tommikaikkonen/redux-orm/tree/3c36fa804d2810b2aaaad89ff1d99534b847ea35). For the API reference, clone the repo, `npm install`, `make build` and open up `index.html` in your browser. Sorry for the inconvenience.
API can be unstable until 1.0.0. Minor version bumps before 1.0.0 can and will introduce breaking changes. They will be noted in the [changelog](https://github.com/tommikaikkonen/redux-orm#changelog).

@@ -24,11 +28,2 @@

Or with a script tag
```html
<script src="https://tommikaikkonen.github.io/redux-orm/dist/redux-orm.js"></script>
```
- [Browser build following master](https://tommikaikkonen.github.io/redux-orm/dist/redux-orm.js)
- [Browser build following master (minimized)](https://tommikaikkonen.github.io/redux-orm/dist/redux-orm.min.js)
## Usage

@@ -38,7 +33,7 @@

You can declare your models with the ES6 class syntax, extending from `Model`. You are not required to declare normal fields, only fields that relate to another model. `redux-orm` supports one-to-one and many-to-many relations in addition to foreign keys (`oneToOne`, `many` and `fk` imports respectively). Non-related properties can be accessed like in normal JavaScript objects.
You can declare your models with the ES6 class syntax, extending from `Model`. You should declare all fields you want to persist for a model, including non-relational fields. `redux-orm` supports one-to-one and many-to-many relations in addition to foreign keys (`oneToOne`, `many` and `fk` imports respectively). Non-related properties can be accessed like in normal JavaScript objects.
```javascript
// models.js
import {fk, many, Model} from 'redux-orm';
import {fk, many, attr, Model} from 'redux-orm';

@@ -55,2 +50,4 @@ class Book extends Model {

Book.fields = {
id: attr(), // non-relational field for any value
name: attr(),
authors: many('Author', 'books'),

@@ -61,10 +58,107 @@ publisher: fk('Publisher', 'books'),

### Write Your Model-Specific Reducers
### Register Models and Generate an Empty Database State
Every `Model` has it's own reducer. It'll be called every time Redux dispatches an action and by default it returns the previous state. You can declare your own reducers inside your models as `static reducer()`, or write your reducer elsewhere and connect it to `redux-orm` later. The reducer receives the following arguments: the previous state, the current action, the model class connected to the state, and finally the `Session` instance. Here's our extended Book model declaration with a reducer:
Defining fields on a Model specifies the table structure in the database for that Model. In order to generate a description of the whole database's structure, we need a central place register all Models we want to use.
An instance of the ORM class registers Models and handles generating a full schema from all the models and passing that information to the database. Often you'll want to have a file where you can import a single ORM instance across the app, like this:
```javascript
// models.js
// orm.js
import { ORM } from 'redux-orm';
import { Book, Author, Publisher } from './models';
const orm = new ORM();
orm.register(Book, Author, Publisher);
export default orm;
```
You could also define *and* register the models to an ORM instance in the same file, and export them all.
Now that we've registered Models, we can generate an empty database state. Currently that's a plain, nested JavaScript object that is structured similarly to relational databases.
```javascript
// index.js
import orm from './orm';
const emptyDBState = orm.getEmptyState();
```
### Applying Updates to the Database
When we have a database state, we can start an ORM session on that to apply updates. The ORM instance provides a `session` method that accepts a database state as it's sole argument, and returns a Session instance.
```javascript
const session = orm.session(emptyDBState);
```
Session-specific classes of registered Models are available as properties of the session object.
```javascript
const Book = session.Book;
```
Models provide an interface to query and update the database state.
```javascript
Book.withId(1).update({ name: 'Clean Code' });
Book.all().filter(book => book.name === 'Clean Code').delete();
Book.hasId(1)
// false
```
The initial database state is not mutated. A new database state with the updates applied can be found on the `state` property of the Session instance.
```javascript
const updatedDBState = session.state;
```
## Redux Integration
To integrate Redux-ORM with Redux at the most basic level, you can define a reducer that instantiates a session from the database state held in the Redux atom, then when you've applied all of your updates, you can return the next state from the session.
```javascript
import { orm } from './models';
function ormReducer(dbState, action) {
const sess = orm.session(dbState);
// Session-specific Models are available
// as properties on the Session instance.
const { Book } = sess;
switch (action.type) {
case 'CREATE_BOOK':
Book.create(action.payload);
break;
case 'UPDATE_BOOK':
Book.withId(action.payload.id).update(action.payload);
break;
case 'REMOVE_BOOK':
Book.withId(action.payload.id).delete();
break;
case 'ADD_AUTHOR_TO_BOOK':
Book.withId(action.payload.bookId).authors.add(action.payload.author);
break;
case 'REMOVE_AUTHOR_FROM_BOOK':
Book.withId(action.payload.bookId).authors.remove(action.payload.authorId);
break;
case 'ASSIGN_PUBLISHER':
Book.withId(action.payload.bookId).publisher = action.payload.publisherId;
break;
}
// the state property of Session always points to the current database.
// Updates don't mutate the original state, so this reference is not
// equal to `dbState` that was an argument to this reducer.
return sess.state;
}
```
Previously Redux-ORM advocated for reducers specific to Models by attaching a static `reducer` function on the Model class. If you want to define your update logic on the Model classes, you can specify a `reducer` static method on your model which accepts the action as the first argument, the session-specific Model as the second, and the whole session as the third.
```javascript
class Book extends Model {
static reducer(state, action, Book) {
static reducer(action, Book, session) {
switch (action.type) {

@@ -91,4 +185,4 @@ case 'CREATE_BOOK':

}
return Book.getNextState();
// Return value is ignored.
return undefined;
}

@@ -100,33 +194,35 @@

}
// Below we would declare Author and Publisher models
```
### Connect to Redux
To get a reducer for Redux that calls these `reducer` methods:
To create our data schema, we create a Schema instance and register our models.
```javascript
// schema.js
import {Schema} from 'redux-orm';
import {Book, Author, Publisher} from './models';
import { createReducer } from 'redux-orm';
import { orm } from './models';
const schema = new Schema();
schema.register(Book, Author, Publisher);
export default schema;
const reducer = createReducer(orm);
```
`Schema` instances expose a `reducer` method that returns a reducer you can use to plug into Redux. Preferably in your root reducer, plug the reducer under a namespace of your choice:
`createReducer` is really simple, so I'll just paste the source here.
```javascript
// main.js
import schema from './schema';
import {combineReducers} from 'redux';
function createReducer(orm, updater = defaultUpdater) {
return (state, action) => {
const session = orm.session(state || orm.getEmptyState());
updater(session, action);
return session.state;
};
}
const rootReducer = combineReducers({
orm: schema.reducer()
});
function defaultUpdater(session, action) {
session.sessionBoundModels.forEach(modelClass => {
if (typeof modelClass.reducer === 'function') {
modelClass.reducer(action, modelClass, session);
}
});
}
```
As you can see, it just instantiates a new Session, loops through all the Models in the session, and calls the `reducer` method if it exists. Then it returns the new database state that has all the updates applied.
### Use with React

@@ -168,3 +264,3 @@

Selectors created with `schema.createSelector` can be used as input to any additional `reselect` selectors you want to use. They are also great to use with `redux-thunk`: get the whole state with `getState()`, pass the ORM branch to the selector, and get your results. A good use case is serializing data to a custom format for a 3rd party API call.
Selectors created with `createSelector` can be used as input to any additional `reselect` selectors you want to use. They are also great to use with `redux-thunk`: get the whole state with `getState()`, pass the ORM branch to the selector, and get your results. A good use case is serializing data to a custom format for a 3rd party API call.

@@ -176,4 +272,4 @@ Because selectors are memoized, you can use pure rendering in React for performance gains.

import PureComponent from 'react-pure-render/component';
import {authorSelector} from './selectors';
import {connect} from 'react-redux';
import { authorSelector } from './selectors';
import { connect } from 'react-redux';

@@ -217,40 +313,59 @@ class App extends PureComponent {

### Reducers and Immutability
### Immutability
Say we're inside a reducer and want to update the name of a book.
Say we start a session from an initial database state situated in the Redux atom, update the name of a certain book.
First, a new session:
```javascript
const book = Book.withId(action.payload.id)
book.name // 'Refactoring'
book.name = 'Clean Code'
book.name // 'Refactoring'
import { orm } from './models';
const dbState = getState().db; // getState() returns the redux state.
const sess = orm.session(dbState);
```
Assigning a new value has no effect on the current state. Behind the scenes, an update to the data was recorded. When you call
The session maintains a reference to a database state. We haven't
updated the database state, therefore it is still equal to the original
state.
```javascript
Book.getNextState()
// the item we edited will have now values {... name: 'Clean Code'}
sess.state === dbState
// true
```
the update will be reflected in the new state. The same principle holds true when you're creating new instances and deleting them.
Let's apply an update.
### How Updates Work Internally
```javascript
const book = sess.Book.withId(1)
By default, each Model has the following JavaScript object representation:
book.name // 'Refactoring'
book.name = 'Clean Code'
book.name // 'Clean Code'
sess.state === dbState
// false.
```
The update was applied, and because the session does not mutate the original state, it created a new one and swapped `sess.state` to point to the new one.
Let's update the database state again through the ORM.
```javascript
{
items: [],
itemsById: {},
}
// Save this reference so we can compare.
const updatedState = sess.state;
book.name = 'Patterns of Enterprise Application Architecture'
sess.state === updatedState
// true. If possible, future updates are applied with mutations. If you want
// to avoid making mutations to a session state, take the session state
// and start a new session with that state.
```
This representation maintains an array of object ID's and an index of id's for quick access. (A single object array representation is also provided for use. It is possible to subclass `Backend` to use any structure you want).
If possible, future updates are applied with mutations. In this case, the database was already mutated, so the pointer doesn't need to change. If you want to avoid making mutations to a session state, take the session state and start a new session with that state.
`redux-orm` runs a mini-redux inside it. It queues any updates the library user records with action-like objects, and when `getNextState` is called, it applies those actions with its internal reducers. Updates within each `getNextState` are implemented with batched mutations, so even a big number of updates should be pretty performant.
### Customizability
Just like you can extend `Model`, you can do the same for `QuerySet` (customize methods on Model instance collections) and `Backend` (customize store access and updates).
Just like you can extend `Model`, you can do the same for `QuerySet` (customize methods on Model instance collections). You can also specify the whole database implementation yourself (documentation pending).

@@ -263,5 +378,5 @@ ### Caveats

### Schema
### ORM
See the full documentation for Schema [here](http://tommikaikkonen.github.io/redux-orm/Schema.html)
See the full documentation for ORM [here](http://tommikaikkonen.github.io/redux-orm/ORM.html)

@@ -271,3 +386,3 @@ Instantiation

```javascript
const schema = new Schema(); // no arguments needed.
const orm = new ORM(); // no arguments needed.
```

@@ -277,8 +392,10 @@

- `register(model1, model2, ...modelN)`: registers Model classes to the `Schema` instance.
- `define(name, [relatedFields], [backendOpts])`: shortcut to define and register simple models.
- `from(state, [action])`: begins a new `Session` with `state`. If `action` is omitted, the session can be used to query the state data.
- `reducer()`: returns a reducer function that can be plugged into Redux. The reducer will return the next state of the database given the provided action. You need to register your models before calling this.
- `createSelector([...inputSelectors], selectorFunc)`: returns a memoized selector function for `selectorFunc`. `selectorFunc` receives `session` as the first argument, followed by any inputs from `inputSelectors`. Read the full documentation for details.
- `register(...models: Array<Model>)`: registers Model classes to the `ORM` instance.
- `session(state: any)`: begins a new `Session` with `state`.
### Redux Integration
- `createReducer(orm: ORM)`: returns a reducer function that can be plugged into Redux. The reducer will return the next state of the database given the provided action. You need to register your models before calling this.
- `createSelector(orm: ORM, [...inputSelectors], selectorFunc)`: returns a memoized selector function for `selectorFunc`. `selectorFunc` receives `session` as the first argument, followed by any inputs from `inputSelectors`. Read the full documentation for details.
### Model

@@ -295,3 +412,3 @@

- `get(matchObj)`: to get a Model instance based on matching properties in `matchObj`,
- `create(props)`: to create a new Model instance with `props`. If you don't supply an id, the new `id` will be `Math.max(...allOtherIds) + 1`. You can override the `nextId` class method on your model.
- `create(props)`: to create a new Model instance with `props`. If you don't supply an id, the new `id` will be `Math.max(...allOtherIds) + 1`.

@@ -306,5 +423,5 @@ You will also have access to almost all [QuerySet instance methods](http://tommikaikkonen.github.io/redux-orm/QuerySet.html) from the class object for convenience.

- `equals(otherModel)`: returns a boolean indicating equality with `otherModel`. Equality is determined by shallow comparison of both model's attributes.
- `set(propertyName, value)`: marks a supplied `propertyName` to be updated to `value` at `Model.getNextState`. Returns `undefined`. Is equivalent to normal assignment.
- `update(mergeObj)`: marks a supplied object of property names and values (`mergeObj`) to be merged with the Model instance at `Model.getNextState()`. Returns `undefined`.
- `delete()`: marks the Model instance to be deleted at `Model.getNextState()`. Returns `undefined`.
- `set(propertyName, value)`: updates `propertyName` to `value`. Returns `undefined`. Is equivalent to normal assignment.
- `update(mergeObj)`: merges `mergeObj` with the Model instance properties. Returns `undefined`.
- `delete()`: deletes the record for this Model instance in the database. Returns `undefined`.

@@ -322,3 +439,5 @@ **Subclassing**:

return {
author: fk('Author')
id: attr(),
name: attr(),
author: fk('Author'),
};

@@ -329,3 +448,5 @@ }

Book.fields = {
author: fk('Author')
id: attr(),
name: attr(),
author: fk('Author'),
}

@@ -367,5 +488,4 @@ ```

- `exists()`: return `true` if number of entities is more than 0, else `false`.
- `filter(filterArg)`: returns a new `QuerySet` with the entities that pass the filter. For `filterArg`, you can either pass an object that `redux-orm` tries to match to the entities, or a function that returns `true` if you want to have it in the new `QuerySet`, `false` if not. The function receives a model instance as its sole argument.
- `exclude` returns a new `QuerySet` with the entities that do not pass the filter. Similarly to `filter`, you may pass an object for matching (all entities that match will not be in the new `QuerySet`) or a function. The function receives a model instance as its sole argument.
- `map(func)` map the entities in `QuerySet`, returning a JavaScript array.
- `filter(filterArg)`: returns a new `QuerySet` representing the records from the parent QuerySet that pass the filter. For `filterArg`, you can either pass an object that `redux-orm` tries to match to the entities, or a function that returns `true` if you want to have it in the new `QuerySet`, `false` if not. The function receives a model instance as its sole argument.
- `exclude` returns a new `QuerySet` represeting entities in the parent QuerySet that do not pass the filter. Similarly to `filter`, you may pass an object for matching (all entities that match will not be in the new `QuerySet`) or a function. The function receives a model instance as its sole argument.
- `all()` returns a new `QuerySet` with the same entities.

@@ -375,29 +495,5 @@ - `at(index)` returns an `Model` instance at the supplied `index` in the `QuerySet`.

- `last()` returns an `Model` instance at the `querySet.count() - 1` index.
- `delete()` marks all the `QuerySet` entities for deletion on `Model.getNextState`.
- `update(mergeObj)` marks all the `QuerySet` entities for an update based on the supplied object. The object will be merged with each entity.
- `delete()` deleted all entities represented by the `QuerySet`.
- `update(mergeObj)` updates all entities represented by the `QuerySet` based on the supplied object. The object will be merged with each entity.
**withRefs/withModels flagging**
When you want to iterate through all entities with `filter`, `exclude`, `forEach`, `map`, or get an item with `first`, `last` or `at`, you don't always need access to the full Model instance - a reference to the plain JavaScript object in the database could do. QuerySets maintain a flag indicating whether these methods operate on plain JavaScript objects (a direct reference from the store) or a Model instances that are instantiated during the operations.
```javascript
const queryset = Book.withRefs.filter(book => book.author === 'Tommi Kaikkonen')
// `book` is a plain javascript object, `queryset` is a QuerySet
//
const queryset2 = Book.filter(book => book.name === 'Tommi Kaikkonen - An Autobiography')
// `book` is a Model instance. `queryset2` is a QuerySet equivalent to `queryset`.
```
The flag persists after setting the flag. If you use `filter`, `exclude` or `orderBy`, the returned `QuerySet` will have the flag set to operate on Model instances either way. The default is to operate on Model instances. You can get a copy of the current `QuerySet` with the flag set to operate on references from the `withRefs` attribute. Likewise a `QuerySet` copy with the flag set to operate on model instances can be gotten by accessing the `withModels` attribute.
```javascript
queryset.filter(book => book.isReleasedAfterYear(2014))
// The `withRefs` flag was reverted back to using models after the `filter` operation,
// so `book` here is a model instance.
// You rarely need to use `withModels`, unless you're unsure which way the flag is.
queryset2.withRefs.filter(book => book.release_year > 2014)
// `book` is once again a plain JavaScript object, a direct reference from the store.
```
### Session

@@ -407,8 +503,7 @@

**Instantiation**: you don't need to do this yourself. Use `schema.from`.
**Instantiation**: you don't need to do this yourself. Use `orm.session`.
**Instance properties**:
- `getNextState(opts)`: applies all the recorded updates in the session and returns the next state. You may pass options with the `opts` object.
- `reduce()`: calls model-specific reducers and returns the next state.
- `state`: the current database state in the session.

@@ -418,3 +513,3 @@ Additionally, you can access all the registered Models in the schema for querying and updates as properties of this instance. For example, given a schema with `Book` and `Author` models,

```javascript
const session = schema.from(state, action);
const session = orm.session(state);
session.Book // Model class: Book

@@ -425,20 +520,2 @@ session.Author // Model class: Author

### Backend
Backend implements the logic and holds the information for Models' underlying data structure. If you want to change how that works, subclass `Backend` or implement your own with the same interface, and override your models' `getBackendClass` classmethod.
See the full documentation for `Backend` [here](http://tommikaikkonen.github.io/redux-orm/Backend.html)
**Instantiation**: will be done for you. If you want to specify custom options, you can override the `YourModelClass.backend` property with the custom options that will be merged with the defaults. For most cases, the default options work well. They are:
```javascript
{
idAttribute: 'id',
arrName: 'items',
mapName: 'itemsById',
};
```
## Changelog

@@ -448,12 +525,6 @@

### 0.8.4
### 0.9.0
Adds UMD build to partially fix [#41](https://github.com/tommikaikkonen/redux-orm/issues/41). You can now use or try out `redux-orm` through a script tag:
A lot. See [the migration guide](https://github.com/tommikaikkonen/redux-orm/wiki/0.9-Migration-Guide).
```html
<script src="https://tommikaikkonen.github.io/redux-orm/dist/redux-orm.js"></script>
```
`redux-orm.js` will point to the master version of the library; If you need to stick to a version, make a copy or build it yourself.
### 0.8.3

@@ -460,0 +531,0 @@

export const UPDATE = 'REDUX_ORM_UPDATE';
export const DELETE = 'REDUX_ORM_DELETE';
export const CREATE = 'REDUX_ORM_CREATE';
export const FILTER = 'REDUX_ORM_FILTER';
export const EXCLUDE = 'REDUX_ORM_EXCLUDE';
export const ORDER_BY = 'REDUX_ORM_ORDER_BY';
export const SUCCESS = 'SUCCESS';
export const FAILURE = 'FAILURE';
import difference from 'lodash/difference';
import UPDATE from './constants';
import {
m2mFromFieldName,
m2mToFieldName,
normalizeEntity,

@@ -26,3 +23,3 @@ includes,

const declaredToModel = currentSession[declaredToModelName];
const thisId = this.getId();
let toId;

@@ -35,11 +32,3 @@ if (value instanceof declaredToModel) {

this.getClass().addUpdate({
type: UPDATE,
payload: {
idArr: [thisId],
updater: {
[fieldName]: toId,
},
},
});
this.update({ [fieldName]: toId });
},

@@ -87,3 +76,8 @@ };

// Both sides of Many to Many, use the reverse flag.
function manyToManyDescriptor(declaredFromModelName, declaredToModelName, throughModelName, reverse) {
function manyToManyDescriptor(
declaredFromModelName,
declaredToModelName,
throughModelName,
throughFields,
reverse) {
return {

@@ -97,4 +91,4 @@ get() {

const fromFieldName = m2mFromFieldName(declaredFromModel.modelName);
const toFieldName = m2mToFieldName(declaredToModel.modelName);
const fromFieldName = throughFields.from;
const toFieldName = throughFields.to;

@@ -107,7 +101,10 @@ const lookupObj = {};

}
const throughQs = throughModel.filter(lookupObj);
const toIds = throughQs.withRefs.map(obj => obj[reverse ? fromFieldName : toFieldName]);
const toIds = throughQs.toRefArray().map(obj => obj[reverse ? fromFieldName : toFieldName]);
const qsFromModel = reverse ? declaredFromModel : declaredToModel;
const qs = qsFromModel.getQuerySetFromIds(toIds);
const qs = qsFromModel.filter(attrs =>
includes(toIds, attrs[qsFromModel.idAttribute])
);

@@ -119,7 +116,8 @@ qs.add = function add(...args) {

const existingQs = throughQs.withRefs
.filter(through => includes(idsToAdd, through[filterWithAttr]));
const existingQs = throughQs.filter(through => includes(idsToAdd, through[filterWithAttr]));
if (existingQs.exists()) {
const existingIds = existingQs.withRefs.map(through => through[filterWithAttr]);
const existingIds = existingQs
.toRefArray()
.map(through => through[filterWithAttr]);

@@ -133,3 +131,2 @@ const toAddModel = reverse

: declaredFromModel.modelName;
throw new Error(`Tried to add already existing ${toAddModel} id(s) ${existingIds} to the ${addFromModel} instance with id ${thisId}`);

@@ -154,8 +151,12 @@ }

const attrInIdsToRemove = reverse ? fromFieldName : toFieldName;
const entitiesToDelete = throughQs.withRefs
.filter(through => includes(idsToRemove, through[attrInIdsToRemove]));
const entitiesToDelete = throughQs.filter(
through => includes(idsToRemove, through[attrInIdsToRemove])
);
if (entitiesToDelete.count() !== idsToRemove.length) {
// Tried deleting non-existing entities.
const entitiesToDeleteIds = entitiesToDelete.withRefs.map(through => through[attrInIdsToRemove]);
const entitiesToDeleteIds = entitiesToDelete
.toRefArray()
.map(through => through[attrInIdsToRemove]);
const unexistingIds = difference(idsToRemove, entitiesToDeleteIds);

@@ -162,0 +163,0 @@

@@ -1,12 +0,305 @@

const Field = class Field {
constructor(toModelName, relatedName) {
this.toModelName = toModelName;
this.relatedName = relatedName;
import findKey from 'lodash/findKey';
import {
forwardManyToOneDescriptor,
backwardManyToOneDescriptor,
forwardOneToOneDescriptor,
backwardOneToOneDescriptor,
manyToManyDescriptor,
} from './descriptors';
import {
m2mName,
reverseFieldName,
reverseFieldErrorMessage,
} from './utils';
/**
* @module fields
*/
export const Attribute = class Attribute {
constructor(opts) {
this.opts = (opts || {});
if (this.opts.hasOwnProperty('getDefault')) {
this.getDefault = this.opts.getDefault;
}
}
install() {}
};
export const ForeignKey = class ForeignKey extends Field {};
export const ManyToMany = class ManyToMany extends Field {};
export const OneToOne = class OneToOne extends Field {};
const RelationalField = class RelationalField {
constructor(...args) {
if (args.length === 1 && typeof args[0] === 'object') {
const opts = args[0];
this.toModelName = opts.to;
this.relatedName = opts.relatedName;
this.through = opts.through;
} else {
this.toModelName = args[0];
this.relatedName = args[1];
}
}
getClass() {
return this.constructor;
}
};
export const ForeignKey = class ForeignKey extends RelationalField {
install(model, fieldName, orm) {
const toModelName = this.toModelName;
const toModel = toModelName === 'this' ? model : orm.get(toModelName);
// Forwards.
Object.defineProperty(
model.prototype,
fieldName,
forwardManyToOneDescriptor(fieldName, toModel.modelName)
);
model.definedProperties[fieldName] = true;
// Backwards.
const backwardsFieldName = this.relatedName
? this.relatedName
: reverseFieldName(model.modelName);
if (toModel.definedProperties[backwardsFieldName]) {
const errorMsg = reverseFieldErrorMessage(
model.modelName,
fieldName,
toModel.modelName,
backwardsFieldName
);
throw new Error(errorMsg);
}
Object.defineProperty(
toModel.prototype,
backwardsFieldName,
backwardManyToOneDescriptor(fieldName, model.modelName)
);
toModel.definedProperties[backwardsFieldName] = true;
const ThisField = this.getClass();
toModel.virtualFields[backwardsFieldName] = new ThisField(model.modelName, fieldName);
}
};
export const ManyToMany = class ManyToMany extends RelationalField {
install(model, fieldName, orm) {
const toModelName = this.toModelName;
const toModel = toModelName === 'this' ? model : orm.get(toModelName);
// Forwards.
const throughModelName =
this.through ||
m2mName(model.modelName, fieldName);
const throughModel = orm.get(throughModelName);
let throughFields;
if (!this.throughFields) {
const toFieldName = findKey(
throughModel.fields,
field =>
field instanceof ForeignKey &&
field.toModelName === toModel.modelName
);
const fromFieldName = findKey(
throughModel.fields,
field =>
field instanceof ForeignKey &&
field.toModelName === model.modelName
);
throughFields = {
to: toFieldName,
from: fromFieldName,
};
} else {
const [fieldAName, fieldBName] = throughFields;
const fieldA = throughModel.fields[fieldAName];
if (fieldA.toModelName === toModel.modelName) {
throughFields = {
to: fieldAName,
from: fieldBName,
};
} else {
throughFields = {
to: fieldBName,
from: fieldAName,
};
}
}
Object.defineProperty(
model.prototype,
fieldName,
manyToManyDescriptor(
model.modelName,
toModel.modelName,
throughModelName,
throughFields,
false
)
);
model.definedProperties[fieldName] = true;
model.virtualFields[fieldName] = new ManyToMany({
to: toModel.modelName,
relatedName: fieldName,
through: this.through,
});
// Backwards.
const backwardsFieldName = this.relatedName
? this.relatedName
: reverseFieldName(model.modelName);
if (toModel.definedProperties[backwardsFieldName]) {
// Backwards field was already defined on toModel.
const errorMsg = reverseFieldErrorMessage(
model.modelName,
fieldName,
toModel.modelName,
backwardsFieldName
);
throw new Error(errorMsg);
}
Object.defineProperty(
toModel.prototype,
backwardsFieldName,
manyToManyDescriptor(
model.modelName,
toModel.modelName,
throughModelName,
throughFields,
true
)
);
toModel.definedProperties[backwardsFieldName] = true;
toModel.virtualFields[backwardsFieldName] = new ManyToMany({
to: model.modelName,
relatedName: fieldName,
through: throughModelName,
});
}
getDefault() {
return [];
}
};
export const OneToOne = class OneToOne extends RelationalField {
install(model, fieldName, orm) {
const toModelName = this.toModelName;
const toModel = toModelName === 'this' ? model : orm.get(toModelName);
// Forwards.
Object.defineProperty(
model.prototype,
fieldName,
forwardOneToOneDescriptor(fieldName, toModel.modelName)
);
model.definedProperties[fieldName] = true;
// Backwards.
const backwardsFieldName = this.relatedName
? this.relatedName
: model.modelName.toLowerCase();
if (toModel.definedProperties[backwardsFieldName]) {
const errorMsg = reverseFieldErrorMessage(
model.modelName,
fieldName,
toModel.modelName,
backwardsFieldName
);
throw new Error(errorMsg);
}
Object.defineProperty(
toModel.prototype,
backwardsFieldName,
backwardOneToOneDescriptor(fieldName, model.modelName)
);
toModel.definedProperties[backwardsFieldName] = true;
toModel.virtualFields[backwardsFieldName] = new OneToOne(model.modelName, fieldName);
}
};
/**
* Defines a value attribute on the model.
* You need to define this for each non-foreign key you wish to use.
* You can use the optional `getDefault` parameter to fill in unpassed values
* to {@link Model#create}, such as for generating ID's with UUID:
*
* ```javascript
* import getUUID from 'your-uuid-package-of-choice';
*
* fields = {
* id: attr({ getDefault: () => getUUID() }),
* title: attr(),
* }
* ```
*
* @param {Object} [opts]
* @param {Function} [opts.getDefault] - if you give a function here, it's return
* value from calling with zero arguments will
* be used as the value when creating a new Model
* instance with {@link Model#create} if the field
* value is not passed.
* @return {Attribute}
*/
export function attr(opts) {
return new Attribute(opts);
}
/**
* Defines a foreign key on a model, which points
* to a single entity on another model.
*
* You can pass arguments as either a single object,
* or two arguments.
*
* If you pass two arguments, the first one is the name
* of the Model the foreign key is pointing to, and
* the second one is an optional related name, which will
* be used to access the Model the foreign key
* is being defined from, from the target Model.
*
* If the related name is not passed, it will be set as
* `${toModelName}Set`.
*
* If you pass an object to `fk`, it has to be in the form
*
* ```javascript
* fields = {
* author: fk({ to: 'Author', relatedName: 'books' })
* }
* ```
*
* Which is equal to
*
* ```javascript
* fields = {
* author: fk('Author', 'books'),
* }
* ```
*
* @param {string|boolean} toModelNameOrObj - the `modelName` property of
* the Model that is the target of the
* foreign key, or an object with properties
* `to` and optionally `relatedName`.
* @param {string} [relatedName] - if you didn't pass an object as the first argument,
* this is the property name that will be used to
* access a QuerySet the foreign key is defined from,
* from the target model.
* @return {ForeignKey}
*/
export function fk(...args) {

@@ -16,2 +309,72 @@ return new ForeignKey(...args);

/**
* Defines a many-to-many relationship between
* this (source) and another (target) model.
*
* The relationship is modeled with an extra model called the through model.
* The through model has foreign keys to both the source and target models.
*
* You can define your own through model if you want to associate more information
* to the relationship. A custom through model must have at least two foreign keys,
* one pointing to the source Model, and one pointing to the target Model.
*
* If you have more than one foreign key pointing to a source or target Model in the
* through Model, you must pass the option `throughFields`, which is an array of two
* strings, where the strings are the field names that identify the foreign keys to
* be used for the many-to-many relationship. Redux-ORM will figure out which field name
* points to which model by checking the through Model definition.
*
* Unlike `fk`, this function accepts only an object argument.
*
* ```javascript
* class Authorship extends Model {}
* Authorship.modelName = 'Authorship';
* Authorship.fields = {
* author: fk('Author', 'authorships'),
* book: fk('Book', 'authorships'),
* };
*
* class Author extends Model {}
* Author.modelName = 'Author';
* Author.fields = {
* books: many({
* to: 'Book',
* relatedName: 'authors',
* through: 'Authorship',
*
* // this is optional, since Redux-ORM can figure
* // out the through fields itself as there aren't
* // multiple foreign keys pointing to the same models.
* throughFields: ['author', 'book'],
* })
* };
*
* class Book extends Model {}
* Book.modelName = 'Book';
* ```
*
* You should only define the many-to-many relationship on one side. In the
* above case of Authors to Books through Authorships, the relationship is
* defined only on the Author model.
*
* @param {Object} options - options
* @param {string} options.to - the `modelName` attribute of the target Model.
* @param {string} [options.through] - the `modelName` attribute of the through Model which
* must declare at least one foreign key to both source and
* target Models. If not supplied, Redux-Orm will autogenerate
* one.
* @param {string[]} [options.throughFields] - this must be supplied only when a custom through
* Model has more than one foreign key pointing to
* either the source or target mode. In this case
* Redux-ORM can't figure out the correct fields for
* you, you must provide them. The supplied array should
* have two elements that are the field names for the
* through fields you want to declare the many-to-many
* relationship with. The order doesn't matter;
* Redux-ORM will figure out which field points to
* the source Model and which to the target Model.
* @param {string} [options.relatedName] - the attribute used to access a QuerySet
* of source Models from target Model.
* @return {ManyToMany}
*/
export function many(...args) {

@@ -21,4 +384,21 @@ return new ManyToMany(...args);

/**
* Defines a one-to-one relationship. In database terms, this is a foreign key with the
* added restriction that only one entity can point to single target entity.
*
* The arguments are the same as with `fk`. If `relatedName` is not supplied,
* the source model name in lowercase will be used. Note that with the one-to-one
* relationship, the `relatedName` should be in singular, not plural.
* @param {string|boolean} toModelNameOrObj - the `modelName` property of
* the Model that is the target of the
* foreign key, or an object with properties
* `to` and optionally `relatedName`.
* @param {string} [relatedName] - if you didn't pass an object as the first argument,
* this is the property name that will be used to
* access a Model the foreign key is defined from,
* from the target Model.
* @return {OneToOne}
*/
export function oneToOne(...args) {
return new OneToOne(...args);
}
import QuerySet from './QuerySet';
import Backend from './Backend';
import Model from './Model';
import Schema from './Schema';
import { DeprecatedSchema, ORM } from './ORM';
import Session from './Session';
import {
createReducer,
createSelector,
} from './redux';
import {
ForeignKey,

@@ -13,9 +16,21 @@ ManyToMany,

oneToOne,
attr,
} from './fields';
const Schema = DeprecatedSchema;
const Backend = function RemovedBackend() {
throw new Error(
'Having a custom Backend instance is now unsupported. ' +
'Documentation for database customization is upcoming, for now ' +
'please look at the db folder in the source.'
);
};
export {
QuerySet,
Backend,
Model,
ORM,
Schema,
Backend,
Session,

@@ -27,5 +42,8 @@ ForeignKey,

many,
attr,
oneToOne,
createReducer,
createSelector,
};
export default Model;

@@ -46,6 +46,6 @@ import values from 'lodash/values';

* selector args
* @param {Schema} modelSchema - a redux-orm Schema instance
* @param {ORM} orm - a redux-orm ORM instance
* @return {Function} `func` memoized.
*/
export function memoize(func, equalityCheck = eqCheck, modelSchema) {
export function memoize(func, equalityCheck = eqCheck, orm) {
let lastOrmState = null;

@@ -57,6 +57,6 @@ let lastResult = null;

return (...args) => {
const [ormState, ...otherArgs] = args;
const [dbState, ...otherArgs] = args;
const ormIsEqual = lastOrmState === ormState ||
!shouldRun(modelNameToInvalidatorMap, ormState);
const dbIsEqual = lastOrmState === dbState ||
!shouldRun(modelNameToInvalidatorMap, dbState);

@@ -67,7 +67,7 @@ const argsAreEqual = lastArgs && otherArgs.every(

if (ormIsEqual && argsAreEqual) {
if (dbIsEqual && argsAreEqual) {
return lastResult;
}
const session = modelSchema.from(ormState);
const session = orm.session(dbState);
const newArgs = [session, ...otherArgs];

@@ -88,3 +88,3 @@ const result = func(...newArgs);

lastResult = result;
lastOrmState = ormState;
lastOrmState = dbState;
lastArgs = otherArgs;

@@ -91,0 +91,0 @@

@@ -6,3 +6,2 @@ import forOwn from 'lodash/forOwn';

import Session from './Session';
import Backend from './Backend';
import QuerySet from './QuerySet';

@@ -13,6 +12,6 @@ import {

OneToOne,
attr,
} from './fields';
import { CREATE, UPDATE, DELETE } from './constants';
import { CREATE, UPDATE, DELETE, FILTER } from './constants';
import {
match,
normalizeEntity,

@@ -23,11 +22,36 @@ arrayDiffActions,

// Generates a query specification
// to get a single row from a table identified
// by a primary key.
function getByIdQuery(modelInstance) {
const modelClass = modelInstance.getClass();
return {
table: modelClass.modelName,
clauses: [
{
type: FILTER,
payload: {
[modelClass.idAttribute]: modelInstance.getId(),
},
},
],
};
}
/**
* The heart of an ORM, the data model.
* The static class methods manages the updates
* passed to this. The class itself is connected to a session,
* and because of this you can only have a single session at a time
* for a {@link Model} class.
*
* An instance of {@link Model} represents an object in the database.
* The fields you specify to the Model will be used to generate
* a schema to the database, related property accessors, and
* possibly through models.
*
* In each {@link Session} you instantiate from an {@link ORM} instance,
* you will receive a session-specific subclass of this Model. The methods
* you define here will be available to you in sessions.
*
* An instance of {@link Model} represents a record in the database, though
* it is possible to generate multiple instances from the same record in the database.
*
* To create data models in your schema, subclass {@link Model}. To define

@@ -39,3 +63,4 @@ * information about the data model, override static class methods. Define instance

/**
* Creates a Model instance.
* Creates a Model instance from it's properties.
* Don't use this to create a new record; Use the static method {@link Model#create}.
* @param {Object} props - the properties to instantiate with

@@ -50,8 +75,16 @@ */

this._fieldNames = [];
const fieldsDef = this.getClass().fields;
this._fields = Object.assign({}, props);
forOwn(props, (fieldValue, fieldName) => {
if (!fieldsDef.hasOwnProperty(fieldName)) {
throw new Error(
`Unexpected field given to ${ModelClass.modelName} constructor: ${fieldName}. ` +
`If ${ModelClass.modelName} should accept this field, ` +
'add an attr() field to it.'
);
}
this._fields[fieldName] = fieldValue;
this._fieldNames.push(fieldName);
// If the field has not already been defined on the

@@ -61,3 +94,3 @@ // prototype for a relation.

Object.defineProperty(this, fieldName, {
get: () => fieldValue,
get: () => this._fields[fieldName],
set: (value) => this.set(fieldName, value),

@@ -70,10 +103,2 @@ configurable: true,

/**
* Returns the raw state for this {@link Model} in the current {@link Session}.
* @return {Object} The state for this {@link Model} in the current {@link Session}.
*/
static get state() {
return this.session.getState(this.modelName);
}
static toString() {

@@ -84,32 +109,24 @@ return `ModelClass: ${this.modelName}`;

/**
* Returns the options object passed to the {@link Backend} class constructor.
* By default, returns an empty object (which means the {@link Backend} instance
* will use default options). You can either override this function to return the options
* you want to use, or assign the options object as a static property to the
* Returns the options object passed to the database for the table that represents
* this Model class.
*
* Returns an empty object by default, which means the database
* will use default options. You can either override this function to return the options
* you want to use, or assign the options object as a static property of the same name to the
* Model class.
*
* @return {Object} the options object used to instantiate a {@link Backend} class.
* @return {Object} the options object passed to the database for the table
* representing this Model class.
*/
static backend() {
static options() {
return {};
}
static _getBackendOpts() {
if (typeof this.backend === 'function') {
return this.backend();
static _getTableOpts() {
if (typeof this.options === 'function') {
return this.options();
}
return this.backend;
return this.options;
}
/**
* Returns the {@link Backend} class used to instantiate
* the {@link Backend} instance for this {@link Model}.
*
* Override this if you want to use a custom {@link Backend} class.
* @return {Backend} The {@link Backend} class or subclass to use for this {@link Model}.
*/
static getBackendClass() {
return Backend;
}
static get _sessionData() {

@@ -120,106 +137,2 @@ if (!this.session) return {};

/**
* Gets the {@link Backend} instance linked to this {@link Model}.
*
* @private
* @return {Backend} The {@link Backend} instance linked to this {@link Model}.
*/
static getBackend() {
if (!this._sessionData.backend) {
const BackendClass = this.getBackendClass();
const opts = { ...this._getBackendOpts() };
if (this.session && this.session.withMutations) {
opts.withMutations = true;
}
const backend = new BackendClass(opts);
if (!this.session) {
return backend;
}
this._sessionData.backend = backend;
}
return this._sessionData.backend;
}
/**
* Gets the Model's next state by applying the recorded
* updates.
*
* @private
* @param {Transction} tx - the current Transaction instance
* @return {Object} The next state.
*/
static getNextState(_tx) {
const tx = _tx || this.session.currentTx;
let state;
if (this._sessionData.hasOwnProperty('nextState')) {
state = this._sessionData.nextState;
} else {
state = this.state;
}
const updates = tx.getUpdatesFor(this);
if (updates.length > 0) {
const nextState = updates.reduce(this.updateReducer.bind(this, tx), state);
this._sessionData.nextState = nextState;
tx.markApplied(this);
return nextState;
}
return state;
}
/**
* A reducer that takes the Model's state and an internal redux-orm
* action object and applies the update specified by the `action` object
* by delegating to this model's Backend instance.
*
* @private
* @param {Object} state - the Model's state
* @param {Object} action - the internal redux-orm update action to apply
* @return {Object} the state after applying the action
*/
static updateReducer(tx, state, action) {
const backend = this.getBackend();
switch (action.type) {
case CREATE:
return backend.insert(tx, state, action.payload);
case UPDATE:
return backend.update(tx, state, action.payload.idArr, action.payload.mergeObj);
case DELETE:
return backend.delete(tx, state, action.payload);
default:
return state;
}
}
/**
* The default reducer implementation.
* If the user doesn't define a reducer, this is used.
*
* @param {Object} state - the current state
* @param {Object} action - the dispatched action
* @param {Model} model - the concrete model class being used
* @param {Session} session - the current {@link Session} instance
* @return {Object} the next state for the Model
*/
static reducer(state, action, model, session) { // eslint-disable-line
return this.getNextState();
}
/**
* Gets the default, empty state of the branch.
* Delegates to a {@link Backend} instance.
*
* @private
* @return {Object} The default state.
*/
static getDefaultState() {
return this.getBackend().getDefaultState();
}
static markAccessed() {

@@ -231,3 +144,2 @@ this.session.markAccessed(this);

* Returns the id attribute of this {@link Model}.
* Delegates to the related {@link Backend} instance.
*

@@ -237,37 +149,6 @@ * @return {string} The id attribute of this {@link Model}.

static get idAttribute() {
return this.getBackend().idAttribute;
return this.session.db.describe(this.modelName).idAttribute;
}
/**
* A convenience method to call {@link Backend#accessId} from
* the {@link Model} class.
*
* @param {Number} id - the object id to access
* @return {Object} a reference to the object in the database.
*/
static accessId(id) {
this.markAccessed();
return this.getBackend().accessId(this.state, id);
}
/**
* A convenience method to call {@link Backend#accessIdList} from
* the {@link Model} class with the current state.
*/
static accessIds() {
this.markAccessed();
return this.getBackend().accessIdList(this.state);
}
static accessList() {
this.markAccessed();
return this.getBackend().accessList(this.state);
}
static iterator() {
this.markAccessed();
return this.getBackend().iterator(this.state);
}
/**
* Connect the model class to a {@link Session}.

@@ -295,38 +176,5 @@ *

/**
* A convenience method that delegates to the current {@link Session} instane.
* Adds the required backenddata about this {@link Model} to the update object.
*
* @private
* @param {Object} update - the update to add.
*/
static addUpdate(update) {
update.meta = { name: this.modelName };
this.session.addUpdate(update);
}
/**
* Returns the id to be assigned to a new entity.
* You may override this to suit your needs.
* @return {*} the id value for a new entity.
*/
static nextId() {
if (typeof this._sessionData.nextId === 'undefined') {
const idArr = this.accessIds();
if (idArr.length === 0) {
this._sessionData.nextId = 0;
} else {
this._sessionData.nextId = Math.max(...idArr) + 1;
}
}
return this._sessionData.nextId;
}
static getQuerySet() {
return this.getQuerySetFromIds(this.accessIds());
}
static getQuerySetFromIds(ids) {
const QuerySetClass = this.querySetClass;
return new QuerySetClass(this, ids);
return new QuerySetClass(this);
}

@@ -341,6 +189,3 @@

static get query() {
if (!this._sessionData.queryset) {
this._sessionData.queryset = this.getQuerySet();
}
return this._sessionData.queryset;
return this.getQuerySet();
}

@@ -357,4 +202,7 @@

/**
* Records the addition of a new {@link Model} instance and returns it.
* Creates a new record in the database, instantiates a {@link Model} and returns it.
*
* If you pass values for many-to-many fields, instances are created on the through
* model as well.
*
* @param {props} props - the new {@link Model}'s properties.

@@ -364,37 +212,41 @@ * @return {Model} a new {@link Model} instance.

static create(userProps) {
const idAttribute = this.idAttribute;
const props = Object.assign({}, userProps);
if (!props.hasOwnProperty(idAttribute)) {
const nextId = this.nextId();
props[idAttribute] = nextId;
this._sessionData.nextId++;
} else {
const id = props[idAttribute];
if (id > this.nextId()) {
this._sessionData.nextId = id + 1;
}
}
const m2mVals = {};
forOwn(userProps, (value, key) => {
props[key] = normalizeEntity(value);
const allowedFieldNames = Object.keys(this.fields);
// If a value is supplied for a ManyToMany field,
// discard them from props and save for later processing.
if (isArray(value)) {
if (this.fields.hasOwnProperty(key) && this.fields[key] instanceof ManyToMany) {
m2mVals[key] = value;
delete props[key];
// We don't check for extra field values passed here;
// the constructor will throw in that case. So we
// only go through the defined fields.
allowedFieldNames.forEach(key => {
const field = this.fields[key];
const valuePassed = userProps.hasOwnProperty(key);
if (!valuePassed && !(field instanceof ManyToMany)) {
if (field.getDefault) {
props[key] = field.getDefault();
}
} else {
const value = userProps[key];
props[key] = normalizeEntity(value);
// If a value is supplied for a ManyToMany field,
// discard them from props and save for later processing.
if (isArray(value)) {
if (this.fields.hasOwnProperty(key) && this.fields[key] instanceof ManyToMany) {
m2mVals[key] = value;
delete props[key];
}
}
}
});
this.addUpdate({
type: CREATE,
const newEntry = this.session.applyUpdate({
action: CREATE,
table: this.modelName,
payload: props,
});
const ModelClass = this;
const instance = new ModelClass(props);
const instance = new ModelClass(newEntry);

@@ -417,2 +269,4 @@ forOwn(m2mVals, (value, key) => {

* Returns a {@link Model} instance for the object with id `id`.
* This throws if the `id` doesn't exist. Use {@link Model#hasId}
* to check for existence first if you're not certain.
*

@@ -425,10 +279,8 @@ * @param {*} id - the `id` of the object to get

const ModelClass = this;
if (!this.hasId(id)) {
throw new Error(`${this.modelName} instance with id ${id} not found`);
const rows = this._findDatabaseRows({ [ModelClass.idAttribute]: id });
if (rows.length === 0) {
throw new Error(`${ModelClass.modelName} instance with id ${id} not found`);
}
const ref = this.accessId(id);
return new ModelClass(ref);
return new ModelClass(rows[0]);
}

@@ -444,7 +296,19 @@

static hasId(id) {
const ref = this.accessId(id);
const rows = this._findDatabaseRows({ [this.idAttribute]: id });
return rows.length === 1;
}
if (typeof ref === 'undefined') return false;
return true;
static _findDatabaseRows(lookupObj) {
const ModelClass = this;
return ModelClass
.session
.query({
table: ModelClass.modelName,
clauses: [
{
type: FILTER,
payload: lookupObj,
},
],
}).rows;
}

@@ -454,3 +318,4 @@

* Gets the {@link Model} instance that matches properties in `lookupObj`.
* Throws an error if {@link Model} is not found.
* Throws an error if {@link Model} is not found, or multiple records match
* the properties.
*

@@ -461,30 +326,13 @@ * @param {Object} lookupObj - the properties used to match a single entity.

static get(lookupObj) {
if (!this.accessIds().length) {
throw new Error(`No entities found for model ${this.modelName}`);
}
const ModelClass = this;
// We treat `idAttribute` as unique, so if it's
// in `lookupObj` we search with that attribute only.
if (lookupObj.hasOwnProperty(this.idAttribute)) {
const props = this.accessId(lookupObj[this.idAttribute]);
if (typeof props !== 'undefined') {
return new ModelClass(props);
}
const rows = this._findDatabaseRows(lookupObj);
if (rows.length === 0) {
throw new Error('Model instance not found when calling get method');
} else if (rows.length > 1) {
throw new Error(`Expected to find a single row in Model.get. Found ${rows.length}.`);
}
const iterator = this.iterator();
let done = false;
while (!done) {
const curr = iterator.next();
if (match(lookupObj, curr.value)) {
return new ModelClass(curr.value);
}
done = curr.done;
}
throw new Error('Model instance not found when calling get method');
return new ModelClass(rows[0]);
}

@@ -518,3 +366,6 @@

get ref() {
return this.getClass().accessId(this.getId());
const ModelClass = this.getClass();
return ModelClass._findDatabaseRows({
[ModelClass.idAttribute]: this.getId(),
})[0];
}

@@ -528,4 +379,13 @@

toString() {
const className = this.getClass().modelName;
const fields = this._fieldNames.map(fieldName => {
const ThisModel = this.getClass();
const className = ThisModel.modelName;
const fieldNames = Object.keys(ThisModel.fields);
const fields = fieldNames.map(fieldName => {
const field = ThisModel.fields[fieldName];
if (field instanceof ManyToMany) {
const ids = this[fieldName].toModelArray().map(
model => model.getId()
);
return `${fieldName}: [${ids.join(', ')}]`;
}
const val = this._fields[fieldName];

@@ -549,4 +409,4 @@ return `${fieldName}: ${val}`;

/**
* Records a update to the {@link Model} instance for a single
* field value assignment.
* Updates a property name to given value for this {@link Model} instance.
* The values are immediately committed to the database.
*

@@ -562,4 +422,4 @@ * @param {string} propertyName - name of the property to set

/**
* Records an update to the {@link Model} instance for multiple field value assignments.
* If the session is with mutations, updates the instance to reflect the new values.
* Assigns multiple fields and corresponding values to this {@link Model} instance.
* The updates are immediately committed to the database.
*

@@ -570,3 +430,4 @@ * @param {Object} userMergeObj - an object that will be merged with this instance.

update(userMergeObj) {
const relFields = this.getClass().fields;
const ThisModel = this.getClass();
const relFields = ThisModel.fields;
const mergeObj = Object.assign({}, userMergeObj);

@@ -581,5 +442,5 @@

if (field instanceof ManyToMany) {
const currentIds = this[mergeKey].idArr;
const currentIds = this[mergeKey].toRefArray()
.map(row => row[ThisModel.idAttribute]);
// TODO: It could be better to check this stuff in Backend.
const normalizedNewIds = mergeObj[mergeKey].map(normalizeEntity);

@@ -604,26 +465,33 @@ const diffActions = arrayDiffActions(currentIds, normalizedNewIds);

const session = this.getClass().session;
if (session && session.withMutations) {
this._initFields(Object.assign({}, this._fields, mergeObj));
}
this._initFields(Object.assign({}, this._fields, mergeObj));
this.getClass().addUpdate({
type: UPDATE,
payload: {
idArr: [this.getId()],
mergeObj,
},
ThisModel.session.applyUpdate({
action: UPDATE,
query: getByIdQuery(this),
payload: mergeObj,
});
}
/**
* Records the {@link Model} to be deleted.
* Updates {@link Model} instance attributes to reflect the
* database state in the current session.
* @return {undefined}
*/
refreshFromState() {
this._initFields(this.ref);
}
/**
* Deletes the record for this {@link Model} instance.
* You'll still be able to access fields and values on the instance.
*
* @return {undefined}
*/
delete() {
this.getClass().addUpdate({
type: DELETE,
payload: [this.getId()],
this._onDelete();
this.getClass().session.applyUpdate({
action: DELETE,
query: getByIdQuery(this),
});
this._onDelete();
}

@@ -654,3 +522,5 @@

Model.fields = {};
Model.fields = {
id: attr(),
};
Model.definedProperties = {};

@@ -657,0 +527,0 @@ Model.virtualFields = {};

@@ -1,6 +0,6 @@

import reject from 'lodash/reject';
import filter from 'lodash/filter';
import mapValues from 'lodash/mapValues';
import orderBy from 'lodash/orderBy';
import { normalizeEntity } from './utils';
import {
normalizeEntity,
warnDeprecated,
} from './utils';

@@ -10,36 +10,44 @@ import {

DELETE,
FILTER,
EXCLUDE,
ORDER_BY,
} from './constants.js';
/**
* A chainable class that keeps track of a list of objects and
* This class is used to build and make queries to the database
* and operating the resulting set (such as updating attributes
* or deleting the records).
*
* - returns a subset clone of itself with [filter]{@link QuerySet#filter} and [exclude]{@link QuerySet#exclude}
* - records updates to objects with [update]{@link QuerySet#update} and [delete]{@link QuerySet#delete}
* The queries are built lazily. For example:
*
* ```javascript
* const qs = Book.all()
* .filter(book => book.releaseYear > 1999)
* .orderBy('name');
* ```
*
* Doesn't execute a query. The query is executed only when
* you need information from the query result, such as {@link QuerySet#count},
* {@link QuerySet#toRefArray}. After the query is executed, the resulting
* set is cached in the QuerySet instance.
*
* QuerySet instances also return copies, so chaining filters doesn't
* mutate the previous instances.
*/
const QuerySet = class QuerySet {
/**
* Creates a QuerySet.
* Creates a QuerySet. The constructor is mainly for internal use;
* You should access QuerySet instances from {@link Model}.
*
* @param {Model} modelClass - the model class of objects in this QuerySet.
* @param {number[]} idArr - an array of the id's this QuerySet includes.
* @param {any[]} clauses - query clauses needed to evaluate the set.
* @param {Object} [opts] - additional options
*/
constructor(modelClass, idArr, opts) {
constructor(modelClass, clauses, opts) {
Object.assign(this, {
modelClass,
idArr,
clauses: clauses || [],
});
this._opts = opts;
// A flag that tells if the user wants
// the result in plain javascript objects
// or {@link Model} instances.
// Results are plain objects by default.
if (opts && opts.hasOwnProperty('withRefs')) {
this._withRefs = opts.withRefs;
} else {
this._withRefs = false;
}
}

@@ -51,43 +59,10 @@

_new(ids, userOpts) {
_new(clauses, userOpts) {
const opts = Object.assign({}, this._opts, userOpts);
return new this.constructor(this.modelClass, ids, opts);
return new this.constructor(this.modelClass, clauses, opts);
}
/**
* Returns a new QuerySet representing the same entities
* with the `withRefs` flag on.
*
* @return {QuerySet}
*/
get withRefs() {
if (!this._withRefs) {
return this._new(this.idArr, { withRefs: true });
}
return this;
}
/**
* Alias for withRefs
* @return {QuerySet}
*/
get ref() {
return this.withRefs;
}
/**
* Returns a new QuerySet representing the same entities
* with the `withRefs` flag off.
*
* @return {QuerySet}
*/
get withModels() {
if (this._withRefs) {
return this._new(this.idArr, { withRefs: false });
}
return this;
}
toString() {
const contents = this.idArr.map(id =>
this._evaluate();
const contents = this.rows.map(id =>
this.modelClass.withId(id).toString()

@@ -106,15 +81,18 @@ ).join('\n - ');

toRefArray() {
return this.idArr.map(id => this.modelClass.accessId(id));
this._evaluate();
return this.rows;
}
/**
* Returns an array of Model instances represented by the QuerySet.
* Returns an array of {@link Model} instances represented by the QuerySet.
* @return {Model[]} model instances represented by the QuerySet
*/
toModelArray() {
return this.idArr.map((_, idx) => this.at(idx));
this._evaluate();
const ModelClass = this.modelClass;
return this.rows.map(props => new ModelClass(props));
}
/**
* Returns the number of model instances represented by the QuerySet.
* Returns the number of {@link Model} instances represented by the QuerySet.
*

@@ -124,7 +102,9 @@ * @return {number} length of the QuerySet

count() {
return this.idArr.length;
this._evaluate();
return this.rows.length;
}
/**
* Checks if the {@link QuerySet} instance has any entities.
* Checks if the {@link QuerySet} instance has any records matching the query
* in the database.
*

@@ -147,6 +127,5 @@ * @return {Boolean} `true` if the {@link QuerySet} instance contains entities, else `false`.

at(index) {
if (this._withRefs) {
return this.modelClass.accessId(this.idArr[index]);
}
return this.modelClass.withId(this.idArr[index]);
this._evaluate();
const ModelClass = this.modelClass;
return new ModelClass(this.rows[index]);
}

@@ -167,3 +146,4 @@

last() {
return this.at(this.idArr.length - 1);
this._evaluate();
return this.at(this.rows.length - 1);
}

@@ -176,3 +156,3 @@

all() {
return this._new(this.idArr);
return this._new(this.clauses);
}

@@ -187,7 +167,12 @@

filter(lookupObj) {
return this._filterOrExclude(lookupObj, false);
const normalizedLookupObj = typeof lookupObj === 'object'
? mapValues(lookupObj, normalizeEntity)
: lookupObj;
const filterDescriptor = { type: FILTER, payload: normalizedLookupObj };
return this._new(this.clauses.concat(filterDescriptor));
}
/**
* Returns a new {@link QuerySet} instance with entities that do not match properties in `lookupObj`.
* Returns a new {@link QuerySet} instance with entities that do not match
* properties in `lookupObj`.
*

@@ -198,69 +183,23 @@ * @param {Object} lookupObj - the properties to unmatch objects with.

exclude(lookupObj) {
return this._filterOrExclude(lookupObj, true);
const normalizedLookupObj = typeof lookupObj === 'object'
? mapValues(lookupObj, normalizeEntity)
: lookupObj;
const excludeDescriptor = { type: EXCLUDE, payload: normalizedLookupObj };
return this._new(this.clauses.concat(excludeDescriptor));
}
_filterOrExclude(_lookupObj, exclude) {
const func = exclude ? reject : filter;
let lookupObj = _lookupObj;
let operationWithRefs = true;
let entities;
if (typeof lookupObj === 'function') {
// For filtering with function,
// use whatever object type
// is flagged.
if (this._withRefs) {
entities = this.toRefArray();
} else {
entities = this.toModelArray();
operationWithRefs = false;
}
} else {
if (typeof lookupObj === 'object') {
lookupObj = mapValues(lookupObj, normalizeEntity);
}
// Lodash filtering doesn't work with
// Model instances.
entities = this.toRefArray();
_evaluate() {
if (!this._evaluated) {
const session = this.modelClass.session;
const querySpec = {
table: this.modelClass.modelName,
clauses: this.clauses,
};
const { rows } = session.query(querySpec);
this.rows = rows;
this._evaluated = true;
}
const filteredEntities = func(entities, lookupObj);
const getIdFunc = operationWithRefs
? (obj) => obj[this.modelClass.idAttribute]
: (obj) => obj.getId();
const newIdArr = filteredEntities.map(getIdFunc);
return this._new(newIdArr, { withRefs: false });
}
/**
* Calls `func` for each object in the {@link QuerySet} instance.
* The object is either a reference to the plain
* object in the database or a {@link Model} instance, depending
* on the flag.
*
* @param {Function} func - the function to call with each object
* @return {undefined}
*/
forEach(func) {
const arr = this._withRefs
? this.toRefArray()
: this.toModelArray();
arr.forEach(func);
}
/**
* Maps the {@link Model} instances in the {@link QuerySet} instance.
* @param {Function} func - the mapping function that takes one argument, a
* {@link Model} instance or a reference to the plain
* JavaScript object in the store, depending on the
* QuerySet's `withRefs` flag.
* @return {Array} the mapped array
*/
map(func) {
return this.idArr.map((_, idx) => func(this.at(idx)));
}
/**
* Returns a new {@link QuerySet} instance with entities ordered by `iteratees` in ascending

@@ -281,26 +220,4 @@ * order, unless otherwise specified. Delegates to `lodash.orderBy`.

orderBy(iteratees, orders) {
const entities = this.toRefArray();
let iterateeArgs = iteratees;
// Lodash only works on plain javascript objects.
// If the argument is a function, and the `withRefs`
// flag is false, the argument function is wrapped
// to get the model instance and pass that as the argument
// to the user-supplied function.
if (!this._withRefs) {
iterateeArgs = iteratees.map(arg => {
if (typeof arg === 'function') {
return entity => {
const id = entity[this.modelClass.idAttribute];
const instance = this.modelClass.withId(id);
return arg(instance);
};
}
return arg;
});
}
const sortedEntities = orderBy.call(null, entities, iterateeArgs, orders);
return this._new(
sortedEntities.map(entity => entity[this.modelClass.idAttribute]),
{ withRefs: false });
const orderByDescriptor = { type: ORDER_BY, payload: [iteratees, orders] };
return this._new(this.clauses.concat(orderByDescriptor));
}

@@ -317,9 +234,11 @@

update(mergeObj) {
this.modelClass.addUpdate({
type: UPDATE,
payload: {
idArr: this.idArr,
mergeObj,
this.modelClass.session.applyUpdate({
action: UPDATE,
query: {
table: this.modelClass.modelName,
clauses: this.clauses,
},
payload: mergeObj,
});
this._evaluated = false;
}

@@ -332,9 +251,45 @@

delete() {
this.modelClass.addUpdate({
type: DELETE,
payload: this.idArr,
this.toModelArray().forEach(model => model._onDelete());
this.modelClass.session.applyUpdate({
action: DELETE,
query: {
table: this.modelClass.modelName,
clauses: this.clauses,
},
});
this.withModels.forEach(model => model._onDelete());
this._evaluated = false;
}
// DEPRECATED AND REMOVED METHODS
get withModels() {
throw new Error(
'QuerySet.prototype.withModels is removed. ' +
'Use .toModelArray() or predicate functions that ' +
'instantiate Models from refs, e.g. new Model(ref).'
);
}
get withRefs() {
warnDeprecated(
'QuerySet.prototype.withRefs is deprecated. ' +
'Query building operates on refs only now.'
);
}
map() {
throw new Error(
'QuerySet.prototype.map is removed. ' +
'Call .toModelArray() or .toRefArray() first to map.'
);
}
forEach() {
throw new Error(
'QuerySet.prototype.forEach is removed. ' +
'Call .toModelArray() or .toRefArray() first to iterate.'
);
}
};

@@ -348,6 +303,4 @@

'first',
'forEach',
'exists',
'filter',
'map',
'exclude',

@@ -357,7 +310,4 @@ 'orderBy',

'delete',
'ref',
'withRefs',
'withModels',
];
export default QuerySet;

@@ -1,8 +0,7 @@

import Transaction from './Transaction';
import { ops } from './utils';
import { getBatchToken } from 'immutable-ops';
/**
* Session handles a single
* action dispatch.
*/
import { SUCCESS, FAILURE } from './constants';
import { warnDeprecated } from './utils';
const Session = class Session {

@@ -12,16 +11,17 @@ /**

*
* @param {Schema} schema - a {@link Schema} instance
* @param {Database} db - a {@link Database} instance
* @param {Object} state - the database state
* @param {Object} [action] - the current action in the dispatch cycle.
* Will be passed to the user defined reducers.
* @param {Boolean} withMutations - whether the session should mutate data
* @param {Boolean} [withMutations] - whether the session should mutate data
* @param {Object} [batchToken] - used by the backend to identify objects that can be
* mutated.
*/
constructor(schema, state, action, withMutations) {
constructor(schema, db, state, withMutations, batchToken) {
this.schema = schema;
this.state = state || schema.getDefaultState();
this.action = action;
this.db = db;
this.state = state || db.getEmptyState();
this.initialState = this.state;
this.withMutations = !!withMutations;
this.batchToken = batchToken || getBatchToken();
this.currentTx = new Transaction();
this._accessedModels = {};

@@ -43,4 +43,4 @@ this.modelData = {};

markAccessed(model) {
this.getDataForModel(model.modelName).accessed = true;
markAccessed(modelName) {
this.getDataForModel(modelName).accessed = true;
}

@@ -58,3 +58,2 @@

}
return this.modelData[modelName];

@@ -64,110 +63,45 @@ }

/**
* Records an update to the session.
* Applies update to a model state.
*
* @private
* @param {Object} update - the update object. Must have keys
* `type`, `payload` and `meta`. `meta`
* must also include a `name` attribute
* that contains the model name.
* `type`, `payload`.
*/
addUpdate(update) {
if (this.withMutations) {
const modelName = update.meta.name;
const modelState = this.getState(modelName);
applyUpdate(updateSpec) {
const { batchToken, withMutations } = this;
const tx = { batchToken, withMutations };
const result = this.db.update(updateSpec, tx, this.state);
const { status, state } = result;
// The backend used in the updateReducer
// will mutate the model state.
this[modelName].updateReducer(null, modelState, update);
if (status === SUCCESS) {
this.state = state;
} else {
this.currentTx.addUpdate(update);
throw new Error(`Applying update failed: ${result.toString()}`);
}
}
getUpdatesFor(modelName) {
return this.currentTx.getUpdatesFor(modelName);
return result.payload;
}
get updates() {
return this.currentTx.updates.map(update => update.update);
query(querySpec) {
const { table } = querySpec;
this.markAccessed(table);
return this.db.query(querySpec, this.state);
}
/**
* Returns the current state for a model with name `modelName`.
*
* @private
* @param {string} modelName - the name of the model to get state for.
* @return {*} The state for model with name `modelName`.
*/
getState(modelName) {
return this.state[modelName];
}
// DEPRECATED AND REMOVED METHODS
/**
* Applies recorded updates and returns the next state.
* @param {Object} [opts] - Options object
* @param {Boolean} [opts.runReducers] - A boolean indicating if the user-defined
* model reducers should be run. If not specified,
* is set to `true` if an action object was specified
* on session instantiation, otherwise `false`.
* @return {Object} The next state
*/
getNextState(userOpts) {
if (this.withMutations) return this.state;
const prevState = this.state;
const action = this.action;
const opts = userOpts || {};
// If the session does not have a specified action object,
// don't run the user-defined model reducers unless
// explicitly specified.
const runReducers = opts.hasOwnProperty('runReducers')
? opts.runReducers
: !!action;
const tx = this.currentTx;
ops.open();
let nextState = prevState;
if (runReducers) {
nextState = this.sessionBoundModels.reduce((_nextState, modelClass) => {
const modelState = this.getState(modelClass.modelName);
let returnValue = modelClass.reducer(modelState, action, modelClass, this);
if (typeof returnValue === 'undefined') {
returnValue = modelClass.getNextState(tx);
}
return ops.set(modelClass.modelName, returnValue, _nextState);
}, nextState);
}
// There might be some m2m updates left.
const unappliedUpdates = this.currentTx.getUnappliedUpdatesByModel();
if (unappliedUpdates) {
nextState = this.sessionBoundModels.reduce((_nextState, modelClass) => {
const modelName = modelClass.modelName;
if (!unappliedUpdates.hasOwnProperty(modelName)) {
return _nextState;
}
return ops.set(modelName, modelClass.getNextState(tx), _nextState);
}, nextState);
}
ops.close();
tx.close();
this.currentTx = new Transaction();
return nextState;
getNextState() {
warnDeprecated(
'Session.prototype.getNextState function is deprecated. Access ' +
'the Session.prototype.state property instead.'
);
return this.state;
}
/**
* Calls the user-defined reducers and returns the next state.
* If the session uses mutations, just returns the state.
* Delegates to {@link Session#getNextState}
*
* @return {Object} the next state
*/
reduce() {
return this.getNextState({ runReducers: true });
throw new Error(
'Session.prototype.reduce is removed. The Redux integration API ' +
'is now decoupled from ORM and Session - see the 0.9 migration guide ' +
'in the GitHub repo.'
);
}

@@ -174,0 +108,0 @@ };

import { expect } from 'chai';
import Model from '../Model';
import QuerySet from '../QuerySet';
import Schema from '../Schema';
import {
Model,
QuerySet,
ORM,
attr,
} from '../';
import {
createTestSessionWithData,

@@ -12,13 +15,5 @@ } from './utils';

let session;
let schema;
let orm;
let state;
beforeEach(() => {
({
session,
schema,
state,
} = createTestSessionWithData());
});
describe('Immutable session', () => {

@@ -28,2 +23,9 @@ beforeEach(() => {

// mutate the state.
({
session,
orm,
state,
} = createTestSessionWithData());
deepFreeze(state);

@@ -34,3 +36,3 @@ });

expect(state).to.have.all.keys(
'Book', 'Cover', 'Genre', 'Author', 'BookGenres');
'Book', 'Cover', 'Genre', 'Author', 'BookGenres', 'Publisher');

@@ -51,2 +53,5 @@ expect(state.Book.items).to.have.length(3);

expect(Object.keys(state.Author.itemsById)).to.have.length(3);
expect(state.Publisher.items).to.have.length(2);
expect(Object.keys(state.Publisher.itemsById)).to.have.length(2);
});

@@ -63,3 +68,2 @@

const { Book } = session;
expect(session.updates).to.have.length(0);
const book = Book.create({

@@ -69,8 +73,12 @@ name: 'New Book',

releaseYear: 2015,
publisher: 0,
});
expect(session.updates).to.have.length(1);
expect(session.Book.count()).to.equal(4);
expect(session.Book.last().ref).to.equal(book.ref);
});
const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.count()).to.equal(4);
it('Model.getId works', () => {
const { Book } = session;
expect(Book.withId(0).getId()).to.equal(0);
expect(Book.withId(1).getId()).to.equal(1);
});

@@ -86,2 +94,3 @@

genres: [0, 0],
publisher: 0,
};

@@ -96,6 +105,4 @@

Book.withId(0).delete();
const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.count()).to.equal(2);
expect(session.Book.count()).to.equal(2);
expect(session.Book.hasId(0)).to.be.false;
});

@@ -107,9 +114,14 @@

const newName = 'New Name';
expect(session.updates).to.have.length(0);
book.name = newName;
expect(session.updates).to.have.length(1);
expect(session.Book.first().name).to.equal(newName);
});
const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.first().name).to.equal(newName);
it('Model.toString works', () => {
const { Book } = session;
const book = Book.first();
expect(book.toString())
.to.equal(
'Book: {id: 0, name: Tommi Kaikkonen - an Autobiography, ' +
'releaseYear: 2050, author: 0, cover: 0, genres: [0, 1], publisher: 1}'
);
});

@@ -142,2 +154,23 @@

it('many-to-many relationship descriptors work with a custom through model', () => {
const {
Author,
Publisher,
} = session;
// Forward (from many-to-many field declaration)
const author = Author.get({ name: 'Tommi Kaikkonen' });
const relatedPublishers = author.publishers;
expect(relatedPublishers).to.be.an.instanceOf(QuerySet);
expect(relatedPublishers.modelClass).to.equal(Publisher);
expect(relatedPublishers.count()).to.equal(1);
// Backward
const publisher = Publisher.get({ name: 'Technical Publishing' });
const relatedAuthors = publisher.authors;
expect(relatedAuthors).to.be.an.instanceOf(QuerySet);
expect(relatedAuthors.modelClass).to.equal(Author);
expect(relatedAuthors.count()).to.equal(2);
});
it('adding related many-to-many entities works', () => {

@@ -149,6 +182,3 @@ const { Book, Genre } = session;

const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.withId(0).genres.count()).to.equal(3);
expect(session.Book.withId(0).genres.count()).to.equal(3);
});

@@ -164,2 +194,20 @@

it('updating related many-to-many entities works', () => {
const { Book, Genre, Author } = session;
const tommi = Author.get({ name: 'Tommi Kaikkonen' });
const book = tommi.books.first();
expect(book.genres.toRefArray().map(row => row.id))
.to.deep.equal([0, 1]);
const deleteGenre = Genre.withId(0);
const keepGenre = Genre.withId(1);
const addGenre = Genre.withId(2);
book.update({ genres: [addGenre, keepGenre] });
expect(book.genres.toRefArray().map(row => row.id))
.to.deep.equal([1, 2]);
expect(deleteGenre.books.filter({ id: book.id }).exists()).to.be.false;
});
it('removing related many-to-many entities works', () => {

@@ -171,6 +219,3 @@ const { Book, Genre } = session;

const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.withId(0).genres.count()).to.equal(1);
expect(session.Book.withId(0).genres.count()).to.equal(1);
});

@@ -192,6 +237,3 @@

const nextState = session.reduce();
const nextSession = schema.from(nextState);
expect(nextSession.Book.withId(0).genres.count()).to.equal(0);
expect(session.Book.withId(0).genres.count()).to.equal(0);
});

@@ -215,3 +257,4 @@

expect(relatedBooks).to.be.an.instanceOf(QuerySet);
expect(relatedBooks.idArr).to.include(book.getId());
relatedBooks._evaluate();
expect(relatedBooks.rows).to.include(book.ref);
expect(relatedBooks.modelClass).to.equal(Book);

@@ -240,10 +283,42 @@ });

it('applying no updates returns the same state reference', () => {
const nextState = session.reduce();
expect(nextState).to.equal(state);
const book = session.Book.first();
book.name = book.name;
expect(session.state).to.equal(state);
});
it('Model works with default value', () => {
let returnId = 1;
class DefaultFieldModel extends Model {}
DefaultFieldModel.fields = {
id: attr({ getDefault: () => returnId }),
};
DefaultFieldModel.modelName = 'DefaultFieldModel';
const _orm = new ORM();
_orm.register(DefaultFieldModel);
const sess = _orm.session(_orm.getEmptyState());
sess.DefaultFieldModel.create({});
expect(sess.DefaultFieldModel.hasId(1)).to.be.true;
returnId = 999;
sess.DefaultFieldModel.create({});
expect(sess.DefaultFieldModel.hasId(999)).to.be.true;
});
});
describe('Mutating session', () => {
beforeEach(() => {
({
session,
orm,
state,
} = createTestSessionWithData());
});
it('works', () => {
const mutating = schema.withMutations(state);
const mutating = orm.mutableSession(state);
const {

@@ -267,3 +342,3 @@ Book,

const nextState = mutating.reduce();
const nextState = mutating.state;
expect(nextState).to.equal(state);

@@ -277,5 +352,13 @@ expect(state.Book.itemsById[bookId]).to.equal(bookRef);

describe('Multiple concurrent sessions', () => {
beforeEach(() => {
({
session,
orm,
state,
} = createTestSessionWithData());
});
it('works', () => {
const firstSession = session;
const secondSession = schema.from(state);
const secondSession = orm.session(state);

@@ -294,7 +377,4 @@ expect(firstSession.Book.count()).to.equal(3);

const nextFirstSession = schema.from(firstSession.getNextState());
const nextSecondSession = schema.from(secondSession.getNextState());
expect(nextFirstSession.Book.count()).to.equal(4);
expect(nextSecondSession.Book.count()).to.equal(3);
expect(firstSession.Book.count()).to.equal(4);
expect(secondSession.Book.count()).to.equal(3);
});

@@ -306,3 +386,3 @@ });

let Item;
let schema;
let orm;

@@ -312,10 +392,14 @@ beforeEach(() => {

Item.modelName = 'Item';
schema = new Schema();
schema.register(Item);
Item.fields = {
id: attr(),
name: attr(),
};
orm = new ORM();
orm.register(Item);
});
it('adds a big amount of items in acceptable time', function () {
it('adds a big amount of items in acceptable time', function bigDataTest() {
this.timeout(30000);
const session = schema.from(schema.getDefaultState());
const session = orm.session(orm.getEmptyState());
const start = new Date().getTime();

@@ -327,3 +411,2 @@

}
const nextState = session.getNextState();
const end = new Date().getTime();

@@ -330,0 +413,0 @@ const tookSeconds = (end - start) / 1000;

@@ -6,5 +6,9 @@ import chai from 'chai';

const { expect } = chai;
import BaseModel from '../Model';
import {
Model as BaseModel,
ManyToMany,
attr,
} from '../';
import Table from '../db/Table';
import { UPDATE, DELETE } from '../constants';
import { ManyToMany } from '../fields';

@@ -14,10 +18,4 @@ describe('Model', () => {

let Model;
const sessionMock = {};
const stateMock = {};
const actionMock = {};
const sessionMock = { db: { tables: { Model: new Table() } } };
sessionMock.action = actionMock;
sessionMock.getState = () => stateMock;
beforeEach(() => {

@@ -43,181 +41,21 @@ // Get a fresh copy

});
it('getBackend works correctly', () => {
const BackendMockClass = sinon.stub();
Model.getBackendClass = () => BackendMockClass;
const instance = Model.getBackend();
expect(instance).to.be.an.instanceOf(BackendMockClass);
});
});
describe('static method delegates to Backend', () => {
let Model;
let backendMock;
let sessionMock;
let markAccessedSpy;
const stateMock = {};
beforeEach(() => {
Model = Object.create(BaseModel);
Model.modelName = 'Model';
markAccessedSpy = sinon.spy();
sessionMock = { markAccessed: markAccessedSpy };
backendMock = {};
Model.getBackend = () => backendMock;
Model._session = sessionMock;
Object.defineProperty(Model, 'state', {
get: () => stateMock,
});
});
it('accessId correctly delegates', () => {
const accessIdSpy = sinon.spy();
backendMock.accessId = accessIdSpy;
const arg = 1;
Model.accessId(arg);
expect(accessIdSpy).to.have.been.calledOnce;
expect(accessIdSpy).to.have.been.calledWithExactly(stateMock, arg);
expect(markAccessedSpy).to.have.been.calledOnce;
});
it('accessIds correctly delegates', () => {
const accessIdsSpy = sinon.spy();
backendMock.accessIdList = accessIdsSpy;
Model.accessIds();
expect(accessIdsSpy).to.have.been.calledOnce;
expect(accessIdsSpy).to.have.been.calledWithExactly(stateMock);
expect(markAccessedSpy).to.have.been.calledOnce;
});
it('accessList correctly delegates', () => {
const accessIdsSpy = sinon.spy();
backendMock.accessIdList = accessIdsSpy;
Model.accessIds();
expect(accessIdsSpy).to.have.been.calledOnce;
expect(accessIdsSpy).to.have.been.calledWithExactly(stateMock);
expect(markAccessedSpy).to.have.been.calledOnce;
});
});
describe('Instance methods', () => {
let Model;
let Tag;
let instance;
let sessionMock;
let stateMock;
let actionMock;
beforeEach(() => {
sessionMock = {};
stateMock = {};
actionMock = {};
sessionMock.action = actionMock;
sessionMock.getState = () => stateMock;
// Get a fresh copy
// of Model, so our manipulations
// won't survive longer than each test.
Model = class TestModel extends BaseModel {};
Model.modelName = 'Model';
Model.fields = {
tags: new ManyToMany('Tag'),
id: attr(),
name: attr(),
tags: new ManyToMany('_'),
};
Tag = class extends BaseModel {};
Model.markAccessed = () => undefined;
instance = new Model({ id: 0, name: 'Tommi' });
});
it('delete works correctly', () => {
const addUpdateSpy = sinon.spy();
Model.addUpdate = addUpdateSpy;
expect(addUpdateSpy).not.called;
instance.delete();
expect(addUpdateSpy).calledOnce;
expect(addUpdateSpy.getCall(0).args[0]).to.deep.equal({
type: DELETE,
payload: [instance.id],
});
});
it('update works correctly', () => {
const addUpdateSpy = sinon.spy();
Model.addUpdate = addUpdateSpy;
expect(addUpdateSpy).not.called;
instance.update({ name: 'Matt' });
expect(addUpdateSpy).calledOnce;
expect(addUpdateSpy.getCall(0).args[0]).to.deep.equal({
type: UPDATE,
payload: {
idArr: [instance.id],
mergeObj: {
name: 'Matt',
},
},
});
});
it('update works correctly when updating many-to-many relation', () => {
const addSpy = sinon.spy();
const removeSpy = sinon.spy();
const addUpdateSpy = sinon.spy();
Model.addUpdate = addUpdateSpy;
Model.fields = { fakem2m: new ManyToMany('_') };
instance = new Model({ id: 0, name: 'Tommi', fakem2m: [1, 2, 3] });
// instance.fakem2m = ... evokes a setter, needs to use defineProperty
// for mocking
Object.defineProperty(instance, 'fakem2m', {
value: {
add: addSpy,
remove: removeSpy,
idArr: [1, 2, 3],
},
});
instance.update({ fakem2m: [3, 4] });
expect(removeSpy).to.have.been.calledOnce;
expect(removeSpy).to.have.been.calledWith(1, 2);
expect(addSpy).to.have.been.calledOnce;
expect(addSpy).to.have.been.calledWith(4);
});
it('set works correctly', () => {
Model.addUpdate = () => undefined;
const updateSpy = sinon.spy(instance, 'update');
expect(updateSpy).not.called;
instance.set('name', 'Matt');
expect(updateSpy).calledOnce;
expect(updateSpy.getCall(0).args[0]).to.deep.equal({
name: 'Matt',
});
});
it('ref works correctly', () => {
const backendMock = {};
Model.getBackend = () => backendMock;
const dbObj = {};
backendMock.accessId = () => dbObj;
Model._session = sessionMock;
sessionMock.backend = backendMock;
expect(instance.ref).to.equal(dbObj);
});
it('equals works correctly', () => {

@@ -228,10 +66,2 @@ const anotherInstance = new Model({ id: 0, name: 'Tommi' });

it('toString works correctly', () => {
expect(instance.toString()).to.equal('Model: {id: 0, name: Tommi}');
});
it('getId works correctly', () => {
expect(instance.getId()).to.equal(0);
});
it('getClass works correctly', () => {

@@ -238,0 +68,0 @@ expect(instance.getClass()).to.equal(Model);

@@ -7,9 +7,7 @@ import chai from 'chai';

import Model from '../Model';
import Schema from '../Schema';
import QuerySet from '../QuerySet';
import {
UPDATE,
DELETE,
} from '../constants';
Model,
ORM,
QuerySet,
} from '../';
import {

@@ -38,3 +36,4 @@ createTestModels,

const emptyQs = new QuerySet(session.Book, []);
const emptyQs = (new QuerySet(session.Book, [])).filter(() => false);
expect(emptyQs.exists()).to.be.false;

@@ -45,3 +44,3 @@ });

expect(bookQs.at(0)).to.be.an.instanceOf(Model);
expect(bookQs.ref.at(0)).to.equal(session.Book.state.itemsById[0]);
expect(bookQs.toRefArray()[0]).to.equal(session.Book.withId(0).ref);
});

@@ -61,18 +60,26 @@

// Force evaluation of QuerySets
bookQs.toRefArray();
all.toRefArray();
expect(all).not.to.equal(bookQs);
expect(all.idArr).to.deep.equal(bookQs.idArr);
expect(all.rows.length).to.equal(bookQs.rows.length);
for (let i = 0; i < all.rows.length; i++) {
expect(all.rows[i]).to.equal(bookQs.rows[i]);
}
});
it('filter works correctly with object argument', () => {
const filtered = bookQs.withRefs.filter({ name: 'Clean Code' });
const filtered = bookQs.filter({ name: 'Clean Code' });
expect(filtered.count()).to.equal(1);
expect(filtered.ref.first()).to.equal(session.Book.state.itemsById[1]);
expect(filtered.first().ref).to.equal(session.Book.withId(1).ref);
});
it('filter works correctly with object argument, with model instance value', () => {
const filtered = bookQs.withRefs.filter({
const filtered = bookQs.filter({
author: session.Author.withId(0),
});
expect(filtered.count()).to.equal(1);
expect(filtered.ref.first()).to.equal(session.Book.state.itemsById[0]);
expect(filtered.first().ref).to.equal(session.Book.withId(0).ref);
});

@@ -82,3 +89,4 @@

const ordered = bookQs.orderBy(['releaseYear']);
expect(ordered.idArr).to.deep.equal([1, 2, 0]);
const idArr = ordered.toRefArray().map(row => row.id);
expect(idArr).to.deep.equal([1, 2, 0]);
});

@@ -88,3 +96,4 @@

const ordered = bookQs.orderBy([(book) => book.releaseYear]);
expect(ordered.idArr).to.deep.equal([1, 2, 0]);
const idArr = ordered.toRefArray().map(row => row.id);
expect(idArr).to.deep.equal([1, 2, 0]);
});

@@ -95,3 +104,5 @@

expect(excluded.count()).to.equal(2);
expect(excluded.idArr).to.deep.equal([0, 2]);
const idArr = excluded.toRefArray().map(row => row.id);
expect(idArr).to.deep.equal([0, 2]);
});

@@ -101,30 +112,10 @@

const mergeObj = { name: 'Updated Book Name' };
expect(session.updates).to.have.length(0);
bookQs.update(mergeObj);
expect(session.updates).to.have.length(1);
expect(session.updates[0]).to.deep.equal({
type: UPDATE,
payload: {
idArr: bookQs.idArr,
mergeObj,
},
meta: {
name: 'Book',
},
});
bookQs.toRefArray().forEach(row => expect(row.name).to.equal('Updated Book Name'));
});
it('delete records a update', () => {
expect(session.updates).to.have.length(0);
bookQs.delete();
expect(session.updates).to.have.length.of.at.least(1);
expect(session.updates[0]).to.deep.equal({
type: DELETE,
payload: bookQs.idArr,
meta: {
name: 'Book',
},
});
expect(bookQs.count()).to.equal(0);
});

@@ -138,2 +129,3 @@

Author,
Publisher,
} = createTestModels();

@@ -144,3 +136,3 @@

unreleased() {
return this.withRefs.filter(book => book.releaseYear > currentYear);
return this.filter(book => book.releaseYear > currentYear);
}

@@ -152,5 +144,5 @@ }

const schema = new Schema();
schema.register(Book, Genre, Cover, Author);
const { session: sess } = createTestSessionWithData(schema);
const orm = new ORM();
orm.register(Book, Genre, Cover, Author, Publisher);
const { session: sess } = createTestSessionWithData(orm);

@@ -170,6 +162,7 @@ const customQs = sess.Book.getQuerySet();

releaseYear: 2050,
publisher: 1,
});
expect(sess.Book.unreleased().count()).to.equal(1);
expect(sess.Book.withRefs.filter({ name: 'Clean Code' }).count()).to.equal(1);
expect(sess.Book.filter({ name: 'Clean Code' }).count()).to.equal(1);
});
});

@@ -5,4 +5,6 @@ import chai from 'chai';

import Schema from '../Schema';
import {
ORM,
} from '../';
import {
createTestModels,

@@ -17,3 +19,3 @@ isSubclass,

describe('Session', () => {
let schema;
let orm;
let Book;

@@ -23,3 +25,4 @@ let Cover;

let Author;
let defaultState;
let Publisher;
let emptyState;
beforeEach(() => {

@@ -31,6 +34,7 @@ ({

Author,
Publisher,
} = createTestModels());
schema = new Schema();
schema.register(Book, Cover, Genre, Author);
defaultState = schema.getDefaultState();
orm = new ORM();
orm.register(Book, Cover, Genre, Author, Publisher);
emptyState = orm.getEmptyState();
});

@@ -43,4 +47,5 @@

expect(Cover.session).to.be.undefined;
expect(Publisher.session).to.be.undefined;
const session = schema.from(defaultState);
const session = orm.from(emptyState);

@@ -51,6 +56,7 @@ expect(session.Book.session).to.equal(session);

expect(session.Cover.session).to.equal(session);
expect(session.Publisher.session).to.equal(session);
});
it('exposes models as getter properties', () => {
const session = schema.from(defaultState);
const session = orm.session(emptyState);
expect(isSubclass(session.Book, Book)).to.be.true;

@@ -60,14 +66,14 @@ expect(isSubclass(session.Author, Author)).to.be.true;

expect(isSubclass(session.Genre, Genre)).to.be.true;
expect(isSubclass(session.Publisher, Publisher)).to.be.true;
});
it('marks accessed models', () => {
const session = schema.from(defaultState);
const session = orm.from(emptyState);
expect(session.accessedModels).to.have.length(0);
session.markAccessed(Book);
session.markAccessed(Book.modelName);
expect(session.accessedModels).to.have.length(1);
expect(session.accessedModels[0]).to.equal('Book');
session.markAccessed(Book);
session.markAccessed(Book.modelName);

@@ -77,26 +83,14 @@ expect(session.accessedModels[0]).to.equal('Book');

it('adds updates', () => {
const session = schema.from(defaultState);
expect(session.updates).to.have.length(0);
const updateObj = { meta: { name: 'MockModel' } };
session.addUpdate(updateObj);
expect(session.updates).to.have.length(1);
});
describe('gets the next state', () => {
it('without any updates, the same state is returned', () => {
const session = schema.from(defaultState);
const nextState = session.getNextState();
expect(nextState).to.equal(defaultState);
const session = orm.session(emptyState);
expect(session.state).to.equal(emptyState);
});
it('with updates, a new state is returned', () => {
const session = schema.from(defaultState);
const session = orm.session(emptyState);
session.addUpdate({
type: CREATE,
meta: {
name: Author.modelName,
},
session.applyUpdate({
table: Author.modelName,
action: CREATE,
payload: {

@@ -108,64 +102,23 @@ id: 0,

const nextState = session.getNextState();
const nextState = session.state;
expect(nextState).to.not.equal(defaultState);
expect(nextState).to.not.equal(emptyState);
expect(nextState[Author.modelName]).to.not.equal(defaultState[Author.modelName]);
expect(nextState[Author.modelName]).to.not.equal(emptyState[Author.modelName]);
// All other model states should stay equal.
expect(nextState[Book.modelName]).to.equal(defaultState[Book.modelName]);
expect(nextState[Cover.modelName]).to.equal(defaultState[Cover.modelName]);
expect(nextState[Genre.modelName]).to.equal(defaultState[Genre.modelName]);
expect(nextState[Book.modelName]).to.equal(emptyState[Book.modelName]);
expect(nextState[Cover.modelName]).to.equal(emptyState[Cover.modelName]);
expect(nextState[Genre.modelName]).to.equal(emptyState[Genre.modelName]);
expect(nextState[Publisher.modelName]).to.equal(emptyState[Publisher.modelName]);
});
it('runs reducers if explicitly specified', () => {
const session = schema.from(defaultState);
const authorReducerSpy = sinon.spy(Author, 'reducer');
const bookReducerSpy = sinon.spy(Book, 'reducer');
const coverReducerSpy = sinon.spy(Cover, 'reducer');
const genreReducerSpy = sinon.spy(Genre, 'reducer');
session.getNextState({ runReducers: true });
expect(authorReducerSpy).to.be.calledOnce;
expect(bookReducerSpy).to.be.calledOnce;
expect(coverReducerSpy).to.be.calledOnce;
expect(genreReducerSpy).to.be.calledOnce;
});
it('doesn\'t run reducers if explicitly specified', () => {
const session = schema.from(defaultState);
const authorReducerSpy = sinon.spy(Author, 'reducer');
const bookReducerSpy = sinon.spy(Book, 'reducer');
const coverReducerSpy = sinon.spy(Cover, 'reducer');
const genreReducerSpy = sinon.spy(Genre, 'reducer');
session.getNextState({ runReducers: false });
expect(authorReducerSpy).not.to.be.called;
expect(bookReducerSpy).not.to.be.called;
expect(coverReducerSpy).not.to.be.called;
expect(genreReducerSpy).not.to.be.called;
});
});
it('reduce calls getNextState with correct arguments', () => {
const session = schema.from(defaultState);
const getNextStateSpy = sinon.spy(session, 'getNextState');
session.reduce();
expect(getNextStateSpy).to.be.calledOnce;
expect(getNextStateSpy).to.be.calledWithMatch({ runReducers: true });
});
it('two concurrent sessions', () => {
const otherState = schema.getDefaultState();
const otherState = orm.getEmptyState();
const firstSession = schema.from(defaultState);
const secondSession = schema.from(otherState);
const firstSession = orm.session(emptyState);
const secondSession = orm.session(otherState);
expect(firstSession.sessionBoundModels).to.have.lengthOf(5);
expect(firstSession.sessionBoundModels).to.have.lengthOf(6);

@@ -176,3 +129,4 @@ expect(firstSession.Book).not.to.equal(secondSession.Book);

expect(firstSession.Cover).not.to.equal(secondSession.Cover);
expect(firstSession.Publisher).not.to.equal(secondSession.Publisher);
});
});

@@ -1,4 +0,4 @@

import Schema from '../Schema';
import ORM from '../ORM';
import Model from '../Model';
import { fk, many, oneToOne } from '../fields';
import { fk, many, oneToOne, attr } from '../fields';

@@ -58,2 +58,3 @@ /**

releaseYear: 2050,
publisher: 1,
},

@@ -66,2 +67,3 @@ {

releaseYear: 2008,
publisher: 0,
},

@@ -74,5 +76,14 @@ {

releaseYear: 2015,
publisher: 0,
},
];
const PUBLISHERS_INITIAL = [
{
name: 'Technical Publishing',
},
{
name: 'Autobiographies Inc',
},
];

@@ -83,5 +94,9 @@ export function createTestModels() {

return {
id: attr(),
name: attr(),
releaseYear: attr(),
author: fk('Author', 'books'),
cover: oneToOne('Cover'),
genres: many('Genre', 'books'),
publisher: fk('Publisher', 'books'),
};

@@ -93,3 +108,15 @@ }

const Author = class AuthorModel extends Model {};
const Author = class AuthorModel extends Model {
static get fields() {
return {
id: attr(),
name: attr(),
publishers: many({
to: 'Publisher',
through: 'Book',
relatedName: 'authors',
}),
};
}
};
Author.modelName = 'Author';

@@ -99,6 +126,21 @@

Cover.modelName = 'Cover';
Cover.fields = {
id: attr(),
src: attr(),
};
const Genre = class GenreModel extends Model {};
Genre.modelName = 'Genre';
Genre.fields = {
id: attr(),
name: attr(),
};
const Publisher = class PublisherModel extends Model {};
Publisher.modelName = 'Publisher';
Publisher.fields = {
id: attr(),
name: attr(),
};
return {

@@ -109,6 +151,7 @@ Book,

Genre,
Publisher,
};
}
export function createTestSchema(customModels) {
export function createTestORM(customModels) {
const models = customModels || createTestModels();

@@ -120,28 +163,30 @@ const {

Genre,
Publisher,
} = models;
const schema = new Schema();
schema.register(Book, Author, Cover, Genre);
return schema;
const orm = new ORM();
orm.register(Book, Author, Cover, Genre, Publisher);
return orm;
}
export function createTestSession() {
const schema = createTestSchema();
return schema.from(schema.getDefaultState());
const orm = createTestORM();
return orm.session(orm.getEmptytate());
}
export function createTestSessionWithData(customSchema) {
const schema = customSchema || createTestSchema();
const state = schema.getDefaultState();
const mutatingSession = schema.withMutations(state);
export function createTestSessionWithData(customORM) {
const orm = customORM || createTestORM();
const state = orm.getEmptyState();
const { Author, Cover, Genre, Book, Publisher } = orm.mutableSession(state);
AUTHORS_INITIAL.forEach(props => mutatingSession.Author.create(props));
COVERS_INITIAL.forEach(props => mutatingSession.Cover.create(props));
GENRES_INITIAL.forEach(props => mutatingSession.Genre.create(props));
BOOKS_INITIAL.forEach(props => mutatingSession.Book.create(props));
AUTHORS_INITIAL.forEach(props => Author.create(props));
COVERS_INITIAL.forEach(props => Cover.create(props));
GENRES_INITIAL.forEach(props => Genre.create(props));
BOOKS_INITIAL.forEach(props => Book.create(props));
PUBLISHERS_INITIAL.forEach(props => Publisher.create(props));
const normalSession = schema.from(state);
return { session: normalSession, schema, state };
const normalSession = orm.session(state);
return { session: normalSession, orm, state };
}
export const isSubclass = (a, b) => a.prototype instanceof b;
import forOwn from 'lodash/forOwn';
import includes from 'lodash/includes';
import getImmutableOps from 'immutable-ops';
import ops from 'immutable-ops';
import intersection from 'lodash/intersection';
import difference from 'lodash/difference';
/**
* @module utils
*/
/**
* A simple ListIterator implementation.
*/
class ListIterator {
/**
* Creates a new ListIterator instance.
* @param {Array} list - list to iterate over
* @param {Number} [idx=0] - starting index. Defaults to `0`
* @param {Function} [getValue] a function that receives the current `idx`
* and `list` and should return the value that
* `next` should return. Defaults to `(idx, list) => list[idx]`
*/
constructor(list, idx, getValue) {
this.list = list;
this.idx = idx || 0;
if (typeof getValue === 'function') {
this.getValue = getValue;
}
}
/**
* The default implementation for the `getValue` function.
*
* @param {Number} idx - the current iterator index
* @param {Array} list - the list being iterated
* @return {*} - the value at index `idx` in `list`.
*/
getValue(idx, list) {
return list[idx];
}
/**
* Returns the next element from the iterator instance.
* Always returns an Object with keys `value` and `done`.
* If the returned element is the last element being iterated,
* `done` will equal `true`, otherwise `false`. `value` holds
* the value returned by `getValue`.
*
* @return {Object|undefined} Object with keys `value` and `done`, or
* `undefined` if the list index is out of bounds.
*/
next() {
if (this.idx < this.list.length - 1) {
return {
value: this.getValue(this.list, this.idx++),
done: false,
};
} else if (this.idx < this.list.length) {
return {
value: this.getValue(this.list, this.idx++),
done: true,
};
}
return undefined;
}
function warnDeprecated(msg) {
const logger = typeof console.warn === 'function'
? console.warn.bind(console)
: console.log.bind(console);
return logger(msg);
}
/**
* Checks if the properties in `lookupObj` match
* the corresponding properties in `entity`.
*
* @private
* @param {Object} lookupObj - properties to match against
* @param {Object} entity - object to match
* @return {Boolean} Returns `true` if the property names in
* `lookupObj` have the same values in `lookupObj`
* and `entity`, `false` if not.
* @module utils
*/
function match(lookupObj, entity) {
const keys = Object.keys(lookupObj);
return keys.every(key => lookupObj[key] === entity[key]);
}

@@ -244,7 +177,5 @@ function capitalize(string) {

// A global instance of immutable-ops for general use
const ops = getImmutableOps();
const { getBatchToken } = ops;
export {
match,
attachQuerySetMethods,

@@ -255,3 +186,2 @@ m2mName,

reverseFieldName,
ListIterator,
normalizeEntity,

@@ -263,2 +193,4 @@ reverseFieldErrorMessage,

arrayDiffActions,
getBatchToken,
warnDeprecated,
};

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc