dojo-stores
v2.0.0-alpha.8
Published
A new data store in TypeScript
Downloads
34
Readme
dojo-stores
This library provides a data store, and several mixins built using dojo-compose and TypeScript. The mixins provide additional functionality and APIs that can be added to the base store dynamically.
WARNING This is alpha software. It is not yet production ready, so you should use at your own risk.
Features
Storage
The underlying Storage
interface provides the basic CRUD functionality, and is leveraged to provide the Store
interface, which is the interface intended to be consumed. This means that the basic createStore
factory, which defaults to using the provided createInMemoryStorage
can be repurposed to interact with any storage medium by providing an object implementing the simpler Storage
interface at instantiation.
import createStore from store/createStore';
import { Storage } from store/createInMemroyStorage';
const myCustomStorage: Storage = {
// Implement storage API
}
const myCustomStore = createStore({
storage: myCustomStorage
});
Store
The Store
interface provides basic CRUD operations, methods to retrieve records, and methods to create IDs.
get(ids: string[] | string): Promise<T[]>;
Retrieves store items associated with the provided ID or IDs. Will return undefined for any items that don't exist in the store.
identify(items: T[] | T): string[];
Returns the IDs for the passed in items. By default the store will look for an id
property on an item, if another property should be used, the idProperty
can be specified when creating the store. The idFunction
property can be provided if a more complicated, composite ID needs to be generated.
createId(): Promise<string>;
Generates a new ID. The default implementation of createId
involves incrementing an internally stored integer every time it is called.
add(items: T[] | T, options?: O): StoreObservable<T, U>;
Adds the item(s) to the store, failing if they already exist, unless the rejectOverwrite
property is set to false
. For the default store implementation rejectOverwrite
is the only option used by the store.
put(items: T[] | T, options?: O): StoreObservable<T, U>;
Adds or overwrites the specified items in the store. If overwrites should not be allowed, rejectOverwrite
should be set to true
in the provided options.
patch(updates: PatchArgument<T>, options?: O): StoreObservable<T, U>;
Updates the item(s) indicated by PatchArgument in place in the store. The Patch
interface is based on the JSON Patch spec, and can be serialized(not fully tested) to a format compliant with an HTTP patch request
delete(ids: string[] | string): StoreObservable<string, U>;
Delete the item(s) with the provided IDs from the store
fetch(): Promise<T[]>;
Returns a promise that will resolve to all of the data in the store
fetch<U>(query: Query<T, U>): Promise<U[]>;
Returns a promise to the data matching the provided Query
in the store.
Basic Usage
store.fetch().then(function(storeData) {
// storeData = data;
});
store.delete('1').then(function(deleted) {
// deleted = [ '1' ]
});
store.delete([ '2', '3' ]).then(function(deleted) {
// delete = [ '2', '3' ]
});
store.add([
{ id: '1', value: 2 },
{ id: '2', value: 3 },
{ id: '3', value: 4 }
]);
store.put([
{ id: '1', value: 5 },
{ id: '4', value: 4 }
]);
// These won't compile, because they don't match
// the item type. The item type was inferred by the data argument
// in the createStore initialization options, but it can also be
// specified explicitly(i.e. createStore<TypeOfData, CrudOptions>();)
// store.put({ id: '5', value: '' });
// store.add('5');
store.patch({ id: '2', patch: diff(
{ id: '2', value: 3 },
{ id: '2', value: 10 }
)});
store.fetch().then(function(data) {
// data = [
// { id: '1', value: 5 },
// { id: '2', value: 10 },
// { id: '3', value: 4 },
// { id: '4', value: 4 }
// ]);
});
Store Observable
The return type of the CRUD methods on the Store
interface is a StoreObservable
. This type extends Promise
. then
returns the final results of the operation to the callback provided if it is successful, and passes any errors that occurred to the error callback otherwise.
But it is also observable. By default any subscribers will get exactly one UpdateResults
object or an error before being completed.
interface UpdateResults<T> {
currentItems?: T[];
failedData?: CrudArgument<T>[];
successfulData: T[] | string[];
type: StoreOperation;
}
The built in store will only populate the type
and successfulData
properties, but this provides an extension point for store implementations to provide more details about the results of the operation, report results incrementally, or allow for the operation to be retried in the case of recoverable errors(e.g. data conflicts or network errors).
createObservableStoreMixin
This store provides an API for observing the store itself, or specific items within the store.
export interface ObservableStoreMixin<T> {
/**
* Observe the entire store, receiving deltas indicating the changes to the store.
* When observing, an initial update will be sent with the last known state of the store in the `afterAll` property.
* If fetchAroundUpdates is true, the store's local data will by synchronized with the underlying Storage.
* If fetchAroundUpdates is not true, then the data will be the result of locally applying updates to the data
* retrieved from the last fetch.
*/
observe(): Observable<StoreDelta<T>>;
/**
* Receives the current state of the item with the specified ID whenever it is updated. This observable will be
* completed if the item is deleted
* @param id The ID of the item to observe
*/
observe(id: string): Observable<T>;
/**
* Receives the current state of the items in an `ItemUpdate` object whenever they are updated. When any of the
* items are deleted an `ItemUpdate` with the item's ID and no item property will be sent out. When all of the
* observed items are deleted the observable will be completed.
* @param ids - The IDS of the items to observe
*/
observe(ids: string[]): Observable<ItemUpdate<T>>;
}
interface StoreDelta<T> {
updates: T[];
deletes: string[];
adds: T[];
beforeAll: T[];
afterAll: T[];
}
interface ItemUpdate<T> {
item?: T;
id: string;
}
Observing the store
When observing the whole store, an initial update will be received that contains the current data in the store in the afterAll
property, and subsequent updates will represent the changes in the store since the last update.
If the fetchAroundUpdates
property is set to true
in the options when creating the store, then the data in the store will be kept up to date with the underlying storage, and any updates will represent the latest data from the storage. If fetchAroundUpdates
is false
or not specified, then the local data will be modified in place according to the updates indicated by the StoreDelta
, but it may become out of sync with the underlying storage. In this case, when fetching manually, the local data will be synced again. An update is sent after a fetch
, and the update following a fetch may contain new items in the afterAll
property that are not represented by the updates
, adds
, and deletes
of the StoreDelta
when fetchAroundUpdates
is false
and the store is out of sync with its storage.
Example usage
import { createObservableStore } from 'dojo-stores/store/mixins/createObservableStoreMixin';
const observableStore = createObservableStore({
data: [{ id: '1', value: 1 }]
});
observableStore.observe().subscribe(function(update) {
// update = {
// updates: [ any items updated since last notification ],
// deletes: [ any items deleted since last notification ],
// beforeAll: [ Empty for the first update, previous state for following updates],
// afterAll: [ Current state of the sotre ],
// adds: [ any items added since last notification ]
// }
});
observableStore.observe('itemId').subscribe(function(update) {
// update will be the item itself
}, undefined, function() {
// completion callback will be called if the item is deleted
});
observableStore.observe([ 'itemId', 'otherItemId' ]).subscribe(function() {
// update = {
// item: The updated or null if the item was deleted,
// id: The id of the item
// }
}, undefined, function() {
// completion callback will be called if all items are deleted
});
createQueryTransformMixin
This mixin provides the ability to filter, sort, select a range of, transform, or query for items using a custom query function.
export interface QueryTransformMixin<T, S extends ObservableStore<T, any, any>> {
/**
* Creates a query transform result with the provided query
* @param query
*/
query(query: Query<T>): MappedQueryTransformResult<T, S>;
/**
* Creates a query transform result with the provided filter
* @param filter
*/
filter(filter: Filter<T>): MappedQueryTransformResult<T, S>;
/**
* Creates a query transform result with a filter built from the provided test
* @param test
*/
filter(test: (item: T) => boolean): MappedQueryTransformResult<T, S>;
/**
* Creates a query transform result with the provided range
* @param range
*/
range(range: StoreRange<T>): MappedQueryTransformResult<T, S>;
/**
* Creates a query transform result with a range built based on the provided start and count
* @param start
* @param cound
*/
range(start: number, count: number): MappedQueryTransformResult<T, S>;
/**
* Creates a query transform result with the provided sort or a sort build from the provided comparator or a
* comparator for the specified property
* @param sort
* @param descending
*/
sort(sort: Sort<T> | ((a: T, b: T) => number) | string, descending?: boolean): MappedQueryTransformResult<T, S>;
/**
* Create a query transform result that cannot be tracked, and cannot send tracked updates. This is the case because
* the resulting query transform result will have no way to identify items, making it impossible to determine
* whether their position has shifted or differentiating between updates and adds
* @param transformation
*/
transform<V>(transformation: Patch<T, V> | ((item: T) => V)): QueryTransformResult<V, S>;
/**
* Create a trackable query transform result with the specified transformation
* @param transformation
* @param idTransform
*/
transform<V>(transformation: Patch<T, V> | ((item: T) => V), idTransform: string | ((item: V) => string)): MappedQueryTransformResult<V, S>;
}
The result of querying or transforming will be a read only QueryTransformResult
.
export interface QueryTransformResult<T, S extends ObservableStore<any, any, any>> {
query(query: Query<T>): this;
filter(filter: Filter<T>): this;
filter(test: (item: T) => boolean): this;
range(range: StoreRange<T>): this;
range(start: number, count: number): this;
sort(sort: Sort<T> | ((a: T, b: T) => number) | string, descending?: boolean): this;
observe(): Observable<StoreDelta<T>>;
observe(id: string): Observable<T>;
observe(ids: string[]): Observable<ItemUpdate<T>>;
get(ids: string | string[]): Promise<T[]>;
transform<V>(transformation: Patch<T, V> | ((item: T) => V)): QueryTransformResult<V, S>;
transform<V>(transformation: Patch<T, V> | ((item: T) => V), idTransform: string | ((item: V) => string)): this;
fetch(query?: Query<T>): Promise<T[]>;
source: S;
}
The QueryTransformResult
can be further queried or transformed, as well as observed, and has a reference to the source store if updates need to be performed on the original data.
The observation API for the QueryTransformResult
is very similar to the store's with a few changes.
MappedQueryTransformResult
Unless the transform method is called without an idTransform, the result of any queries to a store with the createQueryTransformMixin will be a MappedQueryTransformResult
, which includes additional data in its StoreDelta
updates, and provides a track()
method. The StoreDelta
interface is extended by the TrackedStoreDelta
interface which the MappedQueryTransformResult
provides to observers. This augments the interface by providing data indicating the current and previous indices of items that have been moved within, added to, or removed from, the view represented by the MappedQueryTransformResult
. Unlike updates
, deletes
, and adds
, these properties are not related to specific operations, but instead just represent changes in the position of items within the collection.
export interface TrackableStoreDelta<T> extends StoreDelta<T> {
/**
* Contains info for any items that were formerly in the tracked collection and are now not, regardless of how
* those items were removed
*/
removedFromTracked: { item: T; id: string; previousIndex: number; }[];
/**
* Contains info for any items that are now in the tracked collection and formerly were not, regardless of how
* those items were added
*/
addedToTracked: { item: T; id: string; index: number; }[];
/**
* Contains info were previously and still are in the tracked collection but have changed position, regardless of
* how the items were moved.
*/
movedInTracked: { item: T; id: string; previousIndex: number; index: number }[];
}
As with the observable store mixin, the locally tracked data in a MappedQueryTransformResult
has the potential to become out of sync with the underlying storage. If the source ObservableStore
has fetchAroundUpdates
set to true, then any query transform results produced from it will only send up to date information to observers. If the source is not fetching around updates, the track()
method provided as part of the MappedQueryTransformResult
interface can be used to create a copy of a QueryTransformResult
that will fetch after any updates from its source to make sure it has the latest data. track()
produces a TrackedQueryTransformResult
, which has a release()
method that provides a new, non-tracked query transform result.
When transform()
is called without an idTransform
, the resulting QueryTransformResult
has no way of determining the ID of a transformed item, and so it cannot tell whether changes from the source store represent updates or additions, and cannot keep an index to easily track the position of items within the store. As a result, a QueryTransformResult
created this way will not contain positional information in its updates to observers, and cannot be tracked.
Example Usage
import { createQueryStore } from 'dojo-stores/store/mixins/createQueryTransformMixin';
const data = [
{ id: '1', value: 1 },
{ id: '2', value: 2 },
{ id: '3', value: 3 }
];
const queryStore = createQueryStore({
data: data
});
const filteredView = queryStore.filter((item) => item.value > 1);
filteredView.fetch().then((data) => {
// data = [ { id: '2', value: 2 }, { id: '3', value: 3 } ]
});
filteredView.observe().subscribe((update) => {
/*
initial update = {
deletes: [],
adds: [],
updates: [],
addedToTracked: [],
removedFromTracked: [],
movedInTracked: [],
beforeAll: [],
afterAll: []
}
subsequent update = {
deletes: [],
adds: data,
updates: [],
addedToTracked: [
{
id: '1',
item: data[0],
index: 0
},
{
id: '2',
item: data[1],
index: 1
},
{
id: '3',
item: data[2],
index: 2
}
],
beforeAll: [],
afterAll: []
};
*/
});
If the observer starts observing after the initial add is already resolved, the first update they receive will be the subsequent update
in this example. Here the first update is provided to an observer that subscribes synchronously with the initialization of the store, but the initial add happens asynchronously and so is not yet resolved. For a tracked collection, the first update will contain the data form a fetch
to the source.
How do I contribute?
We appreciate your interest! Please see the Dojo 2 Meta Repository for the Contributing Guidelines and Style Guide.
Testing
Test cases MUST be written using Intern using the Object test interface and Assert assertion interface.
90% branch coverage MUST be provided for all code submitted to this repository, as reported by istanbul’s combined coverage results for all supported platforms.
To test locally in node run:
grunt test
To test against browsers with a local selenium server run:
grunt test:local
To test against BrowserStack or Sauce Labs run:
grunt test:browserstack
or
grunt test:saucelabs
Licensing information
TODO: If third-party code was used to write this library, make a list of project names and licenses here
© 2004–2016 Dojo Foundation & contributors. New BSD license.