From 71ec80c21e9b08996ee3d7f3a8097c365b97f50d Mon Sep 17 00:00:00 2001 From: Mangala SSS Khalsa Date: Wed, 15 Jan 2020 22:24:27 -0700 Subject: [PATCH] Collection: add documentation on SimpleQuery nested property shorthand Fix typos and add syntax highlighting in Collection and Stores docs --- docs/Collection.md | 140 ++++++++++++++++++++++++++++++-------------- docs/Stores.md | 142 +++++++++++++++++++++++++-------------------- 2 files changed, 176 insertions(+), 106 deletions(-) diff --git a/docs/Collection.md b/docs/Collection.md index f5a1486..3febb9e 100644 --- a/docs/Collection.md +++ b/docs/Collection.md @@ -6,9 +6,11 @@ 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. @@ -16,15 +18,21 @@ In addition, the `track()` method may be used to track store changes, ensuring n 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. @@ -45,9 +53,13 @@ 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 @@ -55,6 +67,22 @@ Different stores may implement filtering in different ways. The `dstore/Memory` 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). @@ -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)` @@ -122,7 +159,7 @@ 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. @@ -130,33 +167,45 @@ There is also a corresponding `emit(type, event)` method (from the [Store interf 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. @@ -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 : []; + }; + } + }) +}); +``` diff --git a/docs/Stores.md b/docs/Stores.md index 0d70641..cb0cc16 100644 --- a/docs/Stores.md +++ b/docs/Stores.md @@ -19,20 +19,26 @@ All the stores can be instantiated with an options argument to the constructor, Stores can also be constructed by combining a base store with mixins. The various store mixins are designed to be combined through dojo `declare` to create a class to instantiate a store. For example, if you wish to add tracking and tree functionality to a Memory store, we could combine these: - // create the class based on the Memory store with added functionality - var TrackedTreeMemoryStore = declare([Memory, Trackable, Tree]); - // now create an instance - var myStore = new TrackedTreeMemoryStore({data: [...]}); +```javascript +// create the class based on the Memory store with added functionality +var TrackedTreeMemoryStore = declare([Memory, Trackable, Tree]); +// now create an instance +var myStore = new TrackedTreeMemoryStore({ data: [...] }); +``` The store mixins can only be used as mixins, but stores can be combined with other stores as well. For example, if we wanted to add the Rest functionality to the RequestMemory store (so the entire store data was retrieved from the server on construction, but data changes are sent to the server), we could write: - var RestMemoryStore = declare([Rest, RequestMemory]); - // now create an instance - var myStore = new RestMemoryStore({target: '/data-source/'}); +```javascript +var RestMemoryStore = declare([Rest, RequestMemory]); +// now create an instance +var myStore = new RestMemoryStore({ target: '/data-source/' }); +``` -Another common case is needing to add tracking to the `dstore/Rest` store, which requires client side querying, which be provided by `dstore/SimpleQuery`: +Another common case is needing to add tracking to the `dstore/Rest` store, which requires client side querying, which can be provided by `dstore/SimpleQuery`: +```javascript var TrackedRestStore = declare([Rest, SimpleQuery, Trackable]); +``` ## Memory @@ -40,13 +46,15 @@ The Memory store is a basic client-side in-memory store that can be created from For example: - myStore = new Memory({ - data: [{ - id: 1, - aProperty: ..., - ... - }] - }); +```javascript +myStore = new Memory({ + data: [{ + id: 1, + aProperty: ..., + ... + }] +}); +``` The array supplied as the `data` property will not be copied, it will be used as-is as the store's data. It can be changed at run-time with the `setData` method. @@ -77,9 +85,11 @@ This store extends the Request store, to add functionality for adding, updating, For example: - myStore = new Rest({ - target: '/PathToData/' - }); +```javascript +myStore = new Rest({ + target: '/PathToData/' +}); +``` All modification or retrieval methods (except `getIdentity()`) on `Request` and `Rest` execute asynchronously, returning a promise. @@ -113,36 +123,40 @@ Alternately a number can be provided as a property configuration, and will be us An example database configuration object is: - var dbConfig = { - version: 5, - stores: { - posts: { - name: 10, - id: { - autoIncrement: true, - preference: 100 - }, - tags: { - multiEntry: true, - preference: 5 - }, - content: { - indexed: false - } +```javascript +var dbConfig = { + version: 5, + stores: { + posts: { + name: 10, + id: { + autoIncrement: true, + preference: 100 }, - commments: { - author: {}, - content: { - indexed: false - } + tags: { + multiEntry: true, + preference: 5 + }, + content: { + indexed: false + } + }, + commments: { + author: {}, + content: { + indexed: false } } - }; + } +}; +``` In addition, each store should define a `storeName` property to identify which database store corresponds to the store instance. For example: - var postsStore = new LocalDB({dbConfig: dbConfig, storeName: 'posts'}); - var commentsStore = new LocalDB({dbConfig: dbConfig, storeName: 'comments'}); +```javascript +var postsStore = new LocalDB({ dbConfig: dbConfig, storeName: 'posts' }); +var commentsStore = new LocalDB({ dbConfig: dbConfig, storeName: 'comments' }); +``` Once created, these stores can be used like any other store. @@ -150,9 +164,11 @@ Once created, these stores can be used like any other store. This is a mixin that can be used to add caching functionality to a store. This can also be used to wrap an existing store, by using the static `create` function: - var cachedStore = Cache.create(existingStore, { - cachingStore: new Memory() - }); +```javascript +var cachedStore = Cache.create(existingStore, { + cachingStore: new Memory() +}); +``` This store has the following properties and methods: @@ -161,7 +177,7 @@ Name | Description `cachingStore` | This can be used to define the store to be used for caching the data. By default a Memory store will be used. `isValidFetchCache` | This is a flag that indicates if the data fetched for a collection/store can be cached to fulfill subsequent fetches. This is false by default, and the value will be inherited by downstream collections. It is important to note that only full `fetch()` requests will fill the cache for subsequent `fetch()` requests. `fetchRange()` requests will not fulfill a collection, and subsequent `fetchRange()` requests will not go to the cache unless the collection has been fully loaded through a `fetch()` request. `allLoaded` | This is a flag indicating that the given collection/store has its data loaded. This can be useful if you want to provide a caching store prepopulated with data for a given collection. If you are setting this to true, make sure you set `isValidFetchCache` to true as well to indicate that the data is available for fetching. -`canCacheQuery(method, args)' | This can be a boolean or a method that will indicate if a collection can be cached (if it should have `isValidFetchCache` set to true), based on the query method and arguments used to derive the collection. +`canCacheQuery(method, args)` | This can be a boolean or a method that will indicate if a collection can be cached (if it should have `isValidFetchCache` set to true), based on the query method and arguments used to derive the collection. `isLoaded(object)` | This can be defined to indicate if a given object in a query can be cached (by default, objects are cached). @@ -184,21 +200,23 @@ The Trackable mixin adds functionality for tracking the index positions of objec [Resource Query Language (RQL)](https://github.com/persvr/rql) is a query language specifically designed to be easily embedded in URLs (it is a compatible superset of standard encoded query parameters), as well as easily interpreted within JavaScript for client-side querying. Therefore RQL is a query language suitable for consistent client and server-delegated queries. The dstore packages serializes complex filter/queries into RQL (RQL supersets standard query parameters, and so simple queries are simply serialized as standard query parameters). -dstore also includes support for using RQL as the query language for filtering. This can be enabled by mixin `dstore/extensions/RqlQuery` into your collection type: - - require([ - 'dojo/_base/declare', - 'dstore/Memory', - 'dstore/extensions/RqlQuery' - ], function (declare, Memory, RqlQuery) { - var RqlStore = declare([ Memory, RqlQuery ]); - var rqlStore = new RqlStore({ - ... - }); - - rqlStore.filter('price<10|rating>3').forEach(function (product) { - // return each product that has a price less than 10 or a rating greater than 3 - }); - }}; +dstore also includes support for using RQL as the query language for filtering. This can be enabled by mixing `dstore/extensions/RqlQuery` into your collection type: + +```javascript +require([ + 'dojo/_base/declare', + 'dstore/Memory', + 'dstore/extensions/RqlQuery' +], function (declare, Memory, RqlQuery) { + var RqlStore = declare([ Memory, RqlQuery ]); + var rqlStore = new RqlStore({ + ... + }); + + rqlStore.filter('price<10|rating>3').forEach(function (product) { + // return each product that has a price less than 10 or a rating greater than 3 + }); +}}; +``` Make sure you have installed/included the [rql](https://github.com/persvr/rql) package if you are using the RQL query engine.