Comparing version 3.0.8 to 4.0.0
# Changelog | ||
## `4.0.0` | ||
- Added indexes to optimize filter and find | ||
## `3.0.5` | ||
@@ -4,0 +9,0 @@ |
@@ -6,4 +6,7 @@ import Base from './Base'; | ||
import { CreateOptions, SetOptions, GetOptions, FindOptions, Id } from './types'; | ||
declare type IndexTree<T> = Map<string, Index<T>>; | ||
declare type Index<T> = Map<any, Array<T>>; | ||
export default abstract class Collection<T extends Model> extends Base { | ||
models: IObservableArray<T>; | ||
indexes: Array<string>; | ||
constructor(data?: Array<{ | ||
@@ -13,2 +16,21 @@ [key: string]: any; | ||
/** | ||
* Define which is the primary key | ||
* of the model's in the collection. | ||
* | ||
* FIXME: This contains a hack to use the `primaryKey` as | ||
* an instance method. Ideally it should be static but that | ||
* would not be backward compatible and Typescript sucks at | ||
* static polymorphism (https://github.com/microsoft/TypeScript/issues/5863). | ||
*/ | ||
readonly primaryKey: string; | ||
/** | ||
* Returns a hash with all the indexes for that | ||
* collection. | ||
* | ||
* We keep the indexes in memory for as long as the | ||
* collection is alive, even if no one is referencing it. | ||
* This way we can ensure to calculate it only once. | ||
*/ | ||
readonly index: IndexTree<T>; | ||
/** | ||
* Alias for models.length | ||
@@ -27,10 +49,6 @@ */ | ||
* Returns the URL where the model's resource would be located on the server. | ||
* | ||
* @abstract | ||
*/ | ||
url(): string; | ||
abstract url(): string; | ||
/** | ||
* Specifies the model class for that collection | ||
* | ||
* @abstract | ||
*/ | ||
@@ -65,3 +83,3 @@ abstract model(attributes?: { | ||
*/ | ||
_ids(): Array<Id>; | ||
private readonly _ids; | ||
/** | ||
@@ -80,3 +98,6 @@ * Get a resource at a given position | ||
/** | ||
* Get resources matching criteria | ||
* Get resources matching criteria. | ||
* | ||
* If passing an object of key:value conditions, it will | ||
* use the indexes to efficiently retrieve the data. | ||
*/ | ||
@@ -146,1 +167,2 @@ filter(query: { | ||
} | ||
export {}; |
170
lib/index.js
@@ -17,3 +17,4 @@ 'use strict'; | ||
var difference = _interopDefault(require('lodash/difference')); | ||
var isMatch = _interopDefault(require('lodash/isMatch')); | ||
var intersection = _interopDefault(require('lodash/intersection')); | ||
var entries = _interopDefault(require('lodash/entries')); | ||
@@ -164,7 +165,11 @@ /*! ***************************************************************************** | ||
_this.request = null; | ||
_this.requests.remove(request); | ||
mobx.runInAction('remove request', function () { | ||
_this.requests.remove(request); | ||
}); | ||
return response; | ||
}) | ||
.catch(function (error) { | ||
_this.requests.remove(request); | ||
mobx.runInAction('remove request', function () { | ||
_this.requests.remove(request); | ||
}); | ||
throw new ErrorObject(error); | ||
@@ -217,2 +222,3 @@ }); | ||
var dontMergeArrays = function (_oldArray, newArray) { return newArray; }; | ||
var DEFAULT_PRIMARY = 'id'; | ||
var Model = /** @class */ (function (_super) { | ||
@@ -244,9 +250,7 @@ __extends(Model, _super); | ||
/** | ||
* Determine what attribute do you use | ||
* as a primary id | ||
* | ||
* @abstract | ||
* Define which is the primary | ||
* key of the model. | ||
*/ | ||
get: function () { | ||
return 'id'; | ||
return DEFAULT_PRIMARY; | ||
}, | ||
@@ -401,11 +405,2 @@ enumerable: true, | ||
/** | ||
* Merges old attributes with new ones. | ||
* By default it doesn't merge arrays. | ||
*/ | ||
Model.prototype.applyPatchChanges = function (oldAttributes, changes) { | ||
return deepmerge(oldAttributes, changes, { | ||
arrayMerge: dontMergeArrays | ||
}); | ||
}; | ||
/** | ||
* Saves the resource on the backend. | ||
@@ -446,3 +441,3 @@ * | ||
this.set(patch | ||
? this.applyPatchChanges(currentAttributes, attributes) | ||
? applyPatchChanges(currentAttributes, attributes) | ||
: attributes); | ||
@@ -462,3 +457,3 @@ } | ||
if (keepChanges) { | ||
_this.set(_this.applyPatchChanges(data, changes)); | ||
_this.set(applyPatchChanges(data, changes)); | ||
} | ||
@@ -536,2 +531,11 @@ }); | ||
}(Base)); | ||
/** | ||
* Merges old attributes with new ones. | ||
* By default it doesn't merge arrays. | ||
*/ | ||
var applyPatchChanges = function (oldAttributes, changes) { | ||
return deepmerge(oldAttributes, changes, { | ||
arrayMerge: dontMergeArrays | ||
}); | ||
}; | ||
var getChangedAttributesBetween = function (source, target) { | ||
@@ -557,5 +561,49 @@ var keys = union(Object.keys(source), Object.keys(target)); | ||
_this.models = mobx.observable.array([]); | ||
_this.indexes = []; | ||
_this.set(data); | ||
return _this; | ||
} | ||
Object.defineProperty(Collection.prototype, "primaryKey", { | ||
/** | ||
* Define which is the primary key | ||
* of the model's in the collection. | ||
* | ||
* FIXME: This contains a hack to use the `primaryKey` as | ||
* an instance method. Ideally it should be static but that | ||
* would not be backward compatible and Typescript sucks at | ||
* static polymorphism (https://github.com/microsoft/TypeScript/issues/5863). | ||
*/ | ||
get: function () { | ||
var ModelClass = this.model(); | ||
return (new ModelClass()).primaryKey; | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
Object.defineProperty(Collection.prototype, "index", { | ||
/** | ||
* Returns a hash with all the indexes for that | ||
* collection. | ||
* | ||
* We keep the indexes in memory for as long as the | ||
* collection is alive, even if no one is referencing it. | ||
* This way we can ensure to calculate it only once. | ||
*/ | ||
get: function () { | ||
var _this = this; | ||
var indexes = this.indexes.concat([this.primaryKey]); | ||
return indexes.reduce(function (tree, attr) { | ||
var newIndex = _this.models.reduce(function (index, model) { | ||
var value = model.has(attr) | ||
? model.get(attr) | ||
: null; | ||
var oldModels = index.get(value) || []; | ||
return index.set(value, oldModels.concat(model)); | ||
}, new Map()); | ||
return tree.set(attr, newIndex); | ||
}, new Map()); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
Object.defineProperty(Collection.prototype, "length", { | ||
@@ -584,10 +632,2 @@ /** | ||
/** | ||
* Returns the URL where the model's resource would be located on the server. | ||
* | ||
* @abstract | ||
*/ | ||
Collection.prototype.url = function () { | ||
throw new Error('You must implement this method'); | ||
}; | ||
/** | ||
* Returns a JSON representation | ||
@@ -622,9 +662,13 @@ * of the collection | ||
}); | ||
Object.defineProperty(Collection.prototype, "_ids", { | ||
/** | ||
* Gets the ids of all the items in the collection | ||
*/ | ||
get: function () { | ||
return Array.from(this.index.get(this.primaryKey).keys()); | ||
}, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
/** | ||
* Gets the ids of all the items in the collection | ||
*/ | ||
Collection.prototype._ids = function () { | ||
return this.models.map(function (item) { return item.id; }).filter(Boolean); | ||
}; | ||
/** | ||
* Get a resource at a given position | ||
@@ -640,5 +684,6 @@ */ | ||
var _b = (_a === void 0 ? {} : _a).required, required = _b === void 0 ? false : _b; | ||
var model = this.models.find(function (item) { return item.id === id; }); | ||
var models = this.index.get(this.primaryKey).get(id); | ||
var model = models && models[0]; | ||
if (!model && required) { | ||
throw new Error("Invariant: Model must be found with id: " + id); | ||
throw new Error("Invariant: Model must be found with " + this.primaryKey + ": " + id); | ||
} | ||
@@ -654,10 +699,31 @@ return model; | ||
/** | ||
* Get resources matching criteria | ||
* Get resources matching criteria. | ||
* | ||
* If passing an object of key:value conditions, it will | ||
* use the indexes to efficiently retrieve the data. | ||
*/ | ||
Collection.prototype.filter = function (query) { | ||
return this.models.filter(function (model) { | ||
return typeof query === 'function' | ||
? query(model) | ||
: isMatch(model.toJS(), query); | ||
}); | ||
var _this = this; | ||
if (typeof query === 'function') { | ||
return this.models.filter(function (model) { return query(model); }); | ||
} | ||
else { | ||
// Sort the query to hit the indexes first | ||
var optimizedQuery = entries(query).sort(function (A, B) { | ||
return Number(_this.index.has(B[0])) - Number(_this.index.has(A[0])); | ||
}); | ||
return optimizedQuery.reduce(function (values, _a) { | ||
var attr = _a[0], value = _a[1]; | ||
// Hitting index | ||
if (_this.index.has(attr)) { | ||
var newValues = _this.index.get(attr).get(value) || []; | ||
return values ? intersection(values, newValues) : newValues; | ||
} | ||
else { | ||
// Either Re-filter or Full scan | ||
var target = values || _this.models; | ||
return target.filter(function (model) { return model.get(attr) === value; }); | ||
} | ||
}, null); | ||
} | ||
}; | ||
@@ -669,7 +735,5 @@ /** | ||
var _b = (_a === void 0 ? {} : _a).required, required = _b === void 0 ? false : _b; | ||
var model = this.models.find(function (model) { | ||
return typeof query === 'function' | ||
? query(model) | ||
: isMatch(model.toJS(), query); | ||
}); | ||
var model = typeof query === 'function' | ||
? this.models.find(function (model) { return query(model); }) | ||
: this.filter(query)[0]; | ||
if (!model && required) { | ||
@@ -722,3 +786,3 @@ throw new Error("Invariant: Model must be found"); | ||
if (!model) { | ||
return console.warn(_this.constructor.name + ": Model with id " + id + " not found."); | ||
return console.warn(_this.constructor.name + ": Model with " + _this.primaryKey + " " + id + " not found."); | ||
} | ||
@@ -738,4 +802,4 @@ _this.models.splice(_this.models.indexOf(model), 1); | ||
if (remove) { | ||
var ids = resources.map(function (r) { return r.id; }); | ||
var toRemove = difference(this._ids(), ids); | ||
var ids = resources.map(function (r) { return r[_this.primaryKey]; }); | ||
var toRemove = difference(this._ids, ids); | ||
if (toRemove.length) | ||
@@ -745,3 +809,3 @@ this.remove(toRemove); | ||
resources.forEach(function (resource) { | ||
var model = _this.get(resource.id); | ||
var model = _this.get(resource[_this.primaryKey]); | ||
if (model && change) | ||
@@ -817,2 +881,5 @@ model.set(resource); | ||
__decorate([ | ||
mobx.computed({ keepAlive: true }) | ||
], Collection.prototype, "index", null); | ||
__decorate([ | ||
mobx.computed | ||
@@ -824,2 +891,5 @@ ], Collection.prototype, "length", null); | ||
__decorate([ | ||
mobx.computed | ||
], Collection.prototype, "_ids", null); | ||
__decorate([ | ||
mobx.action | ||
@@ -826,0 +896,0 @@ ], Collection.prototype, "add", null); |
@@ -6,4 +6,8 @@ import { ObservableMap } from 'mobx'; | ||
import { OptimisticId, Id, DestroyOptions, SaveOptions } from './types'; | ||
declare type Attributes = { | ||
[key: string]: any; | ||
}; | ||
export declare const DEFAULT_PRIMARY = "id"; | ||
export default class Model extends Base { | ||
defaultAttributes: {}; | ||
defaultAttributes: Attributes; | ||
attributes: ObservableMap; | ||
@@ -13,7 +17,3 @@ committedAttributes: ObservableMap; | ||
collection: Collection<this> | null; | ||
constructor(attributes?: { | ||
[key: string]: any; | ||
}, defaultAttributes?: { | ||
[key: string]: any; | ||
}); | ||
constructor(attributes?: Attributes, defaultAttributes?: Attributes); | ||
/** | ||
@@ -25,6 +25,4 @@ * Returns a JSON representation | ||
/** | ||
* Determine what attribute do you use | ||
* as a primary id | ||
* | ||
* @abstract | ||
* Define which is the primary | ||
* key of the model. | ||
*/ | ||
@@ -38,3 +36,3 @@ readonly primaryKey: string; | ||
*/ | ||
urlRoot(): any; | ||
urlRoot(): string | null; | ||
/** | ||
@@ -107,7 +105,2 @@ * Return the url for this given REST resource | ||
/** | ||
* Merges old attributes with new ones. | ||
* By default it doesn't merge arrays. | ||
*/ | ||
applyPatchChanges(oldAttributes: {}, changes: {}): {}; | ||
/** | ||
* Saves the resource on the backend. | ||
@@ -128,1 +121,2 @@ * | ||
} | ||
export {}; |
{ | ||
"name": "mobx-rest", | ||
"version": "3.0.8", | ||
"version": "4.0.0", | ||
"description": "REST conventions for mobx.", | ||
@@ -26,2 +26,3 @@ "jest": { | ||
"@typescript-eslint/parser": "1.9.0", | ||
"benchmark": "2.1.4", | ||
"eslint": "5.16.0", | ||
@@ -43,3 +44,3 @@ "husky": "0.13.4", | ||
"build:clean": "rimraf lib", | ||
"build:lib": "yarn build:clean && rollup --config", | ||
"benchmark": "yarn build && node __tests__/benchmark.js", | ||
"jest": "NODE_PATH=src jest --no-cache", | ||
@@ -60,7 +61,7 @@ "lint": "eslint --ext .ts --cache src/ __tests__/", | ||
"dependencies": { | ||
"@types/lodash": "^4.14.134", | ||
"@types/lodash": "4.14.136", | ||
"deepmerge": "3.2.0", | ||
"lodash": "4.17.11", | ||
"lodash": "4.17.13", | ||
"mobx": "5.9.4" | ||
} | ||
} |
@@ -74,15 +74,2 @@ # mobx-rest | ||
You can also overwrite it to provide default attributes like this: | ||
```js | ||
class User extends Model { | ||
constructor(attributes) { | ||
super(Object.assign({ | ||
token: null, | ||
email_verified: false, | ||
}, attributes)) | ||
} | ||
} | ||
``` | ||
#### `defaultAttributes: Object` | ||
@@ -94,4 +81,8 @@ | ||
An `ObservableMap` that holds the attributes of the model. | ||
An `ObservableMap` that holds the attributes of the model in the client. | ||
#### `commitedAttributes: ObservableMap` | ||
An `ObservableMap` that holds the attributes of the model in the server. | ||
#### `collection: ?Collection` | ||
@@ -333,2 +324,18 @@ | ||
#### `indexes: Array<String>` | ||
Indexes allow you to determine which attributes you want to index your collection by. | ||
This allows you to trade-off memory for speed. By default we index all the models by | ||
`primaryKey` but you can add more indexes that will be used automatically when using `filter`, | ||
`find` and `mustFind` with the object form. | ||
```js | ||
users.find({ id: 123 }) // This will hit the index. Fast! | ||
users.find(user => user.get('id') === 123) // This will do a full scan of the table. Slow. | ||
``` | ||
You can query your collection by a combination of attributes that are indexed and others | ||
that are not indexed. `mobx-rest` will take care to sort your query in order to scan the least | ||
number of models. | ||
#### `request: ?Request` | ||
@@ -412,8 +419,16 @@ | ||
```js | ||
// using a query object | ||
const resolvedTasks = tasksCollection.filter({ resolved: true }) | ||
resolvedTasks.length // => 3 | ||
// using a query function | ||
const resolvedTasks = tasksCollection.filter(model => model.resolved) | ||
resolvedTasks.length // => 3 | ||
``` | ||
#### `find(query: Object, { required?: boolean = false }): ?Model` | ||
It's important to notice that using the object API we can optimize | ||
the filtering using indexes. | ||
#### `find(query: Object | Function, { required?: boolean = false }): ?Model` | ||
Same as `filter` but it will halt and return when the first model matches | ||
@@ -425,9 +440,14 @@ the conditions. If `required` it will raise an error if not found. | ||
```js | ||
const pau = usersCollection.find({ name: 'pau' }) | ||
pau.get('name') // => 'pau' | ||
// using a query object | ||
const user = usersCollection.find({ name: 'paco' }) | ||
user.get('name') // => 'paco' | ||
// using a query function | ||
const user = usersCollection.find(model => model.name === 'paco') | ||
user.get('name') // => 'paco' | ||
usersCollection.find({ name: 'foo'}) // => Error(`Invariant: Model must be found`) | ||
``` | ||
#### `mustFind(query: Object): Model` | ||
#### `mustFind(query: Object | Function): Model` | ||
@@ -708,3 +728,4 @@ Same as `find` but it will raise an Error if the model is not found. | ||
```js | ||
import usersCollection from './UsersCollections' | ||
import users from './UsersCollections' | ||
import comments from './CommentsCollections' | ||
import { computed } from 'mobx' | ||
@@ -715,12 +736,12 @@ | ||
author () { | ||
const userId = this.get('userId') | ||
return usersCollection.get(userId) || | ||
usersCollection.nullObject() | ||
return users.mustGet(this.get('user_id')) | ||
} | ||
@computed | ||
comments () { | ||
return comments.filter({ task_id: this.get('id') }) | ||
} | ||
} | ||
``` | ||
I recommend to always fallback with a null object which will facilitate | ||
a ton to write code like `task.author.get('name')`. | ||
## Where is it used? | ||
@@ -734,3 +755,3 @@ | ||
Copyright (c) 2017 Pau Ramon <masylum@gmail.com> | ||
Copyright (c) 2019 Pau Ramon <masylum@gmail.com> | ||
@@ -737,0 +758,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
69381
14
1265
755
15
+ Added@types/lodash@4.14.136(transitive)
+ Addedlodash@4.17.13(transitive)
- Removed@types/lodash@4.17.13(transitive)
- Removedlodash@4.17.11(transitive)
Updated@types/lodash@4.14.136
Updatedlodash@4.17.13