Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collection: add documentation on SimpleQuery nested property shorthand #231

Merged
merged 1 commit into from
Jan 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 96 additions & 44 deletions docs/Collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,33 @@ A Collection is the interface for a collection of items, which can be filtered o

Several methods are available for querying collections. These methods allow you to define a query through several steps. Normally, stores are queried first by calling `filter()` to specify which objects to be included, if the filtering is needed. Next, if an order needs to be specified, the `sort()` method is called to ensure the results will be sorted. A typical query from a store would look like:

store.filter({priority: 'high'}).sort('dueDate').forEach(function (object) {
// called for each item in the final result set
});
```javascript
store.filter({priority: 'high'}).sort('dueDate').forEach(function (object) {
// called for each item in the final result set
});
```

In addition, the `track()` method may be used to track store changes, ensuring notifications include index information about object changes, and keeping result sets up-to-date after a query. The `fetch()` method is an alternate way to retrieve results, providing a promise to an array for accessing query results. The sections below describes each of these methods and how to use them.

## Filtering

Filtering is used to specify a subset of objects to be returned in a filtered collection. The simplest use of the `filter()` method is to call it with a plain object as the argument, that specifies name-value pairs that the returned objects must match. Or a filter builder can be used to construct more sophisticated filter conditions. To use the filter builder, first construct a new filter object from the `Filter` constructor on the collection you would be querying:

var filter = new store.Filter();
```javascript
var filter = new store.Filter();
```

We now have a `filter` object, that represents a filter, without any operators applied yet. We can create new filter objects by calling the operator methods on the filter object. The operator methods will return new filter objects that hold the operator condition. For example, to specify that we want to retrieve objects with a `priority` property with a value of `"high"`, and `stars` property with a value greater than `5`, we could write:

var highPriorityFiveStarFilter = filter.eq('priority', 'high').gt('stars', 5);
```javascript
var highPriorityFiveStarFilter = filter.eq('priority', 'high').gt('stars', 5);
```

This filter object can then be passed as the argument to the `filter()` method on a collection/store:

var highPriorityFiveStarCollection = store.filter(highPriorityFiveStarFilter);
```javascript
var highPriorityFiveStarCollection = store.filter(highPriorityFiveStarFilter);
```

The following methods are available on the filter objects. First are the property filtering methods, which each take a property name as the first argument, and a property value to compare for the second argument:
* `eq`: Property values must equal the filter value argument.
Expand All @@ -45,16 +53,36 @@ The following are combinatorial methods:

A few of the filters can also be built upon with other collections (potentially from other stores). In particular, you can provide a collection as the argument for the `in` or `contains` filter. This provides functionality similar to nested queries or joins. This generally will need to be combined with a `select` to return the correct values for matching. For example, if we wanted to find all the tasks in high priority projects, where the `task` store has a `projectId` property/column that is a foreign key, referencing objects in a `project` store. We can perform our nested query:

var tasksOfHighPriorityProjects = taskStore.filter(
new Filter().in('projectId',
projectStore.filter({priority: 'high'}).select('id')));
```javascript
var tasksOfHighPriorityProjects = taskStore.filter(
new Filter().in('projectId',
projectStore.filter({ priority: 'high' }).select('id')
)
);
```

### Implementations

Different stores may implement filtering in different ways. The `dstore/Memory` will perform filtering in memory. The `dstore/Request`/`dstore/Rest` stores will translate the filters into URL query strings to send to the server. Simple queries will be in standard URL-encoded query format and complex queries will conform to [RQL](https://github.com/persvr/rql) syntax (which is a superset of standard query format).

New filter methods can be created by subclassing `dstore/Filter` and adding new methods. New methods can be created by calling `Filter.filterCreator` and by providing the name of the new method. If you will be using new methods with stores that mix in `SimpleQuery` like memory stores, you can also add filter comparators by overriding the `_getFilterComparator` method, returning comparators for the additional types, and delegating to `this.inherited` for the rest.

`dstore/SimpleQuery` provides a simple shorthand for nested property queries - a side-effect of this is that property names that contain the period character are not supported. Example nested property query:

```javascript
store.filter({ 'name.last': 'Smith' })
```

This would match the object:
```javascript
{
name: {
first: 'John',
last: 'Smith'
}
}
```

For the `dstore/Request`/`dstore/Rest` stores, you can define alternate serializations of filters to URL queries for existing or new methods by overriding the `_renderFilterParams`. This method is called with a filter object (and by default is recursively called by combinatorial operators), and should return a string serialization of the filter, that will be inserted into the query string of the URL sent to the server.

The filter objects themselves consist of tree structures. Each filter object has two properties, the operator `type`, which corresponds to whichever operator was used (like `eq` or `and`), and the `args`, which is an array of values provided to the operator. With `and` and `or` operators, the arguments are other filter objects, forming a hierarchy. When filter operators are chained together (through sequential calls), they are combined with the `and` operator (each operator defined in a sub-filter object).
Expand Down Expand Up @@ -85,17 +113,26 @@ This sorts the collection, returning a new ordered collection. Note that if sort

#### `sort([highestSortOrder, nextSortOrder...])`

This also sorts the collection, but can be called to define multiple sort orders by priority. Each argument is an object with a `property` property and an optional `descending` property (defaults to ascending, if not set), to define the order. For example: `collection.sort([{property:'lastName'}, {property: 'firstName'}])` would result in a new collection sorted by lastName, with firstName used to sort identical lastName values.
This also sorts the collection, but can be called to define multiple sort orders by priority. Each argument is an object with a `property` property and an optional `descending` property (defaults to ascending, if not set), to define the order. For example:
```javascript
collection.sort([
{ property: 'lastName' },
{ property: 'firstName' }
])
```
would result in a new collection sorted by `lastName`, with `firstName` used to sort identical `lastName` values.

#### select([property, ...])
#### `select([property, ...])`

This selects specific properties that should be included in the returned objects.

#### select(property)
#### `select(property)`

This will indicate that the return results will consist of the values of the given property of the queried objects. For example, this would return a collection of name values, pulled from the original collection of objects:

collection.select('name');
```javascript
collection.select('name');
```

#### `forEach(callback, thisObject)`

Expand All @@ -122,41 +159,53 @@ Type | Description

Setting `filterEvents` to `true` indicates the listener will be called only when the emitted event references an item (`event.target`) that satisfies the collection's current filter query. Note: if `filterEvents` is set to `true` for type `update`, the listener will be called only when the item passed to `put` matches the collection's query. The original item will not be evaluted. For example, a store contains items marked "to-do" and items marked "done" and one collection uses a query looking for "to-do" items and one looks for "done" items. Both collections are listening for "update" events. If an item is updated from "to-do" to "done", only the "done" collection will be notified of the update.

If detecting when an item is removed from a collection due to an update is desired, set `filterEvents` to `false` and use the `matchesFilter(item)` method to test if each item updated is currently in the collection.
If detecting when an item is removed from a collection due to an update is desired, set `filterEvents` to `false` and use the `matchesFilter(item)` method to test if each item updated is currently in the collection.

There is also a corresponding `emit(type, event)` method (from the [Store interface](Store.md#method-summary)) that can be used to emit events when objects have changed.

#### `track()`

This method will create a new collection that will be tracked and updated as the parent collection changes. This will cause the events sent through the resulting collection to include an `index` and `previousIndex` property to indicate the position of the change in the collection. This is an optional method, and is usually provided by `dstore/Trackable`. For example, you can create an observable store class, by using `dstore/Trackable` as a mixin:

var TrackableMemory = declare([Memory, Trackable]);
```javascript
var TrackableMemory = declare([Memory, Trackable]);
```

Trackable requires client side querying functionality. Client side querying functionality is available in `dstore/SimpleQuery` (and inherited by `dstore/Memory`). If you are using a `Request`, `Rest`, or other server side store, you will need to implement client-side query functionality (by implemented querier methods), or mixin `SimpleQuery`:
Trackable requires client side querying functionality. Client side querying functionality is available in `dstore/SimpleQuery` (and inherited by `dstore/Memory`). If you are using a `Request`, `Rest`, or other server side store, you will need to implement client-side query functionality (by implementing querier methods), or mixin `SimpleQuery`:

var TrackableRest = declare([Rest, SimpleQuery, Trackable]);
```javascript
var TrackableRest = declare([Rest, SimpleQuery, Trackable]);
```

Once we have created a new instance from this store, we can track a collection, which could be the top level store itself, or a downstream filtered or sorted collection:

var store = new TrackableMemory({data: ...});
var filteredSorted = store.filter({inStock: true}).sort('price');
var tracked = filteredSorted.track();
```javascript
var store = new TrackableMemory({ data: [...] });
var filteredSorted = store.filter({ inStock: true }).sort('price');
var tracked = filteredSorted.track();
```

Once we have a tracked collection, we can listen for notifications:

tracked.on('add, update, delete', function(event){
var newIndex = event.index;
var oldIndex = event.previousIndex;
var object = event.target;
});
```javascript
tracked.on('add, update, delete', function (event) {
var newIndex = event.index;
var oldIndex = event.previousIndex;
var object = event.target;
});
```

Trackable requires fetched data to determine the position of modified objects and can work with either full or partial data. We can do a `fetch()` or `forEach()` to access all the items in the filtered collection:

tracked.fetch();
```javascript
tracked.fetch();
```

Or we can do a `fetchRange()` to make individual range requests for items in the collection:

tracked.fetchRange(0, 10);
```javascript
tracked.fetchRange(0, 10);
```

Trackable will keep track of each page of data, and send out notifications based on the data it has available, along with index information, indicating the new and old position of the object that was modified. Regardless of whether full or partial data is fetched, tracked events and the indices they report are relative to the entire collection, not relative to individual fetched ranges. Tracked events also include a `totalLength` property indicating the total length of the collection.

Expand All @@ -172,20 +221,23 @@ Custom query methods can be created using the `dstore/QueryMethod` module. We ca

For example, we could create a `getChildren` method that queried for children object, by simply returning the children property array from a parent:

declare([Memory], {
getChildren: new QueryMethod({
type: 'children',
querierFactory: function (parent) {
var parentId = this.getIdentity(parent);

return function (data) {
// note: in this case, the input data is ignored as this querier
// returns an object's array of children instead

// return the children of the parent
// or an empty array if the parent no longer exists
var parent = this.getSync(parentId);
return parent ? parent.children : [];
};
}
})
```javascript
declare([Memory], {
getChildren: new QueryMethod({
type: 'children',
querierFactory: function (parent) {
var parentId = this.getIdentity(parent);

return function (data) {
// note: in this case, the input data is ignored as this querier
// returns an object's array of children instead

// return the children of the parent
// or an empty array if the parent no longer exists
var parent = this.getSync(parentId);
return parent ? parent.children : [];
};
}
})
});
```
Loading